Як керувати контекстом AI агента: sliding window, summarization і compression з прикладами

Оновлено:
Як керувати контекстом AI агента: sliding window, summarization і compression з прикладами

TL;DR

Як ефективно керувати контекстом у довгоживучих AI-агентах:

— Sliding Window + Pinning
— Автоматична summarization з розумними тригерами
— Compression та semantic memory

З конкретними цифрами, кодом і архітектурними рішеннями, які значно підвищили стабільність агента.

Ця стаття — частина серії про AI агентів. Якщо ще не знайомий з тим, які типи пам'яті існують і чому LLM stateless — почни з попередньої статті про пам'ять AI агента. Математику контекстного вікна і квадратичну складність детально розібрано в окремій статті Контекстне вікно LLM: чому AI забуває і скільки це коштує.

Коли агент починає забувати — він не падає з помилкою

Будь-який агент який веде розмову довше 10–15 раундів рано чи пізно стикається з одною і тією самою проблемою: він починає губити контекст. Не з помилкою, не з попередженням — просто тихо і поступово.

Я побачив це вперше під час тестування Agent Chat — системи де два AI агенти дискутують між собою. На початку діалогу кожен агент чітко тримав свою позицію. На 8-му раунді перший почав трохи м'якшати. На 12-му — відтворював аргументи опонента як власні, впевнено і без сумнівів.

Він не зламався. HISTORY_SIZE=8 зробив своє діло: перші раунди вийшли з вікна, і агент втратив контекст власної позиції. Без жодного exception. Без жодного alert у моніторингу.

Той самий сценарій — в іншому одязі — відбувається в будь-якому агенті:

  • Агент підтримки "забуває" що клієнт вже пояснював проблему і перепитує
  • Агент аналізу документів суперечить висновку який сам зробив 10 раундів тому
  • Агент e-commerce "губить" розмір і бюджет клієнта і пропонує нерелевантні товари

У цій статті — три стратегії які вирішують цю проблему на різних рівнях складності, реальні цифри вартості кожної, і архітектура яку я використовую в production після кількох болісних ітерацій.

Анатомія одного агентного запиту: де реально йдуть токени

Більшість розробників думають що проблема в history. Насправді картина інакша. Ось реальна анатомія одного запиту з Agent Chat у production:

System prompt (інструкції агента):      ~800 токенів   ← фіксовано
Tool definitions (5 інструментів):      ~600 токенів   ← фіксовано
History (8 turns × ~200 токенів):      ~1 600 токенів  ← змінно
Tool results (Wikipedia + Tavily):      ~2 400 токенів  ← найбільший сюрприз
User message (поточний раунд):             ~50 токенів
────────────────────────────────────────────────────────
Один запит:                             ~5 450 токенів
При Claude Sonnet (~$3/1M input):       ~$0.016 за запит
50-раундовий діалог:                    ~$0.80

Зверни увагу на tool results — вони важать більше за всю history разом. Стаття Wikipedia легко дає 1 500–2 000 токенів на один виклик. При двох паралельних інструментах за раунд — це більше половини всього контексту. Детальніше про оптимізацію tool calls — в статті про Tool RAG.

Це означає: оптимізувати тільки history і ігнорувати tool results — виправити 30% проблеми і залишити 70% нетронутими.

За даними Vantage (квітень 2026), типовий 50-раундовий агентний сеанс накопичує 25 000–35 000 вхідних токенів на кожен запит до кінця сесії — і input-to-output ratio становить приблизно 25:1. Тобто дорогі не відповіді агента, а те що він несе на кожному кроці.

Context drift: агент ламається тихо, без помилок

Є дві різні проблеми які часто плутають — і це плутання коштує грошей.

Context overflow — технічна помилка: контекст перевищив ліміт, API повернув помилку. Виявляється одразу, фіксується за 10 хвилин.

Context drift — тихий вбивця. Модель не падає. Вона просто починає працювати гірше — і ти про це дізнаєшся або з логів, або від користувача.

Механіка drift проста: sliding window відрізає старі повідомлення рівномірно, але їх цінність — нерівномірна. Мета задачі, ключові обмеження, прийняті рішення — все це зазвичай у перших повідомленнях. Вони йдуть першими. Агент продовжує впевнено відповідати — але вже без фундаменту.

За даними Zylos Research (лютий 2026), майже 65% enterprise AI failures у 2025 були спричинені context drift або memory loss під час multi-step reasoning — не переповненням контексту.

Конкретні симптоми які я фіксував в Agent Chat з логами:

  • Раунд 9: агент перепитує факт який сам же навів у раунді 3
  • Раунд 12: аргументи суперечать позиції з раунду 1 — але подаються впевнено, без сумнівів
  • Раунд 15: агент починає відтворювати стиль і логіку опонента як власну
  • У всіх випадках: жодного exception, жодного alert у моніторингу

Саме тому drift небезпечніший за overflow. Overflow зупиняє агента. Drift залишає його працювати — але неправильно. І чим довша сесія, тим глибша яма.

Стратегія 1: Sliding window — мінімум коду, зрозумілий компроміс

Найпростіше рішення: тримати в контексті лише N останніх повідомлень. Все що старіше — відрізається. Жодної БД для резюме, жодних додаткових LLM calls, жодної складної логіки.

Концептуально це виглядає так (мова не важлива):

// Псевдокод — логіка однакова для Java, Python, TypeScript
messages = repository.findAll(sessionId, orderByTime: ASC)
skipTo   = max(0, messages.length - HISTORY_SIZE)
history  = messages[skipTo .. end]
return history

Реалізація займає 5 рядків. Але вибір значення HISTORY_SIZE — це архітектурне рішення яке визначає де і як агент почне деградувати. Ось реальний компроміс при різних значеннях:

