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 позицій щоразу зчитувався моделлю як звичайний текст.
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-бекенд) | Між сесіями та діалогами | профіль, замовлення, збережені списки |
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 для агентів є абстракція сесії: вона сама бере на себе збереження історії повідомлень і дає змогу додатково тримати структурований стан. У псевдокоді це може виглядати приблизно так:
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-бекенді (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.
- За потреби підтягуємо релевантні persistent-дані з БД через інструменти.
- Формуємо контекст для моделі (messages + структурований стан).
- Модель вирішує: відповісти текстом чи викликати інструменти.
- Інструменти оновлюють або session-state, або persistent-дані (через БД).
- Зберігаємо новий session-стан і, якщо потрібно, створюємо checkpoint (про це далі).
- Віддаємо користувачеві відповідь.
Схема в mermaid:
flowchart TD
A[Отримати введення користувача] --> 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, такі-то результати інструментів, такий-то користувацький ввід.
Навіщо вони потрібні:
- відновлення після помилок і падінь;
- можливість для користувача «продовжити пізніше»;
- налагодження: відтворюваність проблемного запуску;
- аудит: що саме агент зробив перед тим, як, наприклад, створити замовлення.
Що зазвичай входить до 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/демо: швидка, але тимчасова памʼять.
- Redis (або інший KV-store) — для session-стану.
- Реляційна/NoSQL БД — для persistent-даних і чекпоінтів.
In-memory store для локальної розробки
Для локального dev-режиму цілком достатньо простого 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;
}
Така штука чудово підходить для локальної розробки, але в продакшені, за горизонтального масштабування (кілька екземплярів сервісу), уже не працюватиме.
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 (або з бізнес-залежною політикою зберігання).
9. Гігієна памʼяті: розміри, приватність, розділення відповідальності
Памʼять агента — це не просто «кудись покладемо обʼєкт і поїхали». Є кілька важливих правил, які економлять гроші й бережуть вам сон.
Не класти все до messages
Історія повідомлень — дорогий ресурс:
- її довжина сильно впливає на вартість запиту до моделі;
- у ній, як правило, багато «шуму».
Тому:
- намагайтеся витягати з історії факти в структурований стан якомога раніше;
- використовуйте узагальнення (summarization) для старих частин історії;
- якщо зберігаєте в чекпоінтах текстову історію, робіть це окремо від того, що надсилається моделі.
Конфіденційність і PII
Особливо для commerce-сценаріїв важливо не зберігати чутливі дані в місцях, куди вони не мають потрапляти. Документи з архітектури памʼяті прямо підкреслюють, що PII-дані не варто тримати в messages або чекпоінтах без очищення.
Практичні правила:
- не кладіть електронну пошту/телефон/адресу напряму в 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-режимі лишається в Redis назавжди. За кілька місяців ви дивитеся на моніторинг і бачите гору «забутих» сесій, що відʼїдають памʼять. Session-рівень потрібно проєктувати з явним TTL, а persistent-рівень — із продуманою політикою зберігання (retention).
Помилка № 5: зберігати PII у state і чекпоінтах без потреби.
Особливо небезпечно, коли в session-state бездумно кладуть електронну пошту, адресу, номер картки, а потім цей обʼєкт серіалізується в логи, «повзе» в аналітику й у чекпоінти. Це створює серйозні ризики з погляду регуляторики та безпеки. Краще зберігати безпечні ідентифікатори й за потреби резолвити їх у реальні дані через окремі захищені інструменти.
Помилка № 6: відсутність стратегії відновлення з чекпоінтів.
Деякі команди чесно записують чекпоінти, але водночас не продумують, як саме агент має з них відновлюватися. Урешті, коли «щось пішло не так», розробники дивляться в таблицю з красивими JSONʼами, але не мають коду, який уміє за ними реконструювати запуск. Чекпоінтинг без сценарію відновлення — це просто дорогий лог, а не інструмент надійності.
Помилка № 7: жорстка привʼязка агента до конкретної реалізації сховища.
Якщо код агента напряму звертається до Redis/Postgres, його складніше переносити, тестувати й розвивати. Під час зміни архітектури (наприклад, появи MCP-ресурсів або окремого state-сервісу) доведеться серйозно переробляти агентну логіку. Набагато краще, коли агент бачить лише абстракції Session і набір tools, а вже інструменти знають, де саме лежать дані.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