Cascading tool failures (як агенти підсилюють аутеджі) + код

  • Побач ранні сигнали, поки рахунок не поліз вгору.
  • Зрозумій, що ламається в проді й чому.
  • Скопіюй guardrails: budgets, stop reasons, validation.
  • Знай, коли це не справжня root cause.
Сигнали виявлення
  • Tool calls на run зростають (або повторюються з args hash).
  • Витрати/токени ростуть без кращих результатів.
  • Retries стають постійними (429/5xx).
Коли tools деградують, наївні ретраї й агент-лупи підсилюють аутедж. Circuit breakers, bulkheads і safe-mode не дають агенту DDoS’ити власні залежності.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) Наївні ретраї
  4. 2) Ретраїть і агент, і tool
  5. 3) Немає circuit breaker
  6. 4) Немає bulkheads (ліміти конкуренції)
  7. 5) Немає safe-mode / fallback
  8. Приклад реалізації (реальний код)
  9. Реальний інцидент (з цифрами)
  10. Компроміси
  11. Коли НЕ варто
  12. Чекліст (можна копіювати)
  13. Безпечний дефолтний конфіг (JSON/YAML)
  14. FAQ (3–5)
  15. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/2: Execution

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.

PYTHON
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()
JAVASCRIPT
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:

  1. circuit breaker (fail fast на 30s після порогу)
  2. per-tool bulkhead concurrency limit
  3. ретраї лише в одному місці, з backoff + jitter
  4. 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)

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)

Ретраї ж корисні?
Корисні з backoff і caps. Unbounded ретраї в loops — це як ти підсилюєш аутедж.
Де мають жити circuit breakers?
У tool gateway, не в промптах. Один choke point.
Що таке safe-mode?
Degraded поведінка: менше tools, read-only, cache, partial results і чіткий stop reason.
Це треба для кожного tool?
Почни з flaky/дорогих/зовнішніх. З часом — так: кожна зовнішня залежність потребує timeouts і budgets.

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 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 6 хв читанняОновлено Бер, 2026Складність: ★★☆
Реалізувати в OnceOnly
Guardrails for loops, retries, and spend escalation.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Kill switch та аварійна зупинка
  • Audit logs та трасування
  • Ідемпотентність і dedupe
  • Дозволами на інструменти (allowlist / blocklist)
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Приклад policy (концепт)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.