HISTORY_SIZE ~Токенів history Що втрачається Коли підходить
4 ~800 Контекст задачі, мета розмови — агент "забуває" навіщо він тут Тільки stateless Q&A, кожен запит незалежний
8 ~1 600 Ранні аргументи і деталі з початку сесії Короткі сесії до 10 раундів
16 ~3 200 Лише найдавніші деталі — задача і обмеження зазвичай зберігаються Середні сесії, є бюджет на токени
32 ~6 400 Практично нічого при типових сесіях Довгі critical сесії де drift неприпустимий

Зверни увагу: вартість зростає лінійно з розміром вікна, але якість — ні. Подвоєння HISTORY_SIZE з 8 до 16 дає відчутне покращення. З 16 до 32 — значно менший приріст при подвоєнні вартості. Знайди свій sweet spot через тестування на реальних сесіях, а не через інтуїцію.

Чому не можна просто поставити HISTORY_SIZE=32 і забути

Проблема не тільки у вартості. Є ефект який я називаю фундаментальною асиметрією вікна: sliding window відрізає повідомлення рівномірно за часом, але їх цінність для агента — нерівномірна.

Типовий розподіл цінності повідомлень у сесії:

  • Повідомлення 1–3: найвища цінність — мета задачі, роль агента, ключові обмеження і контекст. Втрата цих повідомлень = drift.
  • Повідомлення 4–N-5: середня цінність — проміжні результати, аргументи, tool results. Частково замінні через summarization.
  • Повідомлення N-4–N: висока цінність — поточний стан, останні рішення, активний контекст. Завжди в вікні.

Sliding window сліпо відрізає ліворуч. Першими йдуть саме найцінніші повідомлення з мета-контекстом. Агент продовжує впевнено відповідати — але вже без розуміння навіщо він тут і які були початкові обмеження. Саме це я бачив на 12-му раунді в Agent Chat.

Pinning: безкоштовна оптимізація яку варто додати завжди

Часткове вирішення фундаментальної асиметрії — pinning перших N повідомлень. Ідея проста: перші 2–3 повідомлення (де формулюється задача і роль) завжди залишаються в контексті незалежно від розміру вікна.

// Псевдокод pinning + sliding window
pinnedCount = min(PINNED_COUNT, messages.length)
pinned      = messages[0 .. pinnedCount]

skipTo      = max(pinnedCount, messages.length - HISTORY_SIZE)
window      = messages[skipTo .. end]

return pinned + window  // без дублювання якщо overlap

У своєму ContextService я реалізував це через PINNED_COUNT=3. Перші три повідомлення сесії завжди додаються окремо перед sliding window. Результат: агент завжди знає свою роль і мету задачі — навіть на 50-му раунді.

Важливий нюанс: якщо повідомлень менше ніж PINNED_COUNT + HISTORY_SIZE — перевіряй на overlap. Повідомлення не повинні потрапляти в контекст двічі. Формула skipTo = max(pinnedCount, messages.length - HISTORY_SIZE) вирішує це в одному рядку.

Pinning додає ~600 токенів до кожного запиту (3 повідомлення × ~200 токенів). При Claude Sonnet це ~$0.002 на запит. За 50-раундовий діалог — $0.10 додатково. За стабільність агента протягом всієї сесії — це найдешевша інвестиція в цій статті.

Стратегія 2: Rolling summarization — зберегти суть без токенів

Sliding window вирішує проблему розміру контексту, але не вирішує проблему втрати інформації. Summarization — наступний рівень: замість того щоб відрізати старі повідомлення, ми стискаємо їх у резюме і зберігаємо в сесії. При наступному запиті резюме підставляється як системне повідомлення на початку контексту — там де модель звертає найбільшу увагу.

Результат: агент "пам'ятає" суть всієї розмови в ~500 токенах замість повного history на 5 000+.

Три тригери для запуску summarization — і чому вибір тригера важливіший за реалізацію

Питання "коли запускати" складніше за саму реалізацію. Запустиш надто рано — платиш за summarization без реальної потреби. Надто пізно — контекст вже переповнений і агент встиг задрейфувати. Ось три підходи:

Тригер Умова Плюси Мінуси
За кількістю повідомлень messageCount > SUMMARY_THRESHOLD Простий, передбачуваний, легко тестувати Не враховує розмір — 30 коротких повідомлень ≠ 30 з tool results
За токенами estimatedTokens > 4 000 Точніший, реагує на великі tool results Потребує token counter — окрема логіка оцінки
За семантикою Зміна теми розмови Найрозумніший — резюмує завершену "фазу" діалогу Додатковий LLM call для детекції зміни теми, складна реалізація

У своєму ContextService я використовую перший варіант — за кількістю повідомлень з порогами SUMMARY_THRESHOLD=30 і KEEP_RECENT=15. Логіка: коли накопичується більше 30 повідомлень, перші 15 йдуть на резюмування, останні 15 залишаються "живими" у вікні. При наступному тригері нове резюме мержиться з попереднім через previousContext у промпті — це і є rolling механіка.

Для агентів з важкими tool results (Wikipedia, Tavily, API відповіді) я б рекомендував перейти на тригер за токенами: 30 повідомлень без tools це ~6 000 токенів, а 30 повідомлень з Wikipedia results — легко 20 000+. Реагувати треба на токени, а не на лічильник рядків.

Net benefit: чи виправдовує себе summarization фінансово

Частий контраргумент: "summarization сама по собі витрачає токени — яка економія?" Рахуємо на прикладі 50-раундового діалогу в Agent Chat:

  • Без summarization: history росте лінійно, до раунду 50 несемо ~12 000 токенів history на кожен запит
  • З summarization (поріг 30, keep 15): history після тригера ~500 токенів резюме + 15 живих повідомлень ~3 000 токенів = ~3 500 токенів
  • Вартість одного summarization call: ~800 токенів промпт + ~200 токенів відповідь = ~$0.003 при Claude Sonnet

