Перейти к содержимому

2.7 · Стриминг, ретраи, rate limits, prompt caching

Неделя 2 · Основы LLM · ⏱ ~4–5 ч · Теория, Практика

За две недели накопилась пачка скриптов: chat.py и cost.py, классификатор отзывов, extract(), compare.py, цикл с инструментами. В каждом свой вызов модели, и у всех одни и те же слабости. Скрипт из главы 1.6 падает от первого 429, висит 10 минут на зависшем соединении и платит полную цену за один и тот же системный промпт в каждом запросе. Сервис так себя вести не может. Ретраи с backoff, таймауты, учёт лимитов, кэш промптов и batch — то, что отличает «прототип на коленке» от продакшена; на собеседованиях спрашивают «что вы ретраите и как», «как снизили стоимость» и «как устроен prompt caching». В этой главе собираем всё это в одну мини-библиотеку llm_utils.py, через которую дальше пойдут все вызовы моделей в курсе.

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

Код Тип (Anthropic) Смысл Ретраить?
400 invalid_request_error Ошибка в запросе: параметр, формат, слишком длинный промпт Нет — повтор даст то же самое
401 / 403 authentication_error / permission_error Ключ неверен / нет доступа Нет
402 billing_error Проблема с оплатой Нет
404 not_found_error Неверный путь или имя модели Нет
413 request_too_large Тело запроса слишком большое Нет
429 rate_limit_error Превышен лимит запросов/токенов Да, с паузой (с учётом retry-after)
500 api_error Внутренняя ошибка провайдера Да
529 overloaded_error Провайдер перегружен Да, с паузой подлиннее
— таймаут, обрыв соединения Сеть Да (с оговоркой о дублях, см. ниже)

Правило одной строкой: 4xx — ошибка наша, 429/5xx/сеть — временная. Особый случай у OpenAI: 429 бывает двух видов — rate limit (ретраить) и insufficient_quota (кончились деньги — не ретраить, тело ошибки это различает).

Временную ошибку ретраим, но не сразу. Backoff — пауза перед повтором, которая растёт с каждой попыткой; джиттер — случайная добавка к ней. Зачем случайность: если 1000 клиентов получили 429 одновременно и все повторят через ровно 1 секунду — они снова одновременно ударят в API («thundering herd»). Решение:

пауза = random(0, min(cap, base * 2^attempt)) # "full jitter"
attempt 0: 0..1 с attempt 1: 0..2 с attempt 2: 0..4 с ... не больше cap

Если сервер прислал заголовок retry-after — уважаем его (он знает лучше).

import random, time
def backoff_delay(attempt: int, base: float = 1.0, cap: float = 30.0,
retry_after: float | None = None) -> float:
if retry_after is not None:
return retry_after
return random.uniform(0, min(cap, base * 2 ** attempt))

Прежде чем писать свой цикл повторов, стоит знать, что он уже есть. Официальные SDK Anthropic и OpenAI сами ретраят сетевые ошибки, 408/409/429 и 5xx (по умолчанию 2 повтора) с экспоненциальной паузой и учётом retry-after. Отсюда два корректных варианта:

  1. Довериться SDK: настроить max_retries и timeout на клиенте; наша обёртка только логирует и считает деньги.
  2. Отключить ретраи SDK (max_retries=0) и ретраить самим (например, библиотекой tenacity) — когда нужна своя политика (другие паузы, переключение на резервную модель, метрики по попыткам).

Ошибка — сделать и то и другое: 3 наших попытки × 3 попытки SDK = до 9 запросов, и паузы складываются.

Ретрай срабатывает, когда запрос завершился ошибкой. Но запрос может и не завершиться — тогда его обрывает таймаут. Дефолтный таймаут SDK — минуты (у Anthropic Python SDK — 10 минут). Для интерактивного бота это вечность. Таймаут ставим под задачу: короткий для коротких ответов, а длинные генерации — через стриминг (соединение живо, пока идут токены). Важно: таймаут тоже ретраится, и общее время может достигнуть timeout × (max_retries + 1).

