Fallback для LLM-агента: что делать, когда модель молчит
Агент падает не от плохого промпта, а от таймаута провайдера и невалидного JSON. Разбираем, как выстроить деградацию по слоям, чтобы пользователь не увидел белый экран.

Один сбойный ответ модели в проде обходится дороже, чем кажется. Запрос к OpenAI или Anthropic может вернуть таймаут после 60 секунд ожидания, отдать 429 Too Many Requests в час пик, выдать JSON с лишней запятой или просто зациклиться в вызове инструмента. Если на каждый из этих случаев нет заранее прописанной ветки, агент показывает пользователю пустоту или стектрейс. Fallback — это не блок except в конце функции, а спроектированная заранее лестница деградации: от идеального ответа к приемлемому, от приемлемого к честному «не смог».
Почему одного retry недостаточно
Стандартная реакция на ошибку — повторить запрос. Это закрывает часть проблем: сетевые сбои, кратковременные 503, единичные таймауты. Но retry вслепую опасен. Если модель вернула невалидный JSON из-за того, что в промпте противоречивые инструкции, повтор вернёт тот же мусор — вы просто заплатите за токены дважды и добавите задержку.
Ошибки агента делятся на два класса, и лечатся они по-разному:
- Транзиентные — таймаут, rate limit,
500/503на стороне провайдера, обрыв соединения. Лечатся повтором с экспоненциальной задержкой. - Детерминированные — невалидная схема ответа, отказ модели (refusal), превышение контекста, зацикливание в инструментах. Повтор бесполезен: нужно менять вход, модель или сам подход.
Первый шаг проектирования — разложить возможные отказы по этим двум корзинам и решить для каждого, что делать.
Лестница деградации
Хороший fallback работает слоями. Каждый следующий слой дешевле, надёжнее и хуже по качеству предыдущего. Задача — спуститься на минимально необходимую ступень, а не сразу прыгать в самый низ.
- Retry той же модели с backoff — для транзиентных ошибок.
- Переключение на резервную модель — другой провайдер или более лёгкая версия, если основной недоступен.
- Упрощение задачи — убрать инструменты, сократить контекст, снизить требования к формату.
- Детерминированный путь без LLM — шаблонный ответ, поиск по базе, правило.
- Честный отказ — сообщение пользователю с тем, что можно сделать дальше.
Fallback, который сам может упасть — это не fallback. Последняя ступень лестницы обязана быть кодом без единого сетевого вызова.
Резервная модель: не всё так просто
Переключение на второго провайдера — популярный приём, но у него есть цена, которую забывают заложить. Резервная модель может по-другому понимать ваш системный промпт, иначе форматировать JSON, поддерживать другой набор параметров вызова инструментов. Если вы годами затачивали промпт под одну модель, «горячая замена» на другую в момент аварии даст ответы худшего качества как раз тогда, когда всё и так плохо.
Практичный компромисс — держать резервную конфигурацию отдельно и проверять её на том же наборе тестов, что и основную. Тогда переключение не превращается в лотерею.
| Стратегия резерва | Плюс | Минус |
|---|---|---|
| Та же модель, retry | Идентичное качество | Не спасёт от отказа провайдера целиком |
| Другая версия того же вендора | Похожее поведение промпта | Падает вместе с вендором при глобальном сбое |
| Другой вендор | Независимость от одного провайдера | Свой формат ответа, отдельная отладка промпта |
| Локальная лёгкая модель | Работает при полном отсутствии сети наружу | Заметно ниже качество на сложных задачах |
Валидация ответа как часть fallback
Самая частая тихая ошибка агента — не исключение, а внешне успешный ответ, который не проходит по схеме. Модель вернула 200, текст есть, но JSON битый или поле отсутствует. Если вы не валидируете структуру, ошибка всплывёт ниже по коду, где ловить её уже поздно.
Схема ответа должна проверяться до того, как результат уйдёт дальше. Невалидный ответ — это детерминированная ошибка, и на неё есть свой fallback: один повтор с уточнённой инструкцией, а если не помогло — спуск на ступень ниже.
from pydantic import BaseModel, ValidationError
class AgentReply(BaseModel):
intent: str
answer: str
confidence: float
def parse_or_fallback(raw: str) -> AgentReply | None:
try:
return AgentReply.model_validate_json(raw)
except ValidationError:
# не retry вслепую: логируем сырой ответ,
# чтобы понять, почему схема не сошлась
log.warning("schema_mismatch", extra={"raw": raw[:500]})
return None
def run_agent(prompt: str) -> AgentReply:
raw = call_model(prompt) # ступень 1
reply = parse_or_fallback(raw)
if reply is not None:
return reply
raw = call_model(prompt, strict_json=True) # ступень 1b: уточнили формат
reply = parse_or_fallback(raw)
if reply is not None:
return reply
return AgentReply( # ступень 5: честный отказ
intent="unknown",
answer="Не удалось обработать запрос, попробуйте переформулировать.",
confidence=0.0,
)
Обратите внимание: последняя ветка не делает сетевых вызовов и не может выбросить исключение. Это и есть дно лестницы.
Таймауты и бюджет времени
У fallback есть общий бюджет времени, который легко проесть повторами. Если основной вызов ждёт 60 секунд, потом retry ещё 60, потом резервная модель ещё 60 — пользователь ушёл задолго до ответа. Задавайте таймаут на весь пайплайн, а не на отдельный вызов, и уменьшайте лимиты по мере спуска по лестнице: резервная попытка должна быть быстрее основной, а не такой же.
Зацикливание агента
Агент с инструментами умеет уходить в бесконечный цикл: вызывает поиск, получает результат, снова вызывает поиск, и так до исчерпания токенов или таймаута. Это отдельный класс отказа, который не ловится обычным try/except.
- Жёсткий лимит на число шагов (итераций reasoning-tool-loop) — например, не больше 8–10.
- Детектор повторов: если агент дважды подряд вызывает один инструмент с теми же аргументами — это сигнал остановиться.
- При достижении лимита — не обрывать молча, а отдать пользователю то, что уже собрано, с пометкой о неполноте.
Что логировать
Fallback без наблюдаемости превращается в чёрный ящик: система «как-то работает», но вы не знаете, какая доля запросов уходит на резервные ступени. Если 30% трафика тихо обслуживается лёгкой моделью, качество продукта деградирует, а вы об этом не подозреваете.
- На какой ступени лестницы завершился каждый запрос.
- Причина спуска: таймаут, rate limit, невалидная схема, лимит шагов.
- Полный сырой ответ модели при ошибке валидации — иначе не отладить промпт.
- Метрика доли запросов ниже первой ступени как ранний индикатор проблем у провайдера.
Проектировать fallback стоит до того, как агент попадёт в прод, а не после первого инцидента. Начните с одного вопроса по каждой возможной ошибке: транзиентная она или детерминированная, и на какую ступень лестницы она вас опускает. Всё остальное — это код, который переводит ответы на эти вопросы в ветки.
Prompt-инженер: Идеальные запросы для Midjourney, ChatGPT и других моделей.
Спросить за 15 ₽Источники: OpenAI API — Error codes and rate limits, Anthropic API — Errors
Частые вопросы
Сколько раз повторять запрос при таймауте?
Стоит ли делать retry при невалидном JSON?
Обязательно ли держать второго провайдера?
Как понять, что агент зациклился?
Что показывать пользователю на последней ступени fallback?
Нужно ли логировать успешные ответы или только ошибки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.