Економія на history за раунди 31–50: ~8 500 токенів × 20 раундів = 170 000 токенів = ~$0.51. Вартість summarization: 1 виклик × $0.003 = $0.003. Net benefit очевидний.

Але є важливе застереження. За даними Augment Code (квітень 2026), агресивна summarization яка скорочує токени на 75% per step збільшує кількість раундів до завершення задачі з 4 до 14. Загальна економія при цьому — лише 14%, але якість деградує. Summarization має зберігати достатньо, а не мінімум. Ціль — стиснути без втрати операційних деталей, а не максимально скоротити.

Чому промпт для summarization важливіший за код

Найчастіша помилка — загальний промпт "стисни розмову в 5 речень". Модель стисне. Але збереже те що вона вважає важливим — а не те що важливо для твого агента.

Порівняй два промпти для агента e-commerce:

// ❌ Загальний промпт — модель вирішує сама
"Стисни цю розмову в 3-5 речень. Збережи важливе."

// ✅ Доменний промпт — ми вирішуємо що важливо
"Стисни цю частину розмови в 3-5 речень.
Збережи обов'язково: розмір одягу якщо згадувався, бюджет,
  товари додані в кошик, фільтри (наявність, категорія).
Відкинь: привітання, повтори, загальні фрази без конкретики."

Перший промпт збереже "користувач цікавився куртками". Другий збереже "розмір L, бюджет до 3 000 грн, додав куртку id=847 в кошик, тільки в наявності". Різниця в якості агента — колосальна.

Для різних типів агентів список "що зберегти" завжди свій:

  • Агент підтримки: номери тікетів, статуси, пообіцяні дедлайни
  • Агент дискусії: позиції сторін, прийняті аргументи, відхилені тези
  • Агент аналізу документів: знайдені факти, відкриті питання, джерела
  • Агент кодингу: архітектурні рішення, відомі обмеження, зміни які вже зроблено

Підводний камінь: 7B модель погано резюмує технічний контент

Якщо запускаєш summarization локально через Ollama з 7B моделлю — є проблема яку легко пропустити при тестуванні і боляче виявити в production.

Малі моделі непогано стискають human-assistant діалог розмовного характеру. Але вони губляться на технічних tool results: JSON-відповідях API, SQL-запитах, stack traces, структурованих даних. Вони скорочують — але губляться структура і конкретика.

Ось що відбувається з stack trace після 7B summarization:

// Оригінальний tool result (180 токенів)
"java.lang.NullPointerException at OrderService.java:142
  caused by: cartRepository.findById() returned null
  orderId: 8821, userId: 334"

// Після summarization через 7B модель (12 токенів)
"була помилка NullPointerException в сервісі"

Якщо агент потім запитає "яку саме помилку отримали і де?" — резюме не допоможе. А у випадку retry логіки агент може знову наступити на той самий граблі.

Практичне рішення: резюмувати тільки human/assistant turns через локальну модель, а tool results — обрізати за розміром (selective truncation), а не резюмувати. Tool results зберігають структуру при truncation краще ніж при узагальненні через слабку модель. Детальніше — у наступному розділі.

Стратегія 3: Selective compression — різні типи повідомлень, різна логіка

Sliding window і summarization працюють з повідомленнями як з однорідною масою. Але повідомлення в агентному циклі принципово різні за природою: людський запит, відповідь агента, результат tool call, помилка tool call — кожен тип має свою цінність і свій ризик при стисканні. Treating them the same — типова помилка яка або губить критичні дані, або марно витрачає контекст на шум.

За даними Keval Jagani (Medium, січень 2026), найнадійніші production системи використовують каскадний підхід з чіткою черговістю: спочатку стискати tool outputs, потім відрізати старі повідомлення через sliding window, і тільки в останню чергу викликати LLM summarization. Кожен наступний рівень — дорожчий і ризикованіший, тому застосовується лише коли попереднього вже недостатньо.

Матриця рішень по типах повідомлень

Тип повідомлення Стратегія Ризик Чому саме так
Human message Зберігати повністю Низький Зазвичай короткі (20–100 токенів), містять intent користувача
Assistant message Резюмувати після N раундів Середній Довгі відповіді можна стиснути — але нюанси аргументів можуть загубитись
Tool result (короткий, <500 символів) Зберігати повністю Низький Вже компактний, обрізати нема чого
Tool result (довгий, >500 символів) Обрізати до N символів Контрольований Головний "пожирач" токенів — Wikipedia, API, веб-сторінки
Tool error Зберігати повністю завжди Критичний якщо загубиш Потрібен для retry логіки і щоб агент не повторював ту саму помилку

Tool result truncation: де обрізати має значення

Обрізати tool results до N символів — правильна ідея, але наївна реалізація result.substring(0, MAX_LENGTH) може зробити гірше ніж краще. Різні джерела мають різну структуру корисної інформації:

  • Wikipedia / веб-стаття: корисне на початку — lead paragraph містить головний зміст. Залишай перші 800 символів, решту відрізай. Модель отримає суть без "Список джерел", "Зовнішні посилання" і footer.
  • API JSON response: структура важливіша за повноту. Обрізати з кінця відносно безпечно — JSON ламається, але агент бачить головні поля. Краще: витягнути тільки релевантні поля перед передачею в контекст.
  • Stack trace: найнебезпечніший для обрізання. Перший рядок (тип помилки) і рядки з твоїм кодом (не з бібліотек) — найцінніші. Якщо треба обрізати — залишай початок і кінець, виключай середину з at java.base/... і at org.springframework/....
  • Новинна стаття / Tavily result: перший абзац зазвичай містить всю суть. 500 символів достатньо для більшості агентних задач.

Псевдокод розумного truncation:

