Prompt Caching в Claude: Как мы снизили затраты на AI в 2 раза
Кейс по оптимизации затрат на Claude API в проекте по автоматизации поиска работы. AI анализировал вакансии и генерировал сопроводительные письма. При 100 пользователях затраты достигали $180/месяц. Решение: Prompt Caching от Anthropic. Экономия 52% ($0.51 → $0.245 за batch из 50 вакансий). Теперь можно делать в 2 раза больше AI-вызовов с тем же бюджетом.
Кому полезно: всем, кто работает с LLM API и хочет оптимизировать затраты.
История: Когда AI начал съедать бюджет
Делал проект по автоматизации поиска работы — платформа анализирует вакансии с помощью AI и сопоставляет их с резюме пользователя. Даёт score от 0 до 100 и объясняет, почему вакансия подходит или нет.
Логика простая:
Пользователь ищет вакансии (например, "Product Manager в Москве")
Система находит 50-100 вакансий через HH API
Claude AI анализирует каждую вакансию относительно резюме пользователя
Возвращаем список с оценками и рекомендациями
Звучит круто, пока не начинаешь считать затраты.
Считаем: Сколько это стоит?
Дано:
Модель: Claude Sonnet (умная, но дорогая)
Один поиск: 50 вакансий
Один пользователь делает ~10 поисков в месяц
Токены на один анализ вакансии:
System prompt: 500 токенов (инструкции для AI)
Резюме пользователя: 1,500 токенов (опыт, навыки, образование)
Описание вакансии: 400 токенов (требования, обязанности)
Ответ AI: 200 токенов (оценка + объяснение)
Итого: ~2,600 токенов на вакансию
Стоимость Claude Sonnet:
Input: $3 за 1M токенов
Output: $15 за 1M токенов
Batch из 50 вакансий:
Input: 50 × 2,400 токенов = 120,000 токенов = $0.36
Output: 50 × 200 токенов = 10,000 токенов = $0.15
Итого: $0.51 за один поиск
Стоп. Это много. Очень много.
Если у нас 100 пользователей, и каждый делает 10 поисков в месяц:
100 пользователей × 10 поисков × $0.51 = $510/месяц
Пятьсот долларов только на AI. При том что вся инфраструктура (сервер, база данных, домен) обходится в ~$35/месяц.
Так не пойдёт.
Первая оптимизация: Используем Haiku для простых задач
Первое, что пришло в голову — не все задачи требуют самой умной модели. Для простых вопросов (например, "сколько стоит подписка?") можно использовать Claude Haiku — он в 5 раз дешевле.
Новая стратегия:
Haiku (дешёвый) — для FAQ, простых вопросов, базовой фильтрации
Sonnet (дорогой) — для сложного анализа резюме и генерации писем
Помогло, но не сильно. Основные затраты всё равно шли на matching вакансий, где без Sonnet никак.
Нужно было что-то другое.
Открытие: Prompt Caching
Копаясь в документации Anthropic, нашёл фичу Prompt Caching. Суть простая: если отправляешь в Claude один и тот же контекст много раз подряд, можно его закешировать на 5 минут. Anthropic берёт плату только за новую часть запроса.
Как это работает:
Обычный запрос выглядит так:
response = client.messages.create(
model="claude-sonnet-4-20250514",
system="Ты анализируешь вакансии...", # 500 токенов
messages=[
{
"role": "user",
"content": f"Резюме: {resume}\nВакансия: {vacancy}"
}
]
)
Каждый раз Claude читает ВСЁ заново: system prompt, резюме, вакансию. Платим за все токены.
С Prompt Caching:
response = client.messages.create(
model="claude-sonnet-4-20250514",
system=[
{
"type": "text",
"text": "Ты анализируешь вакансии...", # 500 токенов
"cache_control": {"type": "ephemeral"} # ← Кешируем!
},
{
"type": "text",
"text": f"Резюме пользователя: {resume}", # 1,500 токенов
"cache_control": {"type": "ephemeral"} # ← Кешируем!
}
],
messages=[
{
"role": "user",
"content": f"Вакансия: {vacancy}" # 400 токенов — НЕ кешируем
}
]
)
Теперь:
Первый запрос: платим за всё (500 + 1,500 + 400 токенов)
Следующие запросы (в течение 5 минут): платим только за новую вакансию (400 токенов)
System prompt и резюме читаются из кеша — стоимость снижается в 10 раз для cached токенов!
Реализация: Что мы закешировали
1. System Prompt (всегда одинаковый)
SYSTEM_PROMPT = """Ты — AI-ассистент для анализа вакансий.
ЗАДАЧА:
Оценить соответствие вакансии резюме кандидата по шкале 0-100.
КРИТЕРИИ ОЦЕНКИ:
- Совпадение навыков (40%)
- Опыт работы (30%)
- Образование (15%)
- Зарплатные ожидания (15%)
ВАЖНО:
- Учитывай уровень позиции (Junior/Middle/Senior/Lead)
- Если кандидат overqualified (например Senior на Middle позицию), снижай оценку
- Если underqualified, тоже снижай оценку
ФОРМАТ ОТВЕТА:
{
"score": 85,
"reasoning": "Объяснение оценки",
"pros": ["Плюс 1", "Плюс 2"],
"cons": ["Минус 1"]
}
"""
Этот промпт не меняется между вызовами → кешируем целиком.
2. Резюме пользователя (меняется редко)
У каждого пользователя есть резюме. Оно не меняется в рамках одной сессии поиска. Более того — пользователь может делать 5-10 поисков подряд с одним и тем же резюме.
Кешируем его тоже:
resume_text = f"""
ИМЯ: {user.name}
ДОЛЖНОСТЬ: {user.title}
ОПЫТ:
{format_experience(user.experience)}
НАВЫКИ:
{', '.join(user.skills)}
ОБРАЗОВАНИЕ:
{user.education}
"""
3. Описание вакансии (всегда новое)
Это единственная часть, которая меняется для каждого вызова. Её НЕ кешируем — она всегда уникальна.
Код: Как это выглядит в FastAPI
Вот реальный код из нашего проекта (упрощённая версия):
from anthropic import AsyncAnthropic
class VacancyMatcher:
def __init__(self):
self.client = AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
async def match_vacancy(
self,
vacancy: dict,
resume: dict,
use_cache: bool = True
) -> dict:
"""
Анализирует одну вакансию относительно резюме
Args:
vacancy: Данные вакансии из HH API
resume: Резюме пользователя
use_cache: Использовать ли Prompt Caching
"""
# 1. Форматируем резюме в текст
resume_text = self._format_resume(resume)
# 2. Форматируем вакансию
vacancy_text = self._format_vacancy(vacancy)
# 3. Формируем system prompt с кешированием
if use_cache:
system = [
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": f"РЕЗЮМЕ КАНДИДАТА:\n{resume_text}",
"cache_control": {"type": "ephemeral"}
}
]
else:
# Без кеширования (для сравнения)
system = f"{SYSTEM_PROMPT}\n\nРЕЗЮМЕ КАНДИДАТА:\n{resume_text}"
# 4. Вызываем Claude
response = await self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=system,
messages=[
{
"role": "user",
"content": f"ВАКАНСИЯ:\n{vacancy_text}\n\nОцени соответствие."
}
]
)
# 5. Парсим ответ
result = json.loads(response.content[0].text)
return {
"vacancy_id": vacancy["id"],
"score": result["score"],
"reasoning": result["reasoning"],
"pros": result["pros"],
"cons": result["cons"],
"tokens_used": {
"input": response.usage.input_tokens,
"cache_read": getattr(response.usage, "cache_read_input_tokens", 0),
"output": response.usage.output_tokens
}
}
Batch Processing: Обрабатываем 50 вакансий параллельно
Чтобы не ждать 50 секунд (по 1 секунде на вакансию), делаем batch processing:
async def match_batch(
self,
vacancies: list[dict],
resume: dict,
batch_size: int = 5
) -> list[dict]:
"""
Обрабатываем вакансии батчами параллельно
Args:
vacancies: Список вакансий
resume: Резюме пользователя
batch_size: Сколько вакансий обрабатывать одновременно
"""
results = []
# Разбиваем на батчи по 5 вакансий
for i in range(0, len(vacancies), batch_size):
batch = vacancies[i:i + batch_size]
# Запускаем параллельно
tasks = [
self.match_vacancy(vacancy, resume, use_cache=True)
for vacancy in batch
]
batch_results = await asyncio.gather(*tasks)
results.extend(batch_results)
# Небольшая пауза между батчами (чтобы не упереться в rate limit)
if i + batch_size < len(vacancies):
await asyncio.sleep(0.5)
return results
Результат:
50 вакансий обрабатываются за ~20 секунд (вместо 50)
Prompt Cache hit rate: 60-80% (зависит от того, как быстро пользователь делает запросы)
Результаты: Что получили
До Prompt Caching
Один batch (50 вакансий):
Input: 50 × 2,400 токенов = 120,000 токенов × $3/1M = $0.36
Output: 50 × 200 токенов = 10,000 токенов × $15/1M = $0.15
Итого: $0.51 за batch
100 пользователей × 10 поисков/месяц:
$0.51 × 1,000 = $510/месяц
После Prompt Caching
Первый запрос (cache miss):
Input: 2,400 токенов × $3/1M = $0.0072
Output: 200 токенов × $15/1M = $0.003
Итого: $0.0102
Следующие 49 запросов (cache hit):
Cached tokens (system + resume): 2,000 токенов × $0.30/1M = $0.0006
New tokens (vacancy): 400 токенов × $3/1M = $0.0012
Output: 200 токенов × $15/1M = $0.003
Итого за вакансию: $0.0048
Batch из 50 вакансий:
Первая: $0.0102
Остальные 49: 49 × $0.0048 = $0.235
Итого: $0.245 за batch (вместо $0.51)
Экономия: 52%
100 пользователей × 10 поисков/месяц:
$0.245 × 1,000 = $245/месяц (вместо $510)
Сэкономили $265 в месяц. Или $3,180 в год.
Когда Prompt Caching работает лучше всего
Идеальные сценарии:
-
Batch processing — обрабатываете много похожих запросов подряд
Пример: анализ вакансий, модерация контента, генерация описаний товаров
-
Длинный контекст — system prompt или reference материалы занимают много токенов
Пример: база знаний компании, документация, техническая спецификация
-
Повторяющиеся запросы — пользователь делает несколько запросов за короткое время
Пример: поиск с разными фильтрами, итеративное редактирование
Когда кеширование не поможет:
-
Уникальные запросы — каждый раз новый контекст
Пример: генерация креативов для разных клиентов
-
Редкие запросы — между вызовами проходит >5 минут
Пример: чат-бот с низкой активностью
Короткие промпты — кешировать нечего (меньше 1024 токенов)
Подводные камни и нюансы
1. Кеш живёт только 5 минут
Если пользователь долго думает между запросами, кеш протухает. Приходится платить по полной за следующий запрос.
Решение: Группируем запросы. Например, в нашем случае пользователь сначала ищет все вакансии, а потом мы анализируем их batch'ами. Вероятность cache hit высокая.
2. Минимальный размер для кеширования: 1024 токена
Нельзя кешировать маленькие промпты. Если ваш system prompt короткий, кеширование не сработает.
Решение: Объединяйте несколько блоков в один cached блок. Например, мы кешируем system prompt + резюме вместе.
3. Порядок имеет значение
Кешировать можно только последние блоки в system prompt. Нельзя сделать так:
# Не сработает
system = [
{"text": "Часть 1", "cache_control": {"type": "ephemeral"}},
{"text": "Часть 2"}, # НЕ cached
{"text": "Часть 3", "cache_control": {"type": "ephemeral"}}
]
Правильно:
# Работает
system = [
{"text": "Часть 1"}, # НЕ cached
{"text": "Часть 2", "cache_control": {"type": "ephemeral"}},
{"text": "Часть 3", "cache_control": {"type": "ephemeral"}}
]
4. Cache hit не гарантирован
Даже если делаете запросы быстро, cache hit rate может быть 60-80%, а не 100%. Зависит от нагрузки на серверах Anthropic.
Решение: Считайте среднюю экономию, а не максимальную.
Мониторинг: Как считать реальную экономию
Anthropic возвращает детальную статистику по токенам:
response.usage:
{
"input_tokens": 400, # Новые токены
"cache_creation_input_tokens": 2000, # Записали в кеш
"cache_read_input_tokens": 2000, # Прочитали из кеша
"output_tokens": 200
}
Считаем стоимость:
def calculate_cost(usage) -> float:
"""
Считает стоимость запроса с учётом кеширования
Цены Claude Sonnet (на февраль 2025):
- Input: $3.00 / 1M tokens
- Output: $15.00 / 1M tokens
- Cache write: $3.75 / 1M tokens (на 25% дороже обычного input)
- Cache read: $0.30 / 1M tokens (в 10 раз дешевле!)
"""
input_cost = usage.input_tokens * 3.00 / 1_000_000
cache_write_cost = usage.cache_creation_input_tokens * 3.75 / 1_000_000
cache_read_cost = usage.cache_read_input_tokens * 0.30 / 1_000_000
output_cost = usage.output_tokens * 15.00 / 1_000_000
return input_cost + cache_write_cost + cache_read_cost + output_cost
Логируем каждый запрос:
logger.info(
"Claude API call",
extra={
"vacancy_id": vacancy["id"],
"input_tokens": usage.input_tokens,
"cache_read_tokens": usage.cache_read_input_tokens,
"output_tokens": usage.output_tokens,
"cost": cost,
"cache_hit": usage.cache_read_input_tokens > 0
}
)
Дальше можно строить дашборды в Grafana или анализировать логи:
-- Средний cache hit rate за последний день
SELECT
DATE(created_at) as date,
COUNT(*) as total_requests,
SUM(CASE WHEN cache_read_tokens > 0 THEN 1 ELSE 0 END) as cache_hits,
ROUND(
100.0 * SUM(CASE WHEN cache_read_tokens > 0 THEN 1 ELSE 0 END) / COUNT(*),
2
) as hit_rate_percent
FROM ai_calls
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY DATE(created_at)
ORDER BY date DESC;
Наши результаты за неделю:
date | total_requests | cache_hits | hit_rate_percent
-----------|----------------|------------|------------------
2025-02-08 | 1,247 | 982 | 78.75
2025-02-07 | 1,893 | 1,456 | 76.93
2025-02-06 | 2,104 | 1,687 | 80.18
Cache hit rate ~78%. Значит экономим не ровно 52%, а ~46% (с учётом cache miss). Всё равно отлично!
Выводы: Стоит ли оно того?
Да, если:
У вас batch processing — обрабатываете десятки запросов подряд
Длинный контекст — system prompt + reference документы >2000 токенов
Частые запросы — пользователи делают много действий за короткое время
Возможно стоит, если:
Средний контекст — 1000-2000 токенов
Умеренная нагрузка — пользователи делают запросы, но не очень часто
Не стоит, если:
Короткие промпты — меньше 1024 токенов
Редкие запросы — между вызовами проходит много времени
Всегда уникальный контекст — нечего кешировать
Наш случай:
Batch processing
Длинный контекст (system prompt + резюме = 2000+ токенов)
Частые запросы (пользователь делает 5-10 поисков подряд)
Итог: Сэкономили $265/месяц при текущей нагрузке. При росте до 1000 пользователей — это будет $2,650/месяц экономии. Вполне достаточно, чтобы покрыть всю инфраструктуру.