Промпт-кэширование, батчи и оптимизация затрат

Если Messages API — это базовый двигатель, то кэширование и batch API — это трансмиссия, которая делает его пригодным для продакшн-нагрузки. Без них хороший прототип легко превращается в счёт на $500 в месяц за то, что могло бы стоить $50.


Экономика prompt caching: за что платите и сколько экономите

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

Prompt caching решает это через механизм контрольных точек (breakpoints). Вы помечаете блок с флагом cache_control, и токены до этой отметки кэшируются на инфраструктуре Anthropic.

ОперацияСтоимость (от базовой цены input)
Запись в кэш (TTL 5 мин)1,25×
Запись в кэш (TTL 1 час)2,00×
Чтение из кэша0,10×
Обычный некэшированный токен1,00×

Запись дороже — первый вызов всегда стоит больше обычного. Зато каждое последующее чтение обходится в десять раз дешевле нормы. Для системного промпта из 2000 токенов и сотни запросов в день арифметика очень быстро сходится в вашу пользу.

Синтаксис: два способа расставить breakpoints

Автоматический — самый простой вариант. Передаёте cache_control на верхнем уровне запроса, и SDK сам двигает breakpoint к последнему кэшируемому блоку по мере роста диалога:

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},   # сюда
    system="Вы — ассистент по анализу кода...",
    messages=[{"role": "user", "content": query}]
)

Явный (block-level) — когда нужно точно контролировать, что именно кэшируется. Например, большой статичный системный промпт + изменяющийся разговор:

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": large_static_instructions,   # 3000 токенов
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[{"role": "user", "content": user_query}]  # меняется
)

В одном запросе можно поставить до четырёх явных breakpoints. Это полезно в длинных диалогах: расставьте их через каждые 15–20 сообщений, иначе кэш может не найти совпадение (система смотрит только на 20 блоков назад).

Проверь себя
Вы поставили `cache_control` на последнее пользовательское сообщение в диалоге. Сработает ли кэш? Что покажет `cache_read_input_tokens` в следующем запросе?

TTL: 5 минут или 1 час

По умолчанию кэш живёт 5 минут — этого хватает для интерактивных сессий. Если промпты используются реже (пакетная обработка, ночные задачи), указывайте явный "ttl": "1h":

"cache_control": {"type": "ephemeral", "ttl": "1h"}

Запись при часовом TTL стоит 2× от базы вместо 1,25×, зато кэш переживёт паузу между запросами. Для batch-задач (о них ниже) часовой TTL — почти обязательный выбор, поскольку батч может обрабатываться дольше 5 минут.

Как проверить, что кэш работает

В ответе смотрите response.usage:

print(response.usage.cache_creation_input_tokens)  # записано в кэш
print(response.usage.cache_read_input_tokens)       # прочитано из кэша
print(response.usage.input_tokens)                  # некэшированный остаток

Если cache_read_input_tokens > 0 — попадание. Если всё в cache_creation_input_tokens — это первый вызов или кэш устарел. Если оба нуля при включённом cache_control — скорее всего, промпт меньше минимального порога (для claude-opus-4-8 нужно не менее 1 024 токенов).

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


Быстрое повторение
Во сколько раз стоит запись в ephemeral кэш (TTL 5 мин) по сравнению с обычным токеном и во сколько раз дешевле чтение из кэша?

Message Batches API: 50% скидки за асинхронность

Когда немедленный ответ не нужен — аналитика, оценка датасета, массовая генерация описаний — переключайтесь на Batches API. Он обрабатывает запросы асинхронно и стоит ровно вдвое дешевле стандартных цен на input и output.

Лимиты батча: до 100 000 запросов или 256 МБ, что наступит раньше. Большинство батчей завершается в течение часа, максимум — 24 часа. Результаты хранятся 29 дней.

import anthropic
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request

client = anthropic.Anthropic()

# Отправляем батч
batch = client.messages.batches.create(
    requests=[
        Request(
            custom_id=f"review-{i}",
            params=MessageCreateParamsNonStreaming(
                model="claude-opus-4-8",
                max_tokens=512,
                messages=[{"role": "user", "content": code_snippets[i]}],
            ),
        )
        for i in range(len(code_snippets))
    ]
)
print(f"Батч создан: {batch.id}")