Повтор запроса к модели безопасен (максимум заплатим дважды). Но в агенте с инструментами (гл. 2.6) повторяем вызов модели, а не уже выполненные действия инструментов. Если инструмент делает что-то необратимое, повтор цикла может сделать это дважды — ещё одна причина держать такие инструменты вне LLM.

Самая частая временная ошибка — 429, превышение лимита. Чтобы не получать её постоянно, надо понимать, как лимиты устроены. Провайдеры ограничивают аккаунт сразу по нескольким осям: запросы в минуту (RPM), input-токены в минуту (ITPM), output-токены в минуту (OTPM), иногда — дневные лимиты. Лимиты зависят от уровня аккаунта (tier) и растут по мере трат. В ответах приходят заголовки с остатками (у Anthropic — семейство anthropic-ratelimit-*, у OpenAI — x-ratelimit-*) и retry-after при 429.

Как жить с лимитами:

  • ограничивать конкурентность (семафор: не больше N запросов одновременно — asyncio.Semaphore, аналог p-limit в Node или пула воркеров в других языках);
  • для массовых фоновых задач — Batch API (ниже);
  • кэш промптов у Anthropic снижает учитываемые input-токены (прочитанное из кэша в большинстве случаев не идёт в ITPM — стоит проверить в документации по лимитам);
  • 429 на старте — нормально, 429 постоянно — сигнал пересмотреть архитектуру или поднять tier.

Ретраи и лимиты отвечают за надёжность, дальше — про деньги и скорость. С кэшем мы уже встречались: в главе 1.6 смотрели, как растёт prompt_cache_hit_tokens у DeepSeek, а в главе 1.7 считали цену попадания. Теперь разберём механизм. Идея: если начало запроса (системный промпт, документация, инструменты, длинная история) повторяется, провайдер может сохранить его вычисленное внутреннее состояние и не обрабатывать заново. Мы платим меньше и получаем ответ быстрее (меньше TTFT).

запрос 1: [tools][system: 20K токенов документации][вопрос 1]
└──────── запись в кэш ───────┘
запрос 2: [tools][system: те же 20K токенов][вопрос 2]
└──────── чтение из кэша ─────┘ ← ~10% цены, быстрее

Как это работает у Anthropic:

  • Кэш — это совпадение префикса. Запрос собирается в порядке tools → system → messages. Любое изменение байта в префиксе инвалидирует всё после него.
  • Границу отмечаем cache_control: {"type": "ephemeral"} на блоке (до 4 точек) или ставим его на верхнем уровне запроса — тогда кэшируется автоматически до последнего подходящего блока.
  • Цена: запись в кэш дороже обычного input (примерно ×1.25 при TTL 5 минут), чтение — примерно ×0.1. Кэш выгоден, если префикс переиспользуется хотя бы пару раз за время жизни.
  • TTL по умолчанию 5 минут (обновляется при каждом попадании), есть вариант на 1 час за более дорогую запись.
  • Минимальная длина кэшируемого префикса зависит от модели (от сотен до нескольких тысяч токенов). Короче — кэширование молча не произойдёт, без ошибки.
  • Проверка — только по usage: cache_creation_input_tokens (записано), cache_read_input_tokens (прочитано), input_tokens (обычные). Если cache_read_input_tokens всегда 0 — что-то ломает префикс.
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
system=[
{"type": "text", "text": "You answer questions about the shop's delivery and return rules."},
{"type": "text", "text": DOCS_TEXT, "cache_control": {"type": "ephemeral"}},
],
messages=[{"role": "user", "content": question}],
)
u = resp.usage
print(u.input_tokens, u.cache_creation_input_tokens, u.cache_read_input_tokens)

Тихие убийцы кэша: datetime.now() или UUID в системном промпте; JSON, сериализованный с разным порядком ключей; инструменты в разном порядке; смена модели (кэш привязан к модели); правка начала истории. Правило: стабильное — в начало, изменчивое — в конец.

У OpenAI кэширование префикса автоматическое для достаточно длинных промптов (порог порядка тысячи токенов), без разметки; закэшированная часть видна в деталях usage (cached_tokens) и тарифицируется со скидкой. Принцип «стабильное в начало» тот же.

