1. Зачем агенту вообще нужна отдельная память
Эта часть опирается на предыдущие уроки модуля 12 про агентов: там мы уже обсудили базовую архитектуру, run‑цикл и инструменты; здесь фокус на памяти и состоянии.
Если проводить аналогию с привычным веб‑приложением, LLM здесь — это очень умный CPU, который может исполнять сложные «текстовые программы». А состояние агента — это комбинация RAM и SSD: короткоживущие данные сессии и долгосрочное хранилище.
В классическом ChatGPT‑чате без вашего кода «память» — это просто список сообщений system/user/assistant/tool, который модель видит в текущем запросе. Для агента этого мало, потому что:
- ему нужно помнить прогресс сложных процессов: какой шаг workflow уже пройден, какие варианты подарков уже отфильтрованы, что пользователь подтвердил;
- ему нужно знать долговременные факты о пользователе: предпочтения, адрес доставки, историю прошлых заказов;
- ему нужно уметь переживать сбои: если сервер упал посреди подбора подарка, пользователь не должен всё вводить заново.
Если пытаться всё это хранить только в prompt‑контексте, быстро упираешься в лимит окна контекста и платишь токенами за одни и те же факты. Параллельно вы рискуете безопасностью: слишком много лишних данных регулярно улетает в модель. Поэтому в агентных системах всегда есть явное состояние — объект(ы), которые живут вне истории сообщений и управляются уже вами.
2. Слои состояния агента: контекст, session, persistent
Начнём с разложения по слоям. У агента обычно есть минимум три уровня «памяти»:
- История сообщений (dialogue context).
- Сессионное состояние (session state).
- Долгоживущее состояние (persistent state).
Важно не сваливать эти понятия в одну кучу.
История сообщений: «грязная память»
История сообщений — это то, что видит LLM при каждом шаге: system‑инструкции, запросы пользователя, ответы агента, результаты инструментов.
Преимущество в том, что вам не нужно это руками вести — Agents SDK и сама платформа берут это на себя через сущность Session/Conversation.
Недостаток — это «грязная» память: там много лишних слов, повторов, случайных данных пользователя. Эти данные дороги в токенах и плохо структурированы. Вы не хотите, чтобы список уже отфильтрованных подарков в 200 элементов каждый раз зачитывался модели в виде plain‑текста.
Session state: рабочая краткосрочная память
Session‑состояние — это структурированный объект, живущий в рамках одной сессии/разговора агента. Его хорошая аналогия для фронтенд‑разработчика — useState или Redux‑store, который живёт, пока открыт таб.
Там живут вещи вроде:
- текущий шаг процесса (например, "collecting_profile" или "filtering_candidates");
- временный кэш результатов инструментов;
- параметры сессии: локаль, выбранный канал, флажки вроде «пользователь подтвердил условия».
Это состояние может храниться где‑то рядом с агентом — в Redis, в in‑memory KV‑хранилище или через встроенный SessionService конкретного SDK. Главное — не пытаться запихнуть это всё в system‑prompt.
Persistent state: долговременные данные
Persistent‑состояние живёт долго: между сессиями, чекаутами, устройствами. Это профиль пользователя, его заказы, сохранённые списки желаний, настройки.
Ключевая идея: агент не «помнит» persistent‑данные магически, он их «читает» через инструменты — например, get_user_profile, get_past_orders. Никаких скрытых глобальных переменных внутри агента; всегда явный вызов.
Сравнительная таблица
| Слой | Где живёт | Жизненный цикл | Примеры данных |
|---|---|---|---|
| Messages | Session / SDK / OpenAI | Один run / диалог | system/user/tool сообщения |
| Session state | KV / SessionService / Redis | Пока жива сессия | шаг workflow, временные кэши |
| Persistent | БД (Postgres/NoSQL/ACP backend) | Между сессиями и диалогами | профиль, заказы, сохранённые списки |
3. Session state: что это и как его хранить
Представьте, что агент GiftGenius ведёт многошаговый процесс:
- Собирает профиль получателя подарка.
- Генерирует список кандидатов.
- Фильтрует их по бюджету, доставке, региону.
- Готовит финальную подборку.
В процессе он постоянно общается с пользователем и вызывает инструменты. Всё, что относится к «прогрессу конкретной сессии подбора подарка», логично держать в session‑state.
Пример структуры сессионного состояния GiftGenius
Опишем тип сессионного состояния на TypeScript:
// Состояние в рамках одного "подбора подарка"
export type GiftSessionState = {
step:
| "collecting_profile"
| "generating_candidates"
| "filtering"
| "finalizing";
// черновик профиля получателя
profileDraft?: {
recipientType?: string;
ageRange?: string;
interests?: string[];
dislikes?: string[];
};
// id товаров-кандидатов, полученных от backend
candidateIds?: string[];
// выбранный пользователем подарок
selectedGiftId?: string;
// технические флажки
locale?: string;
};
Здесь мы сознательно не кладём целые объекты товаров — только их ID. Полные данные пусть живут в БД; когда нужно, агент вызывает инструмент get_gift_details(gift_id).
Session в Agents SDK (концептуально)
Во многих SDK для агентов есть абстракция сессии, которая сама берёт на себя хранение истории сообщений и позволяет вам дополнительно хранить structured‑state. В псевдо‑коде это может выглядеть примерно так:
import { createRunner, OpenAIConversationsSession } from "@openai/agents";
// тип GiftSessionState из примера выше
const session = new OpenAIConversationsSession<GiftSessionState>({
sessionId: "chatgpt-thread-id-or-random",
});
const runner = createRunner({ agent });
const result = await runner.run({
session,
input: "Хочу подарок коллеге до 50$",
});
SDK под капотом:
- достанет историю сообщений для этой сессии;
- добавит новое пользовательское сообщение;
- передаст модели и инструментарий;
- обновлённое состояние (включая session.state) сохранит обратно.
Вы же работаете с session.state как с обычным объектом.
Обновление session‑state из инструментов
Типичный паттерн: инструмент, который что‑то вычисляет, одновременно обновляет сессионное состояние. Например, инструмент, который собирает профиль получателя из ответов пользователя:
export async function updateProfileDraft(
session: GiftSessionState,
answers: { questionId: string; value: string }
): Promise<GiftSessionState> {
const next: GiftSessionState = { ...session };
if (!next.profileDraft) {
next.profileDraft = {};
}
if (answers.questionId === "interests") {
next.profileDraft.interests = answers.value.split(",").map((s) => s.trim());
}
// ...другие поля
next.step = "generating_candidates";
return next;
}
Здесь в инструмент мы передаём не всю Session из SDK, а только её state (тип GiftSessionState). В реальном коде имеет смысл называть такой аргумент, например, currentState, чтобы не путать его с объектом Session.
Агент вызывает этот инструмент, получает новый объект состояния и сохраняет его обратно в session.state.
4. Persistent state: долгосрочная память агента
Теперь вспомним, что GiftGenius работает не только в одном чате. Пользователь может вернуться через неделю, с другого устройства, и сказать: «Подбери подарок тому же другу, что и в прошлый раз, но бюджет увеличился».
Эта информация должна жить не в session‑state, а в persistent‑хранилище: в базе, commerce‑/ACP‑backend’е (commerce‑слое, о котором будет отдельный модуль) и т.п.
Пример persistent‑модели
Опишем модель профиля получателя в БД (упрощённо, как TypeScript‑тип):
// То, что хранится в БД
export type RecipientProfile = {
id: string;
userId: string;
label: string; // "коллега из маркетинга"
recipientType: string;
ageRange?: string;
interests: string[];
dislikes: string[];
lastUsedAt: string; // ISO-дата
};
И репозиторий (пусть пока будет простая Map — в реале вы бы сделали ORM/SQL‑слой):
const profiles = new Map<string, RecipientProfile>();
export const RecipientRepo = {
async findByUser(userId: string): Promise<RecipientProfile[]> {
return [...profiles.values()].filter((p) => p.userId === userId);
},
async save(profile: RecipientProfile): Promise<void> {
profiles.set(profile.id, profile);
},
};
Агент обращается к persistent через инструменты
Важно, чтобы агент не лез напрямую в БД, а работал через tools. Тогда он остаётся «чистой» сущностью: в одном месте — LLM и логика планирования, в другом — реализация интеграций.
Например, инструмент get_recipient_profiles:
export async function getRecipientProfilesTool(input: {
userId: string;
}): Promise<{ profiles: RecipientProfile[] }> {
const profiles = await RecipientRepo.findByUser(input.userId);
return {
profiles,
};
}
Агент в описании инструмента читает: «используй этот tool, чтобы получить сохранённые профили получателей для текущего пользователя». Он сам решает, когда именно его дернуть.
Итого: session‑state — это про прогресс конкретного разговора и временные кэши, которые можно безболезненно потерять. Persistent‑данные — это то, что должно переживать сессии и устройства: профили, заказы, списки желаний. Агент всегда читает их через инструменты, а не «магически помнит».
5. Как session и persistent работают вместе в run‑цикле
Теперь соберём всё в общую схему. На каждом шаге агентного run‑цикла у нас есть короткая последовательность:
- Достаём session‑state по sessionId.
- При необходимости подгружаем relevant persistent‑данные из БД инструментами.
- Формируем контекст для модели (messages + структурированное состояние).
- Модель решает: отвечает текстом или вызывает инструменты.
- Инструменты обновляют либо session‑state, либо persistent‑данные (через БД).
- Сохраняем новое session‑состояние и, если нужно, создаём checkpoint (об этом дальше).
- Отдаём пользователю ответ.
Схема в mermaid:
flowchart TD
A[Получить user input] --> B["Загрузить Session (state + messages)"]
B --> C{Нужны ли persistent-данные?}
C -- Да --> D[Вызвать tools: get_user_profile, get_recipient_profiles]
C -- Нет --> E[Сформировать контекст для LLM]
D --> E
E --> F["Вызвать модель (LLM)"]
F --> G{Модель хочет вызвать tool?}
G -- Да --> H[Выполнить tool, обновить session/persistent]
G -- Нет --> I[Подготовить финальный ответ]
H --> J[Создать checkpoint и сохранить Session]
I --> J
J --> K[Ответ пользователю]
Такой цикл делает поведение агента воспроизводимым: на каждом шаге мы явно знаем, какое было состояние до вызова модели и что поменялось после.
6. Checkpoints: снимки состояния агента
Checkpoints — это сохранённые «снимки состояния» агента на важном шаге процесса. Это не просто «текущий session‑state», а записанный во внешнее хранилище факт: на шаге N у нас был такой‑то state, такие‑то результаты инструментов, такой‑то пользовательский ввод.
Зачем они нужны:
- восстановление после ошибок и падений;
- возможность пользовательского «продолжить позже»;
- отладка: воспроизводимость проблемного run‑а;
- аудит: что именно агент сделал перед тем, как, например, создать заказ.
Что обычно входит в checkpoint
Типичный checkpoint содержит:
- идентификаторы: runId, userId, workflowId, stepId;
- сессионное состояние на этот момент;
- ключевые идентификаторы persistent‑сущностей (например, id черновика заказа);
- метаданные: время создания, версия агента.
Важно не тянуть туда весь текст диалога. Ниже, в разделе про гигиену памяти, мы ещё вернёмся к тому, что именно стоит сохранять, а что — нет.
Лучше хранить ссылку на Session или краткое резюме шагов.
7. Проектируем чекпоинты для GiftGenius
Возьмём наш процесс подбора подарка и решим, где хотим чекпоинты. Например:
- после того как собрали профиль получателя;
- после генерации и первичной фильтрации кандидатов;
- перед тем как предложить пользователю финальный выбор.
Типы для checkpoint и workflow‑состояния
Опишем состояние workflow (очень похоже на GiftSessionState, но это уже «слепок» для чекпоинтов):
export type GiftWorkflowStep =
| "profile_collected"
| "candidates_generated"
| "filtered"
| "final_choice_made";
export type GiftCheckpoint = {
id: string;
runId: string;
userId: string;
step: GiftWorkflowStep;
// часть session-состояния,
// которая нам нужна для восстановления
sessionState: GiftSessionState;
// какие id кандидатов были сгенерированы
candidateIds: string[];
createdAt: string; // ISO
agentVersion: string;
};
Хранилище чекпоинтов (упрощённо)
Сделаем, как и раньше, простую Map вместо настоящей БД:
const checkpoints = new Map<string, GiftCheckpoint>();
export const GiftCheckpointRepo = {
async save(cp: GiftCheckpoint) {
checkpoints.set(cp.id, cp);
},
async findByRun(runId: string): Promise<GiftCheckpoint[]> {
return [...checkpoints.values()].filter((c) => c.runId === runId);
},
async findLastByUser(userId: string): Promise<GiftCheckpoint | undefined> {
return [...checkpoints.values()]
.filter((c) => c.userId === userId)
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
},
};
Создание чекпоинта из кода агента
Представим helper, который мы вызываем после важного шага:
import { randomUUID } from "crypto";
export async function createCheckpoint(params: {
runId: string;
userId: string;
step: GiftWorkflowStep;
sessionState: GiftSessionState;
candidateIds: string[];
}) {
const checkpoint: GiftCheckpoint = {
id: randomUUID(),
runId: params.runId,
userId: params.userId,
step: params.step,
sessionState: params.sessionState,
candidateIds: params.candidateIds,
createdAt: new Date().toISOString(),
agentVersion: "v1.3.0",
};
await GiftCheckpointRepo.save(checkpoint);
}
Агент в нужный момент может вызвать:
await createCheckpoint({
runId,
userId,
step: "filtered",
sessionState,
candidateIds,
});
При восстановлении мы:
- Находим последний чекпоинт по runId или userId.
- Восстанавливаем session.state из checkpoint.sessionState.
- При необходимости дотягиваем из БД свежие данные по candidateIds.
8. Где хранить session, persistent и checkpoints технически
На уровне инфраструктуры у вас обычно три разных класса хранилищ:
- In‑memory — для dev/demos, быстрая, но временная память.
- Redis (или другой KV‑store) — для session‑состояния.
- Реляционная/NoSQL БД — для persistent‑данных и чекпоинтов.
In‑memory store для локальной разработки
Для локального дев режима вполне достаточно простого in‑memory store. Например, мини‑хранилище с TTL для сессий:
type StoredSession<T> = {
state: T;
expiresAt: number;
};
const sessions = new Map<string, StoredSession<GiftSessionState>>();
export function saveSession(sessionId: string, state: GiftSessionState) {
sessions.set(sessionId, {
state,
expiresAt: Date.now() + 30 * 60 * 1000, // 30 минут
});
}
export function loadSession(sessionId: string): GiftSessionState | undefined {
const stored = sessions.get(sessionId);
if (!stored) return undefined;
if (stored.expiresAt < Date.now()) {
sessions.delete(sessionId);
return undefined;
}
return stored.state;
}
Такая штука отлично подходит для local dev, но в проде при горизонтальном масштабировании (несколько инстансов) работать уже не будет.
Redis для session‑state
В продакшене session‑состояние удобно хранить в Redis:
- быстрая запись/чтение;
- TTL «из коробки»;
- доступно всем инстансам сервиса.
Псевдо‑пример (упрощённо):
// Обёртка вокруг Redis клиента
export async function saveSessionToRedis(
sessionId: string,
state: GiftSessionState
) {
const json = JSON.stringify(state);
await redis.set(`session:${sessionId}`, json, "EX", 60 * 30); // 30 минут
}
export async function loadSessionFromRedis(
sessionId: string
): Promise<GiftSessionState | undefined> {
const json = await redis.get(`session:${sessionId}`);
return json ? (JSON.parse(json) as GiftSessionState) : undefined;
}
Postgres/другая БД для persistent и чекпоинтов
Persistent‑состояние и чекпоинты — это уже «серьёзные» сущности, для которых важны транзакции, миграции, индексы и прочие радости жизни. Их размещают в Postgres, MySQL, Firestore и т.п.
Архитектурный паттерн здесь простой:
- session в Redis с TTL;
- persistent и checkpoints в БД без TTL (или с бизнес‑зависимой retention‑политикой).
9. Гигиена памяти: размеры, приватность, разделение ответственности
Память агента — это не просто «куда‑нибудь положим объект и поехали». Есть несколько важных правил, которые экономят деньги и сохраняют вам сон.
Не пихать всё в messages
История сообщений — дорогой ресурс:
- её длина сильно влияет на стоимость запроса к модели;
- там, как правило, много «шума».
Поэтому:
- старайтесь вытаскивать из истории факты в структурированное состояние как можно раньше;
- используйте summarization для старых частей истории;
- если храните в checkpoint’ах текстовую историю, делайте это отдельно от того, что отправляется модели.
Privacy и PII
Особенно для commerce‑сценариев важно не хранить чувствительные данные в местах, куда они не должны попадать. Документы по архитектуре памяти прямо подчёркивают, что PII‑данные не стоит держать в messages или чекпоинтах без очистки.
Практические правила:
- не кладите email/телефон/адрес напрямую в session‑state, если это не нужно для работы агента;
- в логи и чекпоинты старайтесь писать идентификаторы (userId, recipientProfileId) вместо сырых строк;
- если нужно протянуть PII через несколько шагов — используйте отдельные защищённые поля в persistent‑хранилище, а в state передавайте только ключ.
Разделение бизнес‑данных и лога диалога
Хороший паттерн — считать state «чистой» памятью, а messages — «грязной».
То есть:
- бизнес‑сущности (профили, заказы, корзины) всё время живут в БД;
- state/чекпоинты содержат минимум, необходимый для восстановления процесса;
- логи/история чата хранятся отдельно (например, во векторном хранилище) и используются для аналитики, но не подмешиваются к каждому запросу модели.
10. Мини‑практика: что бы вы сохранили?
Чтобы закрепить разницу между слоями памяти, давайте на секунду оторвёмся от теории и подумаем на конкретном кейсе. Не обязательно писать код — достаточно на бумаге или в голове прикинуть структуры.
Представьте, что ваш агент GiftGenius вёл с пользователем следующий диалог:
- Пользователь: «Нужен подарок коллеге‑разработчику, бюджет до 50$, он любит настольные игры и кофеин».
- Агент: задаёт пару уточняющих вопросов.
- Пользователь: «Он ненавидит кружки и уже завален блокнотами».
- Агент: генерирует список из 10 идей, пользователь выбирает одну, но говорит: «Я потом вернусь всё оформить».
Подумайте:
- Что вы положите в session‑state (который может умереть через 30 минут)?
- Что уйдёт в persistent‑хранилище, чтобы пользователь мог вернуться через неделю?
- Как бы выглядел checkpoint после выбора идеи, но до оформления заказа?
Попробуйте набросать соответствующие TypeScript‑типы и функции saveSessionState, savePersistentState, createGiftIdeaCheckpoint по аналогии с примерами в этой лекции. Если хочется, можете прямо набросать эти типы и функции в редакторе по аналогии с примерами выше — это будет хорошим мини‑чекпоинтом перед следующей лекцией.
11. Типичные ошибки при работе с памятью агента
Ошибка №1: пытаться хранить всё только в истории сообщений.
Разработчик радуется: «Ну модель же и так видит весь диалог, зачем придумывать ещё какой‑то state?». В результате через несколько десятков сообщений окно контекста забивается мусором, токены стоят как новый MacBook, а поведение агента становится нестабильным — он просто не видит важные старые факты. Эту проблему нужно решать явным выделением session‑state и persistent‑хранилища, а не увеличением лимитов.
Ошибка №2: смешивать session и persistent в один объект.
Иногда соблазнительно завести одну «большую» сущность AgentState, положить в неё всё подряд и сохранять «как есть» в базу. Тогда размывается граница между временными данными конкретного разговора и долгосрочными данными пользователя. Начинаются истории вида «после деплоя все сессии загадочно восстановились из прошлогодних данных» или «сессия одного пользователя случайно подхватила чужой persistent‑профиль». Разделяйте уровни сознательно.
Ошибка №3: хранить в чекпоинтах слишком много.
Частая ошибка — записывать в checkpoint весь JSON ответа инструментов, всю историю диалога, сырые данные интеграций и прочее. После пары недель эксплуатации база чекпоинтов раздувается до неприличных размеров, резервные копии идут по часу, а запросы к БД тормозят. В чекпоинте должны жить только те факты, которые реально нужны, чтобы продолжить процесс, плюс минимум метаданных.
Ошибка №4: забывать про TTL и очистку session‑state.
Если session‑состояния не имеют срока жизни, то любой случайный эксперимент пользователя в Dev Mode остаётся в Redis навечно. Через пару месяцев вы смотрите на мониторинг и видите гору «забытых» сессий, отжирающих память. Session‑уровень нужно проектировать с явным TTL, а persistent‑уровень — с продуманной политикой retention.
Ошибка №5: хранить PII в state и чекпоинтах без нужды.
Особенно опасно, когда в session‑state бездумно кладут email, адрес, номер карты, а затем этот объект сериализуется в логи, ползёт в аналитику и в checkpoint’ы. Это создаёт серьёзные риски с точки зрения регуляторики и безопасности. Лучше хранить безопасные идентификаторы и при необходимости резолвить их в реальные данные через отдельные защищённые инструменты.
Ошибка №6: отсутствие стратегии восстановления из чекпоинтов.
Некоторые команды честно записывают чекпоинты, но при этом не продумывают, как именно агент должен из них восстанавливаться. В итоге, когда «что‑то пошло не так», разработчики смотрят в таблицу с красивыми JSON’ами, но не имеют кода, который умеет по ним реконструировать run. Чекпоинтинг без сценария восстановления — это просто дорогой лог, а не инструмент надёжности.
Ошибка №7: жёсткая привязка агента к конкретной реализации хранилища.
Если код агента напрямую ходит в Redis/Postgres, его сложнее переносить, тестировать и развивать. При смене архитектуры (например, появлении MCP‑ресурсов или отдельного state‑сервиса) придётся перелопачивать агентную логику. Гораздо лучше, когда агент видит только абстракции Session и набор tools, а уже инструменты знают, где именно лежат данные.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