← Блог·(обновлено: 25 сентября 2026 г.)0df9d04

До какого предела оптимизировать контекст агента

Агент проводит перекличку по спринту: кто владеет задачей, в каком она состоянии, насколько срочная. Ответить на это можно одним запросом списка. Трекер возвращает полные записи задач по восемь полей в каждой, и все эти поля целиком попадают в контекст модели:

[
  {
    "assignee": "heidi",
    "created": "2025-11-24",
    "description": "The our caching layer has issues when token expiration. We should rewrite it to improve reliability.",
    "id": "TASK-0001",
    "name": "Implement metrics dashboard endpoint",
    "priority": "critical",
    "status": "in-progress",
    "updated": "2025-11-27"
  },
  {
    "assignee": "rosa",
    "created": "2025-04-22",
    "description": "The the worker pool has issues when failover events. We should decouple it to support horizontal scaling.",
    "id": "TASK-0002",
    "name": "Extract input sanitization filters",
    "priority": "critical",
    "status": "open",
    "updated": "2025-04-29"
  }
]

Дальше идут ещё три записи такой же формы. На пяти строках весь ответ занимает 1732 байта и 485 токенов в кодировке cl100k_base. Перекличке хватило бы четырёх полей на запись: id, status, assignee, priority. Остальные четыре, включая свободное текстовое description, агент оплатил токенами и тут же выбросил. Тела записей набиты синтетическим наполнителем, здесь важны только их размер и форма.

С этого разрыва и начинается оптимизация контекста. Инженерно интереснее другое: где она заканчивается. Какие уровни окупают свою сложность, а какой из них настолько мал, что обвязка вокруг него стоит дороже сэкономленных токенов.

Статья берёт один зафиксированный сценарий и проводит его по лестнице из шести представлений одних и тех же строк, на четырёх размерах выборки. От ступени к ступени меняются только набор полей и способ записи, сами строки не меняются никогда. Все числа ниже получены на фикстурах, записанных в репозиторий, и посчитаны пакетом tiktoken 0.14.0 в кодировке cl100k_base. Фикстуры, скрипт измерения, тесты и сырые результаты воспроизводимы. Проценты ниже округлены до целых, точные значения и знаменатель каждого из них есть в записанных результатах. Ни одна модель не вызывалась, ни один хост MCP не запускался, ни один боевой API не затрагивался. Поэтому это подсчёты для конкретной нагрузки и конкретного токенизатора, а не оценка экономии для чужой задачи.

Шесть ступеней над одними и теми же строками:

Ступень Что это 5 строк 20 строк 100 строк 500 строк
L0 отформатированный JSON, все восемь полей 485 1957 9836 48 933
L1 сжатый JSON, все восемь полей 346 1398 7037 34 934
L2 отформатированный JSON, четыре поля 186 742 3688 18 433
L3 сжатый JSON, четыре поля 107 423 2089 10 434
L4 заголовок и строки значений, четыре поля 65 239 1154 5748
L5 L4 с однобуквенными именами в заголовке 63 237 1152 5746

Уровень первый: пусть вызывающий сам назовёт поля

Вызывающая сторона уже знает, какие поля ей нужны. Слой запросов, который принимает такой выбор, может вернуть ровно эти четыре поля. Идею сделал привычной GraphQL, здесь она сведена к одной строке, которую можно передать из командной оболочки:

list() { id status assignee priority }

Ситуация: агенту нужна доска спринта с минимальным контекстом, достаточным для переклички. Ожидаемое поведение: в ответе есть четыре названных поля для каждой записи и нет остальных.

[{"assignee":"heidi","id":"TASK-0001","priority":"critical","status":"in-progress"},{"assignee":"rosa","id":"TASK-0002","priority":"critical","status":"open"}]

Если способ записи не трогать и убрать только четыре лишних поля, ответ на тех же строках дешевеет так:

Шаг урезания Знаменатель 5 строк 20 строк 100 строк 500 строк
L1 к L3, сжатый сжатая полная запись 69% 70% 70% 70%
L0 к L2, отформатированный отформатированная полная запись 62% 62% 63% 62%

На ста строках сжатый ответ худеет с 7037 токенов до 2089, а отформатированный с 9836 до 3688. Точные значения идут от 69,08 до 70,31 процента и от 61,65 до 62,51 процента по четырём размерам выборки.

Выбор полей даёт самое большое сокращение во всём измерении, и это единственный уровень, выигрыш которого растёт вместе с объёмом данных: каждое описание, каждая отметка времени и каждое название, о которых агент не спросил, просто не попадают в ответ и ничего не стоят. Это же единственный уровень, который меняет содержимое ответа. Измерение проверяет, что урезанный ответ содержит ровно выбранные поля и ничего сверх, и ничего не утверждает про четыре отброшенных поля. Поэтому у каждой строки таблицы явно назван знаменатель: процент урезания имеет смысл только относительно полной записи в том же способе записи.

