Проблема: платим за одно и то же

Посмотрим на экономику агента честно. Типичный production-агент возит с собой постоянный «багаж»: системная инструкция (500 токенов), описания 10 инструментов (2000), выдержка из базы знаний или few-shot примеры (5000). Итого ~7500 токенов, которые идентичны в каждом запросе.

Теперь вспомним из прошлого урока: агент на один запрос пользователя делает несколько шагов reasoning, и на каждом шаге отправляет весь контекст заново. Пять шагов — пять раз по 7500 токенов постоянной части. Тысяча пользователей в день — и вы оплатили миллионы токенов за пересылку одного и того же текста.

ℹ️ Почему модель вообще может это кэшировать

Чтобы сгенерировать ответ, модель сначала «прочитывает» весь промпт, вычисляя внутреннее представление (attention-состояние) для каждого токена. Это prefill — самая тяжёлая часть обработки входа. Ключевой момент: представление токена зависит только от него и всего, что идёт до него. Значит, если у двух запросов одинаковое начало, то и вычисленное состояние для этого начала одинаково — его можно сохранить и переиспользовать вместо повторного счёта. Отсюда все свойства кэша: он работает только на префиксе и только при точном совпадении.

Как работает кэш префикса

Механика в трёх состояниях. Первый запрос записывает кэш (это чуть дороже обычного), последующие — читают его (сильно дешевле и быстрее).

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Кэш работает на ПРЕФИКСЕ — до первого различия Запрос 1 (cache write) system описания tools база знаний / few-shot вопрос A полная цена + наценка за запись ◄ этот префикс сохранён в кэш ► Запрос 2 (cache read) — префикс ТОТ ЖЕ system описания tools база знаний / few-shot вопрос Б кэш-хит: ~10% цены, быстрее ответ бледное = взято из кэша, заново НЕ считается и почти не оплачивается ❌ Запрос 3 — изменили НАЧАЛО (дата в system) system + время описания tools база знаний вопрос В ВЕСЬ кэш промахнулся одно изменение в начале обесценивает ВСЁ, что идёт после него ПРАВИЛО ПОРЯДКА: статичное → в НАЧАЛО, изменчивое → в КОНЕЦ system → tools → база знаний → история → текущий запрос

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

❌ Динамика в начале промпта убивает кэш

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

Anthropic и OpenAI: два подхода

Идея у всех одна, реализация различается — и это влияет на код.

AnthropicOpenAI
ВключениеЯвно: помечаешь блоки cache_controlАвтоматически для длинных промптов
КонтрольТы сам решаешь, где граница кэшаПровайдер решает сам
Запись в кэшДороже обычного входного токенаБез отдельной наценки
Чтение из кэшаВо много раз дешевле обычного входаЗаметно дешевле обычного входа
Минимальный размерЕсть порог в токенахЕсть порог в токенах
Anthropic: явная пометка кэшируемого префикса
python
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] + [ещё шаг]
        └────────── кэш-хит растёт вместе с диалогом ──────────┘

→ чем длиннее работает агент, тем БОЛЬШЕ доля кэшированного
  (при условии: ничего в начале не менялось!)

Отсюда важное следствие для практики из прошлого урока: суммаризация истории ломает кэш. Когда вы схлопываете старые сообщения в сводку, начало промпта меняется — весь накопленный кэш обесценивается. Это не значит «не сжимать»: это значит делать сжатие редко и предсказуемо (по порогу), а не на каждом шаге, чтобы кэш успевал окупаться между перестройками.

⚠️ Кэш живёт недолго — учитывай TTL

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

Как измерить эффект

Кэширование — единственная оптимизация, где легко думать, что она работает, хотя она не работает. Поэтому обязательна метрика — cache hit rate: какая доля входных токенов пришла из кэша.

Считаем cache hit rate и экономию
python
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: любое изменение структуры промпта стоит проверять не только на качество, но и на сохранность кэша.

Типичные ошибки

❌ Ошибка 1: динамика в начале промпта

Время, дата, user_id, случайные элементы в system делают префикс уникальным — кэш не срабатывает никогда. Всё изменчивое переноси в конец.

❌ Ошибка 2: изменчивое перед статичным

Если история или запрос стоят раньше большого справочника, кэшировать почти нечего. Порядок: system → tools → база знаний → история → запрос.

❌ Ошибка 3: не мерить cache hit rate

Легко думать, что кэш включён, когда он молча промахивается. Логируй cache_read/input_tokens и выводи hit rate на дашборд.

❌ Ошибка 4: кэш на редких запросах

При обращениях раз в час префикс успевает протухнуть (TTL) — каждый раз промах, а у Anthropic ещё и наценка за запись. Кэш окупается на потоке и активных диалогах.

❌ Ошибка 5: суммаризация истории на каждом шаге

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

Шпаргалка

Prompt caching — всё в одном месте
text
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 ·
        кэш на редких запросах · суммаризация на каждом шаге

Практическое задание

Задание: включи и проверь кэш

  1. Разложи промпт своего агента по блокам и отметь: что неизменно между запросами, а что меняется. Правильный ли сейчас порядок?
  2. Найди динамику в начале промпта (время, дата, user_id, случайное). Перенеси её в конец — что изменится для кэша?
  3. Включи кэширование: для Anthropic — поставь cache_control после самого большого статичного блока; для OpenAI — просто выстрой порядок блоков правильно.
  4. Сделай два одинаковых запроса подряд и сравни cache_creation / cache_read / input_tokens. Сработал ли кэш на втором?
  5. Посчитай cache hit rate для многошагового прогона агента. Растёт ли доля кэша с каждым шагом?
  6. Прикинь экономию: сколько токенов постоянной части × число шагов × число запросов в день? Во сколько раз падает счёт при hit rate 0.8?
  7. Со звёздочкой: добавь суммаризацию истории (из прошлого урока) и посмотри, как проседает hit rate в момент сжатия. Подбери порог сжатия так, чтобы кэш успевал окупаться.

Что дальше

Контекст оптимизирован, за повторы больше не переплачиваем. Пора выпустить агента наружу — обернуть его в API, к которому подключится фронтенд. У агентских API своя специфика: ответы долгие (нужен стриминг), задачи фоновые, соединения — длительные. Следующий урок — FastAPI для агентских API: streaming, background tasks и WebSocket.