Проблема: платим за одно и то же
Посмотрим на экономику агента честно. Типичный production-агент возит с собой постоянный «багаж»: системная инструкция (500 токенов), описания 10 инструментов (2000), выдержка из базы знаний или few-shot примеры (5000). Итого ~7500 токенов, которые идентичны в каждом запросе.
Теперь вспомним из прошлого урока: агент на один запрос пользователя делает несколько шагов reasoning, и на каждом шаге отправляет весь контекст заново. Пять шагов — пять раз по 7500 токенов постоянной части. Тысяча пользователей в день — и вы оплатили миллионы токенов за пересылку одного и того же текста.
Чтобы сгенерировать ответ, модель сначала «прочитывает» весь промпт, вычисляя внутреннее представление (attention-состояние) для каждого токена. Это prefill — самая тяжёлая часть обработки входа. Ключевой момент: представление токена зависит только от него и всего, что идёт до него. Значит, если у двух запросов одинаковое начало, то и вычисленное состояние для этого начала одинаково — его можно сохранить и переиспользовать вместо повторного счёта. Отсюда все свойства кэша: он работает только на префиксе и только при точном совпадении.
Как работает кэш префикса
Механика в трёх состояниях. Первый запрос записывает кэш (это чуть дороже обычного), последующие — читают его (сильно дешевле и быстрее).
Отсюда — главное практическое правило: расположи промпт так, чтобы неизменное шло первым, а всё, что меняется от запроса к запросу, — в самом конце. Тогда граница кэша проходит как можно правее, и переиспользуется максимум токенов.
Самая частая и самая обидная ошибка: подставить в системный промпт что-то меняющееся — текущее время, дату, ID пользователя, случайный элемент. Один такой токен в начале делает префикс уникальным при каждом запросе, и кэш не срабатывает никогда: вы платите полную цену плюс наценку за запись, получая ноль экономии. Если время/пользователь нужны — переноси их в конец промпта, ближе к вопросу.
Anthropic и OpenAI: два подхода
Идея у всех одна, реализация различается — и это влияет на код.
| Anthropic | OpenAI | |
|---|---|---|
| Включение | Явно: помечаешь блоки cache_control | Автоматически для длинных промптов |
| Контроль | Ты сам решаешь, где граница кэша | Провайдер решает сам |
| Запись в кэш | Дороже обычного входного токена | Без отдельной наценки |
| Чтение из кэша | Во много раз дешевле обычного входа | Заметно дешевле обычного входа |
| Минимальный размер | Есть порог в токенах | Есть порог в токенах |
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[
{"type": "text", "text": SYSTEM_PROMPT}, # статика
{
"type": "text",
"text": KNOWLEDGE_BASE, # большой статичный блок
"cache_control": {"type": "ephemeral"}, # ← ГРАНИЦА КЭША здесь
},
],
# всё ПОСЛЕ границы меняется от запроса к запросу и не кэшируется
messages=[{"role": "user", "content": user_question}],
)
u = resp.usage
print(u.cache_creation_input_tokens) # записали в кэш (1-й раз)
print(u.cache_read_input_tokens) # прочитали из кэша (дальше) ← цель
print(u.input_tokens) # посчитано заново (не кэшировано)
У Anthropic ты сам выбираешь, где провести границу. Логика: пометить нужно конец той части, которая гарантированно неизменна, — обычно это system + инструменты + база знаний. Всё до метки кэшируется целиком. Если поставить метку слишком рано (например, только после короткого system), большой справочник будет пересчитываться каждый раз, и вы потеряете основную экономию. У OpenAI этого выбора нет — тем важнее правильный порядок блоков.
Кэш и агенты: растущий диалог
У агентов кэширование даёт особенно большой выигрыш, но требует понимания одной детали. Агент на каждом шаге дописывает в конец: новый tool-вызов, новый ответ инструмента. То есть промпт шага N — это промпт шага N−1 плюс хвост. А это ровно тот случай, для которого кэш префикса создан: предыдущая часть совпадает дословно.
Шаг 1: [system+tools+KB] + [вопрос]
└─ кэш записан ─┘
Шаг 2: [system+tools+KB] + [вопрос] + [tool_call] + [tool_result]
└──── кэш-хит: не пересчитываем ────┘ └─ новое ─┘
Шаг 3: [system+tools+KB] + [вопрос] + [tool_call] + [result] + [ещё шаг]
└────────── кэш-хит растёт вместе с диалогом ──────────┘
→ чем длиннее работает агент, тем БОЛЬШЕ доля кэшированного
(при условии: ничего в начале не менялось!)
Отсюда важное следствие для практики из прошлого урока: суммаризация истории ломает кэш. Когда вы схлопываете старые сообщения в сводку, начало промпта меняется — весь накопленный кэш обесценивается. Это не значит «не сжимать»: это значит делать сжатие редко и предсказуемо (по порогу), а не на каждом шаге, чтобы кэш успевал окупаться между перестройками.
Кэшированный префикс хранится ограниченное время (порядка минут, у разных провайдеров и опций по-разному) и продлевается при обращении. Практические следствия: кэш отлично работает для активного диалога и потока запросов, но бесполезен для редких обращений раз в час — там каждый запрос будет промахом (а у Anthropic ещё и с наценкой за запись). Точные значения TTL, порогов и коэффициентов цены смотри в актуальной документации провайдера — они меняются.
Как измерить эффект
Кэширование — единственная оптимизация, где легко думать, что она работает, хотя она не работает. Поэтому обязательна метрика — cache hit rate: какая доля входных токенов пришла из кэша.
def cache_stats(usage) -> dict:
cached = usage.cache_read_input_tokens # прочитано из кэша
fresh = usage.input_tokens # посчитано заново
total = cached + fresh
return {
"cache_hit_rate": cached / total if total else 0,
"cached_tokens": cached,
"fresh_tokens": fresh,
}
# логируем в structured-логи и на дашборд (разделы Observability)
log.info("llm_call", **cache_stats(resp.usage), model="claude-sonnet-5")
# ориентиры:
# hit rate ~0 → кэш НЕ работает: ищи динамику в начале промпта
# hit rate 0.7–0.9 → отлично, платишь за вход в разы меньше
# ⚠️ смотри hit rate ПОСЛЕ первого запроса: первый всегда write, не read
Выведи cache_hit_rate на дашборд рядом с cost (урок про дашборды): падение hit rate — это ранний сигнал, что кто-то добавил динамику в начало промпта, и счёт вот-вот вырастет в несколько раз. Так же полезно связать это с A/B: любое изменение структуры промпта стоит проверять не только на качество, но и на сохранность кэша.
Типичные ошибки
Время, дата, user_id, случайные элементы в system делают префикс уникальным — кэш не срабатывает никогда. Всё изменчивое переноси в конец.
Если история или запрос стоят раньше большого справочника, кэшировать почти нечего. Порядок: system → tools → база знаний → история → запрос.
Легко думать, что кэш включён, когда он молча промахивается. Логируй cache_read/input_tokens и выводи hit rate на дашборд.
При обращениях раз в час префикс успевает протухнуть (TTL) — каждый раз промах, а у Anthropic ещё и наценка за запись. Кэш окупается на потоке и активных диалогах.
Каждая перестройка начала промпта обнуляет накопленный кэш. Сжимай по порогу и редко, чтобы кэш успевал окупаться.
Шпаргалка
PROMPT CACHING = переиспользование обработанного ПРЕФИКСА промпта
экономия до ~90% на входных токенах + быстрее ответ (пропускается prefill)
ПОЧЕМУ РАБОТАЕТ: представление токена зависит от него и ВСЕГО, что ДО него
→ одинаковое начало = одинаковое состояние = можно сохранить
→ отсюда: только ПРЕФИКС и только ТОЧНОЕ совпадение
ГЛАВНОЕ ПРАВИЛО ПОРЯДКА:
статичное → В НАЧАЛО · изменчивое → В КОНЕЦ
system → tools → база знаний/few-shot → история → текущий запрос
❌ одно изменение в начале обесценивает ВЕСЬ кэш после него
ПРОВАЙДЕРЫ:
Anthropic — ЯВНО: cache_control на блоке = граница кэша · запись дороже, чтение сильно дешевле
ставь метку после САМОГО БОЛЬШОГО статичного блока
OpenAI — АВТОМАТИЧЕСКИ для длинных промптов · контроля нет → важен ПОРЯДОК
у обоих: минимальный порог токенов · точные цифры — в актуальной доке
АГЕНТЫ: каждый шаг ДОПИСЫВАЕТ в конец → предыдущая часть совпадает → кэш-хит РАСТЁТ
⚠️ суммаризация истории ЛОМАЕТ кэш → сжимай по порогу и РЕДКО
TTL: префикс живёт минуты, продлевается при обращении
✓ поток запросов / активный диалог · ✗ редкие обращения раз в час (всегда промах)
ИЗМЕРЯЙ: cache_hit_rate = cache_read / (cache_read + input_tokens)
~0 → кэш НЕ работает (ищи динамику в начале) · 0.7–0.9 → отлично
на дашборд рядом с cost: падение hit rate = ранний сигнал роста счёта
ГРАБЛИ: динамика в начале · изменчивое перед статичным · не мерить hit rate ·
кэш на редких запросах · суммаризация на каждом шаге
Практическое задание
Задание: включи и проверь кэш
- Разложи промпт своего агента по блокам и отметь: что неизменно между запросами, а что меняется. Правильный ли сейчас порядок?
- Найди динамику в начале промпта (время, дата, user_id, случайное). Перенеси её в конец — что изменится для кэша?
- Включи кэширование: для Anthropic — поставь
cache_controlпосле самого большого статичного блока; для OpenAI — просто выстрой порядок блоков правильно. - Сделай два одинаковых запроса подряд и сравни
cache_creation/cache_read/input_tokens. Сработал ли кэш на втором? - Посчитай cache hit rate для многошагового прогона агента. Растёт ли доля кэша с каждым шагом?
- Прикинь экономию: сколько токенов постоянной части × число шагов × число запросов в день? Во сколько раз падает счёт при hit rate 0.8?
- Со звёздочкой: добавь суммаризацию истории (из прошлого урока) и посмотри, как проседает hit rate в момент сжатия. Подбери порог сжатия так, чтобы кэш успевал окупаться.
Что дальше
Контекст оптимизирован, за повторы больше не переплачиваем. Пора выпустить агента наружу — обернуть его в API, к которому подключится фронтенд. У агентских API своя специфика: ответы долгие (нужен стриминг), задачи фоновые, соединения — длительные. Следующий урок — FastAPI для агентских API: streaming, background tasks и WebSocket.