Контекст - это бюджет, а не ведро: архитектура памяти агента, которая выживает в проде
Большинство AI-агентов ломаются не из-за плохих промптов, а из-за того, что контекст растёт бесконтрольно, а счёт за API улетает в космос. Разбираю, как я бюджетирую, сжимаю и храню память агента в реальных проектах.
Pavel Duglas
AI Automation & MVP Architect
Каждый агент, который я выкатывал в прод и который потом ломался, ломался одинаково. Не галлюцинация, не кривой tool call - контекстное окно тихо забивалось мусором, модель начинала игнорировать инструкции, а в конце месяца приходил счёт, похожий на смету на сервер. Про качество промптов говорят все. А работает или нет ваш агент на 40-м шаге так же, как на 3-м, решает архитектура контекста.
Эта часть работы с агентами по ощущениям похожа на бэкенд - потому что это и есть бэкенд. Вы проектируете иерархию памяти под жёстким лимитом байтов, где у каждого байта есть цена. Покажу, как я это делаю на практике.
Тот самый режим отказа, который никто не считает
Наивный цикл агента дописывает в контекст всё: сообщение пользователя, рассуждения модели, каждый вызов инструмента, каждый результат. И на следующем шаге отправляет весь этот ком заново. Стоимость шага растёт линейно, а суммарная стоимость задачи из N шагов - квадратично. Это простая математика, но её почему-то обнаруживают только по инвойсу.
Реальные цифры из агента для обогащения данных, который я рефакторил в прошлом году:
- System prompt + схемы инструментов: ~6k токенов
- Средний результат инструмента (обрезанный HTML-фетч): ~4k токенов
- Шагов на задачу: 25–40
Наивный цикл, 35 шагов: последний вызов тащит ~145k токенов, а сумма по задаче - около 2.6M входных токенов. После рефакторинга - та же модель, тот же success rate - 310k входных токенов. Восьмикратная экономия без единого хитрого приёма в prompt engineering. И точность при этом выросла, потому что модель перестала тонуть в протухшем HTML с четвёртого шага.
Вот эту часть недооценивают чаще всего. Длинный контекст не просто дорогой - он вредный. Инструкции в промпте на 6k токенов выполняются. Те же самые инструкции, погребённые под 140k токенов шума от инструментов, выполняются «примерно». Модель начинает деградировать от нерелевантного контекста задолго до того, как упрётся в лимит окна.
Четыре вида памяти с разным временем жизни
Перестаньте думать про «контекст» как про одну сущность. Думайте про четыре хранилища с разными правилами попадания в промпт.
1. Константная память - system prompt, роль, жёсткие правила, схемы инструментов, формат вывода. Пишется один раз, внутри рана не мутирует. Идёт в каждый вызов.
2. Рабочая память - состояние текущей задачи: цель, план, что сделано, что осталось, найденные ключевые факты. Маленькая, структурированная, переписывается на каждом шаге. Идёт в каждый вызов.
3. Эпизодическая память - сырой транскрипт: сообщения, вызовы, результаты. Растёт бесконечно. Не идёт в каждый вызов. Живёт в базе или на диске, доступна по ID.
4. Внешняя память - долговечные артефакты: файлы, строки в БД, спарсенные страницы, сгенерированные отчёты, профиль пользователя. Достаётся по запросу, никогда не пушится сама.
Вся дисциплина сводится к одному: всегда присутствуют только 1 и 2. Всё остальное попадает в промпт через явное, забюджетированное извлечение по требованию.
Каждому слоту - свой бюджет байтов
Я выписываю бюджет до того, как пишу цикл. Дефолт для среднего агента на модели с окном 200k:
| Слот | Бюджет | Поведение при переполнении |
|---|---|---|
| System + правила | 3k | Hard fail - переписывать промпт |
| Схемы инструментов | 4k | Прунить инструменты по фазе |
| Рабочее состояние (JSON) | 2k | Сжать самые старые поля |
| Последние N результатов | 8k | Обрезать + сохранить на диск |
| Извлечённые документы | 10k | Выкинуть чанки с низким скором |
| Хвост диалога | 6k | Свернуть в рабочее состояние |
| Итого на вызов | ~33k | Kill switch на 60k |
Kill switch тут важнее самого бюджета. Если один вызов пробивает жёсткий потолок - ран падает и пишет диагностический дамп. Это превращает молчаливые 400 долларов за выходные в алерт в Slack в пятницу днём. У меня агент-скрапер однажды залип в цикле fetch-retry; потолок отловил это за 90 секунд.
Отдельно про прунинг схем. Агент с 18 инструментами сжигает 6–8k токенов на определения в каждом вызове, и качество выбора инструмента при этом падает. Разбейте ран на фазы (research → decide → write) и показывайте 4–6 инструментов на фазу. Дешевле и точнее одновременно.
Сжимать в структуру, а не в пересказ диалога
Самый популярный подход к компакции - «суммаризировать диалог, когда он разросся». Он теряет ровно то, что терять нельзя: сохраняет общее настроение и выбрасывает идентификаторы. Я видел саммари, где осталось «пользователь спрашивал про тарифы», а account ID, нужный агенту через три шага, испарился.
Вместо этого держите один явный объект состояния, который агент переписывает (а лучше - который вы детерминированно обновляете после каждого шага):
{
"goal": "Обогатить 500 компаний: email основателя + headcount",
"phase": "enrichment",
"progress": { "done": 312, "failed": 9, "remaining": 179 },
"decisions": [
"LinkedIn заблокировал на шаге 14 -> ушли на Apollo fallback",
"Headcount из Crunchbase приоритетнее футера сайта"
],
"open_questions": [],
"artifacts": [
{ "id": "a_71", "kind": "csv", "path": "/run/out/enriched_p1.csv", "rows": 312 }
],
"next_action": "продолжить батч с offset 312"
}
Это ~400 токенов, и они заменяют 60k транскрипта. Модель читает и понимает, где она находится. И главное - объект переживает рестарт процесса: это ваша точка возобновления. Пишите его в Postgres или в JSON-файл после каждого шага. Агент, который не умеет продолжить после падения, - не продакшн-система, а демо.
Моё правило: транскрипт - это лог, состояние - это память. Логи для людей, которые дебажат. Состояние - для модели.
Возвращайте хендлы, а не payload
80% раздувания контекста живёт в выводе инструментов. Инструмент, возвращающий 40k символов HTML или результат запроса на 2000 строк, - это баг проектирования, а не особенность.
Что должны возвращать инструменты:
ПЛОХО: fetch_page(url) -> "<html>...38 000 символов...</html>"
ХОРОШО: fetch_page(url) -> { "artifact": "pg_44",
"path": "/run/cache/pg_44.html",
"bytes": 38214,
"title": "Pricing - Acme",
"preview": "первые 600 символов..." }
А дальше дайте агенту узкие «читалки» по артефактам: grep_artifact(id, pattern), read_lines(id, start, end), extract_fields(id, schema). Модель вытащит нужные 200 токенов вместо того, чтобы платить за 10k ненужных. Именно так работает нормальный инженер с большим файлом - никто не читает лог целиком, все его грепают.
То же с SQL: возвращайте количество строк, первые 5 строк и путь к полному CSV. То же с API: срезайте конверты ответа, выкидывайте null, выкидывайте поля, которых нет в вашей схеме. У меня есть хелпер slim(), который проецирует любой ответ API на белый список полей до того, как он коснётся промпта. Стабильно режет payload на 70%.
Порядок под prefix-кэш
Все крупные провайдеры кэшируют повторяющиеся префиксы и берут за попадание в кэш долю цены. Это скидка в 5–10 раз на самый большой и самый стабильный блок - но только если префикс совпадает побайтово.
Отсюда правило: стабильное вперёд, изменчивое в хвост.
- System prompt и правила (не меняются никогда)
- Схемы инструментов (стабильны внутри фазы)
- Долгоживущий референс (style guide, конфиг клиента)
- Рабочее состояние (меняется каждый шаг)
- Свежие результаты инструментов (меняются каждый шаг)
И перестаньте писать Current time: 2026-02-11T14:03:22Z в начале system prompt. Один изменчивый токен на 12-й позиции инвалидирует кэш для всего, что идёт после. Нужно агенту время - дайте ему get_time() или подставьте время в изменчивый хвост. Я видел, как одно это изменение срезало клиенту счёт больше чем вдвое.
Без метрик вы просто угадываете
На каждый шаг логируйте: step_index, input_tokens, cached_tokens, output_tokens, tool_name, tool_result_bytes, state_bytes, latency_ms. На ран: суммарную стоимость, число шагов, пик контекста, исход.
Метрика, на которую я реально смотрю, - стоимость завершённой задачи, а не стоимость токена. Модель в 3 раза дороже за токен, но закрывающая задачу за 6 шагов вместо 30, дешевле. Дешёвые модели, которые ходят по кругу, - самая дорогая штука в AI-автоматизации.
Вторая метрика - cache hit ratio. Ниже 60% на многошаговом агенте означает, что префикс нестабилен: идите искать таймстемп.
Третья - пик контекста за ран, построенный по времени. Если он ползёт вверх неделя к неделе, значит какой-то инструмент начал возвращать больше данных, чем раньше. Это ваш ранний сигнал - раньше, чем инвойс.
Цикл, примерно так
state = load_state(run_id) or init_state(goal)
while not state["done"] and state["step"] < MAX_STEPS:
ctx = build_context(
constant=SYSTEM, # стабильный префикс
tools=tools_for_phase(state["phase"]), # 4-6 инструментов
state=state, # ~400 токенов
tail=recent_results(limit=3, max_tokens=8000),
)
assert count_tokens(ctx) < HARD_CEILING
step = model.call(ctx)
result = run_tool(step.tool, step.args) # возвращает хендл
store_artifact(result)
state = update_state(state, step, result) # где можно - детерминированно
save_state(run_id, state) # crash-safe
Обратите внимание: update_state детерминирован там, где это возможно. Счётчики, список артефактов, переходы между фазами - это Python, а не вывод LLM. От модели приходит только нечёткое: решения, открытые вопросы. Каждое поле, которое вы можете посчитать в коде, - это поле, которое модель не сможет испортить.
Чего я не делаю
- Не тащу векторную БД по умолчанию. Для большинства агентов объект состояния + grep по файлам + один SQL-запрос бьют семантический поиск. Эмбеддинги нужны, когда у вас тысячи документов и реальная проблема recall, а не потому что «так принято в архитектуре».
- Не беру memory-фреймворки с мнением. Два таких я заменил на ~200 строк кода состояния и артефактов - и получил лучшую наблюдаемость.
- Не храню бесконечную историю диалога. HTML с четвёртого шага не нужен на сороковом. А если внезапно нужен - для этого и есть хранилище артефактов.
Context engineering - неблагодарная работа: считать байты, проецировать поля, следить за кэшем. Но именно она отделяет демо агента от агента, который крутит 500 задач в сутки по цене, которую можно спокойно вписать в коммерческое предложение.
Вопросы и ответы
С чего начать, если у меня уже есть работающий агент и я хочу срезать расходы?
Сначала измерьте: залогируйте на каждом шаге input_tokens, cached_tokens и tool_result_bytes. Почти всегда 70–80% раздувания приходится на 1–2 инструмента, возвращающие сырой HTML или большие JSON-ответы. Переведите их на возврат хендла (id артефакта + путь + preview) и добавьте grep/read_lines. Второй шаг - вынести таймстемпы и любые изменчивые данные из начала system prompt, чтобы поднять cache hit ratio. Эти два изменения обычно дают основной эффект без переписывания логики.
Нужна ли векторная база для памяти агента?
В большинстве проектов - нет, по крайней мере не на старте. Если у агента есть структурированный объект состояния, хранилище артефактов на диске и возможность грепать файлы или сделать SQL-запрос, этого хватает для задач с десятками-сотнями документов. Эмбеддинги стоит добавлять, когда у вас реально тысячи документов и вы упираетесь в recall: агент не находит нужный файл, потому что не знает, что искать. Векторная БД, добавленная «на всякий случай», добавляет инфраструктуру, latency и новый источник неточных попаданий в контекст.
Как сделать так, чтобы агент продолжал работу после падения процесса?
Сохраняйте объект состояния после каждого шага в Postgres или JSON-файл по run_id, и держите в нём всё, что нужно для возобновления: фазу, прогресс со счётчиками и offset, список артефактов с путями, next_action. Артефакты пишите на диск или в S3 сразу, а не в конце. Тогда рестарт - это load_state(run_id) и продолжение цикла с того же места. Ключевой момент: счётчики и переходы между фазами должны обновляться кодом, а не выводом модели, иначе состояние после рестарта может оказаться несогласованным.