Второй способ сэкономить — для задач, где ответ не нужен сразу (разметка датасета, ночной анализ, evals): провайдеры дают Batch API: отправляем пачку запросов, результаты приходят асинхронно (обычно в пределах 24 часов, часто быстрее) примерно за половину цены. Результаты приходят в произвольном порядке — сопоставляем их по своему custom_id, а не по позиции. Кэш и batch можно комбинировать.

Последний кусок мозаики — стриминг из главы 1.6. Он усложняет ретраи: если ошибка случилась до первого токена, повторяем как обычно; если посреди ответа, а текст уже показан пользователю, прозрачный повтор невозможен — либо начинаем ответ заново (и предупреждаем), либо отдаём ошибку. Usage и stop_reason в стриме приходят в конце — логируем их после завершения.

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

llm_utils.py
PROVIDERS ← base_url и переменная с ключом, как в главе 2.4
PRICES, estimate_cost() ← из главы 1.7
@dataclass CallResult ← text, content blocks, usage, cost, latency, stop_reason, attempts
call_llm(messages, *, provider, system, model, max_tokens, tools=None,
cache_system=False, timeout=60, max_retries=3) -> CallResult
stream_llm(...) -> Iterator[str] + итоговый CallResult
log_call(...) ← JSONL: время, модель, токены (вкл. кэш), стоимость, latency, попытки, ошибка
is_retryable(exc) -> bool

Мы пишем такую обёртку сами, чтобы понять механику; в других экосистемах её часто берут готовой. Vercel AI SDK — популярная библиотека для JavaScript-экосистемы с единым интерфейсом к разным провайдерам (generateText, streamText, generateObject со схемой Zod, инструменты, встроенные ретраи). Она пригодится, если веб-интерфейс к LLM пишется на JavaScript. Полезно увидеть, что под капотом там те же концепции: роли, стриминг, tool loop, схемы, ретраи с backoff.

Всё сказанное выше — на примере Claude и OpenAI. Для DeepSeek и Ollama мы используем пакет openai, поэтому ретраи, таймауты и классы исключений — те же, что у OpenAI SDK (openai.APIConnectionError, APITimeoutError, RateLimitError, APIStatusError с status_code; по умолчанию 2 повтора). Различаются коды ошибок, природа лимитов и, сильнее всего, кэш.

По странице Error Codes (13.11.2026):

Код Смысл у DeepSeek Ретраить?
400 неверный формат тела (в том числе пропущенный reasoning_content при tools, принудительный tool_choice в режиме размышления) нет
401 неверный ключ нет
402 закончился баланс нет — пополнить (аналог insufficient_quota у OpenAI, но отдельным кодом)
422 неверные параметры нет
429 превышен лимит конкурентности / частоты да, с backoff
500 ошибка сервера да
503 сервер перегружен да, с паузой подлиннее (аналог 529 у Anthropic)
408 / 413 / 504 загрузка файлов, OCR и т. п. 408/504 — да, 413 — нет

У Ollama другой мир: APIConnectionError чаще всего значит «сервер не запущен» (ретраи не помогут — нужно запустить приложение), 404 — модель не скачана (ollama pull), падения 500 на локальной машине бывают от нехватки памяти. На 8 ГБ RAM это реальный сценарий, если модель больше класса 3–4B или num_ctx велик; на 16 ГБ тот же предел наступает на моделях крупнее 7–8B.

У DeepSeek лимит — число одновременных запросов на аккаунт (по Rate Limit & Isolation: 2500 для deepseek-flash, 500 для deepseek-v4-pro), а не RPM/TPM по уровням, как у Anthropic и OpenAI. Заголовков с остатками и retry-after в документации не описано — значит, наш backoff_delay работает без подсказки сервера. Семафор всё равно нужен: для учёбы хватит 4–8 одновременных запросов.

Важная особенность под нагрузкой: если инференс ещё не начался, DeepSeek держит соединение до 10 минут, присылая пустые строки (без стриминга) или SSE-комментарии : keep-alive (со стримингом). Таймаут чтения у HTTP-клиентов обычно считается от последнего полученного байта — а байты приходят, так что «таймаут 30 секунд» может не сработать. Для интерактивного сценария ставим общий дедлайн на весь вызов (например, asyncio.wait_for вокруг запроса) и проверяем его по стене часов.

