Deadlocks у multi-agent системах (failure mode + фікси + код)

  • Побач ранні сигнали, поки рахунок не поліз вгору.
  • Зрозумій, що ламається в проді й чому.
  • Скопіюй guardrails: budgets, stop reasons, validation.
  • Знай, коли це не справжня root cause.
Сигнали виявлення
  • Tool calls на run зростають (або повторюються з args hash).
  • Витрати/токени ростуть без кращих результатів.
  • Retries стають постійними (429/5xx).
Агенти, які чекають агентів — це distributed deadlock із красивішими логами. Як deadlocks стаються в проді й як leases, timeouts і orchestration не дають системі зависати.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) Цикли дуже легко створити
  4. 2) Немає таймаутів на “очікування”
  5. 3) Спільні ресурси без leases
  6. 4) “Питай іншого агента” стає retry loop
  7. 5) Фікс — це orchestration, а не “ще один промпт”
  8. Приклад реалізації (реальний код)
  9. Реальний інцидент (з цифрами)
  10. Компроміси
  11. Коли НЕ варто
  12. Чекліст (можна копіювати)
  13. Безпечний дефолтний конфіг (JSON/YAML)
  14. FAQ (3–5)
  15. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/2: Execution

Normal path: execute → tool → observe.

Проблема (з реального продакшену)

Ти будуєш multi-agent сетап:

  • “research agent”
  • “planner agent”
  • “executor agent”
  • “reviewer agent”

На діаграмі — краса.

У проді — один запит зависає назавжди, бо:

  • Agent A чекає output від Agent B
  • Agent B чекає approval від Agent C
  • Agent C чекає контекст від Agent A

Ніхто не “помилився”. Вони просто чекають.

Це deadlock.

Multi-agent deadlocks болючі, бо вони не падають. Вони висять. А “висіння” тихо спалює бюджети.

Чому це ламається в продакшені

Multi-agent системи успадковують усі failure modes distributed systems, плюс неоднозначність LLM.

1) Цикли дуже легко створити

Спокуса розділити відповідальність так:

  • planner питає researcher
  • researcher питає reviewer
  • reviewer питає planner

Вітаю: ти побудував цикл.

2) Немає таймаутів на “очікування”

У багатьох системах є таймаути на HTTP, але не на “agent messages”. Тому агент чекає вічно, а воркер зайнятий.

3) Спільні ресурси без leases

Якщо агенти ділять:

  • тікет
  • документ
  • lock

…і ти не використовуєш leases/TTLs — креш може залишити систему заблокованою назавжди.

4) “Питай іншого агента” стає retry loop

Коли агент невпевнений, часто він робить:

  • запитай іншого
  • якщо не відповіли — запитай ще раз
  • запитай третього

Так deadlock перетворюється на spam.

5) Фікс — це orchestration, а не “ще один промпт”

Ти не “промптнеш” deadlock. Потрібно:

  • один оркестратор (або хоча б лідер)
  • явні переходи state machine
  • timeouts і leases
  • stop reason, коли система не може прогресувати

Приклад реалізації (реальний код)

Мінімальний “lease lock” для спільної роботи:

  • агент бере lease на resource_id
  • якщо він падає — lease закінчується
  • оркестратор може відновитись і переназначити
PYTHON
from dataclasses import dataclass
import time


@dataclass
class Lease:
  owner: str
  expires_at: float


class LeaseLock:
  def __init__(self) -> None:
      self._leases: dict[str, Lease] = {}

  def try_acquire(self, *, resource_id: str, owner: str, ttl_s: int) -> bool:
      now = time.time()
      lease = self._leases.get(resource_id)
      if lease and lease.expires_at > now and lease.owner != owner:
          return False
      self._leases[resource_id] = Lease(owner=owner, expires_at=now + ttl_s)
      return True

  def release(self, *, resource_id: str, owner: str) -> None:
      lease = self._leases.get(resource_id)
      if lease and lease.owner == owner:
          del self._leases[resource_id]


def run_work(orchestrator_id: str, resource_id: str, lock: LeaseLock) -> str:
  if not lock.try_acquire(resource_id=resource_id, owner=orchestrator_id, ttl_s=30):
      return "blocked: lease held"

  try:
      # orchestrate agents here (pseudo)
      return orchestrate(resource_id)  # (pseudo)
  finally:
      lock.release(resource_id=resource_id, owner=orchestrator_id)