Одна оговорка нужна прямо здесь. Это урезание выбрасывает длинное свободное текстовое description и оставляет четыре коротких поля. Урезание, которое оставило бы описание и выбросило четыре коротких поля, сэкономило бы заметно меньше. Это проценты для данного сценария, а не норма, которую стоит переносить куда-то ещё.

Знаменатель решает, кому достанется заслуга

Два порядка приводят к одной и той же нагрузке: сначала сжать пробелы, потом урезать поля, или сначала урезать, потом сжать. Накопленная экономия совпадает, а вот какой из двух шагов запишет её на свой счёт, зависит от порядка:

Порядок Первый шаг Второй шаг Итого
сжать, потом урезать L0 к L1: 28% L1 к L3: 70% 79%
урезать, потом сжать L0 к L2: 63% L2 к L3: 43% 79%

На ста строках урезание убирает 70 процентов записи, которая уже сжата, и 63 процента записи, которая ещё отформатирована. Операция та же, отличается только запись, от которой считают процент. Ни одно из чисел не является неверным. Неверным является число, названное без своего знаменателя.

Именно здесь живёт отраслевой фольклор про сжатие вывода для агентов. Привычное число от 70 до 90 процентов близко к двум настоящим величинам этого сценария и не равно ни одной: урезание сжатой полной записи убирает от 69 до 70 процентов, а весь путь от отформатированного полного JSON до урезанного компактного вида убирает от 87 до 88 процентов. Обе величины принадлежат этому сценарию с выбором четырёх полей из восьми, этому набору фикстур и этому токенизатору.

Уровень второй: назвать каждое поле один раз

Выбор полей убирает лишние поля. Дальше расход зависит от того, как записаны оставшиеся. В сжатом JSON имя каждого из четырёх полей повторяется в каждой записи, так что сто записей выписывают четыре имени четыреста раз. Заголовок со строками значений называет каждое имя один раз:

id,status,assignee,priority
TASK-0001,in-progress,heidi,critical
TASK-0002,open,rosa,critical
TASK-0003,review,ivan,critical
TASK-0004,review,bob,medium
TASK-0005,open,heidi,low

Строки несут те же четыре поля, что и урезанный JSON, и измерение раскодирует компактный вид обратно в записи фикстуры, чтобы это подтвердить.

Набор полей тот же, четыре поля сценария. Меняется только способ записи, с урезанного JSON на заголовок со строками значений, то есть шаг L3 к L4:

Размер выборки Сжатый урезанный JSON Заголовок и строки значений Сокращение
5 107 65 39%
20 423 239 43%
100 2089 1154 45%
500 10 434 5748 45%

Это то сравнение, которого прошлая версия статьи сделать не могла. В её фикстуре не было компактного ответа с урезанными полями, поэтому она сравнивала способы записи при одном наборе полей и урезание при одном способе записи, а совмещать их отказывалась. В этой лестнице набор полей между L3 и L4 один и тот же, так что сравнение меняет только способ записи и ничего больше. Точные значения идут от 39,25 до 44,91 процента.

У способа записи есть предел, которого нет у выбора полей: выигрыш растёт с числом имён полей и знаков вокруг них, а не с объёмом данных за ними. Больше всего выигрывают широкие записи с короткими значениями. Меньше всего выигрывает ответ, в котором преобладает длинный свободный текст, потому что этот текст остаётся нагрузкой при любом способе записи. Лестница показывает это прямо: сжатие пробелов в полной записи, где описания ещё на месте, убирает от 28 до 29 процентов токенов, а то же сжатие урезанной записи, где длинного поля уже нет, убирает от 42 до 43 процентов.

Если пройти весь путь целиком, от отформатированного полного JSON до компактного вида с четырьмя полями, то есть от L0 к L4, ответ теряет от 87 до 88 процентов токенов. На ста строках 9836 токенов превращаются в 1154.

Что каждый переход стоит один раз

Меньший ответ экономит токены на каждом запросе, а сессия в целом выходит в плюс только после того, как эта экономия покроет входные токены, которые агент потратил на текст, прочитанный ради самого перехода на новую ступень. Считать этот разовый расход надо так: переход стоит ровно столько, сколько агент должен прочитать ради целевой ступени и не читал ради исходной. Текст, который агент читает ради обеих ступеней, лежит по обе стороны перехода и в расход не входит.