У Ollama лимитов нет, но по умолчанию модель обрабатывает один запрос за раз (OLLAMA_NUM_PARALLEL); остальные ждут в очереди, а каждый дополнительный параллельный слот требует памяти под свой контекст. На 8 ГБ — семафор на 1–2 и никакого gather на сотню запросов.

По гайду Context Caching:

  • Включён у всех, разметка не нужна — cache_control не существует (через Anthropic-совместимый эндпоинт он просто игнорируется).
  • Кэш — совпадение префикса; кэшируются «единицы» на границах реплик и через фиксированные интервалы на длинных текстах. Общий префикс начинает попадать после того, как система увидит его в нескольких запросах (в примере документации первые два запроса — промах, третий — попадание).
  • Живёт от нескольких часов до нескольких дней, best effort — 100% попаданий не гарантируется.
  • В usage: prompt_cache_hit_tokens / prompt_cache_miss_tokens (и prompt_tokens_details.cached_tokens).
  • Наценки за запись нет — промах стоит как обычный input, попадание примерно в 50 раз дешевле (глава 1.7). Поэтому вопрос «может ли кэш стоить дороже» у DeepSeek не стоит, а у Anthropic стоит.

Правило «стабильное в начало, изменчивое в конец» и список тихих убийц кэша работают один в один.

Нативный ответ Ollama содержит prompt_eval_cached_count — сколько токенов промпта взято из уже посчитанного состояния, пока модель в памяти. Денег это не экономит (их нет), но экономит секунды: на обычном ноутбуке обработка длинного промпта — заметная часть времени. Выгрузка модели (keep_alive, по умолчанию 5 минут простоя) сбрасывает и это.

Batch API в документации DeepSeek нет. Аналог по смыслу для фоновых задач — запускать их вне пиковых часов (скидка 50%, глава 1.7): разметка датасетов, evals, ночные прогоны. Для этого нужен не новый API, а планировщик и проверка времени UTC перед запуском.

