$ sudo teach IT

Пользователь в понедельник сообщает ассистенту: «Меня зовут Анна, я работаю в Казани, отвечай коротко». В среду он открывает новый чат, и агент снова спрашивает, как его зовут, а на вопрос о доставке отвечает про Москву и длинным текстом. Память диалога из второго модуля здесь не помогает: она привязана к 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: состоянием, узлами и переходами, из которых собирается любой агент.

На следующий урок