Опрос статуса и получение результатов

import time

# Ждём завершения
while True:
    batch = client.messages.batches.retrieve(batch.id)
    if batch.processing_status == "ended":
        break
    print(f"Обработано: {batch.request_counts.succeeded}/{batch.request_counts.processing}")
    time.sleep(60)  # проверяем раз в минуту

# Читаем результаты (JSONL)
for result in client.messages.batches.results(batch.id):
    if result.result.type == "succeeded":
        text = result.result.message.content[0].text
        print(f"{result.custom_id}: {text[:80]}...")
    else:
        print(f"{result.custom_id}: ошибка — {result.result.error}")

Ограничения: стриминг (stream: true), fast mode и max_tokens: 0 в батчах не поддерживаются — они специфичны для синхронного режима. Для батчей с общим системным промптом используйте "ttl": "1h" в cache_control, иначе кэш сгорит до того, как все запросы из батча успеют его прочитать.

Проверь себя
Вы отправляете батч из 500 запросов с общим системным промптом на 5000 токенов. Какой TTL для `cache_control` вы выберете — 5 минут или 1 час — и почему?

Быстрое повторение
На сколько процентов дешевле обходятся запросы через Message Batches API по сравнению с синхронными запросами?

Подсчёт токенов до отправки

Сюрпризы в счёте — следствие незнания реального размера запроса. SDK предоставляет отдельный эндпоинт подсчёта без генерации ответа:

# Не генерирует ответ — только считает токены
count = client.messages.count_tokens(
    model="claude-opus-4-8",
    system=system_prompt,
    tools=tool_definitions,
    messages=conversation_history,
)
print(f"Input: {count.input_tokens} токенов")

# Проверяем, не близко ли к лимиту окна
if count.input_tokens > 180_000:
    # Пора compactить историю или использовать /compact
    truncate_history(conversation_history)

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


Стратегии снижения затрат: практический чеклист

Кэшируйте всё статичное первым. Порядок блоков в запросе имеет значение — cacheable-контент должен идти перед меняющимся. Типичный порядок: системный промпт → определения инструментов → примеры few-shot → история диалога → текущий вопрос пользователя. Breakpoint — после последнего статичного элемента.

Выбирайте модель под задачу. Haiku 4.5 стоит в 5 раз меньше Opus 4.8 по output-токенам ($5 против $25 за миллион). Для classification, извлечения структурированных данных и коротких ответов Haiku справляется отлично. Opus — для сложного рассуждения, архитектурных решений, кода со сложными зависимостями.

Batch API для всего асинхронного. Код-ревью на PR, анализ логов, пакетная генерация документации — всё это не требует мгновенного ответа и сразу получает скидку 50%.

Не засоряйте контекст. Длинная история диалога — это деньги. Используйте /compact в Claude Code и аналогичное суммирование в API-приложениях. Подробнее об управлении контекстом — в статье Управление контекстным окном.

Pre-warming кэша перед нагрузкой. Если знаете, что запросы начнутся через 10 минут — отправьте фиктивный запрос с max_tokens: 0 заранее, чтобы кэш уже был тёплым:

# Прогреваем перед пиковой нагрузкой
client.messages.create(
    model="claude-opus-4-8",
    max_tokens=0,  # не генерирует ответ
    system=[{"type": "text", "text": big_system_prompt,
             "cache_control": {"type": "ephemeral"}}],
    messages=[{"role": "user", "content": "warmup"}]
)

Мониторьте реальный cache hit rate. Добавьте логирование cache_read_input_tokens / (cache_read + cache_creation + input_tokens) в production. Если hit rate ниже 70% при высокой доле одинаковых промптов — значит, breakpoint расставлен неправильно или контент меняется незаметно.

Проверь себя
У вас есть агентный цикл с tool use: системный промпт (2000 токенов), 5 определений инструментов (1500 токенов), история диалога (растёт), текущий вопрос. Где поставить breakpoint, чтобы максимизировать cache hit rate?

Быстрое повторение
Какой hit rate кэша считается нормальным в production при высокой доле повторяющихся промптов?

See also