Claude OpenAI DeepSeek Ollama
Ретраи в SDK anthropic, 2 по умолчанию openai, 2 по умолчанию те же, что у openai те же, что у openai (или свои у ollama)
Перегрузка 529 5xx 503 — (локально: OOM / медленно)
Кончились деньги 402 billing_error 429 insufficient_quota 402 —
Лимиты RPM/ITPM/OTPM по tier, заголовки RPM/TPM по tier, заголовки конкурентность на аккаунт, заголовки не документированы 1 запрос на модель по умолчанию, память
Кэш ручная разметка cache_control, запись ×1.25, чтение ×0.1, TTL 5 мин / 1 ч автоматический автоматический, без наценки, часы–дни, best effort prompt_eval_cached_count, пока модель загружена
Поля кэша cache_creation_input_tokens, cache_read_input_tokens cached_tokens prompt_cache_hit_tokens, prompt_cache_miss_tokens prompt_eval_cached_count
Batch −50% −50% нет; −50% вне пика —
Особое при таймаутах — — keep-alive до 10 мин, read-timeout может не сработать первый запрос — загрузка модели
  • llm_utils.py — с провайдерами deepseek и ollama как основными (один код на openai SDK, разные base_url, цены и поля кэша); openrouter из главы 2.4 добавляется ещё одной строкой в PROVIDERS; anthropic — опциональная ветка для сравнения.
  • Опыт с кэшем — на DeepSeek (кэш автоматический, смотрим только на usage), опыт с ручной разметкой cache_control — у Claude, для сравнения.
  • Длинный текст для кэша — публичный: правила доставки, возврата и гарантии магазина «Полка» (их можно сгенерировать моделью) или публичная документация любого API. Данные клиентов, рабочий код и всё, что нельзя показывать посторонним, в облачные модели не отправляем.
  1. Anthropic: Prompt caching (≈45 мин). Читаем: как работает префикс, где ставить cache_control, сколько стоит запись и чтение, TTL, минимальная длина по моделям, что инвалидирует кэш, как читать usage. Выписываем: таблицу минимальных длин для выбранной модели и список инвалидаторов.
  2. Страница ошибок Anthropic (docs.anthropic.com/en/api/errors) и страница rate limits (docs.anthropic.com/en/api/rate-limits) (≈30 мин). Выписываем: коды и типы ошибок, оси лимитов, заголовки ответа, как кэш влияет на учёт input-токенов.
  3. Статья AWS «Exponential Backoff And Jitter» (aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter) (≈15 мин). Выписываем: почему джиттер, чем «full jitter» отличается от других вариантов.
  4. Batch API (≈20 мин): Anthropic: Batch processing и OpenAI: Batch. Выписываем: скидку, сроки, формат custom_id, что результаты приходят в любом порядке.
  5. OpenAI для сравнения (≈15 мин): Prompt caching и Rate limits. Отмечаем: кэш автоматический, 429 бывает «квотой».
  6. README Python SDK Anthropic на GitHub (anthropic-sdk-python) → разделы Retries, Timeouts, Handling errors (≈15 мин). Выписываем: max_retries, timeout, классы исключений.
  7. (Сравнение с JavaScript-экосистемой) Vercel AI SDK → Getting Started и страница про streamText / generateObject (≈20 мин).
  8. DeepSeek (≈30 мин): Error Codes, Rate Limit & Isolation (конкурентность, keep-alive, 10 минут), Context Caching (как строится и живёт кэш, поля usage). Выписываем: таблицу «ретраить / нет» по кодам и правила попадания в кэш.
  9. Ollama (≈15 мин): FAQ — keep_alive, OLLAMA_NUM_PARALLEL, память; в Chat API — поле prompt_eval_cached_count и длительности.
  10. README Python SDK OpenAI на GitHub (openai-python) → Retries, Timeouts, Handling errors (≈10 мин): именно этот SDK работает у нас с DeepSeek и Ollama.
  11. Практика (≈2 ч).
  • Не ретраим 400-е. Это ошибка в запросе, повтор не поможет и только сожжёт лимит. Особенно коварны 400 из-за изменений API (убранные параметры, prefill, принудительный tool_choice на новых моделях).
  • Двойные ретраи (SDK + свои) — умножение попыток и задержек.
  • DeepSeek 402 — не ретраить, это пустой баланс. И не путать с 429: у DeepSeek 429 — про конкурентность, а не про деньги.
  • Ollama на 8 ГБ: параллельные запросы и большой num_ctx съедают память, которую модель делит с браузером, IDE и остальными программами. Семафор 1–2 и скромный контекст.
  • Кэш, который не кэширует: промпт короче минимума, datetime.now() в system, смена модели. Проверяем cache_read_input_tokens, а не веру.
  • Кэш может стоить дороже: если префикс используется один раз за TTL, платим надбавку за запись и не получаем чтения.
  • Таймаут × ретраи = реальное максимальное время ожидания. Стоит посчитать его для интерактивного сценария.
  • Логи стоимости включают кэш-токены по своим ценам; иначе estimate_cost врёт.
  • Ключи — только из окружения, в логах их нет (log_call не пишет заголовки).
  • Конкурентность без ограничения (asyncio.gather на 500 запросов) = гарантированный шквал 429.

