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 закінчується
- оркестратор може відновитись і переназначити
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)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:
- timeouts на inter-agent waits
- orchestrator-owned leases на incident id
- stop reasons: “blocked waiting for tool” vs “blocked waiting for approval”
- 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)
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)
Використовується в патернах
Пов’язані відмови
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 лінків)
- Foundations: Planning vs reactive agents · Чому агенти ламаються в продакшені
- Failure: Partial outage · Tool spam loops
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack