Проблема: единственный провайдер = единая точка отказа

Ретрай отвечает на «моргнуло — повторим». Fallback — на другой вопрос: «а что если не починится, сколько ни повторяй?». Провайдеры LLM — внешние сервисы, и они падают по-настоящему: были громкие многочасовые простои OpenAI, Anthropic, облаков. Если весь агент завязан на одного — это single point of failure: его падение = падение твоего продукта, и ты ничего не можешь сделать, кроме как ждать.

Fallback снимает эту зависимость: когда основной путь мёртв, есть запасной. Разница с ретраем принципиальна:

Retry (прошлый урок)Fallback (этот урок)
Против чегоМгновенный transient-сбойДлительный отказ / исчерпанный ретрай
ДействиеПовторить то же самоеПереключиться на другое
Тот же провайдер?ДаНет — запасной провайдер/модель
РезультатТот же ответ, чуть позжеОтвет от альтернативы (возможно, хуже)
ℹ️ Retry и fallback — уровни одной обороны

Они не конкурируют, а образуют лестницу: сначала ретрай (несколько попыток с backoff) — вдруг быстро восстановится; исчерпали → fallback на запасной вариант; и он мёртв → graceful degradation (частичный ответ — финальный урок раздела). Каждый уровень включается, когда предыдущий сдался. Fallback стоит между ретраем и полной деградацией.

Виды fallback

«Запасной вариант» — понятие широкое. У агента есть несколько осей, по которым можно откатиться:

  • Другой провайдер, та же модель-класс. Лёг OpenAI → идём в Azure OpenAI или к аналогичной модели Anthropic. Самый прямой fallback: качество почти то же.
  • Другая модель того же провайдера. Недоступна флагманская → берём модель попроще/подешевле у того же вендора. Работает, если проблема в конкретной модели, а не во всём провайдере.
  • Более дешёвая/быстрая модель. Осознанная деградация качества ради доступности: лучше ответ похуже, чем никакого.
  • Кэш / заготовленный ответ. Если запрос уже был — отдать закэшированный; для типовых — шаблонный ответ.
  • Локальная/self-hosted модель как последний рубеж независимости от внешних API.
✅ Настоящий fallback — у другого вендора

Ключевой нюанс: fallback на другую модель того же провайдера не спасёт, если лёг весь провайдер (а падает обычно именно так — целиком). Для реальной устойчивости запасной вариант должен быть у независимого вендора/инфраструктуры (другое облако, другая компания). Иначе это иллюзия резервирования: обе «руки» держатся за одну упавшую опору.

Fallback chain

На практике fallback оформляют как цепочку: упорядоченный список вариантов от лучшего к запасному. Запрос идёт по цепочке: не сработал первый (после своих ретраев) → второй → третий. Первый успешный ответ возвращается.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Fallback chain: запрос спускается до первого живого Запрос агента ① OpenAI (осн.) + retry с backoff ✗ лёг → дальше ② Anthropic (вендор 2) независимый провайдер ✗ тоже недоступен ③ Дешёвая/локальная хуже, но ЖИВА ✓ ответ! первый успешный ответ → пользователю По цепочке качество/стоимость обычно ДЕГРАДИРУЮТ: лучшее качество сравнимо хуже, но доступно ⚠️ вариант ② — у ДРУГОГО вендора (иначе оба лягут вместе) каждый уровень: свои retry → не помог → следующий · логируй, КТО ответил
Fallback chain (концептуально)
python
import structlog
log = structlog.get_logger()

# цепочка от лучшего к запасному; ② — ДРУГОЙ вендор (не тот же провайдер!)
PROVIDERS = [
    ("openai:gpt-4o",        call_openai),      # ① основной
    ("anthropic:claude",     call_anthropic),   # ② независимый вендор
    ("local:llama",          call_local),       # ③ последний рубеж, хуже но жив
]

def generate_with_fallback(prompt: str) -> str:
    last_err = None
    for name, call in PROVIDERS:
        try:
            result = call(prompt)              # внутри у call — свой retry+backoff
            if name != PROVIDERS[0][0]:
                log.warning("fallback_used", provider=name)  # НЕ основной → сигнал!
            return result
        except Exception as e:
            log.error("provider_failed", provider=name, error=str(e))
            last_err = e
            continue                            # переходим к следующему в цепочке
    raise RuntimeError("all providers failed") from last_err   # → graceful degradation

# фреймворки дают это из коробки: LangChain .with_fallbacks([...]),
# прокси-роутеры (LiteLLM и т.п.) маршрутизируют между провайдерами

Подводный камень: модели не взаимозаменяемы

Соблазнительно думать «просто подставим другую модель». Но модели разных провайдеров/семейств не эквивалентны, и наивная подмена ломается на деталях:

  • Разный формат API. Структура запроса/ответа, имена полей, способ передачи tool-вызовов отличаются между OpenAI и Anthropic. Нужен слой абстракции или прокси, приводящий к единому виду.
  • Разное поведение на том же промпте. Промпт, вылизанный под одну модель, на другой может дать иной формат/качество. Иногда fallback-модели нужен свой промпт.
  • Разные возможности. Не у всех одинаковый tool calling, structured output, размер контекста. Fallback на слабую модель может не уметь того, на чём построен агент.
  • Разный контекст-лимит. Запрос, влезавший в 200k токенов, не влезет в модель со 8k — fallback упадёт по другой причине.
⚠️ Fallback-путь надо тестировать, а не «надеяться»

Классическая ловушка: fallback настроен, но никогда не проверялся — и в момент реальной аварии выясняется, что запасная модель не понимает формат tool-вызовов, и агент падает в fallback тоже. Fallback-путь — такой же код, он должен быть покрыт интеграционными тестами (прошлый раздел): регулярно прогоняй запросы через запасные провайдеры, проверяй совместимость промптов и форматов. Непротестированный fallback — это не резерв, а ложное чувство безопасности.

Цена fallback: качество и стоимость

Fallback — это компромисс, а не бесплатная страховка. Спускаясь по цепочке, ты обычно жертвуешь:

  • Качеством. Запасная (более дешёвая/локальная) модель отвечает хуже. Осознанное решение: «хуже, но есть» лучше, чем «идеально, но недоступно» — но пользователь получает деградированный ответ.
  • Стоимостью/латентностью. Иногда наоборот: запасной вариант дороже или медленнее. Плюс двойной расход, если считать неудачные попытки основного.
  • Консистентностью. Ответы от разных моделей отличаются по стилю/формату — пользователь может заметить «скачок».
📊 Мониторь fallback-rate как сигнал тревоги

Опаснейшая ошибка — тихий fallback: система молча работает на запасном варианте, никто не знает, и все думают, что всё хорошо, — пока не приходит счёт или жалобы на качество. Fallback-rate (доля запросов, ушедших не на основной провайдер) — обязательная метрика на дашборде (раздел observability). Рост fallback-rate = основной провайдер деградирует прямо сейчас → алерт. Логируй, кто именно ответил на каждый запрос. Fallback без наблюдаемости маскирует проблему вместо того, чтобы её решать.

Типичные ошибки

❌ Ошибка 1: fallback на тот же провайдер

Запасная модель у того же вендора не спасёт при падении всего провайдера (а падает обычно целиком). Настоящий резерв — у независимого вендора/инфраструктуры.

❌ Ошибка 2: непротестированный fallback-путь

Настроен, но никогда не проверялся → в аварию выясняется, что запасная модель не умеет tool calling/формат. Покрывай fallback интеграционными тестами и прогоняй регулярно.

❌ Ошибка 3: тихий fallback без метрики

Система молча деградирует, никто не знает — до счёта/жалоб. Мониторь fallback-rate, логируй, кто ответил, ставь алерт на рост.

❌ Ошибка 4: игнор совместимости моделей

Промпт/контекст/возможности под одну модель не переносятся автоматом на другую. Учитывай формат API, свои промпты, лимиты контекста для fallback-модели.

Шпаргалка

Fallback стратегии — всё в одном месте
text
FALLBACK = план Б при ДЛИТЕЛЬНОМ отказе (когда retry исчерпан, провайдер лёг надолго)
  единственный провайдер = SINGLE POINT OF FAILURE (падают по-настоящему, надолго)
  retry: повторить ТО ЖЕ · fallback: переключиться на ДРУГОЕ
  лестница: retry → fallback → graceful degradation

ВИДЫ: другой ВЕНДОР (та же модель-класс) · др. модель того же вендора ·
  дешевле/быстрее (осознанная деградация) · кэш/шаблон · локальная модель
  ✓ настоящий резерв — у НЕЗАВИСИМОГО вендора (иначе лягут вместе)

FALLBACK CHAIN: упорядоченный список лучший→запасной, первый живой отвечает
  каждый уровень: свой retry → не помог → следующий
  качество/стоимость по цепочке обычно ДЕГРАДИРУЮТ («хуже, но есть» > «идеально, но нет»)
  инструменты: LangChain .with_fallbacks([...]) · прокси-роутеры (LiteLLM и т.п.)

⚠️ МОДЕЛИ НЕ ВЗАИМОЗАМЕНЯЕМЫ:
  разный формат API · разное поведение на промпте (нужен свой промпт) ·
  разные возможности (tool calling/structured output) · разный контекст-лимит
  → fallback-путь ТЕСТИРОВАТЬ (интеграционные тесты), а не надеяться

ЦЕНА: качество↓ · стоимость/латентность (иногда↑) · консистентность (разный стиль)
  📊 МОНИТОРЬ FALLBACK-RATE (доля не-основного) → алерт на рост = провайдер деградирует
  логируй КТО ответил · тихий fallback = маскировка проблемы

ГРАБЛИ: тот же провайдер · непротестированный путь · тихо без метрики · игнор совместимости

Практическое задание

Задание: построй fallback для агента

  1. Для агента из модулей 03–05 определи single point of failure: какой один провайдер/сервис уронит весь агент, если ляжет надолго?
  2. Спроектируй fallback chain из 2–3 уровней. Убедись, что хотя бы один уровень — у независимого вендора. Обоснуй порядок.
  3. Проверь совместимость: одинаков ли формат tool-вызовов/structured output у основной и запасной модели? Что придётся адаптировать (промпт, парсинг)?
  4. Реализуй цепочку (вручную или через .with_fallbacks). Залогируй, кто ответил на каждый запрос.
  5. Смоделируй падение основного провайдера (заставь его кидать ошибку) и проверь, что агент дошёл до запасного и вернул ответ. Работает ли fallback-путь на самом деле?
  6. Придумай метрику fallback-rate и алерт: при какой доле не-основных ответов ты хочешь получить сигнал? Почему тихий fallback опасен?
  7. Со звёздочкой: добавь в цепочку «дешёвую» модель как предпоследний уровень и оцени, насколько просела бы quality-метрика (из раздела оценки), если бы весь трафик пошёл через неё.

Что дальше

Ретрай и fallback реагируют на отказ постфактум. Но часть отказов можно предотвратить — и самый частый из них у LLM это 429 «слишком много запросов». Управлять своим темпом запросов, чтобы не упираться в лимиты провайдера, — тема следующего урока: rate limiting и управление квотами.