Deadlocks en systèmes multi-agents (failure + fixes + code)

  • Repère la panne tôt, avant que la facture grimpe.
  • Comprends ce qui casse en prod, et pourquoi.
  • Copie des garde-fous : budgets, stop reasons, validation.
  • Sache quand ce n’est pas la vraie cause.
Signaux de détection
  • Tool calls/run explosent (ou se répètent avec args hash).
  • Spend/tokens montent sans amélioration des outputs.
  • Retries passent de rares à constants (429/5xx).
Des agents qui attendent des agents = deadlock distribué avec des logs plus jolis. Leases, timeouts et orchestration empêchent les runs bloqués en prod.
Sur cette page
  1. Le problème (côté prod)
  2. Pourquoi ça casse en prod
  3. 1) Les cycles sont faciles à créer
  4. 2) Pas de timeouts sur “waiting”
  5. 3) Ressources partagées sans leases
  6. 4) “Demander à un autre agent” devient une loop
  7. 5) La solution est l’orchestration
  8. Exemple d’implémentation (code réel)
  9. Incident réel (avec chiffres)
  10. Compromis
  11. Quand NE PAS l’utiliser
  12. Checklist (copier-coller)
  13. Config par défaut sûre (JSON/YAML)
  14. FAQ (3–5)
  15. Pages liées (3–6 liens)
Flux interactif
Scénario:
Étape 1/2: Execution

Normal path: execute → tool → observe.

Le problème (côté prod)

Tu construis un setup multi-agents. Et un jour en prod, un run ne se termine jamais parce que :

  • A attend B
  • B attend C
  • C attend A

Personne n’est “buggé” dans sa tête. Tout le monde attend. Deadlock.

Le problème : ça ne crashe pas. Ça hang. Et un hang consomme budget + workers.

Pourquoi ça casse en prod

1) Les cycles sont faciles à créer

“Planner → researcher → reviewer → planner”. Et voilà.

2) Pas de timeouts sur “waiting”

On met des timeouts sur HTTP, pas sur les messages entre agents.

3) Ressources partagées sans leases

Sans TTL/lease, un crash peut bloquer le système indéfiniment.

4) “Demander à un autre agent” devient une loop

Incertitude → demande → re-demande → escalade.

5) La solution est l’orchestration

Un orchestrator (ou un leader), une state machine, des timeouts, des leases, des stop reasons.

Exemple d’implémentation (code réel)

Lease lock minimal :

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:
      return orchestrate(resource_id)  # (pseudo)
  finally:
      lock.release(resource_id=resource_id, owner=orchestrator_id)
JAVASCRIPT
export class LeaseLock {
constructor() {
  this.leases = new Map();
}

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);
}
}

Ça évite le deadlock “lock held forever”. Ça ne fixe pas les cycles logiques : ça, il faut les éviter.

Incident réel (avec chiffres)

Flow multi-agent de tri incident :

  • A collecte les signaux
  • B écrit l’hypothèse
  • C valide via runbook

Le tool runbook a dégradé. C attend. B attend C. A attend B.

Impact :

  • 43 runs bloqués
  • workers saturés, nouvelle file d’attente
  • ~2h on-call à annuler et nettoyer l’état

Fix :

  1. timeouts sur les waits
  2. leases orchestrator-owned par incident_id
  3. stop reasons claires (“blocked waiting for tool”)
  4. fallback single-agent quand deps dégradées

Compromis

  • L’orchestration, c’est du code.
  • Les leases peuvent expirer → idempotence + replay nécessaires.
  • Fallback single-agent = moins de qualité, plus de liveness.

Quand NE PAS l’utiliser

  • Petites tâches : multi-agent = overhead.
  • Pas d’observabilité / orchestration : ne shippe pas ça en prod.
  • Besoin de consistance : workflows + state machine explicite.

Checklist (copier-coller)

  • [ ] Éviter cycles (graph)
  • [ ] Timeouts sur waits
  • [ ] Leases/TTLs pour ressources partagées
  • [ ] Orchestrator owns transitions
  • [ ] Idempotence sur writes
  • [ ] Stop reasons + alerting
  • [ ] Mode fallback

Config par défaut sûre (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 = toujours mauvais ?
Non, mais ça ajoute coordination + modes de panne. Il faut de l’orchestration.
Les leases fixent les deadlocks ?
Ils fixent les deadlocks de locks (crash). Ils ne fixent pas les cycles logiques.
La prévention la plus simple ?
Un orchestrator + des timeouts sur waits. Sans timeouts, waiting devient stuck.
Comment déboguer ?
Logs de transitions + chaîne d’attente visible. Sinon tu devines.

Q : Multi-agent = toujours mauvais ?
R : Non, mais ça ajoute coordination + modes de panne. Il faut de l’orchestration.

Q : Les leases fixent les deadlocks ?
R : Ils fixent les deadlocks de locks (crash). Ils ne fixent pas les cycles logiques.

Q : La prévention la plus simple ?
R : Un orchestrator + des timeouts sur waits. Sans timeouts, waiting devient stuck.

Q : Comment déboguer ?
R : Logs de transitions + chaîne d’attente visible. Sinon tu devines.

Pages liées (3–6 liens)

Pas sur que ce soit votre cas ?

Concevez votre agent ->
⏱️ 5 min de lectureMis à jour Mars, 2026Difficulté: ★★☆
Implémenter dans OnceOnly
Guardrails for loops, retries, and spend escalation.
Utiliser dans 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 }
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Kill switch & arrêt incident
  • Audit logs & traçabilité
  • Idempotence & déduplication
  • Permissions outils (allowlist / blocklist)
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.
Exemple de policy (concept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Auteur

Cette documentation est organisée et maintenue par des ingénieurs qui déploient des agents IA en production.

Le contenu est assisté par l’IA, avec une responsabilité éditoriale humaine quant à l’exactitude, la clarté et la pertinence en production.

Les patterns et recommandations s’appuient sur des post-mortems, des modes de défaillance et des incidents opérationnels dans des systèmes déployés, notamment lors du développement et de l’exploitation d’une infrastructure de gouvernance pour les agents chez OnceOnly.