Главная / Статьи / История диалога для чат-бота на LangChain с помощью сообщений

История диалога для чат-бота на LangChain с помощью сообщений

Собираем простой чат-бот, передавая модели прошлые сообщения через MessagesPlaceholder, и разбираемся, почему история — это не то же самое, что встроенная память модели.

1268 слов

До сих пор каждый вызов модели был сам по себе. Задали вопрос, получили ответ, и следующий вызов начинается с нуля. Чтобы получить что-то похожее на настоящий чат-бот, сначала нужно разобраться с этим.

Вот два вызова подряд:

model.invoke("What is Python?")
model.invoke("What is it used for?")

Второй понятия не имеет, что речь шла о Python. Модель видит только то, что есть в отправленных ей сообщениях, поэтому предыдущий обмен нужно включить.

Основная идея

Разговор — это список реплик. Первая:

Human → What is Python?
AI    → Python is a programming language.

И следующая:

Human → What is it used for?
AI    → Python is used for web development, AI, automation...

Чтобы модель поняла второй вопрос, вместе с ним по порядку отправляются предыдущие реплики:

SystemMessage
HumanMessage
AIMessage
HumanMessage

Вот почему AIMessage так важен. Именно так собственные прошлые ответы модели возвращаются в промпт.

Сначала разговор вручную

Стоит проделать это руками, прежде чем добавлять какие-либо помощники. Пример ниже использует Ollama, так что он работает с локальной моделью и не требует квоты API.

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

model = ChatOllama(model="llama3.2")

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

chain = prompt | model

history = []


def chat(question):
    response = chain.invoke({
        "history": history,
        "question": question
    })

    history.append(HumanMessage(content=question))
    history.append(AIMessage(content=response.content))

    return response.content


print(chat("What is Python?"))
print(chat("What is it used for?"))
print(chat("Can you give me three examples?"))

Присмотритесь к этой строке:

messages.append(response)

Она возвращает ответ модели обратно в разговор. После неё в списке лежат системное сообщение, первый вопрос, первый ответ и уточняющий вопрос:

SystemMessage
HumanMessage("What is Python?")
AIMessage("Python is...")
HumanMessage("What is it used for?")

Поэтому при таком вызове модель видит всю ветку:

model.invoke(messages)

Почему это работает

Представьте, что вы отправили только это:

HumanMessage(content="What is it used for?")

Ничто не подсказывает модели, к чему относится "it". С приложенной историей она читает вот что:

Human: What is Python?
AI: Python is a programming language.
Human: What is it used for?

Теперь она может понять местоимение:

"it" = Python

История — это просто контекст, переданный модели.

LangChain не даёт модели памяти

Этот момент легко упустить. Использование LangChain не заставляет модель что-либо помнить. Как правило, модель получает только те сообщения, которые вы передали для конкретного взаимодействия. Грубо говоря:

Your application
      ↓
Conversation history
      ↓
Messages
      ↓
Model

Состоянием разговора владеет ваше приложение. Держите это в голове: это станет очень важным, когда дойдёте до управления состоянием и LangGraph.

Добавляем шаблон промпта

История хорошо сочетается с шаблонами промптов из прошлых уроков. Начните с импортов:

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

Затем определите промпт:

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful Python teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

Всю работу делает вот эта строка:

MessagesPlaceholder(variable_name="history")

Читайте её как "вставь сюда предыдущие сообщения".

Собираем цепочку

Пропустите промпт через модель:

chain = prompt | model

Начните с пустого списка истории:

history = []

Сделайте первый вызов и выведите ответ:

response = chain.invoke({
    "history": history,
    "question": "What is Python?"
})print(response.content)

Затем сохраните обе стороны обмена: сначала сообщение пользователя

history.append(
    HumanMessage(content="What is Python?")
)

а потом ответ модели:

history.append(response)

Теперь задайте уточняющий вопрос:

response = chain.invoke({
    "history": history,
    "question": "What is it used for?"
})
print(response.content)

То, что модель реально получает, выглядит примерно так — сначала системная инструкция:

System:
You are a helpful Python teacher.

а за ней разговор на текущий момент:

Human:
What is Python?AI:
Python is a programming language.Human:
What is it used for?

Оборачиваем в чат-бот

Всё это умещается в одну функцию:

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

model = ChatOllama(model="llama3.2")

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

chain = prompt | model

history = []


def chat(question):
    response = chain.invoke({
        "history": history,
        "question": question
    })

    history.append(HumanMessage(content=question))
    history.append(AIMessage(content=response.content))

    return response.content


print(chat("What is Python?"))
print(chat("What is it used for?"))
print(chat("Can you give me three examples?"))

Теперь у вас есть простейшее диалоговое приложение.

Деталь о порядке

Сообщение пользователя попадает в историю после вызова модели, а не до него:

response = chain.invoke(...)
history.append(HumanMessage(content=question))
history.append(AIMessage(content=response.content))

Причина в том, что промпт уже принимает текущий вопрос как отдельный вход:

"question": question

так что добавление его ещё и в history привело бы к дублированию. Можно сделать и наоборот: положить всё в историю и передавать модели только её. Оба варианта рабочие. Главное — следите, чтобы текущее сообщение не появлялось дважды.

История и память

В старых туториалах по LangChain много говорят о «памяти» ("memory."). Не думайте, что это всегда одно и то же:

history = memory

На том уровне, что разобран здесь, история означает просто сообщения из предыдущих реплик:

History = messages from previous turns

Более продвинутая работа с состоянием будет позже, когда приложение хранит и обновляет состояние разговора между шагами. А пока запомните такой поток:

Conversation history
        ↓
Previous messages
        ↓
Model gets context

Получившаяся архитектура

Собранный чат-бот работает так: приложение хранит историю, подаёт её в промпт, а модель отвечает на её основе.

Почему это важно в реальных приложениях

На этой же идее построено множество продуктов:

Customer support chatbot
AI tutor
Personal assistant
HR assistant
Coding assistant
Sales assistant

Возьмём простой обмен, где пользователь представляется:

User: My name is Wasim.

Если следующий вопрос — как его зовут, модель ответит правильно, потому что предыдущие реплики были переданы как контекст:

AI: Nice to meet you, Wasim.User: What is my name?AI: Your name is Wasim.

Ваша мысленная модель на данный момент

Вы прошли такие элементы — от голого вызова модели до цепочки с историей:

1. Model
       ↓
   invoke()2. Messages
       ↓
   System / Human / AI
3. Prompt Templates
       ↓
   {variables}
4. Structured Output
       ↓
   predictable data
5. Chains
       ↓
   prompt | model | parser
6. Conversation History
       ↓
   previous messages → context

Что дальше

Следующая тема — инструменты и их вызов. Именно там LLM начинает делать что-то за пределами самой модели, вот по такому пути:

User
 ↓
LLM
 ↓
"I need a calculation"
 ↓
Python tool
 ↓
Result
 ↓
LLM
 ↓
Final answer

Это фундамент для понимания агентов.