Всё — в common/llm_utils.py (его будем импортировать во всех следующих проектах) и exercises/week-02/reliability/.

  • 1. is_retryable(exc). Основное — исключения пакета openai (через него ходим в DeepSeek и Ollama): openai.APIConnectionError / APITimeoutError → да; openai.APIStatusError → по exc.status_code (429, 500, 502, 503, 504 → да; 400, 401, 402, 422 → нет; 429 с кодом insufficient_quota от настоящего OpenAI → нет). Для anthropic (по желанию) — аналогично, плюс 529 → да. Покрываем тестами на фейковых исключениях.
  • 2. call_llm(). Параметр provider (deepseek | ollama | по желанию anthropic). Клиент с max_retries=0, свой цикл ретраев с backoff_delay (с учётом retry-after из exc.response.headers, если есть), таймаут, подсчёт попыток. Возвращает CallResult со стоимостью через estimate_cost (с кэш-токенами). Каждый вызов (и каждая неудачная попытка) — строка в logs/llm_calls.jsonl. Для DeepSeek добавляем общий дедлайн на вызов (из-за keep-alive), для Ollama — семафор на 1–2 одновременных запроса.
  • 3. Проверка ретраев без реального API. Подменяем функцию отправки фейком, который первые 2 раза бросает 503 (перегрузка DeepSeek; для ветки anthropic — 529), а потом отвечает. Проверяем: 3 попытки, паузы растут, в логе всё видно. Второй фейк бросает 400 — проверяем, что повтора нет.
  • 4. Prompt caching на DeepSeek. Берём длинный публичный текст ≥ нескольких тысяч токенов — например, правила доставки, возврата и гарантии «Полки», сгенерированные моделью и сохранённые в файл. Делаем 6 запросов подряд с разными вопросами («сколько стоит доставка в другой город?», «можно ли вернуть вскрытый чай?»), документ — в начале system. Для каждого записываем prompt_cache_hit_tokens, prompt_cache_miss_tokens, completion_tokens, латентность, стоимость. Ожидаемо: первые запросы — промах, дальше префикс начинает попадать (кэш строится за секунды и best effort — если попаданий нет, делаем паузу и повторяем). Сравниваем стоимость input первого и последнего запроса.
  • 4б. (Для сравнения) Prompt caching у Claude. Тот же опыт с cache_control: (а) без него, (б) с ним; записываем cache_creation_input_tokens, cache_read_input_tokens. Сравниваем модели оплаты: наценка за запись у Anthropic против её отсутствия у DeepSeek.
  • 4в. Ollama. Тот же документ (с num_ctx, в который он влезает без перегрузки памяти) дважды подряд в нативном API: записываем prompt_eval_cached_count и prompt_eval_duration. Потом выгружаем модель (keep_alive=0) и повторяем — что стало с кэшем и временем?
  • 5. Сломать кэш. В опыт шага 4 добавляем в начало системного промпта f"Current time: {datetime.now()}". Показываем, что prompt_cache_hit_tokens у DeepSeek (и cache_read_input_tokens у Claude, если сравниваем) упал почти до 0. Переносим время в конец сообщения пользователя — кэш снова работает.
  • 6. stream_llm(). Стриминговая версия: ретраит только до первого токена; после — пробрасывает ошибку. По завершении — тот же лог с usage и стоимостью.
  • 7. Таблица. В notes/reliability.md:
    | провайдер / вариант | запросов | input (промах) | cache write | cache read (попадание) | $ всего | ср. latency, с |
    | DeepSeek, стабильный префикс | 6 | ... |
    | DeepSeek, время в начале | 6 | ... |
    | Ollama (cached_count, время) | 2 | ... |
    | (Claude без / с кэшем) | 6 | ... |
    и 3–4 вывода: когда кэш выгоден, что его ломает, что ретраить, какой таймаут выбран для чата и почему.
  • 8. Ночной тариф вместо batch. Добавляем в llm_utils.py функцию is_offpeak(now_utc) -> bool по правилам DeepSeek и прогоняем классификацию 20 текстов из главы 2.1 вне пика. Сравниваем стоимость с прогоном в пик (через estimate_cost).
  • 8б. (Опционально, для сравнения) Batch. Та же классификация через Batch API Anthropic или OpenAI, сопоставление по custom_id.
  • llm_utils.py содержит call_llm() для DeepSeek и Ollama (ретраи с backoff и джиттером, таймаут/дедлайн, логирование токенов, кэш-полей и стоимости), stream_llm(); ретраи покрыты тестами на фейках, 402 и 400 не ретраятся.
  • Есть таблица по кэшу (DeepSeek, Ollama и, по желанию, Claude) по стоимости и времени и демонстрация того, как кэш ломается.
  • Можем объяснить, какие ошибки ретраить, а какие нет, и почему не нужно сочетать свои ретраи с ретраями SDK.

Вопрос 1

Какие из этих ответов стоит ретраить: 400, 401, 429, 500, 529, таймаут соединения?

429, 500, 529 и таймаут — да (временные). 400 и 401 — нет: это ошибка запроса или ключа, повтор даст то же самое.

