Разработка ИИ-агентов · Модуль 2 · Урок 2.2
Управление контекстом: окно, обрезка, суммаризация, внешнее состояние
Контекстное окно — ограниченный и платный ресурс
У модели есть контекстное окно — максимум токенов, которые она видит за один запрос (история + системный промпт + схемы инструментов + текущий ввод). Окно конечно, и за каждый токен вы платите при каждом вызове. В агенте история растёт с каждым оборотом цикла, поэтому без управления контекстом агент рано или поздно упрётся в лимит или станет неоправданно дорогим и медленным.
Кроме того, есть эффект «потери в середине» (lost in the middle): чем длиннее контекст, тем хуже модель замечает детали в его середине. Больше контекста ≠ лучше. Цель — держать в окне только то, что реально нужно для следующего шага.
Три приёма: обрезка, суммаризация, внешнее состояние
- Обрезка (truncation) — самый простой приём: оставляем системный промпт и последние N сообщений, старое отбрасываем. Подходит, когда давняя история не важна. Минус — теряются факты из начала диалога.
- Суммаризация — когда история разрослась, просим модель сжать её в краткое резюме и заменяем им старые сообщения. Факты сохраняются, токены экономятся; минус — резюме может потерять нюанс, и это лишний вызов модели.
- Внешнее состояние — выносим данные из диалога наружу: в переменные, файлы, БД. В контекст кладём не полный текст, а ссылку/идентификатор («заказ #4471 сохранён»), а полный объект достаём инструментом по запросу. Это самый масштабируемый приём.
На практике их комбинируют: свежие сообщения — как есть, давние — в резюме, крупные данные — во внешнем хранилище.
Считать токены и кешировать
Чтобы управлять контекстом, его надо измерять. Грубая прикидка: токен ≈ несколько символов (для русского — заметно меньше, чем для английского), но для точности используйте токенайзер/счётчик провайдера. Заведите бюджет: «не больше X токенов истории» — и применяйте обрезку/суммаризацию при его превышении.
Отдельный рычаг — prompt caching: если большой стабильный кусок (системный промпт, длинная инструкция, справочные данные) повторяется из запроса в запрос, его можно пометить как кэшируемый, и провайдер не будет тарифицировать его заново по полной. Держите стабильную часть в начале — так она лучше кешируется. (Детали и заголовки кеша версионно-зависимы — сверяйтесь с доками провайдера.)
// trimHistory держит системные сообщения и последние maxTail реплик.
func trimHistory(messages []Message, maxTail int) []Message {
var system, rest []Message
for _, m := range messages {
if m.Role == "system" {
system = append(system, m)
} else {
rest = append(rest, m)
}
}
if len(rest) > maxTail {
rest = rest[len(rest)-maxTail:] // оставляем хвост
}
return append(system, rest...)
}
// Замечание: при обрезке нельзя «разрывать» пару assistant(tool_calls) → tool.
// Обрезайте по границам завершённых обменов, иначе провайдер вернёт ошибку.// summarizeOld заменяет старую часть диалога кратким резюме от модели.
func summarizeOld(messages []Message, keepTail int) ([]Message, error) {
if len(messages) <= keepTail+1 {
return messages, nil
}
head := messages[:len(messages)-keepTail] // то, что сжимаем
tail := messages[len(messages)-keepTail:] // свежее оставляем как есть
summary, err := callLLM([]Message{
{Role: "system", Content: "Сожми диалог в 5–7 пунктов: факты, решения, открытые вопросы."},
{Role: "user", Content: renderDialog(head)},
}, nil)
if err != nil {
return messages, err
}
sumMsg := Message{Role: "system", Content: "Резюме предыдущего диалога:\n" + summary.Content}
return append([]Message{sumMsg}, tail...), nil
}Anti-patterns
| Анти-паттерн | Почему плохо | Как правильно |
|---|---|---|
| Складывать всю историю без ограничений | Упор в лимит окна, рост цены и задержки | Бюджет токенов + обрезка/суммаризация по мере роста |
| «Больше контекста — лучше» | Эффект lost in the middle, шум, цена | В окне — только нужное для следующего шага |
| Обрезать посреди пары assistant→tool | Невалидная история — ошибка провайдера | Резать по границам завершённых обменов |
| Держать крупные данные прямо в диалоге | Дорого и нечитаемо | Внешнее состояние: в контексте — ссылка/id, данные — по инструменту |
Практическое задание
- Добавьте в агент подсчёт примерного размера истории и установите бюджет токенов.
- Реализуйте обрезку, сохраняющую
systemи не разрывающую парыassistant(tool_calls) → tool. - Реализуйте суммаризацию давней части истории и подставьте резюме вместо старых сообщений.
- Перенесите один крупный результат инструмента во внешнее состояние, а в контекст положите только id.
- Если провайдер поддерживает prompt caching — пометьте стабильный системный промпт как кэшируемый и сравните стоимость.
Проверка знаний
Почему «затолкать в контекст как можно больше» — плохая стратегия?
Верный ответ: B
B верно. Больше токенов — дороже, медленнее и хуже по качеству из-за эффекта lost in the middle. Нужно держать в окне только релевантное. C — неверно, модель видит весь вход, но хуже замечает середину.
В чём смысл приёма «внешнее состояние»?
Верный ответ: B
B верно. Внешнее состояние снимает нагрузку с окна: модель оперирует идентификаторами, а полный объект подтягивается инструментом при необходимости. Это самый масштабируемый из трёх приёмов.
Что опасно сделать при обрезке истории?
Верный ответ: B
B верно. Если оставить запрос вызовов без результатов (или наоборот), история станет невалидной и провайдер вернёт ошибку. Резать нужно по границам завершённых обменов.