2.7 · Стриминг, ретраи, rate limits, prompt caching
Неделя 2 · Основы LLM · ⏱ ~4–5 ч · Теория, Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»За две недели накопилась пачка скриптов: 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 с джиттером
Заголовок раздела «Экспоненциальный backoff с джиттером»Временную ошибку ретраим, но не сразу. 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 — не удваиваем их
Заголовок раздела «Ретраи уже есть в SDK — не удваиваем их»Прежде чем писать свой цикл повторов, стоит знать, что он уже есть. Официальные SDK Anthropic и OpenAI сами ретраят сетевые ошибки, 408/409/429 и 5xx (по умолчанию 2 повтора) с экспоненциальной паузой и учётом retry-after. Отсюда два корректных варианта:
- Довериться SDK: настроить
max_retriesиtimeoutна клиенте; наша обёртка только логирует и считает деньги. - Отключить ретраи SDK (
max_retries=0) и ретраить самим (например, библиотекойtenacity) — когда нужна своя политика (другие паузы, переключение на резервную модель, метрики по попыткам).
Ошибка — сделать и то и другое: 3 наших попытки × 3 попытки SDK = до 9 запросов, и паузы складываются.
Таймауты
Заголовок раздела «Таймауты»Ретрай срабатывает, когда запрос завершился ошибкой. Но запрос может и не завершиться — тогда его обрывает таймаут. Дефолтный таймаут SDK — минуты (у Anthropic Python SDK — 10 минут). Для интерактивного бота это вечность. Таймаут ставим под задачу: короткий для коротких ответов, а длинные генерации — через стриминг (соединение живо, пока идут токены). Важно: таймаут тоже ретраится, и общее время может достигнуть timeout × (max_retries + 1).
Ретраи и побочные эффекты
Заголовок раздела «Ретраи и побочные эффекты»Повтор запроса к модели безопасен (максимум заплатим дважды). Но в агенте с инструментами (гл. 2.6) повторяем вызов модели, а не уже выполненные действия инструментов. Если инструмент делает что-то необратимое, повтор цикла может сделать это дважды — ещё одна причина держать такие инструменты вне LLM.
Rate limits
Заголовок раздела «Rate limits»Самая частая временная ошибка — 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.
Prompt caching
Заголовок раздела «Prompt caching»Ретраи и лимиты отвечают за надёжность, дальше — про деньги и скорость. С кэшем мы уже встречались: в главе 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.usageprint(u.input_tokens, u.cache_creation_input_tokens, u.cache_read_input_tokens)Тихие убийцы кэша: datetime.now() или UUID в системном промпте; JSON, сериализованный с разным порядком ключей; инструменты в разном порядке; смена модели (кэш привязан к модели); правка начала истории. Правило: стабильное — в начало, изменчивое — в конец.
У OpenAI кэширование префикса автоматическое для достаточно длинных промптов (порог порядка тысячи токенов), без разметки; закэшированная часть видна в деталях usage (cached_tokens) и тарифицируется со скидкой. Принцип «стабильное в начало» тот же.
Batch API
Заголовок раздела «Batch API»Второй способ сэкономить — для задач, где ответ не нужен сразу (разметка датасета, ночной анализ, evals): провайдеры дают Batch API: отправляем пачку запросов, результаты приходят асинхронно (обычно в пределах 24 часов, часто быстрее) примерно за половину цены. Результаты приходят в произвольном порядке — сопоставляем их по своему custom_id, а не по позиции. Кэш и batch можно комбинировать.
Стриминг в обёртке
Заголовок раздела «Стриминг в обёртке»Последний кусок мозаики — стриминг из главы 1.6. Он усложняет ретраи: если ошибка случилась до первого токена, повторяем как обычно; если посреди ответа, а текст уже показан пользователю, прозрачный повтор невозможен — либо начинаем ответ заново (и предупреждаем), либо отдаём ошибку. Usage и stop_reason в стриме приходят в конце — логируем их после завершения.
Каркас llm_utils.py
Заголовок раздела «Каркас llm_utils.py»Всё разобранное выше собирается в один модуль. Каждый следующий скрипт и проект вызывает модель только через него, поэтому ретраи, таймауты, учёт кэша и стоимости пишутся один раз:
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.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Всё сказанное выше — на примере Claude и OpenAI. Для DeepSeek и Ollama мы используем пакет openai, поэтому ретраи, таймауты и классы исключений — те же, что у OpenAI SDK (openai.APIConnectionError, APITimeoutError, RateLimitError, APIStatusError с status_code; по умолчанию 2 повтора). Различаются коды ошибок, природа лимитов и, сильнее всего, кэш.
Ошибки DeepSeek и что ретраить
Заголовок раздела «Ошибки DeepSeek и что ретраить»По странице 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.
Лимиты: конкурентность, а не RPM
Заголовок раздела «Лимиты: конкурентность, а не RPM»У 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 на сотню запросов.
Кэш у DeepSeek: автоматический и без наценки
Заголовок раздела «Кэш у DeepSeek: автоматический и без наценки»По гайду 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: скорость, а не деньги
Заголовок раздела «Кэш у Ollama: скорость, а не деньги»Нативный ответ Ollama содержит prompt_eval_cached_count — сколько токенов промпта взято из уже посчитанного состояния, пока модель в памяти. Денег это не экономит (их нет), но экономит секунды: на обычном ноутбуке обработка длинного промпта — заметная часть времени. Выгрузка модели (keep_alive, по умолчанию 5 минут простоя) сбрасывает и это.
Batch: у DeepSeek его нет — есть ночной тариф
Заголовок раздела «Batch: у DeepSeek его нет — есть ночной тариф»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как основными (один код наopenaiSDK, разныеbase_url, цены и поля кэша);openrouterиз главы 2.4 добавляется ещё одной строкой вPROVIDERS;anthropic— опциональная ветка для сравнения.- Опыт с кэшем — на DeepSeek (кэш автоматический, смотрим только на
usage), опыт с ручной разметкойcache_control— у Claude, для сравнения. - Длинный текст для кэша — публичный: правила доставки, возврата и гарантии магазина «Полка» (их можно сгенерировать моделью) или публичная документация любого API. Данные клиентов, рабочий код и всё, что нельзя показывать посторонним, в облачные модели не отправляем.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Anthropic: Prompt caching (≈45 мин). Читаем: как работает префикс, где ставить
cache_control, сколько стоит запись и чтение, TTL, минимальная длина по моделям, что инвалидирует кэш, как читатьusage. Выписываем: таблицу минимальных длин для выбранной модели и список инвалидаторов. - Страница ошибок Anthropic (docs.anthropic.com/en/api/errors) и страница rate limits (docs.anthropic.com/en/api/rate-limits) (≈30 мин). Выписываем: коды и типы ошибок, оси лимитов, заголовки ответа, как кэш влияет на учёт input-токенов.
- Статья AWS «Exponential Backoff And Jitter» (aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter) (≈15 мин). Выписываем: почему джиттер, чем «full jitter» отличается от других вариантов.
- Batch API (≈20 мин): Anthropic: Batch processing и OpenAI: Batch. Выписываем: скидку, сроки, формат
custom_id, что результаты приходят в любом порядке. - OpenAI для сравнения (≈15 мин): Prompt caching и Rate limits. Отмечаем: кэш автоматический, 429 бывает «квотой».
- README Python SDK Anthropic на GitHub (anthropic-sdk-python) → разделы Retries, Timeouts, Handling errors (≈15 мин). Выписываем:
max_retries,timeout, классы исключений. - (Сравнение с JavaScript-экосистемой) Vercel AI SDK → Getting Started и страница про
streamText/generateObject(≈20 мин). - DeepSeek (≈30 мин): Error Codes, Rate Limit & Isolation (конкурентность, keep-alive, 10 минут), Context Caching (как строится и живёт кэш, поля
usage). Выписываем: таблицу «ретраить / нет» по кодам и правила попадания в кэш. - Ollama (≈15 мин): FAQ —
keep_alive,OLLAMA_NUM_PARALLEL, память; в Chat API — полеprompt_eval_cached_countи длительности. - README Python SDK OpenAI на GitHub (openai-python) → Retries, Timeouts, Handling errors (≈10 мин): именно этот SDK работает у нас с DeepSeek и Ollama.
- Практика (≈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:и 3–4 вывода: когда кэш выгоден, что его ломает, что ретраить, какой таймаут выбран для чата и почему.| провайдер / вариант | запросов | input (промах) | cache write | cache read (попадание) | $ всего | ср. latency, с || DeepSeek, стабильный префикс | 6 | ... || DeepSeek, время в начале | 6 | ... || Ollama (cached_count, время) | 2 | ... || (Claude без / с кэшем) | 6 | ... | - 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 retriesfor 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 может увеличить стоимость?
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 увеличить стоимость?
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.