function truncateToolResult(content, type, maxChars):
  if content.length <= maxChars:
    return content  // не чіпаємо якщо і так влазить

  if type == "stack_trace":
    head = content[0 .. 300]         // тип помилки і перший фрейм
    tail = content[end-200 .. end]   // останні фрейми (твій код)
    return head + "\n...[скорочено]...\n" + tail

  if type == "json_api":
    // краще витягнути ключові поля ніж обрізати
    return extractRelevantFields(content, agentContext)

  // default: Wikipedia, news, веб
  return content[0 .. maxChars] + "...[скорочено]"

Importance scoring: коли category-based вже не вистачає

Category-based truncation — це детерміноване правило: завжди обрізати tool results довші за X символів. Простий, передбачуваний, легко дебажити. Для більшості проектів це правильний вибір.

Наступний рівень — importance scoring: кожному повідомленню присвоюється динамічна вага на основі кількох факторів:

  • Recency: нові повідомлення важливіші — але не завжди (перші повідомлення з метою задачі важливі постійно)
  • Relevance до поточного запиту: семантична близькість до того що питає користувач зараз
  • Entity density: повідомлення з іменами, числами, ID, датами — важливіші за загальні фрази
  • Interaction metadata: повідомлення після яких користувач виправляв агента — підвищений пріоритет

При наповненні контексту першими відрізаються повідомлення з найнижчим composite score. Це дозволяє зберегти критичне повідомлення з раунду 2 навіть коли вікно вже давно мало б його відрізати.

Але є реальна ціна: importance scoring потребує або окремого LLM call (дорого і повільно), або ML моделі для scoring (складна підтримка), або евристик (швидко але неточно). За правилом 80/20: category-based truncation з правильними порогами дає 80% ефекту importance scoring при 5% його складності. Починай з простого — переходь до scoring тільки якщо виміряєш що category-based реально губить важливі повідомлення у твоєму сценарії.

Трирівнева архітектура: як поєднати всі підходи

Кожна з трьох стратегій вирішує окрему проблему — але жодна не вирішує всі три одночасно. Sliding window дешевий але губить ранній контекст. Summarization зберігає суть але текст дрейфує при перезаписах. Selective compression економить токени але не гарантує збереження критичних фактів. Реальна production архітектура — це комбінація всіх трьох.

Ось як я прийшов до фінального рішення через три ітерації — кожна з яких закривала конкретний баг а не гіпотетичну проблему:

Ітерація 1: тільки HISTORY_SIZE=8
  Симптом: агент на 12-му раунді переплутав опонента з собою.
  Причина: перші повідомлення з роллю і позицією вийшли з вікна.
  Рішення: недостатньо — просто збільшити вікно дорого.

Ітерація 2: HISTORY_SIZE=20 + rolling summary + pinning
  Симптом: агент "забуває" що клієнт хотів розмір L.
  Причина: факт про розмір був у резюме, але після трьох перезаписів
    розмився до "клієнт цікавився одягом".
  Рішення: текстове резюме дрейфує — факти треба зберігати окремо.

Ітерація 3: + critical facts як окрема структура (JSONB)
  Результат: структуровані факти не дрейфують. Або є — або нема.
  {"size": "L"} не може стати "приблизно середній" через 3 перезаписи.

Фінальна архітектура — чотири шари в контексті з чіткою роллю кожного:

┌──────────────────────────────────────────────────┐
│                 КОНТЕКСТ АГЕНТА                  │
├──────────────────────────────────────────────────┤
│ [System] Резюме попередньої розмови              │
│   "Обговорювали переваги RAG над fine-tuning,    │ ← rolling summary
│    агент А наполягав на cost efficiency..."      │   ~300-500 токенів
├──────────────────────────────────────────────────┤
│ [System] Відомі факти про сесію:                 │
│   {size: "L", budget: 5000, in_stock: true,      │ ← critical facts
│    cart_items: [847, 923]}                       │   JSONB, не дрейфує
├──────────────────────────────────────────────────┤
│ [User]      Повідомлення 1 (pinned)              │
│ [Assistant] Відповідь 1    (pinned)              │ ← перші PINNED_COUNT=3
│ [User]      Повідомлення 2 (pinned)              │   завжди в контексті
├──────────────────────────────────────────────────┤
│ [User]      Повідомлення N-19                    │
│ [Assistant] Відповідь N-19                       │ ← sliding window
│ [Tool]      Result (truncated якщо >500 chars)   │   HISTORY_SIZE=20
│ ...                                              │
│ [User]      Повідомлення N (поточне)             │
└──────────────────────────────────────────────────┘

Чому critical facts — окремий шар, а не частина резюме

Це найважливіший архітектурний інсайт всієї статті, тому поясню детально.

Rolling summary — це текст. Текст генерується LLM і кожен раз трохи інтерпретується по-новому. Факт "розмір L" в першому резюме може стати "клієнт шукав верхній одяг" в другому, і "користувач цікавився великими розмірами" в третьому. Семантично близько — але агент більше не знає точний розмір.

JSONB структура працює інакше: {"size": "L"} або є або її немає. Вона не інтерпретується, не перефразовується, не "округляється" при стисканні. І головне — extractCriticalFacts мержить нові факти з існуючими через existingFacts.putAll(newFacts). Одного разу витягнутий факт живе до кінця сесії незалежно від кількості summarization циклів.

Є ще одна перевага яку легко не помітити: structured facts дозволяють агенту використовувати факти програмно, а не тільки в тексті. Ти можеш перевірити if (session.getCriticalFacts().containsKey("budget")) і прийняти рішення в коді — не чекаючи поки модель "прочитає" і "зрозуміє" резюме.

Порядок шарів у контексті — не випадковий

Розміщення summary і critical facts як SystemMessage на самому початку — не просто зручність. Це пряма відповідь на ефект "lost in the middle": модель краще пам'ятає інформацію з початку і кінця контексту. Якщо покласти резюме в середину history — воно потоне і частково ігноруватиметься.

Правило просте: що важливіше — те вище. System prompt і мета-контекст завжди зверху. Поточний запит користувача завжди знизу. History — між ними.

Що відбувається при кожному новому повідомленні

