EnglishРусский Map

Кэширование промптов в агентах

title
Кэширование промптов в агентах
type
summary
summary
Как повторное использование KV-кэша определяет стоимость, задержку и дизайн инструментов в coding-агентах, и что показывает Pi
tags
llm, coding-agent, inference, caching, cost
created
2026-07-29
updated
2026-09-14
lang
ru
source_updated
2026-09-14
translated
2026-09-14
translator
lllm/antigravity/gemini-3.7-flash-medium

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

Все примеры в тексте взяты из pi-coding-agent, а код сбора статистики кэша, на который ссылается статья, находится в earendil-works/pi, а не по пути badlogic/pi-mono, указанному на странице toolbox.

Что хранится в кэше

Трансформер обрабатывает запрос в две фазы: prefill, на которой он считывает входные токены и вычисляет состояние attention, и decode, где он генерирует новые токены по одному. На каждом слое attention каждый обработанный токен порождает ключ (key) и значение (value) - массивы чисел, а не записи в хэш-таблице. Query нового токена сравнивается с предыдущими keys, чтобы оценить релевантность каждого предшествующего токена, и эти оценки формируют взвешенную смесь соответствующих values. Сохранение этих ключей и значений, чтобы последующие токены могли обращаться к ним через attention без повторных вычислений, и называется KV-кэшем; кэширование промптов продлевает срок его жизни за пределы одной генерации.

Важное свойство: это состояние привязано к конкретному префиксу токенов. Два промпта с одинаковым смыслом, но разной токенизацией, не разделяют ничего. Измените один токен в середине - и всё, что идёт после него, станет уже другим продолжением. В kv-cache-sizing приведена арифметика расхода памяти на токен для этого состояния; само примечание в посте подчёркивает, что с обычными оптимизациями кэш длинного диалога укладывается в считанные гигабайты, что меньше, чем обычно предполагают.

Где живёт кэш и как это просачивается в поведение продукта

Есть два основных подхода к реализации. Session affinity хранит кэш на том же GPU (или рядом с ним), где он был вычислен, и направляет следующий запрос на тот же worker. Идентификатор сессии или ключ кэша выступает в роли подсказки для маршрутизации - это достаточно дёшево, чтобы обрабатывать на HTTP-балансировщике без разбора полезной нагрузки. По сети не передаются большие объёмы данных, но планирование оказывается ограниченным: worker может оказаться перегружен, перезапуститься, вытеснить запись из памяти или уступить маршрутизатору, решившему, что балансировка пула важнее кэша одной сессии. Альтернатива - распределённый кэш: хранить KV-блоки на другом уровне памяти или распределять между worker'ами, чтобы запрос не был привязан к одному GPU. Это даёт гибкость планирования и устойчивость к сбоям ценой индексации, перемещения и вытеснения блоков. Вариант с affinity сталкивается с той же проблемой адресации, что описана в stateful-agent-routing: нужно имя, позволяющее попасть в конкретный процесс, хранящий состояние, а не просто в любой здоровый бэкенд.

Сессии в Pi представляют собой деревья, а не списки, и это плохо сочетается с обоими подходами. Команда /tree перемещает активный диалог к более ранней точке и продолжает его по другой ветке; откат (rewind) отбрасывает активный суффикс, не удаляя его из файла сессии. Все ветки делят один session ID, поэтому маршрутизатор видит одну сессию, тогда как кэш видит три частично пересекающиеся последовательности токенов. Переход на соседнюю ветку может переиспользовать общий корень, если провайдер сохраняет блоки префиксов, либо не переиспользовать почти ничего, если сохраняется только самое активное продолжение. Команда /fork порождает зеркальную проблему: новый session ID с почти идентичным контекстом, в котором кэш, изолированный по ключу сессии, даже не заметит возможность переиспользования. Идентификатор сессии лишь помогает инфраструктуре угадать, куда смотреть. Но именно переиспользуемый префикс определяет, что можно пропустить.

