Почему агенты отвечают хуже по мере роста контекста
Деградация контекста (context rot) в конкретных терминах. Каждый ход — это один самодостаточный запрос к модели; длинный или насыщенный инструментами запрос измеримо снижает точность ответа. Что в этом запросе, что отбрасывается, когда он не влезает, и почему платформа пересобирает его перед тем, как писать ответ, вместо того чтобы пересказывать результаты инструментов.
Агент, который в понедельник отвечал хорошо, в пятницу отвечает расплывчато. Он перестаёт выполнять инструкцию, которую выполнял двадцать сообщений назад. Он вызывает инструмент, получает верные данные и затем пишет ответ, игнорирующий половину из них. Ничего не падает, ничего не пишется в лог, и разговор выглядит нормально. Это принято называть context rot, и привычное объяснение — «окно контекста заполнилось» — не то, что мы измерили.
Один ход, от начала до конца
Ход начинается как один запрос, разрастается, пока работают инструменты, и пересобирается перед тем, как будет написан ответ. Смотреть стоит именно на промежуточное состояние: как раз из него отвечает большинство агентов.
Каждый ход — один самодостаточный запрос
Модель не хранит между ходами ничего. На той стороне нет сессии, нет памяти о сказанном час назад и нет способа найти то, что ей не передали. На каждом ходу платформа собирает полный запрос и отправляет его целиком; ответ порождается из этого запроса и больше ни из чего.
Если у вас есть сетевая интуиция, это близко к дейтаграмме: самодостаточная, отправляется один раз, несёт всё, что нужно получателю, и ограничена размером, который отправитель обязан соблюдать. Две вещи ломают аналогию, и обе важны:
- По дороге ничего не теряется. Всё отправленное доходит. Меняется то, сколько из этого модель на самом деле удерживает во внимании — потеря происходит внутри получателя, а не на проводе.
- Компоновка меняет ответ. Маршрутизатор не ведёт себя иначе оттого, что вы переставили полезную нагрузку. Модель ведёт. Одни и те же факты, в одном и том же запросе, разложенные двумя способами, дали в одном случае верный ответ, а в другом — неверный. Из этого результата вытекает почти всё дальнейшее.
Что находится в запросе
Порядок — тот, который видит модель. Колонка «сохраняется/срезается» — приоритет, который платформа применяет, когда запрос не влезает; места относительные, а не фиксированное число блоков.
Всё, что выше истории, — одно системное сообщение. Это сделано намеренно: границы агента, ваша база знаний и язык ответа — не разговор, а модель обращается с ними по-разному в зависимости от того, где они стоят.
Что отбрасывается, когда всё не влезает
Запрос, вышедший за бюджет, не обрезается с конца. Блоки отбрасываются по приоритету, пока он не поместится:
- Сначала самая старая история — и отброшенные ходы уплотняются в накопительную выжимку, а не выбрасываются, так что сказанное сохраняется, даже если формулировки нет.
- Память с наименьшим счётом, затем найденные фрагменты с наименьшим счётом — с хвоста, чтобы самый релевантный материал уходил последним.
- Профиль, затем выжимка.
- Никогда — инструкции агента, ваша база знаний и сам вопрос. Если бюджет переполняет уже один вопрос, он обрезается и помечается как обрезанный: модели об этом сообщают, а не оставляют догадываться, почему фраза обрывается на полуслове.
Этот порядок — честная часть контекстного бюджета: что-то должно уйти, а система, которая не говорит что именно, будет тихо отбрасывать то, что случайно оказалось последним.
Влезть — не то же самое, что быть прочитанным
Вот что нас удивило. Мы собрали тест, в котором факты, нужные для ответа, присутствовали во всех условиях: ничего не отбрасывалось, ничего не обрезалось, нигде не не хватало информации. Менялось только одно — как был скомпонован запрос. Задача фильтрации по нескольким условиям на нескольких сотнях записей, на нашей собственной локальной модели и на облачной, по несколько прогонов:
| Как были разложены одни и те же факты | Верно |
|---|---|
| Одно чистое сообщение только с релевантными строками | 8 / 8 |
| Те же самые строки, возвращённые как результат инструмента | 0 / 8 |
| Полный набор записей плюс пять посторонних результатов инструментов | 0 / 6 |
| Это накопление, пересобранное в одно чистое сообщение | 8 / 8 |
Два контроля отсекают очевидные трактовки. Разбиение чистого запроса на два сообщения той же общей длины по-прежнему давало 8 / 8 — значит, дело не в длине. Возврат тех же отобранных строк в виде результата инструмента по-прежнему давал 0 / 8 — значит, дело и не в том, что данные должны быть ближе к вопросу. Строительные леса инструментов и посторонний материал конкурируют за внимание модели где бы они ни находились.
Облачная модель оказалась устойчивее, но не неуязвимой: она держалась на большинстве задач и всё равно упала до 0 / 3 на поиске одного значения, стоило добавить в запрос накопленные за ход результаты инструментов.
Два честных ограничения. Это наш собственный тест на наших задачах, а не публичный бенчмарк. И он не поднимает потолок модели: более сложный вариант той же задачи дал ноль при любой компоновке — конкретная модель просто не справлялась. Перекомпоновка возвращает точность, которую отнимала сама компоновка; она не покупает способностей, которых у модели нет.
Итак, у хода две фазы: работа и ответ
Именно это разделение показывает анимация в начале страницы.
Работе нужен «грязный» запрос. Модель должна видеть собственные вызовы инструментов и их сырые результаты, чтобы решить, вызывать ли следующий. Эта фаза не изменилась.
Ответу — не нужен. Когда инструменты отработали, платформа собирает свежий запрос: те же системные инструкции, короткая выжимка из разговора, факты, действительно найденные на этом ходу, и вопрос — и ответ, который вы получаете, порождается из него. Вызовы инструментов, сырые данные и промежуточные рассуждения в него не попадают.
Выжимка нужна потому, что настоящий вопрос часто звучит как «да» или «а второй?». Компоновка из одного сообщения, показавшая лучший результат, однохода, а разговор — нет; выжимка сворачивается в то же самое сообщение, а не восстанавливается отдельными ходами, что не стоит ничего измеримого и сохраняет разрешимость местоимений.
Чего мы намеренно не делаем
Обычное решение, когда вывод инструментов затапливает окно контекста, — поручить модели пересказывать каждый результат. Мы так не делаем, и причина — тест выше: компоновка, набравшая максимум, использовала сырые данные, пересобранные заново — никакого шага пересказа для возврата точности не потребовалось. Добавить его означало бы попросить модель решать, какие факты важны, в системе, чей принцип — не отдавать модели работу, которую код делает точно. Пересказчик, потерявший ровно то число, о котором спрашивал клиент, ошибается беззвучно и выглядит как хороший ответ.
Когда факты действительно не влезают, они обрезаются, и обрезка заявляется прямо в запросе вместе с указанием не додумывать недостающее. Агент, который говорит «я видел только первые сорок заказов», стоит больше, чем тот, кто уверенно выдумывает остальное.
Что это значит для вас
Настраивать нечего. Так работает каждый агент на платформе.
Что меняется на практике:
- Длинные разговоры остаются рабочими. Двадцатый ход собирается так же, как первый, а выпавшее из окна сохраняется как выжимка, а не исчезает.
- Инструмент, возвращающий много данных, не отравляет ответ. Результат ограничивается, а ответ пишется из запроса, в котором сырых данных уже нет.
- Ход с инструментами стоит одного лишнего вызова модели — около секунды на коротком ответе. Пока он идёт, чат показывает, чем занят, а не молчит.
Если вы всё же видите, что агент игнорирует то, что ему точно дали, полезный вопрос не «не мало ли окно контекста», а «в каком блоке это было и что ещё ехало вместе с ним в запросе». Где живёт разговор между ходами отвечает на вторую половину этой страницы: если модель ничего не хранит, что хранится на нашей стороне и из чего собирается запрос. Обоснованные ответы — о том, что находится в поиске, Память о клиенте — о том, что запоминается, а Инструменты и MCP — о бюджетах, которые не дают одному результату занять всё окно.