Normal path: execute → tool → observe.
El problema (en producción)
Montas un setup multi-agente:
- “research agent”
- “planner agent”
- “executor agent”
- “reviewer agent”
En diagramas queda precioso.
Luego en producción una request se queda colgada para siempre porque:
- el agente A espera el output del agente B
- el agente B espera la aprobación del agente C
- el agente C espera el contexto del agente A
Nadie está “equivocado”. Están esperando.
Eso es un deadlock.
Los deadlocks multi-agente duelen porque no crashean. Se cuelgan. Y los cuelgues queman budgets en silencio.
Por qué esto se rompe en producción
Los sistemas multi-agente heredan todos los failure modes de sistemas distribuidos, más la ambigüedad del LLM.
1) Es facilísimo crear dependencias circulares
Es tentador repartir responsabilidades así:
- “planner” pregunta al “researcher”
- “researcher” pregunta al “reviewer”
- “reviewer” pregunta al “planner”
Felicidades: acabas de construir un ciclo.
2) No hay timeouts en “esperas”
La gente pone timeouts a HTTP, pero no a “mensajes entre agentes”. Así que el agente espera para siempre mientras el worker sigue ocupado.
3) Recursos compartidos sin leases
Si los agentes comparten:
- un ticket
- un documento
- un lock
…y no usas TTLs/leasing, un crash puede dejar el sistema bloqueado para siempre.
4) “Pregúntale a otro agente” se convierte en retry loop
Cuando un agente duda, el patrón típico es:
- pregunta a otro
- pregunta otra vez si no responde
- pregunta a un tercero
Eso convierte deadlock en tool spam.
5) El fix es orquestación, no más prompting
No vas a “promptear” para salir de deadlocks. Necesitas:
- un orquestador (o al menos un líder)
- transiciones explícitas de una state machine
- timeouts y leases
- stop reason cuando el sistema no puede progresar
Ejemplo de implementación (código real)
Un patrón mínimo de “lease lock” para trabajo compartido:
- un agente adquiere un lease para
resource_id - si crashea, el lease expira
- el orquestador recupera y reasigna
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);
}
}Esto no arregla todos los deadlocks (los ciclos siguen siendo ciclos), pero evita el peor: “el sistema está bloqueado porque un agente murió sosteniendo el lock”.
Y por favor: pon timeouts a “esperas de agente”. Una espera sin timeout es un sleep que pagas.
Incidente real (con números)
Teníamos un flow multi-agente de “incident triage”:
- el agente A juntaba señales
- el agente B escribía una hipótesis
- el agente C validaba con un runbook
Cuando el tool de runbook se degradó, el agente C se quedó esperando. El agente B esperaba al C. El agente A esperaba al B.
Impacto:
- 43 runs atascados en estado “waiting”
- workers saturados y nuevas requests en cola
- on-call quemó ~2 horas cancelando runs y limpiando estado a mano
Fix:
- timeouts en esperas inter-agente
- leases por incident id (propiedad del orquestador)
- stop reasons: “bloqueado esperando tool” vs “bloqueado esperando approval”
- fallback: modo single-agent cuando dependencias degradan
Multi-agente hace la coordinación tu problema. No la puedes delegar al LLM.
Trade-offs
- El código de orquestación cuesta. Es más barato que deadlocks.
- Los leases pueden expirar a mitad de trabajo; necesitas idempotencia y replay.
- El fallback single-agent baja calidad, sube liveness.
Cuándo NO usarlo
- Si la tarea es pequeña, multi-agente es overhead innecesario.
- Si no puedes construir orquestación y observabilidad, no shipees multi-agente en prod.
- Si necesitas orden estricto y consistencia, usa workflows con state machines explícitas.
Checklist (copiar/pegar)
- [ ] Evita dependencias circulares (dibújalo como grafo)
- [ ] Añade timeouts a estados “waiting”
- [ ] Usa leases/TTLs para recursos compartidos
- [ ] Un orquestador dueño de transiciones de estado
- [ ] Idempotency keys para cualquier write
- [ ] Stop reasons para estados bloqueados + alertas
- [ ] Fallback cuando dependencias degradan
Config segura por defecto (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)
Usado por patrones
Fallos relacionados
- AI Agent Infinite Loop (Detectar + arreglar, con código)
- Explosión de presupuesto (cuando un agente quema dinero) + fixes + código
- Tool Spam Loops (fallo del agente + fixes + código)
- Incidentes de exceso de tokens (prompt bloat) + fixes + código
- Corrupción de respuestas de tools (schema drift + truncation) + código
Gobernanza requerida
Q: ¿Multi-agente siempre es mala idea?
A: No. Ayuda en tareas complejas, pero añade coordinación y failure modes. Diseña orquestación.
Q: ¿Los leases arreglan deadlocks?
A: Arreglan deadlocks por locks tras crashes. No arreglan ciclos lógicos — evita ciclos con diseño explícito.
Q: ¿La prevención más simple?
A: Un orquestador + timeouts en esperas. Sin timeouts, “waiting” se vuelve “stuck”.
Q: ¿Cómo debuggeo deadlocks?
A: Loggea transiciones con run_id y un grafo de dependencias. Si no puedes dibujar la cadena de espera, estás adivinando.
Páginas relacionadas (3–6 links)
- Foundations: Planning vs reactive agents · Por qué fallan agentes en producción
- Failure: Outage parcial · Tool spam loops
- Governance: Tool permissions (allowlists)
- Production stack: Production agent stack