1. Зберегти нове повідомлення користувача в БД
2. Перевірити needsSummarization():
     if messageCount > SUMMARY_THRESHOLD:
       a. Читати повідомлення для резюмування (окрема транзакція, закрити)
       b. LLM call: generateSummary() — БД вільна під час виклику
       c. LLM call: extractCriticalFacts() — мержить з існуючими
       d. Зберегти summary + facts в сесії (нова коротка транзакція)
3. buildHistory():
     - SystemMessage з summary (якщо є)
     - SystemMessage з critical facts (якщо є)
     - Pinned повідомлення (перші PINNED_COUNT)
     - Sliding window (останні HISTORY_SIZE, без дублювання з pinned)
4. Викликати LLM з побудованим контекстом
5. Зберегти відповідь агента в БД

Три окремі транзакції в кроці 2 — не бюрократія. Це критично для production: LLM call може тривати 5–30 секунд, і тримати відкриту транзакцію БД весь цей час при паралельних сесіях означає виснаження connection pool. Читання → LLM → запис: кожна операція своя транзакція, БД вільна під час дорогого виклику.

Бонус 2026: Compaction API від Anthropic

Поки я будував власне рішення, Anthropic у січні 2026 випустила Compaction API (beta) — серверну автоматичну компакцію контексту для Claude Opus 4.6 і Sonnet 4.6. Це важливо знати навіть якщо не плануєш використовувати — щоб усвідомлено вибирати між власним рішенням і готовим.

Механіка: коли розмова наближається до налаштованого порогу токенів, API автоматично генерує резюме попередніх частин і замінює їх спеціальним compaction block. Всі наступні запити продовжуються з компактного стану — без жодного клієнтського коду для управління контекстом.

За даними InfoQ (березень 2026), Anthropic називає деградацію контексту "context rot" і описує compaction як якісний зсув у тому скільки контексту модель може реально використовувати при збереженні peak performance. На бенчмарку MRCR v2 при 1M токенів Opus 4.6 досягає 76% точності multi-needle retrieval — проти 18.5% у Sonnet 4.5.

Compaction API доступний через Claude API, AWS Bedrock, Google Vertex AI і Microsoft Foundry. Підтримує Zero Data Retention — що робить його придатним для regulated industries: healthcare, legal, finance. Beta header обов'язковий у кожному запиті: betas=["compact-2026-01-12"].

Ключове обмеження яке визначає вибір: Compaction API не дозволяє контролювати що саме зберігається в резюме. Модель вирішує сама. Якщо тобі потрібні structured critical facts, кастомні retention правила або domain-specific промпт для summarization — власне рішення незамінне.

Критерій Compaction API Власне рішення
Час до production Години — мінімум коду Дні — повна реалізація
Провайдер Тільки Anthropic Будь-який (Ollama, OpenRouter, мультипровайдер)
Контроль над резюме Немає — модель вирішує Повний — свій промпт, свої правила
Structured facts Недоступно JSONB, будь-яка структура
Zero Data Retention Підтримується Залежить від твоєї інфраструктури
Вартість компакції LLM call на стороні Anthropic LLM call на твоїй стороні (або Ollama = безкоштовно)

Моя рекомендація: якщо ти на Claude API, немає доменно-специфічних вимог до збереження фактів і потрібен мінімум коду — починай з Compaction API. Якщо в тебе мультипровайдерна архітектура (як у моєму випадку з OpenRouter в production і Ollama локально), або потрібні structured facts — будуй власне рішення за архітектурою вище.

Реальні цифри: вартість при різних стратегіях

Абстрактні рекомендації без цифр — марні. Порівняємо стратегії на конкретному прикладі: 50-раундовий діалог в Agent Chat, 5 інструментів, mix Wikipedia (~1 800 токенів/result) і Tavily (~600 токенів/result), в середньому 1.5 tool calls за раунд. Ціни станом на травень 2026, Claude Sonnet ~$3/1M input tokens.

Спочатку базова анатомія одного запиту без оптимізацій:

System prompt:              ~800 токенів  (фіксовано)
Tool definitions (5 tools): ~600 токенів  (фіксовано)
Tool results (1.5 calls):  ~1 800 токенів (середнє)
History (зростає лінійно): ~200 × N токенів
User message:                ~50 токенів

До раунду 10:  ~5 650 токенів/запит
До раунду 30:  ~8 650 токенів/запит
До раунду 50:  ~12 650 токенів/запит  ← без оптимізацій

Тепер порівняємо стратегії. Вартість діалогу = сума токенів по всіх 50 запитах × $3/1M:

Стратегія Токенів/запит (середнє) Вартість діалогу Якість пам'яті Складність
Без оптимізації (full history) ~15 000 ~$2.25 Висока (поки влазить у вікно) Нуль
Window=8 ~5 450 ~$0.82 Середня — drift після 8 раундів Мінімальна
Window=8 + pinning ~6 050 ~$0.91 Краща — задача не губиться Мінімальна
Window=20 + rolling summary ~7 000 ~$1.05 + ~$0.08 summary Висока Середня
Window=20 + summary + critical facts ~7 200 ~$1.08 + ~$0.12 summary+facts Максимальна Середня
Все вище + tool result truncation ~5 200 ~$0.78 + ~$0.12 Максимальна Середня

Звідки береться вартість summarization

У таблиці "$0.08 summary" — не магія. Ось розрахунок для одного summarization циклу (тригер на 30-му повідомленні, резюмуємо перші 15):

Input summarization call:
  15 повідомлень × ~200 токенів     = ~3 000 токенів
  Промпт інструкція                 =   ~100 токенів
  Попереднє резюме (якщо є)         =   ~400 токенів
  Разом input:                        ~3 500 токенів = ~$0.011

Output (нове резюме):
  ~400 токенів × $15/1M output      = ~$0.006

Один summarization call:            ~$0.017

