Normal path: execute → tool → observe.
Проблема (з реального продакшену)
Одна залежність стає flaky.
Агент реагує тим, що викликає її частіше.
Тепер залежність ще більш flaky.
Агент викликає її ще частіше.
Це й є cascading failures в агентних системах: вони підсилюють зворотний зв’язок.
У проді шкода — не лише “агент не відповів”. Це:
- ловляться rate limits на не пов’язаних сервісах
- черги забиваються
- on-call перестає відрізняти “реальний інцидент” від “агентського шуму”
- агент перетворюється на load test, який ніхто не просив
Чому це ламається в продакшені
Агенти — це loops. Loops підсилюють feedback. Це не “AI”, це control systems.
1) Наївні ретраї
Ретраї потрібні. Ретраї без backoff/jitter — це thundering herd.
Якщо 1,000 runs ретраять той самий tool одночасно — ти зробив другий аутедж.
2) Ретраїть і агент, і tool
Зазвичай є:
- retries у HTTP клієнті
- retries у wrapper’і tool
- поведінка агент-лупа “спробуй ще”
Помнож це — і отримай storms.
3) Немає circuit breaker
Коли tool явно деградує (timeouts, 5xx), треба перестати його викликати на cooling period. Без breaker ти б’єш по падаючій залежності й робиш гірше.
4) Немає bulkheads (ліміти конкуренції)
Якщо один tool повільний, він не має “з’їсти” всі воркери. Per-tool concurrency limits не дають одній залежності забрати все.
5) Немає safe-mode / fallback
Іноді правильна поведінка:
- віддати partial results
- зупинитись із чіткою причиною
- переключитись на cache/last-known-good
Агенти, які “must succeed”, починають thrash.
Приклад реалізації (реальний код)
Невеликий circuit breaker + bulkhead, який можна поставити перед tool.
from dataclasses import dataclass
import time
from typing import Callable, Any
@dataclass
class Breaker:
fail_threshold: int = 5
open_for_s: int = 30
failures: int = 0
opened_at: float | None = None
def allow(self) -> bool:
if self.opened_at is None:
return True
if time.time() - self.opened_at > self.open_for_s:
# half-open: reset and try again
self.failures = 0
self.opened_at = None
return True
return False
def on_success(self) -> None:
self.failures = 0
self.opened_at = None
def on_failure(self) -> None:
self.failures += 1
if self.failures >= self.fail_threshold:
self.opened_at = time.time()
class Bulkhead:
def __init__(self, *, max_in_flight: int) -> None:
self.max_in_flight = max_in_flight
self.in_flight = 0
def enter(self) -> None:
if self.in_flight >= self.max_in_flight:
raise RuntimeError("bulkhead full")
self.in_flight += 1
def exit(self) -> None:
self.in_flight = max(0, self.in_flight - 1)
def guarded_tool_call(fn: Callable[..., Any], *, breaker: Breaker, bulkhead: Bulkhead, **kwargs) -> Any:
if not breaker.allow():
raise RuntimeError("circuit open (fail fast)")
bulkhead.enter()
try:
out = fn(**kwargs)
breaker.on_success()
return out
except Exception:
breaker.on_failure()
raise
finally:
bulkhead.exit()export class Breaker {
constructor({ failThreshold = 5, openForS = 30 } = {}) {
this.failThreshold = failThreshold;
this.openForS = openForS;
this.failures = 0;
this.openedAt = null;
}
allow() {
if (!this.openedAt) return true;
const elapsedS = (Date.now() - this.openedAt) / 1000;
if (elapsedS > this.openForS) {
this.failures = 0;
this.openedAt = null;
return true;
}
return false;
}
onSuccess() {
this.failures = 0;
this.openedAt = null;
}
onFailure() {
this.failures += 1;
if (this.failures >= this.failThreshold) this.openedAt = Date.now();
}
}
export class Bulkhead {
constructor({ maxInFlight = 10 } = {}) {
this.maxInFlight = maxInFlight;
this.inFlight = 0;
}
enter() {
if (this.inFlight >= this.maxInFlight) throw new Error("bulkhead full");
this.inFlight += 1;
}
exit() {
this.inFlight = Math.max(0, this.inFlight - 1);
}
}
export async function guardedToolCall(fn, { breaker, bulkhead, args }) {
if (!breaker.allow()) throw new Error("circuit open (fail fast)");
bulkhead.enter();
try {
const out = await fn(args);
breaker.onSuccess();
return out;
} catch (e) {
breaker.onFailure();
throw e;
} finally {
bulkhead.exit();
}
}Це не “enterprise resilience”. Це ремінь безпеки. Без нього агенти перетворюють flaky залежності на system-wide інциденти.
Реальний інцидент (з цифрами)
У нас був агент, який робив enrichment через vendor API. Vendor почав інколи таймаутити.
У системі було:
- клієнтські ретраї (2)
- ретраї в wrapper’і tool (2)
- агент-луп “try again” (фактично без ліміту)
Impact:
- vendor API з “flaky” став “down”
- наш worker pool наситився
- p95 latency по не пов’язаних ендпойнтах виросла ~3x
- on-call витратив ~2 години на локалізацію blast radius
Fix:
- circuit breaker (fail fast на 30s після порогу)
- per-tool bulkhead concurrency limit
- ретраї лише в одному місці, з backoff + jitter
- safe-mode: пропустити enrichment і віддати partial
Агент не створив первинний фейл. Він його масштабував.
Компроміси
- Fail fast знижує “success rate” під час partial outage. Зате не дає стати full outage.
- Bulkheads можуть відхиляти частину запитів під навантаженням. Це краще, ніж глобальна сатурація.
- Safe-mode відповіді менш повні. Вони тримають систему живою.
Коли НЕ варто
- Якщо tool повністю внутрішній і має сильні SLO — можливо, не треба breaker на кожен tool (але budgets лишай).
- Якщо ти не можеш описати safe-mode — не запускай автономні loops під час аутеджів.
- Якщо потрібна строгая повнота — краще async workflow, ніж синхронний агент.
Чекліст (можна копіювати)
- [ ] Timeouts на кожен tool call
- [ ] Retries в одному місці (gateway) з backoff + jitter
- [ ] Circuit breaker per tool (fail fast)
- [ ] Bulkhead concurrency limits per tool
- [ ] Budgets per run (time/tool calls/spend)
- [ ] Safe-mode fallback (partial results)
- [ ] Алерти: breaker open rate, tool error rates, tool latency
Безпечний дефолтний конфіг (JSON/YAML)
tools:
timeouts_s: { default: 10 }
retries: { max_attempts: 2, backoff_ms: [250, 750], jitter: true }
circuit_breaker:
fail_threshold: 5
open_for_s: 30
bulkhead:
max_in_flight: 10
safe_mode:
enabled: true
allow_partial: true
FAQ (3–5)
Використовується в патернах
Пов’язані відмови
Q: Ретраї ж корисні?
A: Корисні з backoff і caps. Unbounded ретраї в loops — це як ти підсилюєш аутедж.
Q: Де мають жити circuit breakers?
A: У tool gateway, не в промптах. Один choke point.
Q: Що таке safe-mode?
A: Degraded поведінка: менше tools, read-only, cache, partial results і чіткий stop reason.
Q: Це треба для кожного tool?
A: Почни з flaky/дорогих/зовнішніх. З часом — так: кожна зовнішня залежність потребує timeouts і budgets.
Пов’язані сторінки (3–6 лінків)
- Foundations: Як агенти використовують tools · Production-ready агент
- Failure: Partial outage · Tool spam loops
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack