Продакшн-разработка ИИ-агентов · Модуль 1 · Урок 1.2
Глава 2. Context engineering
Цели главы
- Относиться к контекстному окну (context window) как к бюджету и осознанно распределять его между system/tools/history/RAG.
- Запускать компакцию и суммаризацию истории по триггерам (порог токенов), а не наугад.
- Правильно ставить точки prompt caching и понимать, что инвалидирует кэш.
- Понимать context rot / «lost in the middle» и размещать важное в начале/конце контекста.
- Перейти от «дать всё сразу» к just-in-time retrieval — выборочной подаче контекста.
Что нового (дельта к базовому курсу)
В базовом курсе управление контекстом сводилось к двум приёмам: обрезка (truncation) старых сообщений и суммаризация при переполнении. Этого достаточно, чтобы не упереться в лимит окна, но это реактивно и грубо.
Здесь мы углубляем ту же тему до инженерной дисциплины — context engineering:
- от «обрезать, когда не влезает» к бюджетированию окна по зонам и компакции по триггерам;
- добавляем prompt caching и понимание KV-кэша на уровне инференса — то, чего в базовом курсе не было вовсе;
- объясняем почему длинный контекст вредит качеству (context rot), а не только тем, что он «дорогой»;
- меняем стратегию подачи: just-in-time вместо «загрузить весь возможный контекст заранее».
Контекстное окно — это бюджет, а не свалка
Контекст (context) — это набор токенов, который модель видит на данном шаге инференса. Окно конечно, и каждый токен «конкурирует за внимание» с остальными. Поэтому окно полезно представлять как бюджет, разбитый на зоны:
- system — инструкции, роль, политика. Должно быть стабильным (об этом ниже — это основа кэша).
- tools — спецификации инструментов. Тоже относительно стабильны.
- history — диалог и результаты вызовов инструментов. Растёт быстрее всего и быстрее всего «протухает».
- RAG / retrieved — подтянутые документы. Самая «шумная» зона: легко переполнить нерелевантным.
Мысленная модель: не «сколько влезет», а «какая конфигурация контекста с наибольшей вероятностью даст нужное поведение». Лишний релевантно выглядящий, но ненужный токен — не бесплатен: он размывает внимание и повышает context rot.
Context rot и «lost in the middle»
Увеличение окна не значит линейного роста качества. У моделей ограниченное внимание: чем больше токенов, тем труднее удерживать фокус и точно вспоминать детали. Это явление называют context rot — деградация качества по мере роста контекста.
Связанный эффект — «lost in the middle»: информация в середине длинного контекста извлекается хуже, чем в его начале и конце. Модель надёжнее «видит» края.
Практические выводы:
- Кладите самое важное (ключевая инструкция, актуальный вопрос, критичные факты) в начало (system) и конец (последнее сообщение) контекста.
- Боритесь с раздуванием history: компактируйте старое, не тащите полные сырые результаты инструментов вечно.
- Меньше, но релевантнее — обычно лучше, чем «всё на всякий случай».
Just-in-time retrieval вместо «всё сразу»
Соблазн RAG-агента — заранее «напихать» в контекст все потенциально полезные документы. Это бьёт по бюджету и по вниманию (context rot) сразу.
Альтернатива — just-in-time retrieval: давать модели не сами данные, а инструменты и ссылки для их получения, и подтягивать конкретный фрагмент только когда он реально понадобился на конкретном шаге. Так контекст остаётся компактным и релевантным, а решение «что подтянуть» принимается по ходу задачи, а не угадывается заранее.
Это перекликается с главой 1: оркестратор может хранить артефакты вне окна (файлы, БД) и подавать их выборочно, а не держать всё в messages.
Prompt caching и KV-кэш: почему важен стабильный префикс
На уровне инференса модель считает внутреннее представление токенов — KV-кэш (ключи/значения механизма внимания). Для префикса, который не менялся между запросами, это представление можно переиспользовать вместо пересчёта. Это и есть физическая причина, почему стабильный префикс ускоряет и удешевляет инференс.
Prompt caching в API — это управляемая версия идеи: вы помечаете границы блоков cache_control, и при следующем запросе совпадающий префикс (system + tools + начало истории) читается из кэша дешевле и быстрее, а не обрабатывается заново.
Ключевые правила, вытекающие из природы префиксного кэша:
- Кэш работает только на общий префикс. Любое изменение раньше точки кэша инвалидирует всё, что после.
- Поэтому стабильное (system, спецификации tools) ставьте в начало, а изменчивое (свежие сообщения) — в конец.
- Не вставляйте динамику (таймстемпы, счётчики) в начало system — каждый такой токен ломает кэш для всего запроса.
- Точки кэша ставьте на границах стабильных крупных блоков: конец system, конец tools, конец «замороженной» части истории.
Компакция и суммаризация по триггерам
Компакция (compaction) — это сжатие истории так, чтобы освободить бюджет, не потеряв смысл. Запускать её стоит по триггеру, а не на каждом шаге:
- порог токенов — history превысила X% окна (например 70%);
- давность — сообщения старше N шагов сворачиваем в краткое резюме;
- объём результата инструмента — большой сырой ответ заменяем на выжимку + ссылку на полные данные (just-in-time).
Тонкость кэша: компакция переписывает префикс, а значит инвалидирует prompt cache. Поэтому компактировать выгодно редко и крупными порциями (по триггеру), а не понемногу каждый шаг — иначе вы постоянно сбрасываете кэш и теряете его экономию. Это пример того, как две техники главы (кэш и компакция) конфликтуют и требуют осознанного баланса.
Практика на Go: cache_control и компакция
Ниже — работа с github.com/anthropics/anthropic-sdk-go (v1.x; сверьтесь с актуальными доками по точным именам типов CacheControl и слагам моделей). Сначала — расстановка точек кэша на стабильном префиксе, затем — компакция истории по порогу токенов.
package contexteng
import "github.com/anthropics/anthropic-sdk-go"
// buildStableParams кладёт стабильное в начало и помечает точки кэша.
// Идея: кэшируем общий ПРЕФИКС, который не меняется между запросами.
func buildStableParams(systemText string, tools []anthropic.ToolParam,
history []anthropic.MessageParam) anthropic.MessageNewParams {
// system — самый стабильный блок: ставим cache_control в КОНЦЕ system.
// Всё, что раньше этой точки и совпадает с прошлым запросом, читается из кэша.
system := []anthropic.TextBlockParam{{
Text: systemText,
CacheControl: anthropic.NewCacheControlEphemeralParam(),
}}
// tools тоже стабильны: помечаем последний инструмент как границу кэша,
// чтобы префикс system+tools кэшировался целиком.
if n := len(tools); n > 0 {
tools[n-1].CacheControl = anthropic.NewCacheControlEphemeralParam()
}
return anthropic.MessageNewParams{
Model: anthropic.ModelClaudeSonnet4_5, // сверьтесь с актуальным слагом
MaxTokens: 1024,
System: system,
Tools: toUnion(tools),
Messages: history, // изменчивое — В КОНЦЕ, после точек кэша
}
}// compactByThreshold сворачивает СЕРЕДИНУ истории при превышении порога.
// Бережём начало и конец: там модель «видит» лучше (lost in the middle).
func compactByThreshold(ctx context.Context, cl anthropic.Client,
history []anthropic.MessageParam, usedTokens, windowLimit int,
summarize SummarizeFn) ([]anthropic.MessageParam, bool, error) {
// Триггер: компактируем только когда реально близко к лимиту.
if usedTokens < windowLimit*70/100 {
return history, false, nil // кэш не трогаем — выгоднее не дёргать
}
if len(history) < 6 {
return history, false, nil // нечего сворачивать
}
// Оставляем "голову" (контекст задачи) и "хвост" (свежие шаги) как есть,
// а середину заменяем кратким резюме.
head := history[:2]
tail := history[len(history)-2:]
middle := history[2 : len(history)-2]
summary, err := summarize(ctx, cl, middle) // одна выжимка вместо N сообщений
if err != nil {
return history, false, err
}
summaryMsg := anthropic.NewUserMessage(
anthropic.NewTextBlock("Резюме предыдущих шагов: " + summary),
)
out := make([]anthropic.MessageParam, 0, len(head)+1+len(tail))
out = append(out, head...)
out = append(out, summaryMsg)
out = append(out, tail...)
// Внимание: префикс изменился -> prompt cache инвалидирован.
// Поэтому компактируем РЕДКО и крупно, по триггеру, а не каждый шаг.
return out, true, nil
}Anti-patterns
| Грабля | Почему плохо | Как избегать |
|---|---|---|
| «Дать всё сразу»: напихать весь RAG в контекст | Бьёт по бюджету и по вниманию (context rot); качество падает | Just-in-time retrieval: давать инструменты для выборки, подтягивать фрагмент по необходимости |
| Динамика (таймстемп, счётчик) в начале system | Меняющийся префикс инвалидирует prompt cache на каждом запросе | Стабильное — в начало; динамику — в конец, после точек кэша |
| Компактировать историю каждый шаг | Постоянно переписывает префикс и сбрасывает кэш — экономия теряется | Компакция по триггеру (порог токенов), редко и крупными порциями |
| Важное прячется в середину длинного контекста | Эффект lost in the middle: модель хуже извлекает середину | Ключевые инструкции и вопрос — в начало (system) и в конец (последнее сообщение) |
| Тащить полные сырые результаты инструментов вечно | History раздувается, ускоряя context rot и расход токенов | Заменять большие результаты выжимкой + ссылкой на полные данные вне окна |
| Считать, что большее окно само по себе лучше | Деградация (context rot) растёт с длиной; «больше токенов» != «лучше» | Курировать контекст: минимум релевантного, компакция, выборочная подача |
Практическое задание (P2)
- Разметьте бюджет окна своего агента по зонам (system/tools/history/RAG) и зафиксируйте целевые доли в процентах.
- Вынесите всё стабильное (system, спецификации tools) в начало и поставьте точки
cache_controlна их границах. - Замерьте
cache_read/cache_creationтокены вusageдо и после и подтвердите, что префикс реально кэшируется. - Внесите динамику (таймстемп) в начало system и убедитесь по метрикам, что кэш сломался — затем верните на место.
- Реализуйте компакцию по триггеру (порог токенов) с сохранением головы и хвоста истории; убедитесь, что середина сворачивается в резюме.
- Переведите один тяжёлый источник данных на just-in-time: вместо вставки документа дайте инструмент его выборки и подтягивайте фрагмент по запросу модели.
Проверка знаний
Команда добавила в начало system-промпта строку с текущей датой и временем запроса, чтобы агент «знал, который час». После этого расходы на токены выросли, хотя промпт почти не менялся.
Что произошло и как чинить?
Верный ответ: B
B верно. Prompt caching работает только на общий префикс: любое изменение раньше точки кэша инвалидирует всё, что после. Таймстемп в начале system меняется каждый запрос, поэтому кэш ни разу не переиспользуется — отсюда рост стоимости. Динамику выносят в конец контекста (например в последнее сообщение), а стабильное — в начало. A игнорирует механику кэша. C и D не связаны с причиной.
Почему «lost in the middle» и context rot заставляют класть важное в начало и конец контекста?
Верный ответ: B
B верно. Context rot — деградация качества с ростом контекста из-за ограниченного внимания модели; «lost in the middle» — частный эффект, при котором информация в середине длинного контекста извлекается хуже, чем на краях. Поэтому ключевые инструкции и актуальный вопрос размещают в начале (system) и в конце (последнее сообщение). A неверно: API не обрезает середину. C путает с кэшем. D выдумка про цену по позиции.
Чтобы держать историю компактной, инженер запускает суммаризацию середины истории на каждом шаге агента. Качество приемлемо, но кэш-метрики показывают почти нулевой cache_read.
В чём корень проблемы и как её решить?
Верный ответ: A
A верно. Компакция переписывает префикс истории, а prompt cache живёт именно на стабильном префиксе — поэтому ежешаговая суммаризация постоянно сбрасывает кэш (cache_read около нуля) и экономия теряется. Это известный конфликт двух техник: кэш любит стабильность, компакция её ломает. Баланс — компактировать по триггеру (порог токенов), редко и крупными порциями. B выплёскивает полезную технику. C неверно: стабильная часть истории кэшируется. D не относится к делу.