AI-агенты - это цикл обмена сообщениями между пользователем и языковой моделью (LLM), где для ответа пользователю модель может обратиться к доступным ей напрямую инструментам (tools) или через настроенные для неё MCP. Каждое такое взаимодействие дописывает историю диалога. Для простоты часто считают, что вся эта история и уходит в любой следующий вызов модели - иначе она «не вспомнит», о чём шла речь, и криво соберёт ответ.
История «не резиновая» - у современных моделей контекстное окно может быть огромным, но умение работать с длинным логом сильно зависит от того, насколько он структурирован. Плюс накопленная история разговора и реальный контекст, который модель видит на очередном ходе, не всегда одно и то же. В последнее время это один из фокусов развития агентских систем в рамках context engineering: что сжать, что оставить снаружи, что подтянуть инструментами только когда нужно.
В этой статье хочу рассказать про эволюцию подхода работы с длинной историей и где мы находимся сейчас. Примеры будут на LangChain - на открытом стеке легко посмотреть реализацию, которая в готовых продуктах часто спрятана. При этом LangChain достаточно популярный, развивающийся фреймворк - остальные либо делают похожие вещи, либо сами опираются на него как на базу.
Почему история агента вообще становится проблемой
Мозг агента - большая языковая модель (LLM). Текст режется на токены, модель предсказывает следующий токен, из цепочки токенов снова собирают текст. У модели есть контекстное окно: сколько информации она может учесть, когда делает очередное предсказание.
Часто на учебных слайдах рисуют «весь вход → модель → весь ответ» и контекстным окном называют «весь вход», но генерация считает окно иначе: каждый новый токен предсказывается уже с учётом предыдущих. Для последнего токена в полном ответе контекстом будет весь вход + опционально большой блок thinking/reasoning (в UI его может и не быть) + все уже сгенерированные токены ответа, кроме самого последнего, который как раз сейчас предсказывают. Т.е. ответ сам увеличивает требования к размеру контекстного окна.
Размеры контекстных окон за последние годы сильно выросли. Когда-то нормой были тысячи токенов, потом десятки тысяч, сейчас они могут считаться в миллионах. Гонка лимитов не отменила вторую проблему: большой контекст не значит, что конкретный факт в нем так же легко найти, как в маленьком. Три чётких факта в коротком окне модель вытащит увереннее, чем их же среди тысячи похожих записей в длинном контексте. Более того, качество зависит от места данных внутри контекста. Классическая работа Lost in the Middle показывает U-образную кривую: начало и конец длинного входа используются лучше, середина - хуже.
Отсюда две причины работать над сокращением истории агента - вернее, того, что попадает в контекст. Либо окно физически кончается, и его надо как-то сжать. Либо, даже при огромном теоретическом размере контекста, много разнородной информации в контексте часто ухудшает работу с конкретными фактами из истории.
Практический вывод, который современные harness’ы уже усвоили: один бесконечный неструктурированный диалог «обо всём» - плохой режим. Удобнее держать контекст вокруг текущей задачи, а не копить годы переписки в одном потоке сообщений (messages) - даже если для пользователя снаружи это всё ещё один чат.
Пробежимся по истории эволюции подходов.
Обрезка хвоста: ещё не суммаризация, но близка по идее
Самый простой способ уменьшить контекст - «помнить только недавнее» т.е. оставить последние N сообщений, а остальное выбросить. Это как «память золотой рыбки», которая просто забывает всё, что было за пределами последних 3 секунд (чтобы не оскорбить золотых рыбок, биологи давно доказали, что память у них на самом деле гораздо лучше).

В экосистеме 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)
Следующий ход очевидный - старое не просто удалить, а сжать отдельным запросом к модели и положить в историю короткий блок сводки (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), следующим логичным шагом для агентов стало оставить завязку диалога и свежий хвост, а серединку сжать. Для агентских сценариев это часто даже важнее, чем в общих LLM-кейсах из той работы: в начале обычно лежит постановка задачи и ключевые ограничения, а середина - длинное обсуждение деталей, которое как раз удобнее свернуть.
Форма:
[начало] + [сводка середины] + [свежий хвост]

По сути это тот же приём «сжать старое в summary», только в «старое» не пускают первые N сообщений: их явно защищают. Тогда режется именно середина между защищённым началом и хвостом.
Штатный SummarizationMiddleware в LangChain так не умеет: у него есть keep (свежий хвост), но нет параметра «оставь ещё и первые N». Если такая защита нужна, её несложно дописать.
Сам подход живой, например в Hermes Agent как раз protect_first_n + защита хвоста и LLM-сводка куска между ними - сборка head / summary / tail. У OpenRouter близкий по мотиву 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 обычно только в начале разговора.

