Пользователь в понедельник сообщает ассистенту: «Меня зовут Анна, я работаю в Казани, отвечай коротко». В среду он открывает новый чат, и агент снова спрашивает, как его зовут, а на вопрос о доставке отвечает про Москву и длинным текстом. Память диалога из второго модуля здесь не помогает: она привязана к thread_id, то есть к одному разговору, и в новом разговоре всё начинается с нуля.
Чтобы агент помнил пользователя между разговорами, нужна долговременная память. В LangChain за неё отвечает хранилище store: туда агент сам записывает важные факты и оттуда же достаёт их в любом следующем диалоге. После урока вы сможете подключить к агенту такое хранилище, дать ему инструменты для записи и чтения воспоминаний и настроить поиск по ним по смыслу.
Что уже умеем
Во втором модуле вы подключали к агенту checkpointer и разделяли разговоры через thread_id. В этом модуле вы освоили эмбеддинги, векторное хранилище и RAG-агента, который ищет по документам. Сегодня применим похожие идеи не к документам компании, а к знаниям о конкретном пользователе.
Что вы узнаете в рамках статьи
- чем долговременная память отличается от памяти диалога;
- как устроено хранилище
InMemoryStore: пространства имён, ключи, значения; - как подключить
storeк агенту и обращаться к нему из инструментов черезToolRuntime; - как передать агенту идентификатор пользователя через контекст;
- как искать воспоминания по смыслу.
Ключевые термины
- Долговременная память — данные, которые сохраняются между разными разговорами и доступны в любом
thread_id. - Store (хранилище) — объект для долговременной памяти, который хранит записи в формате «ключ — словарь».
- Пространство имён (namespace) — кортеж строк, который группирует записи, например
("users", "u42", "memories"). - ToolRuntime — специальный параметр инструмента, через который он получает доступ к хранилищу и контексту; модель этот параметр не видит.
- Контекст запуска — неизменяемые данные одного вызова агента, например идентификатор пользователя.
Две памяти агента
У агента теперь может быть два вида памяти, и они решают разные задачи. Checkpointer сохраняет историю сообщений одного разговора: это кратковременная память, которая помогает понимать уточнения вроде «а можно подробнее?». Store хранит отдельные факты, которые живут дольше разговора: имя, город, предпочтения, принятые решения.
Различие удобно запомнить так: checkpointer отвечает на вопрос «что было сказано в этом чате», а store — «что мы знаем об этом пользователе». В store вы кладёте не всю переписку, а короткие выжимки. Это экономит контекст: вместо сотен прошлых сообщений агент получает пять-шесть строк о пользователе.
Хранилище: namespace, put, get, search
Начнём с самого хранилища, без агента. Записи в нём группируются по пространствам имён, внутри пространства у каждой записи есть ключ, а значение — обычный словарь.
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
namespace = ("users", "u42")
store.put(namespace, "profile", {"name": "Анна", "city": "Казань"})
store.put(namespace, "style", {"answer_length": "коротко"})
item = store.get(namespace, "profile")
print(item.value["name"], item.key)
for it in store.search(namespace):
print(it.key, it.value)
print(store.get(("users", "u99"), "profile"))
Что увидим в консоли: Анна profile, затем две записи profile и style с их словарями и, наконец, None — у пользователя u99 профиля нет. Метод get возвращает объект с полями key, value, namespace и временем создания, а search — список таких объектов. Повторный put с тем же ключом перезаписывает значение, а store.delete(namespace, key) удаляет запись.
Пространство имён — главный инструмент разграничения данных. Включайте в него идентификатор пользователя: тогда воспоминания одного человека никогда не попадут в ответы другому. InMemoryStore хранит данные в оперативной памяти и подходит для разработки; в продакшене его заменяют хранилищем на базе данных, например PostgresStore, а код работы с ним остаётся тем же.
Память в агенте: store и ToolRuntime
Теперь дадим агенту инструменты для работы с памятью. Хранилище передаётся в create_agent параметром store, а инструменты получают к нему доступ через параметр типа ToolRuntime. Идентификатор пользователя модели знать не нужно и опасно: она может его перепутать. Поэтому передадим его через контекст запуска.
import uuid
from dataclasses import dataclass
from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
@dataclass
class Context:
user_id: str
@tool
def save_memory(fact: str, runtime: ToolRuntime[Context]) -> str:
"""Сохраняет важный факт о пользователе: имя, город, предпочтения."""
namespace = ("users", runtime.context.user_id, "memories")
runtime.store.put(namespace, str(uuid.uuid4()), {"fact": fact})
return "Запомнил."
@tool
def recall_memories(runtime: ToolRuntime[Context]) -> str:
"""Возвращает всё, что известно о пользователе из прошлых разговоров."""
namespace = ("users", runtime.context.user_id, "memories")
items = runtime.store.search(namespace, limit=20)
return "\n".join(f"- {it.value['fact']}" for it in items) or "Ничего не известно."
agent = create_agent(
model="openai:gpt-6-luna",
tools=[save_memory, recall_memories],
system_prompt=(
"Ты личный помощник. В начале разговора вызови recall_memories. "
"Когда пользователь сообщает о себе что-то устойчивое, сохрани это через save_memory."
),
checkpointer=InMemorySaver(),
store=InMemoryStore(),
context_schema=Context,
)
Параметр runtime модель не видит: в схеме инструмента save_memory для неё есть только аргумент fact. LangChain сам подставляет в runtime хранилище агента и контекст текущего вызова. Проверим память на двух разных разговорах одного пользователя:
ctx = Context(user_id="u42")
agent.invoke(
{"messages": [{"role": "user", "content": "Я Анна из Казани, отвечай коротко."}]},
config={"configurable": {"thread_id": "chat-1"}},
context=ctx,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Сколько дней идёт доставка в мой город?"}]},
config={"configurable": {"thread_id": "chat-2"}},
context=ctx,
)
print(result["messages"][-1].text)
Что увидим: в первом разговоре агент один или несколько раз вызовет save_memory. Во втором, в новом thread_id, он вызовет recall_memories, узнает, что пользователь из Казани и просил краткости, и ответит одной фразой про доставку в Казань. Если запустить тот же вопрос с Context(user_id="u7"), агент ничего не вспомнит: пространства имён разные.
Поиск воспоминаний по смыслу
Когда фактов о пользователе накапливаются сотни, возвращать их все в контекст дорого. Здесь пригодится то, что вы изучили в начале модуля: хранилище умеет искать по эмбеддингам. Для этого при создании передайте параметр index:
from langchain_openai import OpenAIEmbeddings
from langgraph.store.memory import InMemoryStore
store = InMemoryStore(
index={
"embed": OpenAIEmbeddings(model="text-embedding-3-small"),
"dims": 1536,
"fields": ["fact"],
}
)
ns = ("users", "u42", "memories")
store.put(ns, "m1", {"fact": "Живёт в Казани"})
store.put(ns, "m2", {"fact": "Аллергия на орехи"})
store.put(ns, "m3", {"fact": "Предпочитает короткие ответы"})
for it in store.search(ns, query="Что пользователю нельзя есть?", limit=1):
print(it.value["fact"], round(it.score, 2))
Что увидим: Аллергия на орехи со значением сходства, например 0.48. Поле fields указывает, какое поле словаря векторизовать, а dims должно совпадать с размерностью модели. В инструменте recall_memories теперь достаточно добавить аргумент query и передавать его в search: агент будет получать только несколько воспоминаний, относящихся к текущему вопросу.
Частые ошибки
- Путать store и checkpointer. Новый
thread_idначинает разговор с чистой историей, и это нормально. Долговременные факты должны лежать в store, а не в истории сообщений. - Не включать пользователя в namespace. Общее пространство имён для всех смешает воспоминания разных людей, и агент расскажет одному пользователю о другом.
- Передавать
user_idаргументом инструмента. Модель может подставить неверное значение. Берите идентификатор изruntime.context. - Сохранять всё подряд. Если записывать каждую реплику, память засоряется. Инструкция в системном промпте должна чётко описывать, какие факты сохранять.
Наиболее частые вопросы
- Сохраняется ли
InMemoryStoreпосле перезапуска? — Нет, он живёт в памяти процесса. Для постоянного хранения используютPostgresStoreили другие хранилища с тем же интерфейсом. - Чем store отличается от Chroma? — Chroma предназначена для больших корпусов документов, store — для небольших структурированных записей агента с разграничением по пространствам имён.
- Может ли пользователь попросить забыть факт? — Да, добавьте инструмент, который ищет запись и вызывает
runtime.store.delete(namespace, key). - Обязательно ли давать агенту инструмент для чтения? — Нет, воспоминания можно подгружать и программно, но инструмент проще и позволяет агенту самому решать, когда вспоминать.
Итоги
Вы завершили модуль про внешние данные, и это серьёзное достижение! Вы прошли путь от одного эмбеддинга до агента, который ищет по документам компании, отвечает со ссылками на источники и помнит пользователя между разговорами. Сегодня вы разобрались, чем store отличается от checkpointer, научились раскладывать воспоминания по пространствам имён, работать с ними из инструментов через ToolRuntime и искать их по смыслу.
Дальше начинается модуль о продвинутых агентах и продакшне. В следующем уроке вы заглянете под капот create_agent и познакомитесь с LangGraph: состоянием, узлами и переходами, из которых собирается любой агент.