Каждую ступень этой лестницы выдаёт один и тот же слой запросов, поэтому ответ со схемой агент читает по обе стороны любого перехода. Прочитав его один раз, агент не платит ничего сверх ни за то, чтобы написать урезанный запрос вместо полного, ни за то, чтобы попросить компактный вид вместо JSON. Измерены три текста, которые агент читает один раз за сессию: ответ со схемой для этого сценария стоит 535 токенов, для поставляемого примера утилиты командной строки он стоит 1154 токена, а словарь однобуквенных имён стоит 21 токен.

Переход Списывается Читается на обеих ступенях Разовый расход Точка безубыточности
урезание, L1 к L3 ничего ответ со схемой 0 1 запрос
компактный вид, L3 к L4 ничего ответ со схемой 0 1 запрос
однобуквенные имена, L4 к L5 словарь имён ответ со схемой 21 11 запросов

Добавочный расход есть только у перехода к однобуквенным именам: словарь имён агент читает дополнительно. Урезание и компактный вид не требуют читать ничего нового, поэтому в сессии, которая и так работает через слой запросов, они бесплатны и окупаются с первого же запроса.

Прошлая версия этой статьи списывала весь ответ со схемой, 535 токенов, на счёт урезания и компактного вида, как будто сессия с полными запросами его не читает. На пяти строках урезание экономит 239 токенов на ответ, а компактный вид ещё 42, и из такого счёта выходили точки безубыточности в три и тринадцать запросов и отрицательный итог для компактного вида в любой сессии короче тринадцати запросов. Те числа считали расходом перехода ответ, который агент читает по обе стороны того же самого перехода, и они отзываются.

Один вопрос эта лестница снять не может, и его стоит назвать, потому что именно его задают чаще всего: стоит ли вообще переходить на слой запросов. В лестнице нет ступени без этого слоя: даже полная запись L1 приходит из того же слоя запросов, а не из обычного REST-ответа. Поэтому измерение не говорит, окупится ли ответ со схемой по сравнению с обычным клиентом, которому контракт не нужен: у такого сравнения другая точка отсчёта, и здесь её никто не измерял. Что лестница показывает, так это более узкую и всё же полезную вещь: агенту, который уже работает через слой запросов, урезание и компактный вид не стоят ни одного лишнего токена. Да и ответ со схемой агент оплачивает, только если сам решит её запросить: когда грамматика описана в файле навыка, этот расход переезжает в файл навыка, а не исчезает.

Уровень третий: те же два решения поверх MCP

Ни один из двух уровней не привязан к какому-то одному транспорту. Инструмент, опубликованный поверх протокола MCP, принимает тот же аргумент выбора полей и возвращает то же представление, потому что fields в его входной схеме является обычным аргументом инструмента. Урезание относится к решениям автора сервера, а не к возможностям протокола, и ничто в этом измерении не говорит, что MCP не умеет урезать ответ или отдавать компактный вид.

Весь этот раздел надо читать с одной оговоркой. Контракты инструментов, которые здесь сравниваются, построены из настоящего ответа со схемой, а не сняты с работающего сервера: ни один хост не запускался, ни один SDK не исполнялся, ни одна запись обмена не снималась. В записанных результатах это отмечено отдельной пометкой: синтетический контракт, а не измерение хоста. Ревизию протокола мы не приняли на веру, а проверили: 24 сентября 2026 года путь latest в спецификации перенаправлял на ревизию 2026-07-28, а контрольная ревизия, которой не может существовать, отвечала кодом 404, так что перебой на сайте нельзя было прочитать как отозванную ревизию.

Сколько агент читает до первого вызова, зависит от того, насколько подробно сервер описывает свои инструменты, поэтому опубликованы два профиля контракта. Скромный профиль содержит только обязательные поля описания инструмента и то, что ответ со схемой и так публикует. Полный профиль добавляет схему выходных данных, которую нынешняя ревизия протокола ожидает от сервера со структурированными ответами; она выведена из того же списка полей.

Контракт Инструментов Профиль Список инструментов MCP Ответ со схемой Разница
сценарий 3 скромный 471 535 -64
сценарий 3 полный 676 535 +141
пример утилиты 9 скромный 1490 1154 +336
пример утилиты 9 полный 1774 1154 +620

У этого сравнения нет устойчивого победителя. Для контракта сценария знак разницы меняется вместе с профилем: скромный список инструментов на 64 токена короче ответа со схемой, полный на 141 токен длиннее. Значит, по расходу на описание инструментов не выигрывает ни одна сторона, и любое утверждение в любую сторону обязано называть свой профиль. Зато на каждом измеренном профиле исключена народная цифра в две-три тысячи токенов мёртвого груза за сессию для набора инструментов такого размера: самое большое значение, измеренное здесь, равно 1774.

