LLM API не упал, а прод всё равно встал: 6 ошибок в таймаутах и ретраях

Большинство разработчиков оборачивают вызов LLM в три ретрая и считают задачу решённой.

В продакшене такая схема превращает один медленный запрос в минуту ожидания, умножает расходы и иногда повторно запускает действия агента.

Разберу шесть ошибок, из-за которых механизм отказоустойчивости сам становится причиной сбоя.

1. Один таймаут на весь запрос

Вызов LLM состоит из нескольких этапов:

— подключение к провайдеру; — ожидание первого токена; — генерация ответа; — вызов инструментов; — обработка результата.

Если поставить единый таймаут в 60 секунд, система может потратить почти всё время на основной провайдер. Для fallback-модели уже не останется запаса.

Я использую общий временной бюджет запроса. Например, 15 секунд:

— до 8 секунд основной модели; — до 5 секунд резервной; — 2 секунды на сеть и обработку.

Fallback должен укладываться в общий дедлайн, а не запускать новый полный цикл.

2. Ретраи выполняются для любых ошибок

Повторять запрос имеет смысл при:

— 429 Too Many Requests; — временных сетевых ошибках; — ответах 502, 503 и 504; — разрыве соединения до получения ответа.

Но ретрай не исправит:

— неверный API-ключ; — ошибочный формат запроса; — превышение контекстного окна; — запрещённый контент; — отсутствующую модель.

Повторение таких запросов только увеличивает задержку и расходы.

3. Повторный вызов агента создаёт повторное действие

Для обычного чат-бота дублированный ответ неприятен, но не критичен.

У агента последствия серьёзнее. Повторный запрос может:

— дважды создать задачу; — повторно отправить письмо; — продублировать запись в CRM; — провести одну операцию несколько раз.

Каждому действию нужен idempotency key. Перед выполнением инструмента система должна проверить, не обрабатывалась ли эта операция ранее.

Ретраить генерацию текста и ретраить действие во внешней системе — не одно и то же.

4. Все запросы повторяются одновременно

Допустим, провайдер начал отвечать медленнее. Сотни запросов получают ошибку и через секунду одновременно запускаются повторно.

Возникает retry storm: нагрузка растёт именно в момент, когда сервис уже испытывает проблемы.

Поэтому между попытками нужен случайный интервал — jitter. Например, от 200 до 800 мс.

Также стоит ограничить общее количество параллельных повторов через очередь или semaphore.

5. Fallback существует только на схеме

Часто в конфигурации указаны две модели, но резервная получает промпт и параметры основного провайдера.

В результате она возвращает другой формат, не поддерживает нужный инструмент или не помещает запрос в своё контекстное окно.

Fallback необходимо тестировать как отдельный рабочий сценарий:

— совместим ли формат tools; — совпадает ли структура JSON; — поддерживается ли нужное контекстное окно; — проходит ли валидация ответа; — устраивает ли качество результата.

Наличие второй модели ещё не означает отказоустойчивость.

6. В логах нет истории попыток

Сообщение «LLM request failed» почти бесполезно.

Для каждого запроса я сохраняю:

— correlation ID; — провайдера и модель; — длительность попытки; — код ошибки; — число ретраев; — факт переключения на fallback; — количество входных и выходных токенов; — итоговый статус.

Тогда можно быстро понять: проблема в модели, сети, лимитах или собственной логике.

Надёжная схема — это не «попробовать ещё три раза»

Для большинства проектов достаточно:

1. Общего дедлайна на весь запрос. 2. Одной дополнительной попытки только для временных ошибок. 3. Backoff с jitter. 4. Idempotency key для действий агента. 5. Проверенного fallback-провайдера. 6. Полной трассировки всех попыток.

Иначе ретраи не спасают систему, а просто делают сбой более долгим и дорогим.

Как у вас устроена работа с ошибками LLM API: простой retry или отдельный gateway с бюджетами и fallback?