Проблема: «всё или ничего» — плохой выбор

Агент устроен так, что редко всё ломается сразу. Гораздо чаще отказывает часть: один из пяти инструментов недоступен, fallback-модель отвечает хуже, дедлайн истёк на середине reasoning. Вопрос: что делать с этим частичным состоянием?

Есть два подхода, и «всё или ничего» — почти всегда неправильный:

❌ Fail hard (всё или ничего)

Любой сбой любого компонента → весь запрос падает с ошибкой. Проделанная работа (найденные документы, частичные вычисления) выброшена. Пользователь получает 500 и пустой экран, хотя 80% ответа было готово.

✅ Graceful degradation

Сбой части → отдаём то, что успели, честно помечая, чего не хватает. «Вот сводка по документам; актуальные цены сейчас недоступны, попробуйте позже». Пользователь получает пользу, а не ошибку.

ℹ️ Деградация — это последний уровень обороны

Вспомни лестницу из раздела: retry (повтори) → fallback (переключись) → graceful degradation (отдай, что есть). Первые два пытаются получить полный результат. Деградация включается, когда это не удалось, и отвечает на вопрос «как достойно проиграть». Это не признание поражения, а инженерное решение: максимизировать пользу в условиях частичного отказа, вместо того чтобы обнулять её ошибкой.

Уровни деградации

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

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Ступени деградации: от полного к минимальному ① Полный ответ всё сработало — идеал ② Полный, но от запасной модели fallback сработал — чуть хуже качество ③ Частичный ответ часть инструментов/данных недоступна — отдаём, что есть ④ Кэш / устаревшие данные свежее недоступно → отдаём последнее известное, помечаем ⑤ Честное «сейчас не могу» даже минимума нет → понятное сообщение + что делать (НЕ голый 500) полезность падает, но НЕ до нуля спускайся на ОДНУ ступень за раз — отдай наилучшее из достижимого, а не 500

Для конкретного агента ступени наполняются смыслом:

  • Частичный результат. Отработали 3 из 5 инструментов → отдаём ответ на основе трёх, отметив, что двух данных нет.
  • Кэш / устаревшее. Свежие данные недоступны → отдаём последние закэшированные с пометкой «данные на такое-то время».
  • Упрощённый режим. Не работает reasoning-цикл с инструментами → отвечаем прямой генерацией LLM без инструментов (хуже, но что-то).
  • Заготовленный ответ. Для типовых запросов — шаблонный ответ вместо динамического.
  • Честный отказ. Совсем ничего нет → понятное сообщение с указанием, что делать (не голый стектрейс).

Дизайн: чтобы отказ части не ронял всё

Деградация возможна, только если система спроектирована под неё. Если один упавший инструмент кидает исключение, которое роняет весь пайплайн, — деградировать нечему. Принципы «деградируемого» дизайна:

  • Изоляция сбоев. Отказ одного компонента (инструмента, источника) не должен распространяться на весь агент. Каждый рискованный вызов обёрнут так, что его падение — обрабатываемое событие, а не крах.
  • Обязательное vs опциональное. Раздели, что критично для ответа, а что — «приятное дополнение». Упал опциональный источник (например, обогащение) → продолжаем без него. Упал критичный → тогда деградируем сильнее.
  • Значения по умолчанию. Компонент недоступен → у него есть разумный дефолт/заглушка, а не None, ломающий следующий шаг.
  • Собирать частичное состояние. Агент должен уметь ответить из того, что уже накоплено в state (модуль 03), а не только из полного набора данных.
Деградируемый сбор данных: опциональное не роняет ответ
python
def gather(query: str) -> dict:
    data, missing = {}, []

    # КРИТИЧНЫЙ источник — без него ответ бессмыслен
    try:
        data["docs"] = search_docs(query)      # уже с retry/timeout внутри
    except Exception:
        raise CriticalFailure("нет доступа к базе знаний")  # тут деградируем СИЛЬНО

    # ОПЦИОНАЛЬНЫЕ источники — упал → продолжаем без них, помечаем пропуск
    for name, fetch in [("prices", get_prices), ("reviews", get_reviews)]:
        try:
            data[name] = fetch(query)
        except Exception as e:
            log.warning("optional_source_failed", source=name, error=str(e))
            missing.append(name)               # НЕ падаем — копим, чего не хватило

    data["_missing"] = missing                 # прокинем в ответ для честной пометки
    return data

