Alejandro Rioja.
AI Agents Operations

Кэширование промптов в Claude API: снижаем затраты на ввод без смены модели

Alejandro Rioja
Alejandro Rioja
8 мин чтения
TL;DR

Кэширование промптов снижает стоимость больших стабильных вводных данных — вашего системного промпта, определений инструментов, few-shot примеров — примерно до 10% от обычной цены на ввод при повторных запросах. Механизм — это совпадение префикса: поставьте маркер cache_control в конце стабильного контента и держите всё изменчивое после него. Ошибка, которая убивает процент попаданий в кэш, — это когда временная метка или UUID просачивается в префикс.

Бесплатная рассылка

Каждую среду. 28 400+ читателей. Никакой воды.

Table of contents

Open Table of contents

Что на самом деле делает кэширование промптов

Каждый вызов Claude API отправляет токены. Без кэширования каждый токен в вашем запросе — системный промпт, определения инструментов, few-shot примеры и сообщение пользователя — оплачивается по обычной ставке за ввод. С кэшированием префикс этих токенов сохраняется на серверах Anthropic после первого запроса. На последующих запросах, которые разделяют этот точный префикс, вы платите цену чтения из кэша вместо повторной обработки токенов с нуля.

Разница в стоимости реальна:

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

Инвариант совпадения префикса

Это то единственное правило, из которого следует всё остальное: ключ кэша — это совпадение префикса вашего отрендеренного промпта.

Серверы Anthropic хранят отрендеренный контент от начала вашего промпта до маркера cache_control. Чтобы при следующем запросе произошло попадание в кэш, каждый токен от начала промпта до этого маркера должен быть идентичным — побайтово.

Порядок рендеринга для совпадения префикса такой: tools → system → messages. То есть сначала хэшируется массив ваших инструментов, затем системный блок, затем сообщения по порядку.

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

На что ставить маркер кэша

Цели с наибольшей отдачей такие:

1. Ваш системный промпт

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

typescript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: `You are a content operations agent for alejandrorioja.com.
Your job is to draft blog posts in Alejandro's voice: direct, practitioner, 
first-person, numbered lists, honest caveats. No hedging. No filler. 
Every section must earn its place.

[... 2000 more tokens of stable instructions ...]`,
      cache_control: { type: "ephemeral" },
    },
  ],
  messages: [
    {
      role: "user",
      content: "Draft a post about prompt caching.",
    },
  ],
});

cache_control: { type: "ephemeral" } на системном блоке говорит Claude кэшировать всё вплоть до этого блока включительно. Массив messages изменчив — разный при каждом запросе — и остаётся за границей кэша.

2. Определения инструментов

Если ваш агент использует инструменты, их определения могут быть весьма объёмными. Хорошо документированная схема инструмента с описанием, именами параметров и значениями enum может занимать 500–1 000 токенов на инструмент. С 5 инструментами это до 5 000 токенов, за повторную обработку которых вы платите при каждом вызове:

typescript
const response = await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  tools: [
    {
      name: "search_airtable",
      description: "Search the Airtable content queue...",
      input_schema: { type: "object", properties: { query: { type: "string" } } },
    },
    // ... more tools ...
    {
      name: "post_to_kit",
      description: "Schedule a broadcast via the Kit API...",
      input_schema: { /* ... */ },
      // Mark the last tool to cache the entire tools array
    } as Anthropic.Tool & { cache_control: { type: "ephemeral" } },
  ],
  system: "...",
  messages: [...],
});

Отметьте последний инструмент в массиве. Совпадение префикса покроет весь массив инструментов начиная с этой точки.

3. Few-shot примеры в сообщениях

Если вы передаёте статические few-shot примеры как первые сообщения в массиве messages, их тоже можно кэшировать. Структурируйте их как первые N сообщений и отметьте последний ход с примером:

typescript
const messages: Anthropic.MessageParam[] = [
  {
    role: "user",
    content: [
      {
        type: "text",
        text: "Here are examples of posts in my voice:\n\n[Example 1...]\n\n[Example 2...]",
        cache_control: { type: "ephemeral" },
      } as Anthropic.TextBlockParam & { cache_control: { type: "ephemeral" } },
    ],
  },
  {
    role: "assistant",
    content: "Understood. I'll follow that voice.",
  },
  // The actual user turn follows — this is volatile, no cache marker
  {
    role: "user",
    content: actualUserRequest,
  },
];

Что НЕ кэшировать (скрытые инвалидаторы)

Это вещи, которые выглядят стабильными, но таковыми не являются, — и они тихо убьют ваш процент попаданий. API вас не предупредит. Вы просто будете видеть cache_creation_input_tokens на каждом запросе и недоумевать, почему.

Временные метки в системном промпте. Самая распространённая ошибка:

typescript
// This invalidates the cache on every request
const system = `You are an agent. Current time: ${new Date().toISOString()}`;

Перенесите временные метки в сообщение пользователя, где им и место:

typescript
// Stable system prompt — cacheable
const system = `You are an agent. Use the current time provided by the user.`;

