Почему уплотнения недостаточно
Context Compaction выбрасывает старые результаты вызовов. Это необходимо и этого мало.
Если один результат весит пять тысяч токенов, выброс следующего уже ничего не спасает: модель их прочитала, и держать их в окне придётся ещё несколько ходов. Ущерб нанесён в момент возврата, а не в момент накопления.
Отсюда порядок: чинить надо выше по течению. Инструмент, который по умолчанию отдаёт маленький, структурированный, ограниченный результат, снимает задачу до её появления.
Где действительно появляется O(N²)
Фраза «tool calls растут квадратично» смешивает две разные величины. Если каждый из N шагов добавляет примерно c токенов и следующий запрос снова несёт всю необрезанную историю, размер одного позднего запроса растёт линейно: O(N). Квадратичной становится сумма обработанного input за весь прогон:
c + 2c + … + Nc = c · N(N + 1) / 2.
Это условная модель, а не закон любого агента. Context editing и удаление старых tool_result меняют сам рост истории; bounded output уменьшает c; programmatic tool calling не пускает промежуточные результаты в transcript; prompt caching оставляет логический размер контекста прежним, но снижает цену повторно используемого префикса. Поэтому по одной длине transcript нельзя заключить ни фактическую стоимость, ни экономию от конкретного приёма.
Делегирование шумной операции субагенту переносит её промежуточную траекторию в другое окно и возвращает наверх ограниченный результат. Это экономит родительский контекст только если родителю действительно не понадобятся детали; стоимость запуска, вход субагента и его собственные шаги остаются в полном бюджете (Agent Instruction Surfaces). Межсессионная передача состояния — отдельный контракт Session Handoff.
Контракт усечения: три обязательные части
Инструмент, способный вернуть неограниченный объём, обязан делать три вещи, и ни одну нельзя пропустить:
- Ограничить вывод разумным потолком.
- Сказать модели, что вывод усечён и насколько — иначе она считает, что видит всё.
- Дать способ добрать остальное — смещение и предел у чтения файла, более узкий шаблон у поиска.
Второй пункт — тот, ради которого заметка существует. Инструмент, который молча обрезает вывод, хуже инструмента без ограничения вовсе: модель уверена, что располагает полной картиной, и действует на неполных данных. Дефект при этом молчаливый — ни ошибки, ни исключения, ни следа в логе; наружу он выходит уже неверным решением.
Правильно устроенное сообщение об усечении несёт два действующих факта: остального больше, чем показано, и сколько именно. С этим агент может сузить поиск или пойти постранично; без числа он может только гадать.
Четвёртый выход: не резать, а переложить
Контракт выше исходит из того, что лишнее выбрасывается. Есть вариант, при котором ничего не выбрасывается: инструмент кладёт полный результат в файл и возвращает ссылку на него вместе с короткой сводкой. Потолок соблюдён, данные целы, а решение «нужны ли подробности» откладывается до момента, когда оно понадобится.
Когда это лучше усечения: результат ценен целиком и понадобится позже — выдача поиска, содержимое страницы, длинный отчёт. Когда хуже: результат нужен целиком сейчас — тогда ссылка просто добавляет лишний вызов.
Сводка при выгрузке служит навигации, а не сохранению смысла, и это надо решить явно. Цель формулируется так: помочь агенту понять, что у него уже собрано, чтобы решить, искать ли дальше, — а не сохранить содержание. Из этого следуют свойства, неочевидные при обратной цели:
- жёсткий и маленький потолок — порядка полутора сотен слов; сводка, из которой можно ответить на вопрос, для навигации избыточна, потому что снова заполняет окно;
- состав фиксирован: о чём это, какого рода информация, один-два самых значимых пункта. Не пересказ;
- имя файла тоже генерируется по содержанию и работает вторым слоем навигации: список файлов сам по себе отвечает на вопрос «что уже собрано», без чтения сводок.
Смешение двух назначений — типовая ошибка этого места. Сводка, написанная «чтобы не потерять смысл», разрастается и начинает конкурировать с оригиналом; сводка «чтобы понять, что собрано» остаётся дешёвой и позволяет держать десятки источников в поле зрения за цену одного.
Где живут выгруженные файлы — отдельное решение. Диск, песочница или поле состояния графа: последнее делает рабочую область частью снимка, поэтому откат возвращает и её (LangGraph Checkpointers).
Пятый выход: запретить дорогое чтение, а не чинить его результат
Все предыдущие приёмы работают после того, как инструмент отработал: усечь, отфильтровать по классу строки, выгрузить в файл. У них общее уязвимое место — каждый из них меняет то, по чему агент судит, и молча. Есть подход с противоположной стороны: не трогать вывод вовсе, а не дать сделать дорогой вызов.
Рабочая форма устроена так: сторож перед вызовом инструмента отклоняет чтение файла длиннее заданного числа строк и возвращает вместо результата указание, чем это чтение заменить; то же для команд оболочки, вываливающих файл целиком. Адресные чтения — с указанием смещения и числа строк — проходят нетронутыми. Команда либо не исполняется, либо исполняется полностью: промежуточного состояния «исполнилась, но вернула не то» не возникает.
Почему это стоит держать отдельным пунктом, а не разновидностью потолка:
- запрет ничего не искажает. Усечение и фильтр оставляют агента с обрезанной картиной, о полноте которой он судит по пометке; запрет оставляет его без картины, и это состояние он различает надёжно;
- он переносит решение «что именно нужно» туда, где оно принимается осознанно — агент вынужден сформулировать диапазон или запрос вместо «покажи всё»;
- он дешевле в проверке. У фильтра надо проверять, что именно он выбросил; у запрета — только что он срабатывает на большом и пропускает адресное.
Цена у приёма тоже своя, и она не в токенах: запрет ломает шаг, а не ухудшает его. Если замены нет — подходящего инструмента адресного чтения, поиска по содержимому, графа символов, — агент упирается в стену вместо того, чтобы работать с худшими данными. Поэтому запрет вводится в паре с тем, на что он перенаправляет, и никогда в одиночку.
Отсюда и разделение зон: запрет уместен на входе в контекст — там, где решается, попадёт ли сырьё в окно; усечение и фильтр — на выводах, которые уже произошли и которые нельзя не показать.
Рабочие потолки и почему они такие
| Инструмент | Потолок | Обоснование |
|---|---|---|
| чтение файла | 500 строк | хватает, чтобы понять устройство большинства файлов, и не хоронит модель |
| поиск по коду | 50 совпадений | поиск, вернувший полсотни, на вопрос ответил; пятьсот — это свалка данных |
| выполнение команды | 5000 символов | вывод обычных команд помещается, а шум установки пакетов модели не нужен |
Числа не священны. Они подобраны прогоном настоящих задач и наблюдением за тем, где становится больно: если в вашем контуре команды регулярно дают длинный и осмысленный вывод, потолок поднимают; если преобладают быстрые поиски, опускают. Универсального значения нет, есть метод — начать с этих и калибровать на своём трафике.
Ограниченный не значит крошечный. Потолок ставится там, где вывод перестаёт отвечать на вопрос и начинает заполнять окно.
Какую половину оставлять
Решение неочевидное и стоит отдельно: у вывода команды сохраняется хвост, а не начало.
Причина в том, где живёт нужное. Упавший тест печатает провалы последними. Упавшая сборка печатает ошибку последней. Трассировка стека заканчивается тем, ради чего её читают. Оставить начало значит отдать модели преамбулу и выбросить причину.
Обратное верно для инструментов, где смысл в начале — у структурированного отчёта, у файла с заголовком. Правило формулируется не как «оставляй хвост», а как «оставляй ту половину, где живёт причина решения», и для команд это хвост.
Сначала по классу строки, потом по позиции
Раздел выше решает, что делать, когда резать приходится вслепую. До него есть шаг дешевле и точнее: выбросить то, что заведомо не несёт причины решения, независимо от того, где оно лежит.
Классы строк упорядочиваются по тому, чем грозит потеря:
| Класс | Что с ним делают |
|---|---|
| ошибки, трассировки стека, падения тестов, ошибки сборки | сохраняются всегда, на любом уровне ужатия |
| подробный вывод успешных проверок | схлопывается до сводной строки |
| рутинные информационные строки | схлопываются, а при сильном ужатии выбрасываются |
| индикаторы прогресса, баннеры, псевдографика | выбрасываются первыми |
Порядок «класс, потом позиция» существенен: классовый фильтр знает, что выбрасывает, а позиционное усечение режет вслепую и с равной вероятностью срежет причину и шум. Позиционный шаг включается только тогда, когда после фильтра секция всё ещё не помещается, — и внутри секции тогда сохраняются оба конца, а вырезается середина.
Уровень ужатия — заявление о риске, а не только об экономии
Настройка агрессивности полезна не сама по себе, а тем, что уровень называется вместе с тем, чем за него платят: «продакшен, где потеря контекста критична» — «повседневная работа» — «длинные сессии, максимум экономии». Формулировка «сжимаем на 70%» этого не сообщает и потому выбирается вслепую.
Практическое следствие: у уровня должна быть явная таблица «что уцелело, что схлопнулось, что выброшено» по классам. Без неё выбор уровня — ставка, а не решение.
Возврат к оригиналу после того, как он выброшен
Третий пункт контракта усечения — «дать способ добрать остальное» — у чтения файла закрывается смещением и пределом. У вывода команды добирать неоткуда: команда отработала один раз, и не сохранённое потеряно навсегда. Значит удержание оригинала — не удобство отладки, а недостающая часть контракта именно для этого класса инструментов.
Рабочая политика удержания состоит из трёх значений, и полезно в ней среднее: не хранить никогда — хранить только для вероятных провалов — хранить всегда. Среднее ловит ровно те случаи, ради которых оригинал и понадобится, не платя за хранение успешных прогонов.
Два требования к хранилищу, без которых удержание превращается в утечку:
- секреты вычищаются до записи, а не при чтении: токены, ключи доступа и присваивания вида
token=,secret=,password=в выводе команд встречаются постоянно; - в аналитику идёт указатель, размер и хеш, но не содержимое. Иначе выброшенный из контекста объём возвращается через телеметрию и оплачивается второй раз.
Цена, которую платит агент
Ограничение вывода делает часть задач медленнее: чтобы прочитать файл на две тысячи строк, агенту нужно четыре вызова вместо одного. Это правильный размен — четыре ограниченных чтения дешевле одного огромного, потому что огромное отравляет контекст до конца сессии, а ограниченные уходят из него по мере уплотнения.
Побочный эффект приятный: пагинация заставляет агента формулировать, что именно он ищет, вместо запроса всего сразу.
Где это проверять
Дефект молчаливый и проверяется формой кода — значит годится и в пункт чеклиста, и в правило структурного поиска: вызов инструмента, возвращающий результат внешней команды или чтения без ограничения длины. Признак ищется по возврату stdout целиком либо по отсутствию среза перед возвратом.
Отдельно стоит проверять второй пункт контракта, а не только первый: ограничение без сообщения об усечении формально закрывает пункт и оставляет ровно тот дефект, ради которого он заведён.
Связано с
- Context Compaction — уборка старых результатов; здесь профилактика, и одно без другого не работает
- Tool Calling — где ставится ограничение
- Tool Catalog — контракт инструмента; усечение — его часть, а не деталь реализации
- Agent CostControl — вход дороже выхода, поэтому ограничение вывода это прежде всего деньги
- Sandbox Abstraction — второй контракт того же слоя: где инструмент исполняется
- Prompt Caching — что делать с той частью контекста, которая остаётся