# ответ строится из ТОГО, ЧТО ЕСТЬ, с явным указанием пропусков (см. ниже)
✅ Реши заранее: что критично, что опционально

Ключевое проектное решение — классифицировать каждый источник/инструмент: без чего ответ невозможен (критично), а что лишь улучшает (опционально). Это делается на этапе дизайна, а не в момент аварии. Тогда деградация становится предсказуемой: падение опционального — тихо продолжаем с пометкой, падение критичного — спускаемся на ступень ниже. Без этой классификации любой сбой одинаково фатален.

Честная коммуникация ограничений

Частичный ответ обязан честно сообщать, что он частичный. Тихо отдать неполный результат как полный — опаснее, чем ошибка: пользователь примет решение на неполных данных, не зная об этом. Деградация без честности — это ложь.

  • Помечай, чего не хватает. «Актуальные цены временно недоступны» — прямо в ответе, а не в логах.
  • Указывай свежесть. Для кэша/устаревшего — «данные на 14:30», чтобы пользователь понимал степень актуальности.
  • Подсказывай, что делать. «Попробуйте через несколько минут» лучше, чем тупик.
  • Не выдавай деградацию за норму. Пользователь должен различать полный и частичный ответ.
⚠️ Тихая деградация опаснее честной ошибки

Самый коварный антипаттерн: система молча отдаёт неполный/устаревший ответ, выглядящий как полный. Пользователь (или другой агент, если это A2A из модуля 05) доверяет ему и действует — на неверных данных. Особенно критично для фактов, цен, медицины, финансов. Правило: деградация всегда видима — в ответе для пользователя И в метриках для тебя (degradation-rate на дашборде, как fallback-rate). Молчаливая деградация маскирует проблему и подрывает доверие сильнее, чем честное «не смог».

Синтез: контур надёжности агента

Этим уроком закрывается весь раздел. Соберём пять механизмов в единую эшелонированную оборону — каждый ловит свой класс отказа, а вместе они превращают хрупкого агента в устойчивый production-сервис:

Запрос агента
   │
   ▼  ⏱ TIMEOUT/DEADLINE — общий бюджет времени + лимит итераций (урок 4)
   │
   ├─▶ 🚦 RATE LIMITING — держим темп ниже лимитов, не создаём 429 (урок 3)
   │
   ▼  вызов LLM/инструмента
   │
   ├─ сбой transient? ─▶ 🔁 RETRY + backoff+jitter (урок 1) ── ок ─┐
   │                                     │ исчерпан                │
   │                                     ▼                         │
   ├─ провайдер лёг? ──▶ 🔀 FALLBACK на др. вендора/модель (урок 2)─┤
   │                                     │ все легли               │
   │                                     ▼                         │
   └─ ничего не вышло ─▶ 🛟 GRACEFUL DEGRADATION (урок 5) ──────────┤
                          частичный / кэш / честное «не смог»       │
                                                                    ▼
                                          Пользователь ВСЕГДА получает ответ
                                          (полный · частичный · честный отказ)
       + сквозь всё: observability (трейсы/логи/метрики) — видеть, что происходит
🎯 Надёжность — это слои, а не один приём

Ни один механизм не универсален: retry бессилен против долгого падения, fallback — против исчерпанного дедлайна, timeout — против плохого качества. Сила в композиции: transient ловит retry, длительный отказ — fallback, перегрузку — rate limiting, зависание — timeout, а остаток (когда ничего не помогло) — graceful degradation. Плюс сквозная observability из первого раздела, чтобы всё это видеть. Вместе — агент, который под нагрузкой и в авариях продолжает приносить пользу, а не отдаёт 500.

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

❌ Ошибка 1: «всё или ничего»

Любой сбой части → весь запрос падает с 500, проделанная работа выброшена. Отдавай частичный результат, когда полный недостижим.

❌ Ошибка 2: тихая деградация