За 50-раундовий діалог — ~2-3 тригери:
  3 × $0.017 = ~$0.05 тільки summary

extractCriticalFacts (4 окремі calls):
  4 × ~500 токенів input + ~20 output = ~$0.007 за цикл
  3 цикли: ~$0.02

Загальна вартість summarization за діалог: ~$0.07–0.12

Тобто повна трирівнева архітектура коштує ~$0.90 за 50-раундовий діалог замість ~$2.25 без оптимізацій. Економія 60% — при максимальній якості пам'яті агента.

Ключовий висновок який не очевидний з таблиці

Tool result truncation дає більшу економію (~$0.30 на діалог) ніж різниця між Window=8 і Window=20 (~$0.23). При цьому якість пам'яті залишається максимальною. Це підтверджує тезу з початку статті: tool results — головний пожирач токенів, не history. Оптимізуй їх першими.

Щодо агресивної компресії: за даними Factory.ai, надмірне стискання яке змушує агента "перезавантажувати" забуті факти збільшує загальну кількість токенів через повторні запити. Економія per-request обертається на переплату per-task. Якісне резюме дорожче per-call — але дешевше per-задача.

Якщо запускаєш summarization через Ollama локально

Вартість LLM call для summary = $0. Це означає що net benefit трирівневої архітектури для локального стеку — чиста економія на cloud токенах без додаткових витрат на summarization.

У моєму випадку з qwen3:8b через Ollama на Mac M1 summarization займає ~3–5 секунд і коштує $0. При production через OpenRouter — summarization через меншу модель (наприклад llama3.1:8b) коштує ~$0.001 за виклик замість $0.017 через Sonnet. Вибір моделі для summarization — окрема оптимізація яка може скоротити вартість summary ще в 10–15 разів.

Decision tree: яку стратегію обрати

Чотири стратегії які ми розібрали — не рівноцінні альтернативи. Кожна наступна складніша і дорожча у підтримці. Правило просте: обирай найпростішу яка вирішує твою конкретну проблему. Переускладнення контекст-менеджменту — така ж помилка як його ігнорування.

Очікувана тривалість сесії менше 10 раундів?
├── Так → Sliding window (HISTORY_SIZE=8-12) + pinning перших 2-3 повідомлень
│         Нічого більше не потрібно. Більшість чат-ботів і Q&A агентів
│         живуть тут — і це правильно.
└── Ні ↓

Є специфічні факти які агент НЕ повинен забути незалежно від довжини сесії?
├── Так → + Critical facts як окрема структура (JSONB або dict)
│         Приклади: розмір/бюджет клієнта, номер тікету, прийняті рішення,
│         параметри задачі які не змінюються протягом сесії.
└── Ні ↓

