Проблема: временные сбои неизбежны
Агент — это цепочка сетевых вызовов: к LLM-провайдеру, к инструментам, к БД, к внешним API. В распределённых системах каждый из них периодически даёт временный сбой — не потому что что-то сломано навсегда, а потому что «прямо сейчас» перегрузка, дедлайн, моргнула сеть. Это transient (переходящие) ошибки, и они — норма, а не исключение.
Типичные transient-сбои у агента:
- 429 Too Many Requests — упёрлись в rate limit провайдера (об этом отдельный урок).
- 5xx (500, 502, 503, 504) — провайдер временно перегружен/недоступен.
- Таймауты и сетевые сбои — соединение оборвалось, ответ не пришёл вовремя.
Ключевое наблюдение: почти все они проходят сами через доли секунды. Уронить из-за мгновенного 503 весь запрос пользователя — глупо, когда повтор через 200 мс вернёт успех. Retry — простейший и самый эффективный механизм надёжности, первая линия обороны.
Retry работает ровно для transient-сбоев — тех, что исчезают сами. Повторять то, что сломано детерминированно (неверный API-ключ, кривой запрос, баг в коде), бессмысленно: сколько ни повторяй — будет та же ошибка, только медленнее и дороже. Поэтому центральный вопрос ретрая — не «как повторить», а «что вообще стоит повторять». К нему вернёмся ниже.
Почему backoff экспоненциальный
Первое желание — повторять сразу и часто: «упало → тут же пробуем снова». Это худшее, что можно сделать. Если провайдер упал под нагрузкой, а тысячи клиентов мгновенно и непрерывно ретраят — они создают лавину запросов, которая не даёт ему подняться. Retry без задержки превращает временный сбой в постоянный. Это называют retry storm / thundering herd.
Решение — backoff: пауза между попытками, которая растёт с каждой неудачей. И расти она должна экспоненциально, а не линейно:
Формула проста: задержка = base × 2^(номер попытки), обычно с потолком (cap). Экспонента важна тем, что она быстро отступает: если провайдер серьёзно перегружен, паузы 1→2→4→8 с дают ему реальное время подняться, тогда как линейный рост (1→2→3→4) отступает лениво и продолжает давить.
Тонкость: если 1000 клиентов упали в один момент и все ждут ровно 1, 2, 4, 8 с — они синхронно ударят снова в одни и те же моменты, воспроизводя мини-шторм на каждом шаге. Лечение — jitter: добавить к задержке случайную составляющую (например, ждать случайное время от 0 до вычисленного backoff). Тогда повторы «размазываются» во времени, и нагрузка на провайдера ровная. Exponential backoff без jitter — незаконченный ретрай.
Что ретраить, а что нет
Самое важное решение — не «как повторять», а «что повторять». Повторять надо только сбои, у которых есть шанс пройти сами; повтор детерминированной ошибки — трата времени, денег и, хуже, риск (см. идемпотентность ниже).
| Ошибка | Ретраить? | Почему |
|---|---|---|
| 429 rate limit | ✅ да (с backoff) | Пройдёт, часто есть Retry-After |
| 500 / 502 / 503 / 504 | ✅ да | Временная перегрузка провайдера |
| Таймаут / сетевой сбой | ✅ да (осторожно) | Часто transient; но см. идемпотентность |
| 401 / 403 auth | ❌ нет | Кривой ключ/права — повтор не поможет |
| 400 bad request | ❌ нет | Некорректный запрос — детерминированно упадёт снова |
| 422 / ошибка валидации | ❌ нет | Проблема во входных данных, а не в сервисе |
Слепой retry(any_exception) — антипаттерн: ты будешь пять раз повторять неверный API-ключ (401), кривой промпт (400), баг в коде — тратя секунды и токены на заведомо провальные попытки, и маскируя настоящую проблему. Ретрай — только для явно transient-ошибок (429, 5xx, сетевые). Всё остальное должно падать сразу, чтобы ты увидел настоящую причину.
Идемпотентность: скрытая ловушка
Есть коварный случай: запрос дошёл и выполнился, но ответ потерялся по дороге (таймаут на обратном пути). Клиент думает «сбой» и повторяет — а действие выполняется дважды. Для чтения это безобидно, для действий с побочными эффектами — катастрофа.
Идемпотентность — свойство операции давать тот же результат при повторе. Прежде чем ретраить, спроси: безопасно ли повторить это действие?
- Идемпотентно (повторять безопасно): чтение, поиск, вызов LLM-генерации, GET-запросы — повтор просто даёт ещё один ответ.
- Не идемпотентно (повтор опасен): списание денег, отправка письма, создание заказа, запись в БД — повтор = двойное действие.
Вызовы LLM обычно безопасно ретраить (повторная генерация — не беда). Опасность — в инструментах, меняющих мир: агент вызвал «оформить возврат», получил таймаут, повторил — оформил два возврата. Защита: делать такие операции идемпотентными на стороне API (idempotency key — уникальный ключ операции, по которому сервер отсекает дубли) и/или не ретраить неидемпотентные вызовы вслепую. Ретрай безопасен ровно настолько, насколько идемпотентна операция.
Практика: retry на tenacity
Писать backoff+jitter руками не нужно — есть библиотеки (в Python популярна tenacity; SDK OpenAI/Anthropic уже содержат встроенный ретрай на transient-ошибки). Ключевые параметры, которые ты настраиваешь: какие ошибки ретраить, стратегия задержки, лимит попыток.
from tenacity import (retry, stop_after_attempt, wait_exponential_jitter,
retry_if_exception_type)
import httpx
class TransientError(Exception): ... # 429 / 5xx / сеть — то, что ретраим
@retry(
retry=retry_if_exception_type(TransientError), # ТОЛЬКО transient!
wait=wait_exponential_jitter(initial=1, max=30), # 1,2,4,8… + jitter, потолок 30с
stop=stop_after_attempt(5), # не больше 5 попыток
reraise=True, # исчерпали → пробрасываем ошибку
)
def call_llm(prompt: str) -> str:
resp = httpx.post(LLM_URL, json={"prompt": prompt}, timeout=30)
if resp.status_code == 429 or resp.status_code >= 500:
raise TransientError(resp.status_code) # ретраим
resp.raise_for_status() # 4xx (400/401) → падаем СРАЗУ, без ретрая
return resp.json()["text"]
Обрати внимание на три решения в коде: ретраим только TransientError (не всё подряд), задержка экспоненциальная с jitter и с потолком, и есть жёсткий лимит попыток. 4xx-ошибки поднимаются сразу, без повторов.
При 429 (и иногда 503) провайдер часто присылает заголовок Retry-After — сколько ждать. Если он есть — используй его вместо своего вычисленного backoff: сервер лучше знает, когда будет готов. Хорошие SDK делают это автоматически. Игнорировать Retry-After и долбить по своему графику — способ снова упереться в лимит.
Когда остановиться
Ретрай не бесконечен. Повторять вечно — значит держать пользователя в ожидании и жечь ресурсы на сервис, который, возможно, лёг надолго. Границы:
- Лимит попыток (например, 3–5) или общий бюджет времени — что наступит раньше. Исчерпали → это уже не transient, поднимаем ошибку выше.
- Что дальше после исчерпания? Ретрай — не финал, а первый уровень. Не помогло → включается fallback (запасной провайдер/модель — следующий урок) или graceful degradation (частичный ответ — урок в конце раздела).
- Circuit breaker. Если провайдер стабильно падает, умные системы перестают его дёргать на время («размыкают цепь»), чтобы не тратить попытки впустую и дать ему восстановиться, — а потом осторожно пробуют снова.
Ретрай борется с единичным сбоем. Circuit breaker — с устойчивым: он считает ошибки и, когда их слишком много, «размыкается» — на время возвращает ошибку мгновенно, не нагружая упавший сервис бесполезными попытками. Через паузу пробует один запрос («half-open»): ок → «замыкается» обратно. Это защищает и провайдера (не добиваем), и тебя (не ждём по 30 с на каждый запрос, когда сервис явно лёг). Ретрай + circuit breaker — стандартная связка надёжности.
Типичные ошибки
Мгновенные повторы создают retry storm и добивают упавший провайдер. Всегда используй backoff — и именно экспоненциальный.
Без случайного разброса клиенты синхронно бьют в одни моменты, воспроизводя шторм. Джиттер обязателен.
Повтор 401/400/багов — трата времени и денег, маскировка настоящей проблемы. Ретрай только transient (429/5xx/сеть).
Повтор «оплаты»/«создать заказ» после таймаута может выполнить действие дважды. Делай такие операции идемпотентными (idempotency key) или не ретрай вслепую.
Без лимита попыток/бюджета времени пользователь ждёт вечно. Ограничь, а после исчерпания переходи к fallback / деградации.
Шпаргалка
RETRY = первая линия обороны против TRANSIENT-сбоев (временных, проходят сами)
transient: 429 · 5xx · таймаут/сеть → повтор часто спасает
лечит ВРЕМЕННОЕ, НЕ постоянное (401/400/баг → повтор бесполезен)
BACKOFF (пауза между попытками, РАСТЁТ):
❌ без задержки → retry storm (добивает провайдер)
✓ ЭКСПОНЕНЦИАЛЬНЫЙ: delay = base × 2^attempt + CAP (1,2,4,8…30с)
быстро отступает → даёт провайдеру встать
✓✓ + JITTER (случайный разброс) — иначе клиенты СИНХРОННО бьют = мини-шторм
exponential БЕЗ jitter = незаконченный ретрай
ЧТО РЕТРАИТЬ: 429/5xx/сеть = ДА · 401/403/400/422 = НЕТ (детерминированны → падать сразу)
❌ retry(any_exception) — антипаттерн
ИДЕМПОТЕНТНОСТЬ (безопасно ли повторить?):
✓ чтение/поиск/LLM-генерация — да · ✗ оплата/письмо/заказ/запись — ДВОЙНОЕ действие
ответ потерялся, а действие ВЫПОЛНИЛОСЬ → повтор дублирует
защита: idempotency key на API · не ретрай неидемпотентное вслепую
ПРАКТИКА: tenacity / встроенный ретрай SDK · уважай заголовок Retry-After (429/503)
КОГДА СТОП: лимит попыток (3–5) ИЛИ бюджет времени → дальше FALLBACK / деградация
CIRCUIT BREAKER: много ошибок → «размыкается», мгновенный отказ, не добивает сервис
→ через паузу half-open пробует · ретрай (единичный сбой) + breaker (устойчивый)
ГРАБЛИ: без задержки · без jitter · ретрай всего · неидемпотентное вслепую · бесконечно
Практическое задание
Задание: добавь надёжный ретрай агенту
- Для агента из модулей 03–05 выпиши все сетевые вызовы (LLM, инструменты, БД). Какие из них могут дать transient-сбой?
- Для каждого типа ошибки (429, 500, 503, таймаут, 401, 400) реши: ретраить или нет? Составь таблицу-политику.
- Оберни вызов LLM в exponential backoff + jitter (tenacity или вручную) с лимитом 5 попыток. Проверь, что 4xx падает сразу, а 5xx повторяется.
- Найди среди инструментов агента неидемпотентные (с побочными эффектами). Что случится при их слепом ретрае после таймаута? Как защитить (idempotency key)?
- Смоделируй retry storm: почему 1000 клиентов с ретраем без jitter хуже, чем с jitter? Опиши на пальцах.
- Что твой агент делает, когда все 5 попыток исчерпаны? (Пока — просто зафиксируй; ответ — в следующих уроках про fallback и деградацию.)
- Со звёздочкой: добавь простой circuit breaker: после N ошибок подряд перестань дёргать провайдер на T секунд, потом пробуй один запрос. Как это меняет поведение при долгом падении?
Что дальше
Ретрай спасает от мгновенных сбоев. Но что делать, когда провайдер лёг надолго и повторы исчерпаны? Не падать же совсем. Ответ — переключиться на запасной вариант. Следующий урок — fallback-стратегии: запасные провайдеры и модели, деградация цепочкой, чтобы агент продолжал работать при отказе основного LLM.