Практическое руководство
Память AI-агента: пять причин, почему контекст теряется
Если агент забывает договорённости после перезапуска или неожиданно вспоминает старые данные, проблема обычно находится не в модели, а в пути данных: что записывается, под каким ключом, когда извлекается и как попадает в запрос.
Что именно называют памятью агента
Контекстное окно — это ограниченный объём данных, который модель получает в одном вызове. Оно не является постоянной памятью. После завершения запроса модель сама по себе не сохраняет разговор.
Рабочая память агента обычно состоит из нескольких слоёв:
- текущая история сообщений;
- краткое резюме прошлых шагов;
- постоянное хранилище фактов и предпочтений;
- механизм поиска нужных записей;
- код, который собирает всё это в новый запрос.
Поэтому «агент забыл» и «агент получил неверный контекст» — разные сбои. Сначала определите, исчезла ли запись из хранилища или она сохранилась, но не дошла до модели.
Минимальный воспроизводимый стенд
Ниже приведён пример, а не описание конкретного фреймворка. Он использует SQLite из стандартной библиотеки Python и позволяет проверить постоянство данных без внешних сервисов и секретов.
1. Создайте отдельный каталог
mkdir -p agent-memory-check
cd agent-memory-check
2. Сохраните диагностический скрипт
Создайте файл memory_check.py со следующим содержимым:
import json
import sqlite3
import sys
from pathlib import Path
from datetime import datetime, timezone
DB_PATH = Path(__file__).with_name("memory.db")
def connect():
db = sqlite3.connect(DB_PATH)
db.execute("""
CREATE TABLE IF NOT EXISTS memory (
namespace TEXT NOT NULL,
subject_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TEXT NOT NULL,
expires_at TEXT,
PRIMARY KEY (namespace, subject_id, key)
)
""")
return db
def put(namespace, subject_id, key, value):
now = datetime.now(timezone.utc).isoformat()
payload = json.dumps(value, ensure_ascii=False)
with connect() as db:
db.execute("""
INSERT INTO memory
(namespace, subject_id, key, value, updated_at, expires_at)
VALUES (?, ?, ?, ?, ?, NULL)
ON CONFLICT(namespace, subject_id, key)
DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
""", (namespace, subject_id, key, payload, now))
def get(namespace, subject_id):
with connect() as db:
rows = db.execute("""
SELECT key, value, updated_at
FROM memory
WHERE namespace = ? AND subject_id = ?
ORDER BY updated_at DESC
""", (namespace, subject_id)).fetchall()
return [
{"key": key, "value": json.loads(value), "updated_at": updated_at}
for key, value, updated_at in rows
]
if len(sys.argv) < 4:
raise SystemExit(
"Использование: put NAMESPACE SUBJECT KEY VALUE "
"или get NAMESPACE SUBJECT"
)
command, namespace, subject_id = sys.argv[1:4]
if command == "put":
if len(sys.argv) != 6:
raise SystemExit("Для put нужны KEY и VALUE")
put(namespace, subject_id, sys.argv[4], sys.argv[5])
print("saved")
elif command == "get":
print(json.dumps(get(namespace, subject_id), ensure_ascii=False, indent=2))
else:
raise SystemExit("Неизвестная команда")
3. Запишите контрольный факт
python3 memory_check.py put production user-42 language ru
4. Завершите процесс и прочитайте запись заново
python3 memory_check.py get production user-42
Ожидаемый результат примера — массив с ключом language и значением ru. Если после нового запуска запись отсутствует, сбой находится на уровне сохранения, пути к базе или идентификаторов, а не генерации ответа.
Пять причин потери или загрязнения контекста
1. Память хранится только внутри процесса
Словарь, массив сообщений или встроенный кеш исчезают при перезапуске приложения. В контейнерной среде так же исчезает файл, записанный в непостоянный слой файловой системы.
Как распознать: данные доступны во время одного запуска, но пропадают после рестарта или развёртывания.
Что проверить:
- используется ли постоянная база, подключённый том или внешний сервис хранения;
- является ли путь к файлу абсолютным и одинаковым при чтении и записи;
- не создаётся ли новая база в текущем рабочем каталоге.
Исправление: вынесите долговременные записи из оперативной памяти процесса. Проверяйте доступность хранилища при старте и не сообщайте об успешной записи до завершения транзакции.
2. Запись и чтение используют разные идентификаторы
Контекст может сохраниться под session_id, а извлекаться под user_id. Другой частый случай — случайный идентификатор создаётся заново при каждом запуске.
Как распознать: запись видна при прямом запросе к базе, но поиск агента возвращает пустой результат.
Что проверить: журналируйте без персональных данных четыре поля: namespace, тип субъекта, псевдонимизированный идентификатор и ключ записи. Сравните их на этапах записи и чтения.
Исправление: задайте единую схему ключа, например:
memory_key = "{environment}:{tenant_id}:{subject_type}:{subject_id}"
Не смешивайте среду, организацию, пользователя и сессию в одном неописанном поле. Если идентификатор чувствителен, используйте стабильный HMAC на стороне приложения; обычный хеш предсказуемых значений не обеспечивает достаточной защиты.
3. История не помещается в контекстное окно
Даже сохранённая история может быть обрезана сборщиком запроса. Часто первыми исчезают старые сообщения, системные договорённости или результат важного инструмента.
Как распознать: короткий диалог работает, а после серии больших сообщений агент забывает ранний факт. В хранилище факт остаётся.
Что проверить: измеряйте размер каждого блока до отправки модели: системные инструкции, текущий запрос, найденные воспоминания, история и результаты инструментов. Не полагайтесь только на число сообщений.
Исправление: установите явный бюджет контекста. Сначала резервируйте место под инструкции и текущую задачу, затем добавляйте проверенные факты, резюме и только потом сырую историю. Резюме должно содержать ссылку на исходные записи или их идентификаторы, чтобы его можно было проверить.
4. Конкурентные записи перезаписывают друг друга
Два параллельных шага могут прочитать одну версию памяти, независимо изменить её и сохранить целиком. Последняя запись уничтожит изменения первой.
Как распознать: проблема возникает нерегулярно, чаще при параллельных инструментах или повторной доставке задания. В журнале несколько обновлений одного ключа имеют почти одинаковое время.
Что проверить: добавьте к записи номер версии и идентификатор операции. Повторите два обновления одного ключа параллельно в тестовой среде и проверьте, сохранились ли оба изменения.
Исправление: применяйте транзакции, оптимистическую блокировку или атомарные операции. Для важных событий безопаснее журнал добавлений, чем постоянная перезапись одного большого JSON-документа.
UPDATE memory
SET value = :value, version = version + 1
WHERE id = :id AND version = :expected_version;
Если обновлено ноль строк, перечитайте актуальную версию и разрешите конфликт явно.
5. Старые или нерелевантные данные проходят без фильтра
Постоянство само по себе не делает память полезной. Старый адрес, завершённая задача или факт от другого проекта могут попасть в текущий запрос и выглядеть для модели как актуальные.
Как распознать: агент уверенно использует сохранённый факт, хотя пользователь уже исправил его или сменил рабочую область.
Что проверить: у каждой записи должны быть происхождение, время обновления, область действия, версия схемы и при необходимости срок действия. Посмотрите, учитывает ли извлечение эти поля.
Исправление: отделяйте факты от предположений и эпизодов. Новое явное утверждение пользователя должно либо заменить прежнее значение, либо пометить его неактуальным. Для изменчивых сведений задавайте expires_at; удаление или продление выполняйте по правилам продукта, а не по догадке модели.
Диагностика по слоям
- Зафиксируйте контрольный факт. Используйте искусственное безопасное значение, например
diagnostic-color=amber, а не реальное имя, токен или адрес. - Проверьте подтверждение записи. Запись считается успешной только после фиксации транзакции.
- Перезапустите процесс. Прочитайте факт напрямую из хранилища тем же ключом.
- Проверьте извлечение. Запустите тот же запрос через компонент поиска памяти и сохраните только техническую трассировку: идентификаторы записей, оценки и причины фильтрации.
- Проверьте сборку запроса. Убедитесь, что найденная запись действительно вошла в итоговый контекст и не была обрезана.
- Проверьте ответ отдельно. Если факт присутствует в запросе, но не используется, это уже вопрос инструкций, конфликтов в контексте или поведения модели.
Такой порядок локализует неисправность без отправки содержимого памяти в логи.
Минимальная схема устойчивой памяти
Конкретные поля зависят от приложения, но полезный базовый контракт выглядит так:
{
"namespace": "production",
"subject_type": "user",
"subject_id": "stable-pseudonymous-id",
"key": "preferred_language",
"value": "ru",
"source": "explicit_user_statement",
"created_at": "ISO-8601 timestamp",
"updated_at": "ISO-8601 timestamp",
"expires_at": null,
"schema_version": 1,
"record_version": 3
}
Это пример конфигурации. Метки времени должно формировать приложение или база в UTC. Значение source помогает не выдавать вывод модели за подтверждённый пользовательский факт.
Проверка результата
После исправления выполните четыре проверки:
- Запишите контрольный факт, перезапустите агент и убедитесь, что факт найден.
- Запустите новую сессию того же субъекта: долговременный факт должен сохраниться, временная история — нет, если так задан контракт.
- Запросите память другого тестового субъекта: контрольный факт не должен пересечь границу.
- Обновите факт и убедитесь, что старая версия не попадает в новый контекст как актуальная.
Успех — это не только правильный ответ модели. Трассировка должна показывать одну актуальную запись, правильную область действия и предсказуемое место в бюджете контекста.
Типовые ошибки
- Сохранять весь диалог без отбора. Это увеличивает стоимость, риск утечки и количество противоречий.
- Считать векторный поиск источником истины. Близость текста показывает релевантность, но не актуальность и не достоверность.
- Записывать вывод модели как факт. Непроверенное резюме должно оставаться производной записью с указанием происхождения.
- Логировать полное содержимое памяти. Для диагностики обычно достаточно технических идентификаторов, версий и статусов.
- Использовать один глобальный namespace. Это создаёт риск смешения сред, организаций и пользователей.
- Молча продолжать при недоступной базе. Агент начинает работать с пустым контекстом, а оператор принимает это за забывчивость модели.
Ограничения
Эта методика диагностирует путь данных, но не определяет продуктовую политику хранения. Сроки хранения, право на удаление, перечень допустимых данных и аудит доступа нужно проектировать отдельно с учётом применимых требований.
SQLite подходит для локального воспроизведения, но приведённый пример не является готовым многопользовательским хранилищем. В распределённой системе дополнительно потребуются управление соединениями, резервное копирование, контроль доступа, миграции схемы и стратегия разрешения конфликтов.
Наконец, наличие факта в контексте не гарантирует буквального использования в ответе. Модель может встретить противоречащие инструкции или нерелевантные данные. Поэтому проверяйте отдельно сохранение, извлечение, сборку запроса и генерацию.
Короткий итог
Потеря памяти агента обычно сводится к одной из пяти причин: данные живут только внутри процесса, ключи записи и чтения различаются, история обрезается, параллельные обновления конфликтуют или устаревшие записи извлекаются без фильтра. Надёжная память требует стабильной идентичности, постоянного хранилища, версий, происхождения, срока действия и наблюдаемого конвейера сборки контекста.
Другие практические материалы собраны в руководствах Agent Lab Journal, а определения терминов — в глоссарии.