API провайдеров предоставляют эту функциональность в двух стилях. Традиционный интерфейс Anthropic принимает явные точки останова cache_control после стабильных блоков - системного промпта, описаний инструментов, последнего кэшируемого контента - с соответствующим явным ценообразованием: вы платите за запись в кэш и выбираете уровень удержания (retention tier). Автоматическое кэширование префиксов не требует ничего из этого и само находит переиспользуемый префикс, а любой ключ кэша служит лишь подсказкой для маршрутизации, а не гарантией.

Ленивая загрузка инструментов - это баг кэширования

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

turn 1: [system][read][write][bash][conversation...........]
turn 2: [system][read][write][bash][deploy][conversation...]
                                   |
                                   old conversation is now
                                   after a mismatch

В этом и заключается ловушка систем плагинов и каталогов инструментов в стиле MCP. Загрузка инструмента только тогда, когда он понадобился, кажется эффективной, так как изначально отправляется меньше схем. Но на большинстве моделей это инвалидирует весь последующий диалог: экономия нескольких сотен токенов на схемах оборачивается повторным prefill'ом десятков тысяч токенов. Эту реальную стоимость следует сопоставлять с аргументами в пользу "MCP для доступа к сервисам" из mcp-vs-skills: большой каталог обходится дорого один раз, а меняющий структуру посреди сессии каталог обходится дорого при каждом изменении.

Более свежие API моделей предлагают аддитивную загрузку инструментов, когда инструмент становится доступным в конкретном результате вызова инструмента внутри истории, а не встраивается в исходный список, сохраняя префикс неизменным. Pi поддерживает это там, где позволяет модель: расширение, вносящее чисто аддитивное изменение через setActiveTools(), сохраняет добавленные имена в результате работы инструмента; на поддерживаемых моделях Anthropic они передаются как отложенные определения плюс tool_reference, а на моделях OpenAI - как элементы tool-search. Во всех остальных случаях при следующем запросе отправляется полный список активных инструментов, что работает корректно, но может сбросить кэш.

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

TTL и цена промаха

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

model request --> run tests for 7 minutes --> model request
                  no cache traffic here

Долгая сборка, прогон тестов, обед или пауза на чтение diff'а живут дольше этой записи. Pi придерживается пятиминутного значения по умолчанию, так как не является разрешённым клиентом для подписки Anthropic. При этом автор поста отмечает при анализе кода Claude Code, что для подписчиков Anthropic увеличивает этот таймаут до одного часа: в рамках подписки это имеет смысл, но по тарифам API за токены часто не окупается. Пользователи прямого API могут задать PI_CACHE_RETENTION=long, чтобы запросить более длительное удержание, однако это именно запрос, а не гарантия: Pi не может выбирать политику вытеснения, удерживать GPU активным или сохранять кэш открытым при отсутствии активных запросов.

Итоговый счёт складывается из разницы в тарифах между некэшированным вводом, записью в кэш и чтением из кэша. Для 100 000 токенов истории плюс короткого нового сообщения попадание в кэш тарифицирует почти весь объём по сниженной цене чтения; промах же приводит к повторной обработке всей истории по обычной цене ввода и может потребовать платы за повторную запись в кэш. Вот почему ввод команды continue после кофе-брейка может стоить дороже самого ответа модели, и почему в длинной сессии перечитывание старого контекста обходится заметно дороже генерации нового текста.