Вопрос 2

Зачем в экспоненциальный backoff добавляют случайный джиттер?

Чтобы клиенты, получившие ошибку одновременно, не повторяли запросы синхронно и не создавали новую волну перегрузки.

Вопрос 3

Что не так в этом коде?

client = anthropic.Anthropic() # default retries
for attempt in range(5):
try:
return client.messages.create(**params)
except Exception:
time.sleep(1)

Ретраится всё подряд, включая 400/401; пауза постоянная, без экспоненты и джиттера; retry-after игнорируется; ретраи SDK не отключены (попытки умножаются); последняя ошибка проглатывается — функция вернёт None; нет логирования.

Вопрос 4

Как работает prompt caching у Anthropic: что именно кэшируется и что его инвалидирует?

Кэшируется префикс запроса (tools → system → messages) до точки cache_control; при следующем запросе с побайтово тем же префиксом он читается из кэша. Любое изменение в префиксе (или смена модели, истечение TTL) инвалидирует кэш от места изменения.

Вопрос 5

В системном промпте первой строкой Today is {datetime.now().isoformat()}, дальше 30K токенов документации с cache_control. Почему cache_read_input_tokens всегда 0 и как исправить?

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

Вопрос 6

Когда prompt caching может увеличить стоимость?

  • a) когда префикс используется много раз за 5 минут
  • b) когда префикс используется один раз за время жизни кэша
  • c) когда запросы короткие
  • d) никогда

b. Запись в кэш дороже обычного input; без последующих чтений это чистая переплата.

Вопрос 7

Чем Batch API выгоден и для каких задач не подходит?

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

Вопрос 8

Ошибка 429 от OpenAI с кодом insufficient_quota. Ретраить?

Нет: это закончившаяся квота/оплата, а не временный лимит; повтор не поможет.

Вопрос 9

Стрим оборвался на середине ответа, пользователь уже видит половину текста. Почему нельзя «просто повторить» запрос прозрачно и что делать?

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

Вопрос 10

Дефолтный таймаут 10 минут, max_retries=2. Сколько максимум может ждать пользователь в худшем случае и что с этим делать в чат-боте?

До ~30 минут (10 мин × 3 попытки) плюс паузы. В чат-боте — короткий таймаут под ожидаемую длину ответа, стриминг, ограниченное число повторов и понятное сообщение об ошибке.

Вопрос 11

Почему результаты Batch API нужно сопоставлять по custom_id, а не по порядку?

Пакет обрабатывается асинхронно, и результаты приходят в произвольном порядке; custom_id — единственная надёжная связь запроса и ответа.

Вопрос 12

DeepSeek вернул 402. Ретраить? А 503?

402 — нет: закончился баланс, повтор не поможет, нужно пополнить. 503 — да: сервер перегружен, повторить с backoff и паузой подлиннее.

Вопрос 13

Чем prompt caching у DeepSeek отличается от Anthropic? Может ли он у DeepSeek увеличить стоимость?

  • a) так же, нужен cache_control
  • b) автоматический, без наценки за запись — увеличить стоимость не может, но попадание не гарантировано
  • c) кэша нет
  • d) кэш только в batch

b. Кэш префикса включён всегда, промах стоит как обычный input, попадание — в десятки раз дешевле; но это best effort, и попадание начинается не с первого запроса.

Вопрос 14

У клиента DeepSeek стоит timeout=30, а запрос в час пик висел 8 минут. Как так и что делать?

Под нагрузкой DeepSeek держит соединение до 10 минут, присылая keep-alive (пустые строки/SSE-комментарии), а таймаут чтения отсчитывается от последнего байта и не срабатывает. Нужен общий дедлайн на вызов (asyncio.wait_for или проверка по часам) и понятное сообщение пользователю.

Основы LLM пройдены: у нас есть свой API-слой, промптинг, структурированный вывод, первый eval и tool calling. Дальше — [3.1 · Проект 1: архитектура бота и копия базы (гл. 3.1)](/week-03/3-1/): первый проект строится на llm_utils.py, а eval_extract.py вернётся на этапе evals — держим их в порядке и в git.