Суть
Каждый запрос биллится по полному контексту (см. Context Window), а system-промпт часто один и тот же от запроса к запросу. Если положить статику в начало и закэшировать, повторные запросы переиспользуют префикс — платишь только за новую (динамическую) часть.
Зачем это нужно
Это прямой рычаг unit-экономики агента (см. Agent CostControl): инструкции, структура документов, правила компании, few-shot примеры — всё это неизменно и не должно пересчитываться/переоплачиваться на каждом вызове.
Как работает
- Раздели статику и динамику: неизменное (роль, правила, описание формата/документа, примеры) → в системный промпт (кэшируемый префикс); конкретные данные запроса → в пользовательскую часть.
- Порядок важен: кэшируемая статика впереди, динамика в конце — иначе кэш «ломается».
- Anthropic в «Prompting 101» прямо рекомендует держать статическую информацию (например, структуру страховой формы) в system prompt именно ради prompt caching.
- Связка с Prompt Engineering: «анатомия промпта» (статичные блоки впереди) сама располагает к кэшированию.
- Правило «статика вверх, динамика вниз»: положишь динамику (лог ошибок, свежие данные) перед статикой — кэш слетает на каждой итерации, цена ×10. Конкретика: скидка 90% = $0.30 vs $3.00 за 1M входных токенов (ставка того поколения моделей, сегодня другая — соотношение ×0.1 устойчивее абсолютных цифр); на локальной модели TTFT падает с ~10 c до ~0.5 c. Критично для агента в большой кодовой базе (50k+ строк).
Пример
«Статика вверх, динамика вниз»: неизменный префикс (system prompt, инструменты, документы, few-shot) — в начало (кэшируется), меняющаяся часть — в конец. Ключ кэша = хеш статического префикса.
def build_cache_aware_prompt(static_blocks, dynamic_blocks):
# статика (роль, правила, документы, few-shot) — В НАЧАЛЕ: префикс кэшируется
static_text = "\n\n".join(f"### {t}\n{c}" for t, c in static_blocks)
# динамика (текущий запрос, свежий контекст) — В КОНЦЕ: меняется каждый вызов
dynamic_text = "\n\n".join(f"### {t}\n{c}" for t, c in dynamic_blocks)
full = static_text + "\n\n--- DYNAMIC ---\n\n" + dynamic_text
cache_key = hashlib.sha1(static_text.encode()).hexdigest()[:16] # ключ = хеш статич. префикса
return {"prompt": full, "cache_key": cache_key}
Правило 16 токенов и конфликт с реестром промптов
Кэш префикса работает по хэшу: движок инференса хэширует статическую часть промпта и переиспользует уже посчитанный KV-кэш. Отсюда неочевидное следствие — любая динамика в начале промпта убивает кэш целиком. Строка вида Привет, {{ client_name }}! в первой позиции меняет хэш на каждом запросе, и кэш инвалидируется всегда.
vLLM кэширует блоками по 16 токенов. Практический приём: выравнивать статическую часть промпта строго по границе блока, добивая пробелами, а все динамические переменные переносить в конец контекста. Тогда общий префикс совпадает у всех запросов, и попадание в кэш становится правилом, а не случайностью.
Это гранулярность своего инференса, а не порог API-провайдера — у того минимум длины отдельный и много больше, см. раздел «Минимум провайдера и цена записи».
Отдельный архитектурный конфликт, который из этого вырастает. Бизнесу нужна горячая замена системных промптов без пересборки образов — отсюда паттерн реестра промптов (Langfuse, Promptfoo). Но реестр добавляет сетевой оверхед в 50–150 мс на запрос, а его содержимое меняется независимо от кода. Компромисс — держать реестр за локальным кэшем с TTL порядка пяти минут: промпт можно поменять на лету, но каждый запрос за ним не ходит.
Как раскладывать промпт под кэш
Правило одно и следует прямо из механики: кэшируется префикс, поэтому промпт собирают от самого стабильного к самому изменчивому. Системные инструкции и описания инструментов — в начало; профиль пользователя и состояние сессии — дальше; текущий запрос и свежие результаты вызовов — в конец. Любая переменная величина, попавшая в начало (метка времени, случайный идентификатор, счётчик), обнуляет попадание для всего, что за ней следует, и это самая частая причина, по которой hit rate оказывается низким при формально «стабильном» промпте.
Отсюда же конфликт с реестром промптов, разобранный выше: версионирование, вставляющее что-то в начало, ломает кэш ровно тем же способом.
Замеренный случай: критерий в хвост промпта
Раскладка «стабильное вверх, изменчивое вниз» звучит как совет; вот прогон, где её применили сознательно и измерили эффект.
Задача — оценка агентных траекторий судьёй. Один промпт верификации несёт две полные траектории, около 80 тысяч токенов, и одна и та же пара пересчитывается по нескольким критериям и нескольким повторам. Естественная раскладка — критерий в начале — рушит кэш: у каждого критерия свой префикс, и переиспользовать нечего.
Сделано наоборот, и вещей ровно две:
- критерий поставлен в хвост, так что задача, обе траектории и шкала оценки образуют общий префикс для всех критериев и повторов;
- на каждый различный префикс сначала прогоняется один запрос до конца, и только потом остальные пускаются веером — иначе параллельные запросы уходят раньше, чем префикс попал в кэш, и все промахиваются.
Результат: попадание в кэш 5.2% → 78.4%, некэшированный вход меньше примерно в 3.4 раза.
Второй пункт — тот, который обычно теряют. Первый (стабильное вперёд) в базе уже записан; прогрев одного запроса перед веером — отдельный приём, и без него правильная раскладка сама по себе не срабатывает при параллельном пуске.
Минимум провайдера и цена записи — два места, где «скидка 90%» перестаёт работать
Минимальный кэшируемый префикс — не 16 токенов. Правило 16 токенов из раздела выше — размер блока в vLLM, то есть гранулярность выравнивания на своём инференсе. У API-провайдера отдельно задан минимум длины, ниже которого префикс не кэшируется вовсе, и он в десятки и сотни раз больше: у Anthropic на 2026-08 это 512 токенов (Opus 5, Fable 5), 1024 (Sonnet 5, Opus 4.8), 2048 (Opus 4.7), 4096 (Opus 4.6, Haiku 4.5). Промпт короче минимума проходит как обычный и ошибки не возвращает — пометка cache_control просто ни на что не влияет. Единственная проверка — поля ответа: если cache_creation_input_tokens и cache_read_input_tokens оба нули, кэширования не было.
Запись в кэш дороже обычного ввода. Скидка ×0.1 относится только к чтению. Запись стоит ×1.25 от базовой ставки ввода при пятиминутном TTL и ×2 при часовом. Отсюда точка окупаемости: префикс, прочитанный ровно один раз, обходится дороже, чем без кэша вообще, и выигрыш начинается со второго чтения. Для агента, который крутит один системный промпт в цикле, это не ограничение; для редких одиночных запросов с большим префиксом — прямой убыток.
Конкретные ставки и TTL — параметры провайдера, они меняются: сверять по документации на момент использования, а в системе полагаться на измеряемый hit rate, а не на записанное здесь число.
Hit rate как эксплуатационная метрика
Кэш стоит измерять, а не подразумевать. Норма для хорошо спроектированного агента — 80–90% попаданий; Claude Code держит в проде 92%, и просадка ниже разбирается как инцидент, а не как колебание.
Это разумный подход: hit rate — опережающий индикатор. Он падает раньше, чем вы увидите рост счёта и латентности, и обычно указывает на конкретную поломку — кто-то вставил динамику в начало префикса, поменял системный промпт или изменил порядок блоков.
Второй эффект, менее очевидный. Чтение из кэша не засчитывается в лимит входных токенов в минуту. При 80% попаданий лимит в 2 млн токенов в минуту превращается примерно в 10 млн эффективных — то есть кэш работает не только скидкой к цене (чтение обходится порядка десятой части полной стоимости), но и множителем к пропускной способности. Против упирания в лимиты есть два рычага: поднять tier и завести второго провайдера; хороший кэш добавляет третий.
Как держать кэш горячим на практике: стабильный префикс, изменения дописывать только в конец, запросы пускать сериями, пока префикс ещё горячий. Порядок блоков в промпте — это не стилистика, а деньги и лимиты.
Альтернативный взгляд: аппаратная оптика FinOps
Контекстно-инженерная подача объясняет кэширование через правила разметки: держи префикс стабильным, дописывай только в конец, не переставляй инструменты местами. FinOps-заход объясняет то же самое через устройство GPU — и из этого следуют другие приоритеты.
Обработка входа (prefill) параллельна и хорошо утилизирует тензорные ядра, поэтому кэшируемый префикс стоит дёшево изначально, а с попаданием в кэш — почти ничего. Генерация выхода последовательна и упирается в пропускную способность памяти, поэтому не кэшируется в принципе. Разбор физики — в Prefill Decode Asymmetry.
Отсюда переформулировка привычных правил в финансовых терминах:
- Динамический
Session IDили timestamp в первой строке системного промпта — финансовая дыра. Не «неоптимально», а прямой множитель к счёту: ломается весь кэш префикса, каждый вызов оплачивается по полной ставке. - Порядок величин экономии. 50 шагов агента с работающим кэшем — экономия около 147k токенов по полной ставке; на длинных агентных циклах это основная статья.
- Статический префикс проектируется, а не получается. System instructions, определения инструментов и golden few-shots (3000+ токенов) сознательно собираются в неизменяемый блок, а RAG-чанки, сообщение пользователя и временные метки уходят в динамический суффикс.
Связано с
- Context Window — кэш снижает стоимость повторного префикса в окне
- Prompt Engineering — статичные блоки промпта впереди → кэшируемы
- Agent CostControl — prompt caching как способ резать затраты
- Semantic Cache — кэш по смыслу запроса: отменяет сам вызов, а не удешевляет его