JavaRush /Курсы /ChatGPT Apps /Память и состояние агента: session vs persistent, checkpo...

Память и состояние агента: session vs persistent, checkpoints

ChatGPT Apps
12 уровень , 2 лекция
Открыта

1. Зачем агенту вообще нужна отдельная память

Эта часть опирается на предыдущие уроки модуля 12 про агентов: там мы уже обсудили базовую архитектуру, run‑цикл и инструменты; здесь фокус на памяти и состоянии.

Если проводить аналогию с привычным веб‑приложением, LLM здесь — это очень умный CPU, который может исполнять сложные «текстовые программы». А состояние агента — это комбинация RAM и SSD: короткоживущие данные сессии и долгосрочное хранилище.

В классическом ChatGPT‑чате без вашего кода «память» — это просто список сообщений system/user/assistant/tool, который модель видит в текущем запросе. Для агента этого мало, потому что:

  • ему нужно помнить прогресс сложных процессов: какой шаг workflow уже пройден, какие варианты подарков уже отфильтрованы, что пользователь подтвердил;
  • ему нужно знать долговременные факты о пользователе: предпочтения, адрес доставки, историю прошлых заказов;
  • ему нужно уметь переживать сбои: если сервер упал посреди подбора подарка, пользователь не должен всё вводить заново.

Если пытаться всё это хранить только в prompt‑контексте, быстро упираешься в лимит окна контекста и платишь токенами за одни и те же факты. Параллельно вы рискуете безопасностью: слишком много лишних данных регулярно улетает в модель. Поэтому в агентных системах всегда есть явное состояние — объект(ы), которые живут вне истории сообщений и управляются уже вами.

2. Слои состояния агента: контекст, session, persistent

Начнём с разложения по слоям. У агента обычно есть минимум три уровня «памяти»:

  1. История сообщений (dialogue context).
  2. Сессионное состояние (session state).
  3. Долгоживущее состояние (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 ведёт многошаговый процесс:

  1. Собирает профиль получателя подарка.
  2. Генерирует список кандидатов.
  3. Фильтрует их по бюджету, доставке, региону.
  4. Готовит финальную подборку.

В процессе он постоянно общается с пользователем и вызывает инструменты. Всё, что относится к «прогрессу конкретной сессии подбора подарка», логично держать в 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‑цикла у нас есть короткая последовательность:

  1. Достаём session‑state по sessionId.
  2. При необходимости подгружаем relevant persistent‑данные из БД инструментами.
  3. Формируем контекст для модели (messages + структурированное состояние).
  4. Модель решает: отвечает текстом или вызывает инструменты.
  5. Инструменты обновляют либо session‑state, либо persistent‑данные (через БД).
  6. Сохраняем новое session‑состояние и, если нужно, создаём checkpoint (об этом дальше).
  7. Отдаём пользователю ответ.

Схема в 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,
});

При восстановлении мы:

  1. Находим последний чекпоинт по runId или userId.
  2. Восстанавливаем session.state из checkpoint.sessionState.
  3. При необходимости дотягиваем из БД свежие данные по 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 идей, пользователь выбирает одну, но говорит: «Я потом вернусь всё оформить».

Подумайте:

  1. Что вы положите в session‑state (который может умереть через 30 минут)?
  2. Что уйдёт в persistent‑хранилище, чтобы пользователь мог вернуться через неделю?
  3. Как бы выглядел 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, а уже инструменты знают, где именно лежат данные.

1
Задача
ChatGPT Apps, 12 уровень, 2 лекция
Недоступна
In-memory session state с TTL для “Survey Agent”
In-memory session state с TTL для “Survey Agent”
1
Задача
ChatGPT Apps, 12 уровень, 2 лекция
Недоступна
Persistent user profile как “долгая память”, доступная только через tools
Persistent user profile как “долгая память”, доступная только через tools
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