Интересы участников не всегда совпадают. Пользователям нужны попадания ради низкой задержки и цены; владельцу GPU они тоже выгодны, так как меньший prefill позволяет обрабатывать больше запросов на единицу оборудования. Но шлюз или реселлер, взимающий плату за токены ввода по обычному тарифу, может зарабатывать на промахах больше, причём сторона, управляющая маршрутизацией, за них не платит. Автор поста воздерживается от прямых обвинений во вредительстве и приходит к полезному выводу: производительность кэша должна быть наблюдаемой, а не вычисляться задним числом по неожиданно выросшему счёту. В статье также отмечается неизбежный конфликт: строгое следование кэшу лишает маршрутизатор возможности переключить запрос на более дешёвый или качественный бэкенд посреди сессии, поскольку состояние KV не переносится. Любая система, переключающая запросы между провайдерами или моделями (на чём и построены claude-code-router и cursor-bridge), разменивает попадания в кэш на свободу маршрутизации. Это тот же разрыв в метриках, который ai-token-budget-explosion описывает на корпоративном уровне, где видны общие расходы, но не стоимость отдельных задач.

Почему Pi не урезает контекст

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

one-time rewrite cost
    ~= surviving tokens after the edit * (uncached price - cache-read price)

future savings per turn
    ~= pruned tokens * cache-read price

Перезапись длинного закэшированного контекста может в моменте стоить дороже, чем урезание сэкономит за всю оставшуюся сессию. Помимо финансовых соображений, есть и поведенческий аргумент: старые результаты инструментов содержат факты, на которые модель опиралась при принятии последующих решений, и даже краткая выжимка, сохраняющая суть, может ухудшить поведение модели. Поэтому Pi поддерживает стабильную историю, работающую только на добавление (append-oriented), и оставляет сжатие (compaction) на случай реальной нехватки контекста. В статистике сессии это считается сбросом кэша, а не сбоем кэширования, поскольку осознанно создаётся новый контекст, а не случайно тарифицируется заново неизменный. Урезание всё ещё выигрывает в одном сценарии: если провайдер не даёт скидки на чтение из кэша, либо если окружение в принципе не обеспечивает высокий процент попаданий - тогда более короткий промпт хотя бы позволяет маршрутизатору свободно балансировать нагрузку. Цель формулируется как баланс, а не минимизация: контекст, переиспользование, задержка и цена рассматриваются вместе. Это служит ориентированным на кэш противовесом стремлению всё ужимать и суммаризировать в context-engineering.

Как сделать промахи наглядными

Pi заявляет, что стремится сохранять стабильные входные данные неизменными и сообщать о результате. В интерактивном футере отображаются суммарные операции чтения и записи кэша как R и W, а также CH для процента попаданий в последнем запросе. Команда /session выводит полную картину, включая оценку расходов из-за повторной тарификации при крупных промахах:

Tokens
Input: 7,129,883
  Cached: 6,776,832 (95.0%)
  Uncached: 353,051
Output: 30,013

Cost
Total: $6.054
Cache Re-billed: $0.728 (161,744 tokens, 2 misses)

Два промаха за сессию из 178 сообщений составили двенадцать процентов от её общей стоимости. Включение showCacheMissNotices в настройках заставляет Pi выводить предупреждение прямо в поток диалога после крупного промаха с оценкой повторно оплаченных токенов и долларов; при этом указывается переключение модели или простой, если их удалось зафиксировать, либо просто сообщается о промахе без догадок о внутреннем устройстве провайдера. Для Claude Code утилита tare выполняет аналогичный учёт постфактум на основе локальных логов сессий, списывая на каждый инструмент стоимость повторной отправки контекста, к которой он привёл, а не просто объём возвращённых им данных.

Финальный список типичных причин в посте полезно держать под рукой в качестве чек-листа, поскольку каждая из них связана с одним из описанных выше механизмов: простой дольше окна удержания; смена модели или провайдера (поскольку состояние KV привязано к конкретной модели и не переносится); навигация по веткам через /tree, откаты и fork'и; сжатие или ручная перезапись истории; изменения в инструментах или уровне reasoning; динамические системные промпты с временными метками или сменяющимся контекстом проекта; расширения, модифицирующие старые сообщения перед отправкой; и маршрутизация или вытеснение на стороне провайдера, когда промпт совпадает байт в байт, но нужные блоки попросту отсутствуют на узле, куда попал запрос.