Який бюджет на токени?
├── Обмежений (мета: <5 000 токенів/запит) →
│     Tool result truncation (обов'язково першим)
│     + Window=8 + pinning
│     + Summarization через локальну модель (Ollama) якщо є
│     Не чіпай expensive cloud модель для summary — це з'їсть всю економію.
└── Є бюджет ↓

Ти на Claude API (не OpenRouter, не Ollama, не мультипровайдер)?
├── Так і немає доменних вимог до збереження фактів →
│     Compaction API (beta) — мінімум коду, якісне резюме від самої моделі.
│     Додай betas=["compact-2026-01-12"] і забудь про context management.
└── Ні або є доменні вимоги ↓

→ Трирівнева архітектура:
    Window=15-20 + rolling summary + critical facts + tool result truncation
    Тригер summarization: при > 25-30 повідомленнях
    keep_recent = 10-15
    Summary модель: менша і дешевша ніж chat модель

Коментарі до кожної гілки

Гілка 1 — короткі сесії: не ускладнюй. HISTORY_SIZE=10 і три pinned повідомлення вирішують 90% проблем для агентів які відповідають на конкретні запити без довгого стану. Додавати summarization сюди — платити за complexity якої не потребуєш.

Гілка critical facts: це не окремий рівень складності — це таблиця в БД і один додатковий LLM call при summarization. Якщо агент хоча б раз на сесії має "запам'ятати" конкретний факт і використати його через 20 раундів — critical facts обов'язкові. Без них факт розмиється в тексті резюме.

Гілка обмеженого бюджету: tool result truncation — перша оптимізація, не остання. Вона дає більше економії ніж зменшення HISTORY_SIZE і не погіршує якість пам'яті. Якщо після truncation все одно дорого — зменшуй вікно, не прибирай truncation.

Гілка Compaction API: зручно, але є lock-in на Anthropic. Якщо завтра захочеш перейти на OpenRouter або додати Ollama для локальної розробки — Compaction API не перенесеться. Враховуй це при виборі.

Трирівнева архітектура: це не "найкраще рішення для всіх" — це рішення для агентів з довгими сесіями, доменно-специфічними фактами і мультипровайдерною інфраструктурою. Якщо у тебе простіший сценарій — простіше рішення буде кращим.

Найчастіші помилки при виборі

  • Одразу будувати трирівневу архітектуру — без виміру чи є реальна проблема з drift. Почни з Window=8 + pinning, виміряй, ускладнюй тільки якщо є конкретний симптом.
  • Summarization через ту саму дорогу модель що і chat — summary через Claude Sonnet коштує стільки ж скільки звичайний запит. Для summary достатньо дешевшої моделі: Haiku, llama3.1:8b через Ollama, або deepseek-chat через OpenRouter.
  • Ігнорувати tool results і оптимізувати тільки history — як ми бачили в розділі про вартість, tool results часто важать більше за всю history разом. Truncation tool results при >500 символів — перша оптимізація яку треба зробити.
  • SUMMARY_THRESHOLD занадто малий — якщо запускати summarization кожні 10 повідомлень, вартість summary calls перевищить економію на history. Поріг 25-30 повідомлень — перевірений компроміс.

Підводні камені яких не показують у туторіалах

Більшість туторіалів показують happy path: summarization спрацював, факти витягнулись, контекст зменшився. Реальний production виглядає інакше. Ось п'ять проблем які я знайшов через логи і дивну поведінку агента — а не через документацію.

1. Транзакція відкрита під час LLM call

Найчастіша помилка при реалізації summarization — тримати відкриту транзакцію БД поки модель генерує резюме. Виглядає логічно: відкрив транзакцію, прочитав повідомлення, згенерував резюме, зберіг, закрив. Одна атомарна операція.

Проблема: LLM call може тривати 5–30 секунд. Під час цього часу транзакція тримає connection з пулу. При 10 паралельних сесіях що одночасно тригерять summarization — 10 connections заблоковані на 30 секунд кожен. При типовому connection pool в 20 з'єднань — половина пулу паралізована. Нові запити чекають. Latency росте. При піковому навантаженні — timeout і падіння.

Правильний підхід — три окремі операції з явним закриттям транзакції перед LLM call:

// Крок 1: читаємо дані — транзакція відкрилась і одразу закрилась
List messages = getMessagesForSummarization(session);  // @Transactional(readOnly=true)

// Крок 2: LLM calls — БД повністю вільна під час виклику (може тривати 30 сек)
String summary = generateSummary(messages);            // без @Transactional
Map facts      = extractCriticalFacts(messages);       // без @Transactional

// Крок 3: зберігаємо результат — коротка нова транзакція (мілісекунди)
saveSummary(session, summary, facts);                  // @Transactional

Це контрінтуїтивно — здається що "розрив" транзакції небезпечний. Але між кроком 1 і кроком 3 нічого критичного не відбувається: ми тільки генеруємо текст з вже прочитаних даних. Якщо між кроками хтось додав нове повідомлення — нічого страшного, воно потрапить у наступний summarization цикл.

2. extractCriticalFacts = N окремих LLM calls — рахуй заздалегідь

Якщо витягуєш 4 факти (розмір, бюджет, items в кошику, фільтр наявності) через окремі запити — це 4 LLM calls на кожен summarization цикл. Локально через Ollama — нормально, всі calls безкоштовні. В cloud — інша математика:

4 facts × ~500 токенів input × $3/1M  = ~$0.006 за цикл
3 цикли за 50-раундовий діалог        = ~$0.018
1 000 активних сесій на день          = ~$18/день тільки на facts extraction
30 днів                               = ~$540/місяць

Альтернатива — один запит який повертає JSON з усіма фактами:

// Один call замість чотирьох
"Проаналізуй розмову і поверни JSON:
{
  'size': розмір одягу або null,
  'budget': бюджет числом або null,
  'cart_items': масив id або [],
  'in_stock_only': true/false або null
}
Тільки JSON, без пояснень."

Економія: 4 calls → 1 call, вартість facts extraction падає в 4 рази. Проблема: малі моделі (7B і менше) часто ламають JSON структуру — додають текст до або після, пропускають дужки, пишуть None замість null. Потрібен robust JSON parser з fallback.

Моя рекомендація: якщо використовуєш модель 13B+ або cloud модель — один JSON call. Якщо локальна 7B через Ollama — окремі прості запити надійніші, хоч і повільніші. Надійність важливіша за швидкість для summarization яка відбувається у фоні.

3. Агресивна компресія збільшує загальні витрати

Інтуїція підказує: менше токенів per-request = менше витрат загалом. Це хибна інтуїція для агентів з багатокроковими задачами.

За даними Augment Code (квітень 2026), при скороченні токенів per-step з 8 500 до 2 100 через агресивне резюмування кількість раундів до виконання задачі зросла з 4 до 14. Загальна економія токенів — лише 14%, але якість деградувала суттєво.

Механіка проблеми: агент "забув" факт через надмірне стискання → перепитав користувача або повторно викликав tool → додав 2-3 зайвих раунди → кожен раунд знову коштує токенів. Економія per-request обертається на переплату per-task.

Правило яке я використовую: резюме має зберігати все що агент потенційно запитає в наступних 10 раундах. Якщо сумніваєшся чи зберігати факт — зберігай. Вартість зайвих 50 токенів в резюме менша за вартість одного повторного tool call.

4. Pinned повідомлення дублюються в sliding window

Класичний off-by-one баг який легко пропустити при тестуванні на коротких сесіях і боляче виявити в production на довгих.

Сценарій: PINNED_COUNT=3, HISTORY_SIZE=20, в сесії всього 5 повідомлень. Алгоритм без захисту:

pinned  = messages[0..2]   // повідомлення 1, 2, 3
skipTo  = max(0, 5 - 20)   // = 0
window  = messages[0..4]   // повідомлення 1, 2, 3, 4, 5

result  = pinned + window  // повідомлення 1, 2, 3 додані ДВІЧІ

Модель бачить дублікати і це впливає на якість: вона надає більшої ваги продубльованим повідомленням і може вести себе непередбачувано. Одна строчка виправляє проблему:

skipTo = max(pinnedCount, messages.length - HISTORY_SIZE)

Тепер skipTo завжди починається після pinned блоку. Перевіряй цей кейс явно в тестах — особливо при messages.length <= PINNED_COUNT (сесія тільки почалась, pinned і window повністю перекриваються).

5. Summary "тоне" в середині контексту

Якщо додаєш резюме як звичайне UserMessage або AssistantMessage в history — воно опиниться десь посередині контексту. Саме там де модель працює найгірше через ефект "lost in the middle" який ми розбирали на початку статті.

Ось що відбувається з увагою моделі залежно від позиції резюме:

Варіант А — резюме в середині history (❌ неправильно):
  [User msg 1] [Assist 1] ... [Summary] ... [User msg N] ← поточне
  Увага моделі до Summary: низька — "lost in the middle"

Варіант Б — резюме як SystemMessage на початку (✅ правильно):
  [System: Summary] [System: Facts] [Pinned 1..3] [Window] [User msg N]
  Увага моделі до Summary: максимальна — початок контексту

SystemMessage на початку — не просто конвенція. Це пряме використання архітектурної особливості трансформера: модель завжди добре пам'ятає початок контексту. Поклади туди найважливіше — резюме і critical facts. Поточний запит користувача — в кінці. History — між ними як допоміжний контекст, часткова втрата якого некритична.

Додатково: якщо в тебе є і summary, і critical facts — розміщуй їх у такому порядку: спочатку facts (структуровані, конкретні), потім summary (текстовий контекст). Модель краще "прив'язує" подальший текст до структурованих даних які побачила першими.

Висновки

Управління контекстом — не одна техніка і не разова задача. Це шар архітектури який визначає чи буде агент надійним на 5-му раунді і на 50-му. Я пройшов через три ітерації в реальних проектах перш ніж прийшов до архітектури яку описав у цій статті — і кожна ітерація була реакцією на конкретний баг, а не на теоретичну проблему.

Головне що варто винести:

  • Context drift небезпечніший за context overflow. Overflow зупиняє агента з помилкою. Drift залишає його працювати — але неправильно. 65% enterprise AI failures у 2025 були спричинені саме drift, не переповненням.
  • Tool results — головний пожирач токенів, не history. Оптимізуй їх першими. Truncation tool results до 500–800 символів дає більше економії ніж зменшення HISTORY_SIZE вдвічі.
  • Починай з простого і ускладнюй тільки коли є симптом. Sliding window + pinning реалізується за годину і вирішує 80% проблем. Не будуй трирівневу архітектуру якщо агент живе 8 раундів.
  • Промпт для summarization важливіший за код. Загальний "стисни в 5 речень" дає загальне резюме. Доменний промпт з явним списком що зберегти — дає операційно корисне резюме.
  • Critical facts окремо від summary — не ускладнення, а захист від drift. Текст дрейфує при перезаписах. Структура {"size": "L"} або є або її немає.
  • Агресивна компресія коштує дорожче ніж економить. Агент який "забув" факт і перепитує або повторно викликає tool — з'їдає більше токенів ніж якби ти зберіг це в резюме.

Каскадний порядок застосування стратегій якого я дотримуюсь:

  1. Tool result truncation — першим, завжди
  2. Sliding window з pinning — базовий рівень
  3. Rolling summary — коли сесії довші за 25-30 повідомлень
  4. Critical facts — коли є доменно-специфічні дані які не можна загубити

Якщо ти на Claude API і не потрібен повний контроль — Compaction API (beta) замінює пункти 3 і 4 одним рядком конфігурації. Але він не замінить truncation tool results і не додасть structured facts — ці два рівні все одно твоя відповідальність.


Наступна стаття серії: P-1 — Агент завис без логів: loop detection, maxSteps і graceful degradation.

Управління контекстом вирішує проблему що агент пам'ятає. Наступна стаття — про проблему коли агент взагалі не зупиняється: нескінченний tool call цикл, silent failure і як виявити зависання до того як воно з'їло весь бюджет і терпіння користувача.

📖 Джерела

Останні статті

Читайте більше цікавих матеріалів

Як керувати контекстом AI агента: sliding window, summarization і compression з прикладами

Як керувати контекстом AI агента: sliding window, summarization і compression з прикладами

TL;DR Як ефективно керувати контекстом у довгоживучих AI-агентах: — Sliding Window + Pinning — Автоматична summarization з розумними тригерами — Compression та semantic memory З конкретними цифрами, кодом і архітектурними рішеннями, які значно підвищили стабільність агента. Ця стаття —...

Google Spam Policy 2026: маніпуляції з AI Overview тепер офіційно спам

Google Spam Policy 2026: маніпуляції з AI Overview тепер офіційно спам

15 травня 2026 року Google тихо оновив одне речення у своїй Spam Policy. Але це речення змінює правила гри для всіх хто займається контентом і SEO. Без гучних анонсів, без великої прес-конференції — просто нове формулювання на сторінці документації. Search Engine Roundtable...

Пам'ять AI агента: in-context, episodic, RAG і semantic — коли що використовувати

Пам'ять AI агента: in-context, episodic, RAG і semantic — коли що використовувати

Агент отримав запит — обробив — відповів. Наступний запит — і він не пам'ятає нічого з попереднього. Не тому що щось зламалось. А тому що так влаштована LLM за замовчуванням: кожен виклик — чистий аркуш. Якщо ви будуєте агента і не думали про пам'ять — ви будуєте амнезика з доступом до...

Grok Build від xAI: детальний технічний огляд

Grok Build від xAI: детальний технічний огляд

Grok Build — новий agentic CLI від xAI (early beta, 14 травня 2026). Головні фішки: Plan Mode з обов’язковим затвердженням плану, паралельні субагенти (до 8), контекстне вікно ~1–2M токенів та сучасний TUI на Rust. Працює на Grok 4.3, підтримує ACP, git worktree та MCP....

Ollama 0.24 + Codex App: як запустити локальний AI coding agent

Ollama 0.24 + Codex App: як запустити локальний AI coding agent

Оновлено: 15 травня 2026 14 травня 2026 вийшла Ollama 0.24 — і це не черговий патч з виправленням багів. Цей реліз додає офіційну підтримку Codex App від OpenAI: тепер десктопний AI coding agent можна запустити на будь-якій локальній або хмарній моделі через Ollama....

Tool RAG: що робити коли у агента забагато інструментів

Tool RAG: що робити коли у агента забагато інструментів

У вас 5 tools — все чудово. У вас 15 tools — починаються проблеми. У вас 50 tools — агент деградує. Але є рішення яке вирішує проблему масштабу елегантно — і ви вже знаєте як воно працює, бо використовуєте його для документів. Ця стаття — частина серії про AI агентів на Spring Boot. Якщо...