Пользователь пишет агенту поддержки: «Меня зовут Анна, мой заказ 1042». Через минуту: «А когда он приедет?». Если агент не помнит первое сообщение, он переспросит номер заказа, и клиент справедливо решит, что с ним разговаривает автомат. Без памяти диалог превращается в череду несвязанных вопросов.
При этом память не может расти бесконечно. Каждое сообщение истории уходит модели на каждом шаге, поэтому длинный диалог становится дорогим, медленным и в какой-то момент упирается в контекстное окно. В этом уроке вы подключите к агенту память диалога, научитесь разделять разговоры разных пользователей и ограничивать историю, чтобы агент оставался быстрым и недорогим.
Что уже умеем
Вы создаёте агента через create_agent, даёте ему инструменты и задаёте поведение системным промптом. Из урока 1.4 вы помните, что модель сама ничего не запоминает: историю нужно передавать в каждом запросе, а из урока 1.2 — что длина истории напрямую влияет на стоимость. Сегодня поручим хранение истории LangChain и научимся её обрезать.
Что вы узнаете в рамках статьи
- почему агент без памяти забывает предыдущие сообщения;
- как подключить память через
checkpointerиInMemorySaver; - как
thread_idразделяет диалоги разных пользователей; - как обрезать историю своим middleware с
@before_model; - как сжимать старую часть диалога через
SummarizationMiddleware.
Ключевые термины
- Краткосрочная память — история сообщений одного диалога, которую агент учитывает при ответе.
- Checkpointer — объект, который сохраняет состояние агента после каждого шага и восстанавливает его при следующем вызове.
thread_id— идентификатор диалога; у каждого разговора свой, и истории разных диалогов не смешиваются.- Middleware — функция или объект, который встраивается в цикл агента и выполняется в определённый момент, например перед вызовом модели.
- Суммаризация — замена старых сообщений их кратким пересказом, сделанным моделью.
Агент без памяти
По умолчанию каждый вызов agent.invoke начинается с чистого листа: агент видит только сообщения, переданные в этом вызове.
from langchain.agents import create_agent
agent = create_agent(model="openai:gpt-6-luna", tools=[], system_prompt="Отвечай кратко.")
agent.invoke({"messages": [{"role": "user", "content": "Меня зовут Анна."}]})
result = agent.invoke({"messages": [{"role": "user", "content": "Как меня зовут?"}]})
print(result["messages"][-1].content)
Что увидим в консоли: «Я не знаю вашего имени, вы его не называли». Второй вызов ничего не знает о первом. Можно было бы, как в уроке 1.4, хранить список сообщений самим и передавать его целиком, но в LangChain для этого есть готовый механизм.
Checkpointer и thread_id
Память подключается одним параметром checkpointer. Для обучения и тестов подходит InMemorySaver — он хранит состояние в оперативной памяти процесса. При каждом вызове нужно передать config с thread_id: по нему агент находит сохранённую историю и дописывает в неё новые сообщения.
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="openai:gpt-6-luna",
tools=[],
system_prompt="Отвечай кратко.",
checkpointer=InMemorySaver(),
)
anna = {"configurable": {"thread_id": "anna-1"}}
agent.invoke({"messages": [{"role": "user", "content": "Меня зовут Анна."}]}, anna)
result = agent.invoke({"messages": [{"role": "user", "content": "Как меня зовут?"}]}, anna)
print(result["messages"][-1].content)
print(len(result["messages"]))
Что увидим: «Вас зовут Анна» и число 4. Во втором вызове мы передали только новое сообщение, а агент сам достал из checkpointer два сообщения первого обмена и добавил к ним два новых. В result["messages"] теперь лежит вся история диалога, а не только последний обмен.
Обратите внимание: с памятью в invoke передаётся только новая реплика пользователя. Если по привычке отправить всю историю заново, сообщения задвоятся.
Разные пользователи — разные потоки
Один агент обслуживает многих пользователей одновременно. Их разговоры разделяет thread_id: для каждого диалога — свой идентификатор, обычно связанный с id пользователя или сессии.
boris = {"configurable": {"thread_id": "boris-1"}}
result = agent.invoke({"messages": [{"role": "user", "content": "Как меня зовут?"}]}, boris)
print(result["messages"][-1].content)
result = agent.invoke({"messages": [{"role": "user", "content": "Напомни, как меня зовут?"}]}, anna)
print(result["messages"][-1].content)
Что увидим: Борису агент ответит, что не знает его имени, а Анне — что её зовут Анна. Потоки полностью изолированы. Если по ошибке дать всем пользователям один thread_id, агент будет путать их данные — это и неудобно, и небезопасно.
InMemorySaver теряет всё при перезапуске программы. В рабочих системах используют checkpointer с базой данных, например PostgresSaver из пакета langgraph-checkpoint-postgres. Код агента при этом не меняется — меняется только объект в параметре checkpointer.
Обрезка истории через @before_model
С памятью каждый новый вопрос обходится дороже предыдущего: модели уходит вся накопленная история. Простое решение — перед каждым вызовом модели оставлять только последние сообщения. Для этого в LangChain есть middleware: функция с декоратором @before_model выполняется перед каждым обращением к модели и может изменить состояние.
from typing import Any
from langchain.agents import AgentState
from langchain.agents.middleware import before_model
from langchain.messages import HumanMessage, RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langgraph.runtime import Runtime
MAX_MESSAGES = 6
@before_model
def keep_last_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
messages = state["messages"]
if len(messages) <= MAX_MESSAGES:
return None # история короткая, ничего не меняем
recent = messages[-MAX_MESSAGES:]
while not isinstance(recent[0], HumanMessage):
recent = recent[1:] # история должна начинаться с реплики пользователя
return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *recent]}
agent = create_agent(
model="openai:gpt-6-luna",
tools=[],
system_prompt="Отвечай кратко.",
checkpointer=InMemorySaver(),
middleware=[keep_last_messages],
)
Что увидим: пока в диалоге не больше шести сообщений, всё работает как раньше. Дальше старые сообщения удаляются, и в result["messages"] их становится не больше шести. RemoveMessage(id=REMOVE_ALL_MESSAGES) очищает список, а следом записываются оставшиеся сообщения. Цикл while сдвигает начало до сообщения пользователя: так история не начнётся с ToolMessage, оторванного от вызова, который его породил, — многие провайдеры такую историю отклоняют.
Цена простоты — забывчивость: если Анна представилась десять сообщений назад, после обрезки агент её имени уже не вспомнит. Удалённые сообщения пропадают и из checkpointer.
Суммаризация старых сообщений
Чтобы не терять важные факты, старую часть истории можно не выбрасывать, а пересказать. Готовый SummarizationMiddleware следит за размером истории и, когда он превышает порог, заменяет старые сообщения кратким пересказом, оставляя последние без изменений.
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model="openai:gpt-6-luna",
tools=[],
system_prompt="Отвечай кратко.",
checkpointer=InMemorySaver(),
middleware=[
SummarizationMiddleware(
model="openai:gpt-6-luna",
trigger=("tokens", 4000),
keep=("messages", 10),
),
],
)
Что увидим: пока история меньше 4 000 токенов, ничего не происходит. Когда порог превышен, middleware просит модель пересказать всё, кроме последних 10 сообщений, и заменяет их одним сообщением с пересказом. Имя Анны и номер заказа сохранятся в пересказе, а история снова станет короткой. Пересказ — это отдельный вызов модели, он тоже стоит денег, но происходит редко, а экономия на каждом следующем шаге его окупает.
Частые ошибки
- Забыть передать
configсthread_id. Агент с checkpointer безthread_idне знает, какую историю загрузить, и вызов завершится ошибкой. - Давать всем пользователям один
thread_id. Истории смешаются, и агент сможет пересказать одному клиенту данные другого. - Передавать всю историю при включённой памяти. Checkpointer уже хранит прошлые сообщения. Отправляйте в
invokeтолько новую реплику. - Обрезать историю посреди вызова инструмента. Если история начнётся с
ToolMessageбез соответствующегоAIMessage, модель вернёт ошибку. Начинайте обрезанную историю с сообщения пользователя.
Наиболее частые вопросы
- Чем
thread_idотличается отprevious_response_idиз урока 1.4? — Идея та же — продолжить диалог, но историю хранит ваш checkpointer, а не сервер OpenAI. Поэтому вы сами решаете, что хранить и что обрезать. - Сохранится ли память после перезапуска программы? — С
InMemorySaverнет. Для постоянного хранения используйте checkpointer с базой данных. - Что выбрать: обрезку или суммаризацию? — Обрезка проще и бесплатна, подходит для коротких разговоров. Суммаризация нужна, когда важно помнить факты из начала длинного диалога.
- Помнит ли агент Анну в новом диалоге с другим
thread_id? — Нет, краткосрочная память живёт внутри одного потока. Память между диалогами — тема урока 3.5.
Итоги
Отличный результат! Ваш агент теперь ведёт настоящий диалог: помнит, что говорил пользователь, не путает собеседников и не раздувает контекст до бесконечности. Вы подключили checkpointer, разобрались с thread_id и написали первый собственный middleware, а заодно познакомились с готовой суммаризацией.
Middleware — мощный механизм, и обрезка истории лишь первый пример. В следующем уроке вы научитесь получать от агента ответ строго по схеме Pydantic и с помощью middleware ограничите число шагов агента и обработаете ошибки инструментов.