Проблема
Запит виглядає простим: перевірити статус оплати і дати коротку відповідь клієнту.
У трейсах видно інше: за 11 хвилин один run зробив 33 виклики,
з них 10 повернули 200, 14 — timeout, ще 9 — 502/503.
Для задачі такого класу це може бути ~$1.90 замість звичних ~$0.14.
Сервіс формально "живий": частина викликів проходить, повного падіння немає. Але черга run'ів росте, latency стрибає, а користувачі отримують нестабільний результат.
Система не падає.
Вона просто повільно застрягає між рідкими успіхами і повторними збоями.
Аналогія: уяви касу, де термінал то приймає картку, то зависає. Магазин не закритий, але черга росте щохвилини. Partial outage в агентних системах працює так само: інфраструктура ніби доступна, але стабільного шляху до відповіді вже немає.
Чому це стається
У production зазвичай так:
- залежність починає працювати нестабільно (
timeout,5xx, інколи200); - retries запускаються у кількох шарах одночасно;
- run тримає воркери довше, черга росте;
- інші workflow теж сповільнюються через спільні ресурси;
- без fail-fast і safe-mode система множить витрати замість ізоляції збою.
У trace це видно як змішаний патерн: tool_2xx_rate ще тримається,
але одночасно ростуть timeout_rate, retry_attempts_per_run і queue_backlog.
Проблема не в одному timeout.
Runtime не переводить нестабільну залежність у degraded mode, поки збій ще лишається локальним.
Які збої трапляються найчастіше
У production найчастіше видно чотири патерни partial outage.
Пастка інтермітентного успіху (Intermittent success trap)
Інструмент інколи повертає 200, і це маскує деградацію.
Агент продовжує тиснути в той самий канал, замість контрольованого переключення.
Типова причина: немає порогу "здоров'я" залежності на рівні run.
Retry amplification по шарах
HTTP-клієнт, gateway і runtime кожен робить свої retries. Навіть невеликий сплеск помилок швидко перетворюється на хвилю зайвих викликів.
Типова причина: retry policy не централізована.
Черга забивається "шумними" run'ами (Queue starvation)
Проблемні run'и довго висять у виконанні, займають worker pool і витісняють здорові задачі.
Типова причина: відсутні ліміти тривалості run і budget-gates.
Очікування "ідеальної" відповіді без degrade path
Система намагається дочекатися "ідеального" результату, хоча залежність явно деградує.
Типова причина: немає partial/fallback контракту для користувача.
Як виявляти ці проблеми
Partial outage добре видно по комбінації health-, runtime- і queue-метрик.
| Метрика | Сигнал partial outage | Що робити |
|---|---|---|
degraded_dependency_rate | одна залежність часто дає timeout/5xx | увімкнути degraded mode і знизити fan-out |
tool_2xx_with_high_timeout_rate | одночасно є і 200, і висока частка timeout | додати health threshold, не орієнтуватись лише на 200 |
retry_attempts_per_run | підозріло багато повторів на один run | централізувати retries і обмежити retry budget |
run_duration_p95 | довгі "висячі" run'и | ввести fail-fast timeout і stop reasons |
queue_backlog | черга росте при звичному трафіку | ізолювати деградований шлях і вмикати fallback |
Як відрізнити partial outage від full outage
Не кожна деградація — це повний outage. Ключове питання: чи є стабільний шлях виконання, чи лише випадкові "успіхи".
Нормально для full outage, якщо:
- майже всі виклики падають одноманітно (
5xxабо повна недоступність); - система швидко переходить у fail-fast;
- немає ілюзії "інколи працює".
Небезпечно для partial outage, якщо:
- у тому самому run змішані
200,timeoutі5xx; - агент повторює виклики, бо бачить рідкі успішні відповіді;
- queue/latency ростуть навіть без явного global incident.
Як зупиняти такі збої
Практично це виглядає так:
- фіксуєш health snapshot залежностей на старті run;
- при порушенні порогу одразу вмикаєш degraded mode для workflow;
- тримаєш retries в одному tool gateway з жорстким budget;
- повертаєш partial/fallback і явний stop reason, замість "вічного" очікування.
Мінімальний guard для partial outage:
from dataclasses import dataclass
import time
RETRYABLE = {408, 429, 500, 502, 503, 504}
@dataclass(frozen=True)
class OutageLimits:
max_retry_per_call: int = 2
max_retry_total: int = 6
max_run_seconds: int = 45
max_tool_calls: int = 14
degraded_error_threshold: float = 0.35
min_sample_size: int = 5
class PartialOutageGuard:
def __init__(self, limits: OutageLimits = OutageLimits()):
self.limits = limits
self.started_at = time.time()
self.tool_calls = 0
self.retry_count = 0
self.total_calls = 0
self.error_calls = 0
def before_tool_call(self) -> str | None:
self.tool_calls += 1
if self.tool_calls > self.limits.max_tool_calls:
return "partial_outage:tool_call_budget"
if (time.time() - self.started_at) > self.limits.max_run_seconds:
return "partial_outage:run_timeout"
return None
def on_tool_result(self, status_code: int, attempt: int) -> str | None:
self.total_calls += 1
if status_code in RETRYABLE:
self.error_calls += 1
error_rate = self.error_calls / max(1, self.total_calls)
if (
self.total_calls >= self.limits.min_sample_size
and error_rate >= self.limits.degraded_error_threshold
):
return "partial_outage:degraded_mode"
self.retry_count += 1
if self.retry_count > self.limits.max_retry_total:
return "partial_outage:retry_budget"
if attempt >= self.limits.max_retry_per_call:
return "partial_outage:retry_exhausted"
return "partial_outage:retry_allowed"
error_rate = self.error_calls / max(1, self.total_calls)
if (
self.total_calls >= self.limits.min_sample_size
and error_rate >= self.limits.degraded_error_threshold
):
return "partial_outage:degraded_mode"
return None
У цій версії retry_count рахує всі retryable відповіді в межах run,
а attempt — кількість повторів конкретного виклику.
Це базовий guard.
У production його зазвичай доповнюють per-tool health probes,
circuit breaker і окремим safe-mode шляхом для degraded run'ів.
before_tool_call(...) і on_tool_result(...) викликають у tool gateway,
щоб рішення про деградацію приймалось централізовано, а не в кожному шарі окремо.
Де це реалізується в архітектурі
У production контроль partial outage майже завжди розкладений між трьома шарами системи.
Tool Execution Layer дає health signals:
error rate, timeout patterns, retry budget і circuit breaker.
Саме тут видно, що залежність вже нестабільна, навіть якщо частина викликів ще повертає 200.
Agent Runtime приймає рішення по run: перехід у degraded mode, stop reasons і контрольоване завершення з fallback. Без цього шару система продовжує чекати "ще один вдалий виклик".
Orchestration Topologies визначає, як ізолювати деградований workflow від решти системи (bulkheads, черги, пріоритети). Це не дає локальній деградації перетворитись на спільний інцидент.
Самоперевірка
Швидка перевірка перед релізом. Відмічайте пункти і дивіться статус нижче.
Це короткий sanity-check, а не формальний аудит.
Прогрес: 0/8
⚠ Є сигнали ризику
Бракує базових контролів. Закрийте ключові пункти цього чекліста перед релізом.
FAQ
Q: Чому partial outage часто гірший за full outage?
A: Бо він маскується під "інколи працює".
Система не зупиняється, а довго спалює час, токени і worker pool.
Q: Чи треба одразу вимикати деградований tool?
A: Не завжди. Зазвичай вмикають degraded mode: обмежують retries, зменшують fan-out і переходять на partial/fallback шлях.
Q: Де краще приймати рішення про retries і деградацію?
A: В одному tool gateway. Інакше кожен шар робить свої retries, і partial outage швидко масштабується.
Q: Що показувати користувачу, коли залежність деградує?
A: Явний stop reason, що саме не спрацювало, і контрольований наступний крок: часткова відповідь або повторна спроба після відновлення залежності.
Partial outage майже ніколи не виглядає як гучна аварія. Це тиха деградація, де система ще рухається, але вже не тримає якість і темп. Тому production-агентам потрібні не лише retries, а й жорсткий режим деградації та ізоляції залежностей.
Пов'язані сторінки
Якщо ця проблема виникла у production, корисно також подивитися:
- Чому AI агенти ламаються — загальна карта збоїв у production.
- Tool failure — як локальна помилка інструмента стає інцидентом.
- Deadlocks — як waiting-стани ростуть під час деградації залежностей.
- Cascading failures — як partial outage поширюється на інші workflow.
- Agent Runtime — де керувати degraded mode, stop reasons і fallback.
- Tool Execution Layer — де тримати retries, health signals і circuit breaker.