Проблема
Запит виглядає звичайним: зібрати профіль клієнта і підготувати коротку відповідь.
У трейсах видно інше: один зовнішній tool почав повертати timeout,
агент перейшов у retries, через 4 хвилини перевантажив worker pool,
а ще через 7 хвилин почали деградувати вже не пов'язані workflow і сервіси.
Початковий збій був локальним. Але через цикл агента він став системним.
Система не падає одразу.
Вона поступово тягне за собою все більше залежностей.
Аналогія: уяви затор на одній смузі мосту. Спочатку гальмує лише одна смуга. Потім хвиля зупинки доходить до всіх під'їздів до мосту. Каскадний збій в агенті працює так само: локальна проблема без обмежень швидко стає спільною проблемою системи.
Чому це стається
Каскадний збій виникає не через одну "погану" відповідь інструмента, а через підсилення помилки в кількох шарах одночасно.
У production зазвичай так:
- один
toolдеградує (5xx,429,timeout); - retries запускаються одразу в кількох місцях (SDK, gateway, агент);
- черга росте, воркери блокуються очікуванням;
- latency збільшується і для інших run'ів, навіть без цього
tool; - без fail-fast і safe-mode система продовжує множити виклики.
Проблема не в одному нестабільному сервісі. Runtime не зупиняє хвилю, поки вона ще локальна.
Які збої трапляються найчастіше
У production найчастіше видно чотири патерни cascading failures.
Множення ретраїв між шарами (Retry amplification)
Один збій повторюється в HTTP-клієнті, tool gateway і reasoning loop агента.
Кількість викликів росте геометрично.
Мініприклад: 1 failure -> 3 retries у SDK -> 3 retries у gateway -> 3 retries у agent loop = 27 викликів.
Типова причина: retry policy розмазана по кількох місцях.
Сатурація спільного пулу (Shared pool saturation)
Проблемний tool займає більшість worker-ів.
Інші run'и стоять у черзі, хоча їхні залежності здорові.
Типова причина: немає per-tool bulkhead limits.
Доміно таймаутів у суміжних сервісах (Timeout domino)
Коли черга росте, збільшується wait time.
Через це upstream/downstream сервіси частіше виходять у timeout.
Типова причина: немає жорстких max_seconds і fail-fast при деградації залежності.
Каскад витрат поверх технічного збою (Cost cascade)
Каскад піднімає і вартість run'а: більше retries, більше токенів, довший життєвий цикл. Навіть "успішні" завершення стають занадто дорогими.
Типова причина: відсутні execution budgets (max_tool_calls, max_retries, max_usd).
Як виявляти ці проблеми
Каскадні збої добре видно по комбінації gateway-, runtime- і queue-метрик.
| Метрика | Сигнал cascading failure | Що робити |
|---|---|---|
retry_amplification_rate | один збій дає багато дубльованих retries | централізувати retries в одному gateway |
circuit_open_rate | часто відкривається breaker на одному tool | вмикати safe-mode і знижувати fan-out |
queue_backlog | черга росте при звичному вхідному трафіку | ввести bulkhead limits і timeout на run |
cross_service_timeout_rate | таймаути з'являються в не пов'язаних сервісах | ізолювати проблемний tool і обмежити конкуренцію |
cascading_stop_reason_rate | часті cascade:* stop reasons | перевірити breaker/bulkhead і fallback-стратегію |
Як відрізнити каскадний збій від локальної помилки інструмента
Не кожен tool_timeout означає cascade.
Ключове питання: чи збій лишається локальним, чи вже впливає на інші частини системи.
Нормально, якщо:
- збій ізольований в одному інструменті;
- черга і latency інших run'ів залишаються стабільними;
- після короткого cooldown система повертається до baseline.
Небезпечно, якщо:
- помилка одного
toolпіднімаєqueue_backlogглобально; - таймаути з'являються у не пов'язаних workflow;
- вартість і тривалість run'ів ростуть навіть там, де цей
toolне використовується.
Як зупиняти такі збої
Практично це виглядає так:
- тримаєш retries лише в одному choke point (tool gateway);
- ставиш per-tool circuit breaker + cooldown + bulkhead limits;
- вводиш execution budgets на retries, tool calls, час і вартість;
- при деградації перемикаєш run у safe-mode (partial/fallback), а не "тиснеш далі".
Мінімальний guard проти cascade:
from dataclasses import dataclass
import time
RETRYABLE = {408, 429, 500, 502, 503, 504}
@dataclass(frozen=True)
class CascadeLimits:
max_steps: int = 25
max_seconds: int = 90
max_tool_calls: int = 18
max_retries: int = 4
max_in_flight_per_tool: int = 8
open_circuit_after: int = 3
circuit_cooldown_s: int = 30
class CascadeGuard:
def __init__(self, limits: CascadeLimits = CascadeLimits()):
self.limits = limits
self.steps = 0
self.tool_calls = 0
self.retries = 0
self.in_flight: dict[str, int] = {}
self.fail_streak: dict[str, int] = {}
self.circuit_open_until: dict[str, float] = {}
self.started_at = time.time()
def on_step(self) -> str | None:
self.steps += 1
if self.steps > self.limits.max_steps:
return "cascade:budget_max_steps"
if (time.time() - self.started_at) > self.limits.max_seconds:
return "cascade:budget_timeout"
return None
def before_tool_call(self, tool: str) -> str | None:
if time.time() < self.circuit_open_until.get(tool, 0.0):
return "cascade:circuit_open"
current = self.in_flight.get(tool, 0)
if current >= self.limits.max_in_flight_per_tool:
return "cascade:bulkhead_full"
self.tool_calls += 1
if self.tool_calls > self.limits.max_tool_calls:
return "cascade:budget_tool_calls"
self.in_flight[tool] = current + 1
return None
def after_tool_call(self, tool: str, status_code: int) -> str | None:
self.in_flight[tool] = max(0, self.in_flight.get(tool, 1) - 1)
if status_code in RETRYABLE:
self.retries += 1
if self.retries > self.limits.max_retries:
return "cascade:retry_budget"
streak = self.fail_streak.get(tool, 0) + 1
self.fail_streak[tool] = streak
if streak >= self.limits.open_circuit_after:
self.circuit_open_until[tool] = time.time() + self.limits.circuit_cooldown_s
return "cascade:circuit_open"
return "cascade:retry_allowed"
self.fail_streak[tool] = 0
return None
Це базовий guard.
У цій версії tool_calls рахує саме спроби виклику, а не лише успішно допущені виклики.
У production його зазвичай доповнюють пріоритезацією запитів,
окремими лімітами для критичних tool і явним safe-mode маршрутом.
before_tool_call(...) викликають до зовнішнього виклику,
а after_tool_call(...) одразу після відповіді, щоб cascade гасився якомога раніше.
Де це реалізується в архітектурі
У production контроль cascading failures зазвичай розкладений між трьома шарами системи.
Tool Execution Layer — перший бар'єр: retry policy, circuit breaker, bulkhead, timeout і нормалізація помилок. Якщо цей шар слабкий, локальний збій швидко стає хвилею.
Agent Runtime керує бюджетами,
stop reasons (cascade:*) і safe-mode переходами.
Саме тут важливо зупинити run до системної сатурації.
Orchestration Topologies визначає, як ізолювати проблемні гілки workflow і не дати одному деградованому шляху заблокувати весь workflow.
Самоперевірка
Швидка перевірка перед релізом. Відмічайте пункти і дивіться статус нижче.
Це короткий sanity-check, а не формальний аудит.
Прогрес: 0/8
⚠ Є сигнали ризику
Бракує базових контролів. Закрийте ключові пункти цього чекліста перед релізом.
FAQ
Q: Ретраї ж корисні. Чому вони можуть ламати систему?
A: Корисні лише з backoff, caps і єдиною точкою керування. Коли retries дублюються між шарами, вони множать навантаження швидше, ніж система відновлюється.
Q: Чому агентні системи більш схильні до cascade, ніж звичайні API?
A: Бо агент має reasoning loop і може повторювати ті самі tool_call багато разів. Збій залежності множиться кожним кроком run.
Q: Timeout недостатньо? Навіщо ще breaker і bulkhead?
A: Timeout лише обмежує один виклик. Breaker зупиняє хвилю повторів, а bulkhead не дає одному tool забрати всі воркери.
Q: Safe-mode не псує якість відповіді?
A: Частково так, але це контрольована деградація. Краще віддати коректний частковий результат, ніж дочекатися повного outage.
Cascading failure майже ніколи не виглядає як одна велика помилка. Частіше це ланцюг дрібних збоїв, який система сама підсилює. Ключовий принцип: agent loops підсилюють збої (agents amplify failures). Тому production-агентам потрібні не лише сильні моделі, а й жорсткі межі на рівні runtime та gateway.
Пов'язані сторінки
Якщо ця проблема виникла у production, корисно також подивитися:
- Чому AI агенти ламаються — загальна карта збоїв у production.
- Tool failure — як локальна відмова інструмента переходить у cascade.
- Tool spam — як повторні виклики прискорюють деградацію.
- Budget explosion — як cascade перетворюється на фінансовий інцидент.
- Partial outage — як працювати в режимі часткової деградації залежностей.
- Agent Runtime — де реалізувати budgets, stop reasons і safe-mode.
- Tool Execution Layer — де тримати retries, breaker і bulkhead.