JAVASCRIPT
export class LeaseLock {
constructor() {
  this.leases = new Map(); // resourceId -> { owner, expiresAtMs }
}

tryAcquire({ resourceId, owner, ttlS }) {
  const now = Date.now();
  const lease = this.leases.get(resourceId);
  if (lease && lease.expiresAtMs > now && lease.owner !== owner) return false;
  this.leases.set(resourceId, { owner, expiresAtMs: now + ttlS * 1000 });
  return true;
}

release({ resourceId, owner }) {
  const lease = this.leases.get(resourceId);
  if (lease && lease.owner === owner) this.leases.delete(resourceId);
}
}

Це не вирішує всі deadlocks (цикли все ще цикли), але прибирає найгірше: “система застрягла, бо агент помер із lock’ом”.

Також: став таймаути на “agent waits”. Wait без таймауту — це sleep, за який ти платиш.

Реальний інцидент (з цифрами)

У нас був multi-agent “incident triage”:

  • Agent A збирав сигнали
  • Agent B формував гіпотезу
  • Agent C звіряв із runbook

Коли runbook tool деградував, Agent C завис на відповіді. Agent B чекав Agent C. Agent A чекав Agent B.

Impact:

  • 43 runs застрягли в стані “waiting”
  • воркери наситились, нові запити пішли в чергу
  • on-call витратив ~2 години на ручне скасування та чистку state

Fix:

  1. timeouts на inter-agent waits
  2. orchestrator-owned leases на incident id
  3. stop reasons: “blocked waiting for tool” vs “blocked waiting for approval”
  4. fallback: single-agent mode, коли залежності деградують

Multi-agent робить координацію твоєю проблемою. Її не можна “делегувати” LLM.

Компроміси

  • Orchestration код — це робота. Вона дешевша за deadlocks.
  • Leases можуть закінчитись під час роботи; потрібні idempotency і replay.
  • Single-agent fallback знижує якість, але підвищує liveness.

Коли НЕ варто

  • Якщо задача маленька — multi-agent зайвий.
  • Якщо ти не можеш побудувати orchestration і observability — не шипи multi-agent у прод.
  • Якщо потрібен строгий порядок і консистентність — використовуй workflows зі state machine.

Чекліст (можна копіювати)

  • [ ] Уникай циклів між агентами (намалюй граф)
  • [ ] Додай timeouts на “waiting”
  • [ ] Використовуй leases/TTLs для shared resources
  • [ ] Один orchestrator володіє state transitions
  • [ ] Idempotency keys для writes
  • [ ] Stop reasons для blocked states + алерти
  • [ ] Fallback mode, коли залежності деградують

Безпечний дефолтний конфіг (JSON/YAML)

YAML
multi_agent:
  orchestrator: "single_owner"
  wait_timeouts_s: { default: 30 }
  leases:
    ttl_s: 30
    renew: true
fallback:
  enabled: true
  mode: "single_agent"

FAQ (3–5)

Multi-agent — це завжди погана ідея?
Ні. Для складних задач він допомагає, але додає координацію й failure modes. Плануй orchestration.
Leases фіксять deadlocks?
Вони фіксять lock-based deadlocks після крешів. Вони не фіксять логічні цикли — цикли треба дизайном прибирати.
Найпростіша профілактика?
Один orchestrator + таймаути на waits. Без таймаутів ‘waiting’ легко стає ‘stuck’.
Як дебажити deadlocks?
Логуй state transitions з run_id і графом залежностей. Якщо ти не можеш намалювати ланцюг очікувань — ти вгадуєш.

Q: Multi-agent — це завжди погана ідея?
A: Ні. Для складних задач він допомагає, але додає координацію й failure modes. Плануй orchestration.

Q: Leases фіксять deadlocks?
A: Вони фіксять lock-based deadlocks після крешів. Вони не фіксять логічні цикли — цикли треба дизайном прибирати.

Q: Найпростіша профілактика?
A: Один orchestrator + таймаути на waits. Без таймаутів “waiting” легко стає “stuck”.

Q: Як дебажити deadlocks?
A: Логуй state transitions з run_id і графом залежностей. Якщо ти не можеш намалювати ланцюг очікувань — ти вгадуєш.

Пов’язані сторінки (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.