// Volatile user message — not cached
const userMessage = `Current time: ${new Date().toISOString()}. Run the daily brief.`;

Случайные UUID и trace ID. Та же проблема. Если вы вставляете trace ID в системный блок для логирования, каждый запрос получает новый префикс.

Недетерминированная сериализация JSON. Если вы сериализуете объект в системный промпт, а порядок ключей не гарантирован, отрендеренная строка может отличаться, даже когда исходные данные те же самые. Сериализуйте со стабильным порядком ключей или используйте шаблонную строку.

Динамический выбор few-shot примеров. Если вы выбираете few-shot примеры на основе текущего запроса и помещаете их в кэшируемый префикс, вы сделали «стабильный» префикс зависимым от запроса. Либо зафиксируйте примеры для слоя кэширования, либо перенесите динамические примеры в некэшируемый ход сообщения.

Проверка процента попаданий в кэш

Каждый ответ включает метаданные об использовании. Проверяйте их:

typescript
const response = await client.messages.create({ /* ... */ });

console.log({
  inputTokens: response.usage.input_tokens,
  cacheRead: response.usage.cache_read_input_tokens,
  cacheWrite: response.usage.cache_creation_input_tokens,
  outputTokens: response.usage.output_tokens,
});

На первом запросе: cache_creation_input_tokens будет ненулевым, cache_read_input_tokens будет равен 0. Это запись.

На попадании в кэш: cache_read_input_tokens будет ненулевым, cache_creation_input_tokens будет равен 0. Это чтение.

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

TTL в 1 час: когда дополнительная стоимость записи того стоит

TTL по умолчанию — 5 минут. Если ваш агент работает с низкой частотой — реже одного раза в 5 минут, — вы будете платить за записи в кэш на большинстве запросов, не получая чтений.

typescript
// Opt into a 1-hour TTL
cache_control: { type: "ephemeral", ttl: "1h" }

Запись с TTL в 1 час стоит ~2× от базовой цены на ввод вместо 1,25×. Расчёт: если вы попадаете в кэш 3 или более раз в час, TTL в 1 час экономит деньги. Если ваш агент работает раз в день (как мой ежедневный брифинг), даже TTL в 1 час не поможет — вы платите за запись каждый раз. В этом случае выгода от кэширования скромная, если только системный промпт не огромен.

У моего агента ежедневного брифинга системный промпт на 3 000 токенов, но запускается он раз в день. Кэширование не помогает. Мой агент рассылки запускается десятки раз за сессию во время черновой работы — кэширование экономит существенно.

Предварительный прогрев: как сделать первый запрос дешёвым

Если вы знаете о приближающемся всплеске трафика — пакетном задании, запуске API, — вы можете предварительно прогреть кэш дешёвым фиктивным запросом:

typescript
// Pre-warm: write the cache at near-zero output cost
await client.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1, // minimal output
  system: [{ type: "text", text: stableSystemPrompt, cache_control: { type: "ephemeral" } }],
  messages: [{ role: "user", content: "ping" }],
});

// Now the real requests read from cache

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

Кэширование промптов в агентных циклах

В многоходовом агентном цикле история разговора растёт на каждом ходу. Кэш достаточно умён, чтобы справиться с этим: он использует окно просмотра назад в 20 блоков, находя самый длинный совпадающий префикс в пределах последних 20 блоков контента.

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

На практике мои агенты структурируют ходы так:

code
System (cached) → Tools (cached) → Few-shot (cached) → Turn 1 → Turn 2 → ... → Current turn

Кэш покрывает всё вплоть до маркера few-shot. Растущая история ходов после него обрабатывается заново каждый раз, но это нормально — эти токены специфичны для сессии и малы относительно стабильного префикса.

Как это выглядит в счёте

Возьмём высокочастотного агента: 100 вызовов в день, системный промпт на 4 000 токенов, цены Sonnet.

Без кэширования:

С кэшированием (TTL 5 минут, при допущении 50 вызовов в час на пике):

Это примерно 90% снижение на этих токенах ввода. На масштабе — 1 000 вызовов в день — разница усиливается ещё сильнее. И всё это поверх любой экономии от маршрутизации моделей из математики Haiku против Sonnet: кэширование работает на любом уровне.

Итог для оператора

Кэширование промптов — самая простая оптимизация затрат в Claude API: одно дополнительное поле в блоках контента, которые вы и так уже пишете. Ограничение — это дисциплина в отношении стабильности префикса: ничего динамического перед маркером кэша. Если вы сможете держать ваш системный промпт, инструменты и любые статические примеры свободными от изменчивого контента, вы будете платить ~10% от обычной стоимости ввода на каждом попадании в кэш. Для высокочастотных агентов с большими стабильными промптами это более мощный рычаг, чем смена уровня модели.


Связанные материалы: Математика затрат на ИИ-агентов: когда Haiku обходит Sonnet · Агенты по событиям против агентов по расписанию · 5 ИИ-инструментов, которые я реально использую для управления бизнесом

Читать дальше

Получайте ИИ-руководство на почту

Каждую среду. 28 400+ читателей. Никакой воды.

↵ — все результаты esc esc — закрыть