Неполный/устаревший ответ выдан как полный — пользователь действует на неверных данных. Деградация всегда видима: пометка в ответе + метрика.

❌ Ошибка 3: не спроектирована изоляция сбоев

Один упавший инструмент кидает исключение, роняющее весь пайплайн — деградировать нечему. Изолируй сбои, раздели критичное и опциональное заранее.

❌ Ошибка 4: голый 500 без подсказки

Даже при полном отказе стектрейс/пустой экран — плохо. Дай понятное сообщение и что делать дальше («попробуйте позже»).

Шпаргалка

Graceful degradation — всё в одном месте
text
GRACEFUL DEGRADATION = частичный результат вместо ПОЛНОГО падения
  «всё или ничего» = плохо: сбой части → 500, проделанная работа выброшена
  последний уровень обороны: retry → fallback → DEGRADATION («как достойно проиграть»)

УРОВНИ (ступени полезности, спускайся по одной):
  ① полный ответ · ② полный от запасной модели (fallback) · ③ ЧАСТИЧНЫЙ (что успели) ·
  ④ кэш/устаревшее (с пометкой свежести) · ⑤ честное «сейчас не могу» + что делать
  полезность падает, но НЕ до нуля (не 500)
  формы: частичный · кэш · упрощённый режим (LLM без tools) · шаблон · честный отказ

ДИЗАЙН (деградация возможна, только если ЗАПРОЕКТИРОВАНА):
  изоляция сбоев (падение компонента ≠ крах агента) ·
  КРИТИЧНОЕ vs ОПЦИОНАЛЬНОЕ (решить ЗАРАНЕЕ!) — опц. упал → продолжаем с пометкой ·
  дефолты вместо None · уметь ответить из накопленного state

ЧЕСТНОСТЬ (обязательна!):
  помечай чего не хватает (в ОТВЕТЕ, не в логах) · указывай свежесть · подсказывай что делать
  ⚠️ ТИХАЯ деградация ОПАСНЕЕ ошибки: юзер/агент действует на неполных данных
  деградация ВСЕГДА видима: пометка юзеру + degradation-rate на дашборде

КОНТУР НАДЁЖНОСТИ (весь раздел): timeout+лимит итераций (4) · rate limiting (3) →
  transient→RETRY (1) → провайдер лёг→FALLBACK (2) → ничего→DEGRADATION (5)
  + сквозная observability → юзер ВСЕГДА получает ответ (полный/частичный/честный отказ)
  надёжность = СЛОИ, не один приём

ГРАБЛИ: «всё или ничего» · тихая деградация · нет изоляции сбоев · голый 500 без подсказки

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

Задание: сделай агента деградируемым

  1. Для агента из модулей 03–05 классифицируй все источники/инструменты: что критично для ответа, а что опционально? Что сейчас происходит при падении опционального?
  2. Выпиши ступени деградации для своего агента: как выглядит полный, частичный, кэш-ответ и честный отказ на конкретном сценарии?
  3. Реализуй изоляцию: оберни опциональные вызовы так, чтобы их падение копилось в «missing», а не роняло пайплайн. Проверь на сломанном опциональном источнике.
  4. Сделай ответ честным: как ты пометишь пользователю, что часть данных недоступна/устарела? Покажи формулировку для частичного ответа.
  5. Придумай метрику degradation-rate (по аналогии с fallback-rate). Почему тихая деградация без метрики опасна?
  6. Собери весь контур: нарисуй путь запроса через timeout → rate limit → retry → fallback → degradation. Где какой механизм включается? Что получает пользователь в худшем случае?
  7. Со звёздочкой: смоделируй каскадную аварию (основной провайдер лёг + опциональный источник недоступен + дедлайн жмёт). Что в итоге получит пользователь? Это полезнее, чем 500?

Что дальше

Раздел «Надёжность и обработка ошибок» закрыт: retry, fallback, rate limiting, timeouts и graceful degradation вместе делают агента устойчивым к реальному проду. Твой агент теперь наблюдаем, оценён, протестирован и надёжен. Осталось сделать его эффективным и развёрнутым. Следующий раздел — «Оптимизация и деплой»: context engineering, prompt caching, FastAPI для агентских API, оптимизация latency/cost и деплой.