Настоящая разница не в размере расхода, а в моменте, когда его оплачивают. Хост MCP вкладывает список инструментов в контекст в начале каждой сессии, спросил агент или нет, а ответ со схемой агент оплачивает, только когда сам его запросит. Это различие в поведении хоста, и данное измерение его не оценивает: как конкретный хост кеширует описания инструментов, куда их кладёт и сколько за них берёт, здесь не измерено.

Один довод в пользу компактного вида существует только внутри MCP. Результат инструмента сервер возвращает строкой внутри JSON, поэтому каждая кавычка в самой нагрузке экранируется и оплачивается второй раз. Ниже надбавка MCP над тем же ответом, который слой запросов отдал бы напрямую, по способам записи нагрузки:

Нагрузка Кавычек на 5 строках Кавычек на 500 строках Надбавка на 5 строках на 500 строках
L4, компактный вид 0 0 +51 +51
L3, сжатый урезанный JSON 80 8000 +59 +1049
L1, полный сжатый JSON 160 16 000 +69 +2049

В компактном виде кавычек нет, поэтому надбавка на любом размере выборки равна ровно 51 токену: столько стоит сама обёртка результата. Всё сверх этого растёт вместе с числом кавычек, а не с размером нагрузки: компактный ответ на пятистах строках в пятьдесят три раза больше ответа JSON на пяти строках, а надбавка у него меньше. Иначе говоря, JSON внутри MCP платит за экранирование, которого не платит, когда утилита командной строки просто пишет его в стандартный вывод.

Уровень четвёртый: сокращение заголовка и почему дальше нечего сокращать

После выбора полей и способа записи остаётся одна заметная глазу цель. Строка заголовка всё ещё выписывает четыре имени полей целиком. Если заменить их одиночными буквами, строка станет короче:

i,s,a,p
TASK-0001,in-progress,heidi,critical
TASK-0002,open,rosa,critical

Строки значений побайтово те же, что в предыдущем разделе. Изменился только заголовок.

Размер выборки Заголовок с полными именами Сокращённый заголовок Экономия
5 65 63 2 токена (3,1%)
20 239 237 2 токена (0,8%)
100 1154 1152 2 токена (0,2%)
500 5748 5746 2 токена (0,03%)

Два токена на ответ, сколько бы строк ни шло за заголовком. Причина в структуре: предыдущий уровень уже вынес имена полей из строк в заголовок, так что сокращать осталось одну строку, и её доля в ответе тает по мере роста числа строк. Более раннее исследование на этой же библиотеке замерило ту же картину на более широком заголовке: пять токенов на восемь столбцов, и тоже постоянная величина.

Эти два токена не бесплатны. Агент, встретивший в заголовке s и p, должен узнать, что они значат, а словарь, который его этому учит, стоит 21 токен. Словарь агент читает один раз за сессию, а по два токена экономит на каждом ответе, поэтому однобуквенные имена выходят в плюс только с одиннадцатого запроса и никогда не набирают заметной суммы: десять запросов по пять строк дают минус один токен, сто запросов дают плюс 179, а один ответ на пятистах строках и так стоит 5748 токенов.

Хуже арифметики здесь возможные ошибки. Если словарь частично вытеснился из контекста, заголовок по-прежнему разбирается, но агент понимает его уже иначе. Два инструмента могут назначить одной букве разные поля. Схема, где рядом живут s, sc, sp и st, заставляет сверяться со словарём при каждом чтении. Каждый из этих случаев превращает экономию в два токена в вопрос о правильности ответа, а такой размен плох при любой длине сессии.

Поэтому полезный итог этого уровня не в его балансе, а в границе, которую он показывает: как только формат называет каждое поле один раз, от задачи остаются последние доли процента, и сокращение самих имён становится первым местом, где оптимизация приносит меньше токенов, чем добавляет неоднозначности. Однобуквенные имена мы не поставляем, и у этой ступени нет рабочей реализации.

Где проходит граница в настоящем адаптере

Все уровни выше описывают ответ таким, каким его видит вызывающая сторона. Сколько работы при этом сделал источник данных, они не говорят, а именно от этого зависит, где должен жить выбор полей.

Когда API источника принимает выбор полей, адаптер пробрасывает его дальше, и источник действительно делает меньше работы. Наш адаптер к Jira отображает выбранные поля на имена полей REST в APIFieldsFromSelector, оба обработчика, get и list, передают этот список клиенту, а GetIssue кладёт его в запрос:

