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/месяц экономии. Вполне достаточно, чтобы покрыть всю инфраструктуру.
