Кеш промптов LLM: как одна строка ломает экономию на 98%
Почему кеширование промптов у DeepSeek, OpenAI и Anthropic отключается незаметно и как найти виновника с помощью инструмента prefixcash за пять минут.
Токен из кеша у DeepSeek дешевле обычного в 31 раз, у OpenAI и Anthropic — в 10 раз. Но кеш работает по принципу «всё или ничего»: если первые токены промпта разошлись хоть на одном символе, экономия обнуляется целиком, а не частично. Разработчик под ником Isk4R1oT показал на живых вызовах, как это происходит и как поймать проблему за пару минут.
Если вы строите агента, который дергает LLM тысячи раз в день с одним и тем же системным промптом, эта статья — про деньги, которые вы теряете прямо сейчас и, скорее всего, не замечаете.
Почему кеш вообще экономит деньги
Кеш промптов — это префиксное дерево по токенам. Провайдер сравнивает начало нового запроса с началом предыдущего: если первые N токенов совпали, эти N токенов берутся из кеша и стоят в разы дешевле. Как только встречается первое расхождение, кеш обрывается — и весь остаток промпта, сколько бы там ни было токенов, идёт по полной цене.
Цены на 27 августа 2026 года показывают масштаб разницы:
| Провайдер | Обычный токен | Токен из кеша | Разница | |---|---|---|---| | DeepSeek v4-flash | $0.44 / 1M | $0.014 / 1M | 31x | | OpenAI gpt-5.6-sol | $4.00 / 1M | $0.40 / 1M | 10x | | Anthropic Sonnet | $3.00 / 1M | $0.30 / 1M | 10x |
У DeepSeek и OpenAI кеш включается автоматически. У Anthropic его нужно запрашивать явно:
client.messages.create(
model="claude-sonnet-5",
cache_control={"type": "ephemeral"}, # без этого кеша нет вовсе
system=POLICY,
messages=messages,
)Но там, где кеш включён, он ломается совершенно одинаково — одной строкой в начале промпта.
Самая частая причина обрыва — время в системном промпте
Классический баг выглядит так:
system = f"Current time: {datetime.now()}.\n{POLICY}\n{KNOWLEDGE_BASE}"Время меняется на каждом вызове, значит префикс расходится уже на четвёртом слове. Весь промпт — включая двухтысячетокенную политику и базу знаний, которые не менялись ни разу, — каждый раз тарифицируется по полной цене.
Автор проверил это на живом support-агенте с системным блоком около 1200 токенов (DeepSeek включает кеш примерно от 1024 токенов) и четырьмя вопросами подряд. Две сессии с одинаковым содержимым, но разной сборкой промпта:
live-broken prompt=1177 cache_hit=0
live-broken prompt=1180 cache_hit=0
live-broken prompt=1179 cache_hit=0
live-broken prompt=1178 cache_hit=0
live-fixed prompt=1180 cache_hit=0
live-fixed prompt=1183 cache_hit=1152
live-fixed prompt=1182 cache_hit=1152
live-fixed prompt=1181 cache_hit=1152В первой сессии время стоит первой строкой — ноль попаданий в кеш из восьми вызовов. Во второй время перенесено в конец промпта — 1152 токена из ~1180 берутся из кеша. Это специально худший случай для наглядности: на реальных промптах динамика обычно сидит глубже, и базовый уровень кеша держится на 49–66%, а не падает до нуля.
Для агента с промптом 1200 токенов и 10 000 вызовов в день разница ощутима в деньгах:
| Провайдер | Без кеша | С кешем 98% | Экономия | |---|---|---|---| | DeepSeek v4-flash | $158 / мес | $8 / мес | −$150 | | OpenAI gpt-5.6-sol | $1 440 / мес | $173 / мес | −$1 267 | | Anthropic Sonnet | $1 080 / мес | $130 / мес | −$950 |
Если у вас datetime.now() в первой строке — вы это уже понимаете и без инструментов. Хуже, когда причина не видна глазами вообще.
Четыре бага, которые не найти чтением кода
Автор описывает общий паттерн, характерный для любого провайдера с префиксным кешем — просто на примере Anthropic он нагляднее всего:
Схемы инструментов. У Anthropic кеш покрывает tools, system, messages именно в этом порядке — схемы инструментов лежат в самом начале префикса, раньше системного промпта. Если собирать их из словаря с плавающим порядком ключей, кеш ломается в первой же позиции — и в исходном коде промпта это никак не проявляется.
База знаний из `set()`. Содержимое между вызовами одинаковое, но порядок элементов множества между процессами разный. Текст совпадает, а префикс — нет, и кеша нет.
Кеш есть в коде, а `cache_read_tokens == 0`. Сборка промпта правильная, но попаданий всё равно нет — значит истёк TTL (время жизни записи в кеше) или сам провайдер временно не кеширует запрос. Это чинится маршрутизацией трафика, а не правкой промпта.
Шаблон, который рендерится по-разному под нагрузкой. Условный блок появляется в промпте, скажем, раз в сто вызовов — найти такое чтением кода почти нереально.
Общее у всех четырёх случаев: промпт большой, изменение маленькое, глазами не находится.
Инструмент, который считает по чужим данным
prefixcash — утилита, которая не делает никаких запросов к моделям и ничего не отправляет с вашей машины. Она читает поле usage, которое провайдер и так возвращает в ответе, и сравнивает соседние вызовы в рамках одной сессии.
Формат входных данных — JSONL, по строке на вызов. Логировать нужно всего два поля: сырой usage и список messages.
resp = client.chat.completions.create(model=MODEL, messages=messages)
log.write(json.dumps({
"model": resp.model,
"session_id": session_id,
"usage": resp.usage.model_dump(), # сырой usage, парсится как есть
"messages": messages,
}) + "\n")Это пять строк поверх любого OpenAI-совместимого SDK. С LiteLLM хватит трёх:
litellm.callbacks = [PrefixCashCallback(file="metrics.jsonl")]Дальше запускается диагностика:
prefixcash diagnose --file calls.jsonl --session live-brokenДля сломанной сессии вывод покажет cacheable prefix 1%, общий совпадающий префикс всего 8 слов, а виновником будет назван dynamic_time. На карте зелёным подсвечено то, что реально ушло из кеша, красным — точка разрыва, оранжевым — весь хвост промпта после разрыва, который технически совпадает дословно, но кешу уже не достаётся, потому что он смотрит только на префикс.
После переноса времени в конец промпта та же диагностика на исправленной сессии даёт cacheable prefix 98% и общий префикс в 730 слов вместо восьми.
Инструмент умеет распознавать типовые источники расхождений: iso_datetime, datetime, date, time, uuid, hex_token, number, email, url_query, placeholder (вида {var}). Плюс два универсальных детектора — high_entropy для случайно сгенерированных строк (по энтропии Шеннона) и content_change для любого расхождения, не подошедшего ни под один шаблон. То есть даже нестандартный случай будет назван и локализован.
Как проверить свои промпты
Если вы работаете через Claude Code, интеграция вообще не нужна — лог уже лежит на диске:
pip install prefixcash
python -m examples.import_claude_code --out cc.jsonl
prefixcash report --file cc.jsonlАдаптер переносит только поля usage, тексты промптов не читает — это видно в examples/import_claude_code.py, файл на 70 строк.
Если своего пайплайна логирования нет, придётся добавить пять строк логирования и подождать реального трафика. Ждать долго не нужно: diagnose сравнивает соседние вызовы внутри одной сессии, так что вердикт появится уже на двух вызовах подряд. Не нужны тысячи запросов или сутки накопления данных — хватит одного живого диалога из двух реплик.
Для боевых логов единственное место, которое стоит проверить перед запуском на чувствительных данных, — src/prefixcash/integrations/importers.py. Это единственный файл, где читается ваш лог.
Где ломается
Главная ловушка — считать, что раз кеш «автоматический» (как у DeepSeek и OpenAI), то думать о нём не нужно. На практике порядок ключей в словаре, set() вместо списка или условный блок в шаблоне рушат экономию так же надёжно, как явный datetime.now(), только без единой видимой строчки-виновника.
Вторая ловушка — путать «сборка промпта неправильная» и «TTL кеша истёк». Если diagnose показывает высокий cacheable prefix, а cache_read_tokens всё равно ноль, чинить промпт бесполезно — нужно разбираться с маршрутизацией запросов или временем жизни записи у провайдера.
Третья — доверять чтению кода вместо реальных цифр. Баг с временем в начале строки виден глазами. Баг с плавающим порядком ключей словаря — нет, и без инструмента, считающего usage по факту, его можно не найти месяцами.
Что попробовать дальше
Добавьте логирование usage и messages в существующий пайплайн вызовов LLM, прогоните prefixcash diagnose на паре живых сессий и посмотрите на процент cacheable prefix. Если он далёк от 90-98%, ищите виновника в списке детекторов — чаще всего это время, порядок словаря или множество без сортировки. При агенте с тысячами вызовов в день счёт за месяц может отличаться в разы просто от переноса одной строки в конец промпта.
FAQ
Что такое префиксный кеш в LLM и чем он отличается от обычного кеширования ответов?
Это кеш не готовых ответов, а входных токенов промпта. Провайдер сравнивает начало нового запроса с началом предыдущего и переиспользует совпавшую часть по сниженной цене. Как только встречается первое расхождение, весь остаток промпта после этой точки считается заново по полной стоимости.
У всех ли провайдеров кеш включается автоматически?
Нет. У DeepSeek и OpenAI кеш работает без дополнительных настроек. У Anthropic его нужно явно запросить через параметр cache_control={"type": "ephemeral"} в вызове API — без этого кеша не будет вовсе.
Можно ли проверить кеш без доступа к исходным промптам?
Да, prefixcash работает только с полем usage, которое провайдер и так возвращает в ответе. Тексты промптов инструменту не нужны, сетевых запросов он не делает и ничего не отправляет со своей машины.
Сколько вызовов нужно накопить для первой диагностики?
Минимум два вызова в рамках одной сессии — diagnose сравнивает соседние запросы. Не нужно ждать день или собирать тысячи вызовов, достаточно одного диалога из двух реплик.
Что делать, если cacheable prefix высокий, а попаданий в кеш всё равно нет?
Если сборка промпта верная, но cache_read_tokens равен нулю, дело не в промпте. Скорее всего, истёк TTL записи в кеше провайдера или запрос попал в момент, когда upstream временно не кеширует — это решается маршрутизацией трафика, а не изменением кода.
Источники
Читайте также
Комментарии
Пока никто не написал. Будьте первым.
