- BrainTools - https://www.braintools.ru -
AI-агенты – это цикл обмена сообщениями между пользователем и языковой моделью (LLM), где для ответа пользователю модель может обратиться к доступным ей напрямую инструментам (tools) или через настроенные для неё MCP. Каждое такое взаимодействие дописывает историю диалога. Для простоты часто считают, что вся эта история и уходит в любой следующий вызов модели – иначе она «не вспомнит», о чём шла речь, и криво соберёт ответ.
История «не резиновая» – у современных моделей контекстное окно может быть огромным, но умение работать с длинным логом сильно зависит от того, насколько он структурирован. Плюс накопленная история разговора и реальный контекст, который модель видит на очередном ходе, не всегда одно и то же. В последнее время это один из фокусов развития агентских систем в рамках context engineering: что сжать, что оставить снаружи, что подтянуть инструментами только когда нужно.
В этой статье хочу рассказать про эволюцию [1] подхода работы с длинной историей и где мы находимся сейчас. Примеры будут на LangChain – на открытом стеке легко посмотреть реализацию, которая в готовых продуктах часто спрятана. При этом LangChain достаточно популярный, развивающийся фреймворк – остальные либо делают похожие вещи, либо сами опираются на него как на базу.
Мозг [2] агента – большая языковая модель (LLM). Текст режется на токены, модель предсказывает следующий токен, из цепочки токенов снова собирают текст. У модели есть контекстное окно: сколько информации она может учесть, когда делает очередное предсказание.
Часто на учебных слайдах рисуют «весь вход → модель → весь ответ» и контекстным окном называют «весь вход», но генерация считает окно иначе: каждый новый токен предсказывается уже с учётом предыдущих. Для последнего токена в полном ответе контекстом будет весь вход + опционально большой блок thinking/reasoning (в UI его может и не быть) + все уже сгенерированные токены ответа, кроме самого последнего, который как раз сейчас предсказывают. Т.е. ответ сам увеличивает требования к размеру контекстного окна.
Размеры контекстных окон за последние годы сильно выросли. Когда-то нормой были тысячи токенов, потом десятки тысяч, сейчас они могут считаться в миллионах. Гонка лимитов не отменила вторую проблему: большой контекст не значит, что конкретный факт в нем так же легко найти, как в маленьком. Три чётких факта в коротком окне модель вытащит увереннее, чем их же среди тысячи похожих записей в длинном контексте. Более того, качество зависит от места данных внутри контекста. Классическая работа Lost in the Middle [3] показывает U-образную кривую: начало и конец длинного входа используются лучше, середина – хуже.
Отсюда две причины работать над сокращением истории агента – вернее, того, что попадает в контекст. Либо окно физически кончается, и его надо как-то сжать. Либо, даже при огромном теоретическом размере контекста, много разнородной информации в контексте часто ухудшает работу с конкретными фактами из истории.
Практический вывод, который современные harness’ы уже усвоили: один бесконечный неструктурированный диалог «обо всём» – плохой режим. Удобнее держать контекст вокруг текущей задачи, а не копить годы переписки в одном потоке сообщений (messages) – даже если для пользователя снаружи это всё ещё один чат.
Пробежимся по истории эволюции подходов.
Самый простой способ уменьшить контекст – «помнить только недавнее» т.е. оставить последние N сообщений, а остальное выбросить. Это как «память золотой рыбки», которая просто забывает [4] всё, что было за пределами последних 3 секунд (чтобы не оскорбить золотых рыбок, биологи давно доказали, что память [5] у них на самом деле гораздо лучше).
В экосистеме LangChain чистый rolling window для всей истории агента – обрезка списка сообщений trim_messages:
from langchain_core.messages.utils import trim_messages
trimmed = trim_messages(
messages, # исходный список сообщений истории
max_tokens=4000, # бюджет: сколько токенов максимум оставить
strategy="last", # брать с конца (свежий хвост), старое отбросить
token_counter="approximate", # как считать токены; есть несколько подходов с разным балансом скорости и точности
)
Когда такая обрезка до сих пор уместна? Возьмём простого агента, который назначает встречи в календаре. Источник правды у него – текущее состояние календаря, а не переписка годичной давности. Длинная история почти не нужна: обычно хватает недавнего куска про ход обсуждения, обсуждения вариантов и уточнение вопросов. В идеале каждый новый запрос можно было бы начинать с чистой истории, но если это сложно организовать, то небольшой хвост прошлых реплик как раз и является простой реализацией + он даёт связанность разговора.
Следующий ход очевидный – старое не просто удалить, а сжать отдельным запросом к модели и положить в историю короткий блок сводки (summary). Диалог продолжается «примерно помня», что было. Типичная форма после срабатывания:
[сводка старого] + [свежий хвост сообщений]
Оригинальная старая история теперь уже не исчезает молча – оно превращается в пересказ. Место в истории высвобождается, и смысл должен быть похож на смысл оригинальной полной истории.
В LangChain этот паттерн ближе всего к SummarizationMiddleware у create_agent: при пороге (trigger) по числу сообщений, токенов или доле окна старая часть уходит в вызов суммаризатора, хвост задаётся политикой keep.
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model=model, # основная модель агента
tools=tools, # инструменты агента
middleware=[
SummarizationMiddleware( # middleware сжатия истории через сводку
model=model, # может быть отдельная (часто более дешёвая) модель для сводки
# summary_prompt=..., # свой промпт суммаризатора; если не указать - дефолт из middleware
trigger=("tokens", 4000), # когда сжимать: порог по токенам / messages / fraction
keep=("messages", 20), # сколько свежего хвоста оставить после сводки
),
],
)
В специализированных агентах часто задают свой промпт суммаризатора: что именно оставлять, а что можно свернуть. При этом мы платим отдельным вызовом модели за то, чтобы сократить занятый контекст, и впервые принимаем непростое решение: текст стал короче ценой возможной потери детали.
Когда разобрались, что модели хуже достают факты из середины длинного входа (Lost in the Middle [3]), следующим логичным шагом для агентов стало оставить завязку диалога и свежий хвост, а серединку сжать. Для агентских сценариев это часто даже важнее, чем в общих LLM-кейсах из той работы: в начале обычно лежит постановка задачи и ключевые ограничения, а середина – длинное обсуждение деталей, которое как раз удобнее свернуть.
Форма:
[начало] + [сводка середины] + [свежий хвост]
По сути это тот же приём «сжать старое в summary», только в «старое» не пускают первые N сообщений: их явно защищают. Тогда режется именно середина между защищённым началом и хвостом.
Штатный SummarizationMiddleware в LangChain так не умеет: у него есть keep (свежий хвост), но нет параметра «оставь ещё и первые N». Если такая защита нужна, её несложно дописать.
Сам подход живой, например в Hermes Agent [6] как раз protect_first_n + защита хвоста и LLM-сводка куска между ними – сборка head / summary / tail. У OpenRouter [7] близкий по мотиву middle-out / context compression: края берегут, середину ужимают, чтобы влезть в окно; там чаще truncate/сжатие середины, а не обязательно отдельный вызов суммаризатора – но форма «режем середину, края важнее» та же.
У всех вариантов со сводкой (summary) вместо оригинальной истории одна общая проблема – любое сжатие через LLM это сжатие с потерями (lossy). Модель-суммаризатор решает, что будет важно для продолжения разговора. Она может угадать, и всё будет хорошо, может не угадать и выкинуть нужное, а может перефразировать историю так, что для будущего хода смысл просто поменяется. Ведь сводка – это просто применение определённого промпта к истории. Часть проблем снимается, если агент специализированный под конкретные задачи и вы сами в промпте суммаризатора прописали, что важно оставлять, а что нет, но даже это не всегда спасает.
Без сводки длинный агентский разговор может упереться в окно. Со сводкой он продолжает работать, но уже по «пересказу исходного диалога». Если ваш диалог – каталог артикулов, «сжать без потери смысла» почти невозможно: смысл и есть перечень.
Отсюда развилка. Можно считать, что история сообщений и есть единственная память, и продолжать её переписывать всё более умными сводками. Можно развести два понятия: накопленная информация и рабочий контекст следующего вызова модели (некий view на накопленное).
Инженерия контекста как раз про второй путь: не обязательно скармливать модели всё, что у вас есть. Можно оставить данные снаружи и дать инструменты, чтобы подтянуть нужное по запросу.
На суммаризации истории это выглядит так.
История сообщений копится как раньше – во внутреннем состоянии агента. Пока она небольшая, в контекст вызова модели уходит всё из этого состояния. Как только срабатывает порог «слишком много для окна», harness разделяет накопленное и то, что увидит модель:
Выбирает кусок, который пора сжать в сводку – например всё старше последних N сообщений.
Строит сводку по этому куску и кладёт её в состояние агента (это ещё не контекст вызова, а заготовка для view).
Сам выбранный кусок (или полный лог) сохраняет во внешнее хранилище: файл на backend / виртуальной FS агента – чтобы сырьё не исчезло.
В следующий вызов модели собирает view: свежий хвост (и иногда начало) + сводка вместо вытесненного куска. Полная история формально никуда не делась; в окно идёт собранный срез.
Агенту оставляют инструменты вроде read_file / grep, чтобы при необходимости открыть файл полной истории и достать деталь, которой нет в текущем контексте.
В прошлом году LangChain выпустил дополнительную библиотеку Deep Agents (расширение поверх core функционала). Именно такой подход реализован в классе SummarizationMiddleware: вытесненное дописывается, например, в /conversation_history/{thread_id}.md, а канонический state["messages"] может оставаться непереписанным – меняется то, что уходит в model call. Точные дефолты порогов зависят от версии и профиля модели; смысл паттерна стабильнее имён параметров.
Забавный факт: в базовом LangChain и в Deep Agents слой часто называется одинаково – SummarizationMiddleware, но то что они делают внутри, это вообще абсолютно разные подходы, импорт из другой библиотеки и ваш код работает уже иначе, что не очень хорошо. Знакомые убили несколько часов на дебаг этой проблемы.
Именно сюда эволюция суммаризации свернула после активного развития идеи context engeneering – сводка (summary) перестаёт быть единственной правдой о прошлом. Она становится рабочим сжатием для окна, а сырая история может оставаться отдельным артефактом, куда агент может заглянуть с помощью доступных ему инструментов.
Жать активный контекст – уже обязательная часть современных агентов: без неё сложно держать длинные диалоги (хотя авторы обычно учат пользователей не решать все вопросы в одном чате, чтобы прибегать к ней меньше). Суммаризация – это часть более широкого тренда по управлению контекстом: разделять что агент делает и знает (полная история, большие результаты tools, skills, субагенты со своим контекстом), и что реально кладёт в контекст следующего вызова. Для пользователя это часто неочевидно – в UI видна вся переписка, а в таком виде она попадает в model call обычно только в начале разговора.
Автор: StasCh
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/34323
URLs in this post:
[1] эволюцию: http://www.braintools.ru/article/7702
[2] Мозг: http://www.braintools.ru/parts-of-the-brain
[3] Lost in the Middle: https://aclanthology.org/2024.tacl-1.9/
[4] забывает: http://www.braintools.ru/article/333
[5] память: http://www.braintools.ru/article/4140
[6] Hermes Agent: https://www.intraview.ai/explore/NousResearch/hermes-agent/tours/context-compression/
[7] OpenRouter: https://openrouter.ai/docs/guides/features/message-transforms
[8] Источник: https://habr.com/ru/articles/1069780/?utm_campaign=1069780&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.