История диалога для чат-бота на LangChain с помощью сообщений
Собираем простой чат-бот, передавая модели прошлые сообщения через MessagesPlaceholder, и разбираемся, почему история — это не то же самое, что встроенная память модели.
До сих пор каждый вызов модели был сам по себе. Задали вопрос, получили ответ, и следующий вызов начинается с нуля. Чтобы получить что-то похожее на настоящий чат-бот, сначала нужно разобраться с этим.
Вот два вызова подряд:
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
Это фундамент для понимания агентов.