q := url.Values{}
if len(fields) > 0 {
    q.Set("fields", strings.Join(fields, ","))
}

Выбор доходит до сервера-источника. Ответ становится меньше ещё до того, как его увидит адаптер.

Адаптер к Confluence показывает другую форму, и внутри неё одно частичное исключение. Его обработчик get выводит из выбора полей одно логическое значение и передаёт его вниз:

includeBody := containsField(ctx.Statement.Fields, "body")
page, err := client.GetPage(pageID, includeBody)

До API доходит только это значение. В pages.go оно решает, отправит ли вызов второй версии API параметр body-format вообще и допишет ли вызов первой версии тело страницы к своему списку expand, который в остальном не меняется. Остальной выбор полей так и не покидает обёртку. У обработчика list нет и этого: он забирает все страницы, которые вернуло пространство, и уже потом оставляет выбранные поля. Агент всё равно получает только их, и экономия контекста настоящая. Но источник выполнил почти ту же работу, что и без выбора полей.

Здесь и проходит честная граница обёртки. Выбор полей на стыке с агентом всегда экономит контекст агента, а работу источника экономит только там, где адаптер пробрасывает выбор в запрос к источнику. Ни одна из двух форм не дефект: вторая версия API Confluence просто не даёт для этих вызовов параметра выбора полей. Но это два разных утверждения, и сливать их в один тезис, что урезание ускоряет всё подряд, нельзя. Именованные наборы полей лишь укорачивают запись частых выборок и этого факта не меняют.

В agentquery, библиотеке на Go, на которой стоят оба адаптера, оба уровня занимают немного кода. FormatCompact пишет заголовок, а за ним строки, выборщик полей применяет урезание, а разборщик запросов принимает несколько выражений через точку с запятой, так что пачка запросов укладывается в один вызов. Экономит ли такая пачка токены, зависит от того, сколько хост берёт за обвязку одного вызова, а этого измерение не наблюдает.

Чего это измерение не показывает

Один сценарий, одно распределение размеров полей, один токенизатор. Урезание выбрасывает одно длинное свободное текстовое поле и оставляет четыре коротких; урезание с обратным выбором сэкономило бы заметно меньше, поэтому каждый процент выше следует читать с оговоркой про этот сценарий. Ни сервера REST, ни передачи по сети, ни задержек, ни расходов на стороне источника: здесь всё сводится к подсчёту токенов в записанном ответе. Ни один хост не измерялся, ни для MCP, ни для слоя запросов, так что обвязка хоста, каркас вызова инструментов и текст системной подсказки остались несчитанными. Контракт MCP построен из настоящего ответа со схемой, а не снят с работающего сервера, и оба его профиля дают только нижнюю границу: настолько короткий контракт, как скромный, ни один настоящий сервер нынешней ревизии не опубликует, а полный добавляет к скромному только выведенную схему выходных данных, тогда как настоящий сервер описывает свои инструменты подробнее. Привязка к ревизии свежа на дату проверки, а не на все времена. Отбор строк намеренно оставлен за пределами лестницы, потому что он меняет набор строк, а вся её посылка в том, что набор строк не меняется. И у ступени с однобуквенными именами нет рабочей реализации.

Что из этого строить

Кладите выбор полей в API источника, если он ваш. Только когда аргумент с полями доходит до запроса на источнике, источник действительно делает меньше работы, и только эта экономия растёт вместе с данными, а не со схемой. Делайте компактный вид ответом по умолчанию там, где ответ читает агент: при том же наборе полей он убирает от 39 до 45 процентов того, что стоил урезанный JSON, а агенту, который уже работает через слой запросов, не стоит ни одного лишнего токена. Предлагайте оба уровня поверх того транспорта, который нужен вашим вызывающим сторонам, включая MCP: ни один из уровней не привязан к транспорту.

Называйте каждый процент вместе с его знаменателем. Одно и то же урезание в этом сценарии убирает 70 процентов сжатой записи или 63 процента отформатированной, и число, ушедшее гулять без знаменателя, кто-нибудь отнесёт не к той записи.

Когда API чужой или когда встроенный выбор полей ещё впереди, обёртка остаётся верным решением с одной честной оговоркой: она бережёт контекст модели и ничего не обещает про нагрузку на сервер-источник. Говорите прямо, что из двух вы поставляете.

И останавливайтесь перед заголовком. Два токена на ответ при любом размере выборки против словаря на 21 токен, который агенту надо держать в контексте, и против целого класса ошибок разбора, которых у правильного формата нет: так выглядит оптимизация, которой больше нечего убирать из структуры. Когда единственной оставшейся целью стала одна строка, работа осталась где-то в другом месте.