Por qué los agentes de ejecución prolongada necesitan una máquina de estados: los roles de Líder, Trabajador y Verificador
Cuatro formas en que un bucle de agente único falla en tareas prolongadas, y cómo un equipo de Líder, Trabajador y Verificador impulsado por un motor determinista las soluciona, además de cuándo omitirlo.
Fuentes: MiniMax Agent Team Best Practice: An AI Agent Is a While Loop. A Reliable One Is a State Machine.
pasar de un chatbot a un agente requiere un bucle while, no un avance revolucionario en la investigación. Eso es cierto, y también es donde terminan la mayoría de las explicaciones y comienza el problema en la producción. La pregunta más útil es por qué ese bucle falla silenciosamente en tareas que duran más de diez minutos, y qué estructura puede repararlo. MiniMax ha publicado dos referencias detalladas sobre exactamente esto: el tutorial de código abierto Mini-Agent, que muestra el bucle básico, y un informe técnico de su Equipo de Agentes, que explica qué falló a gran escala y el sistema de ejecución desarrollado como respuesta. Ambos sirven como base aquí, y este patrón se puede aplicar a cualquier stack.
Un solo bucle, de principio a fin
Si se descompone un agente, quedan cuatro componentes: un modelo, una lista en tiempo real de mensajes, un registro de herramientas y un bucle que las gestiona. La referencia de Mini-Agent funciona de la siguiente manera: en cada iteración primero se verifica si el historial está cerca del límite de contexto; si es así, se condensa. Luego se envían los mensajes y las definiciones de herramientas al modelo. Si el modelo no solicita ninguna herramienta, el bucle finaliza. De lo contrario, se ejecutan las herramientas solicitadas, su salida se agrega a los mensajes y el proceso se repite. Aquí hay una versión completa para cualquier endpoint compatible con OpenAI, con dos herramientas reales y la función de resumen:
# naive_loop.py -- a single-agent tool loop,
# Mini-Agent style: summarize if needed ->
# llm.generate(messages, tools) -> execute
# tool_calls -> append results -> repeat.
import json
import subprocess
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-4o-mini"
MAX_HISTORY = 40 # crude context budget
def read_file(path: str) -> str:
with open(path) as f:
return f.read()[:4000]
def run_cmd(cmd: str) -> str:
p = subprocess.run(
cmd, shell=True, timeout=30,
capture_output=True, text=True)
out = p.stdout + p.stderr
return f"exit={p.returncode}\n{out[-2000:]}"
TOOLS = {"read_file": read_file,
"run_cmd": run_cmd}
def schema(name, desc, arg):
return {
"type": "function",
"function": {
"name": name,
"description": desc,
"parameters": {
"type": "object",
"properties": {
arg: {"type": "string"},
},
"required": [arg],
},
},
}
SCHEMAS = [
schema("read_file",
"Read a local text file.", "path"),
schema("run_cmd",
"Run a shell command.", "cmd"),
]
def summarize(msgs):
# Mini-Agent compresses old turns with an
# LLM summary call when the context nears
# its limit; hard truncation is the v0.
if len(msgs) <= MAX_HISTORY:
return msgs
return [msgs[0]] + msgs[-MAX_HISTORY:]
def run(task: str, max_steps: int = 20):
msgs = [{"role": "user", "content": task}]
for _ in range(max_steps):
msgs = summarize(msgs)
r = client.chat.completions.create(
model=MODEL,
messages=msgs,
tools=SCHEMAS,
)
msg = r.choices[0].message
msgs.append(msg)
if not msg.tool_calls:
return msg.content # model says done
for call in msg.tool_calls:
fn = TOOLS[call.function.name]
args = json.loads(
call.function.arguments)
try:
out = fn(**args)
except Exception as e:
out = f"tool error: {e}"
msgs.append({
"role": "tool",
"tool_call_id": call.id,
"content": out,
})
return "hit max_steps -- gave up"
if __name__ == "__main__":
print(run("Count the .py files here, then "
"summarize naive_loop.py."))
El bucle tiene dos formas de finalizar, y ambas son importantes. El éxito lo determina el propio modelo, expresado mediante la ausencia de llamadas a herramientas adicionales. La seguridad se mide mediante un contador simple, el límite de pasos. Esa asimetría, en la que solo el modelo decide cuándo se ha completado el trabajo, es precisamente lo que falla primero.
Donde un único bucle falla en tareas largas
Si se ejecuta el mismo bucle en una tarea de dos horas en lugar de una de dos minutos, comienzan los fallos. El equipo de MiniMax catalogó aquellos con los que seguían topando. Ninguno es un error de programación; son características propias de un contexto específico, por lo que ni una ventana de tiempo más amplia ni una instrucción mejor resuelven ninguno de ellos.
Primero aparece la ansiedad por el contexto: el agente se detiene cuando nadie se lo pide. Después de realizar siete subtareas, termina tres y dice que ha completado las 1, 2 y 3, preguntando si debe continuar. Los modelos tienen una evaluación imprecisa sobre cuándo puede finalizar una tarea larga, y entrenar directamente esa capacidad de juicio resulta extremadamente costoso en términos económicos y de esfuerzo algorítmico. Un bucle simple deja la decisión de detenerse en su parte menos fiable.
Luego viene la degradación: el agente se vuelve más torpe a medida que el contexto crece. A los cinco minutos funciona con precisión, pero a los sesenta y cinco se comporta como un pasante distraído. Un error menor, como que una tarea de investigación se convierta en texto publicitario, contamina todo lo que sigue, ya que cada generación posterior se basa en esa historia “contaminada”.
En tercer lugar, nada verifica el trabajo, ya que el juez es también el concursante. Un agente solitario que genera el resultado final se califica a sí mismo dentro del mismo contexto en el que cometió sus errores. Eso no es deshonestidad; es examinar una escena que el propio modelo acaba de crear.
En cuarto lugar está el silencio. Las interfaces de estilo chat hacen que los usuarios esperen una respuesta en cuestión de segundos, incluso para tareas que duran una hora, pero un bucle oculto dentro de una llamada al modelo no puede indicar que recibió la solicitud ni prometer una respuesta. Según el informe de MiniMax, la pregunta “¿por qué mi agente no responde?” generó más comentarios de los usuarios que cualquier otra cosa.
La solución: tres roles y un motor determinista
MiniMax responde con estructura. Tres tipos de roles son coordinados por código determinista ordinario, no por un prompt ingenioso, y ese código se denomina Motor del Equipo. Un ciclo de vida de tarea corresponde a una Sesión, y el motor hace pasar cada Sesión por tres estados: producción, verificación y finalización, despertando nuevamente al generador cada vez que falla la verificación.
- El Líder divide la solicitud del usuario en un plan estructurado, elige qué partes se ejecutan simultáneamente, responde al usuario en cuestión de segundos mientras el resto continúa de forma invisible, y finalmente fusiona los resultados.
- Los Trabajadores se encargan de subtareas, cada uno en un contexto separado equipado con sus propias herramientas, memoria y formato de salida requerido. Esa separación es importante porque un Trabajador solo ve una subtarea a la vez, nunca dos horas de desviaciones acumuladas.
El núcleo del motor es lo suficientemente pequeño como para poder escribirlo por completo. Esta versión no necesita clave API, ya que los roles de stub sustituyen a las llamadas al LLM y pueden intercambiarse sin afectar la máquina de estados:
# team_engine.py -- the loop promoted to a
# state machine. One task lifecycle is one
# Session; deterministic code owns every
# transition: producing -> verifying -> done.
from dataclasses import dataclass, field
PRODUCING = "producing"
VERIFYING = "verifying"
DONE = "done"
FAILED = "failed"
@dataclass
class Session:
goal: str
state: str = PRODUCING
attempts: int = 0
artifact: str = ""
reviews: list = field(default_factory=list)
def run_session(s, worker, verifier,
max_retries: int = 3):
while s.state not in (DONE, FAILED):
if s.state == PRODUCING:
s.attempts += 1
s.artifact = worker(
s.goal, s.reviews)
s.state = VERIFYING
elif s.state == VERIFYING:
ok, report = verifier(
s.goal, s.artifact)
s.reviews.append(report)
if ok:
s.state = DONE
elif s.attempts >= max_retries:
s.state = FAILED
else:
# Reject: wake the worker with
# the review log in context.
s.state = PRODUCING
return s
def run_team(goals, worker, verifier):
# Leader's job: split the goal, run the
# sessions (a thread pool in real use),
# then merge N artifacts into 1 result.
done, failed = [], []
for g in goals:
s = run_session(Session(g),
worker, verifier)
if s.state == DONE:
done.append(s.artifact)
else:
failed.append(s.goal)
return done, failed
if __name__ == "__main__":
# Stub roles so the engine runs anywhere;
# swap in LLM calls for the real thing.
def worker(goal, reviews):
v = len(reviews) + 1
return f"draft v{v}: {goal}"
def verifier(goal, artifact):
ok = "v2" in artifact
return ok, f"review {artifact!r}: {ok}"
done, failed = run_team(
["intro", "benchmarks", "faq"],
worker, verifier)
print("delivered:", done)
print("failed:", failed)
Al ejecutarlo, la primera versión de cada sesión es rechazada; el Worker se activa con el registro de revisiones en su contexto, y la segunda versión es aceptada. Si se compara con el bucle simple, se destacan algunas diferencias.
- Detención: el bucle único finaliza cuando el modelo deja de emitir llamadas a herramientas (método difuso) o alcanza el límite de pasos, mientras que el motor utiliza estados explícitos de “completado” y “fallido”, con un presupuesto de intentos definido en el código.
- Verificación: en el bucle, el mismo contexto que generó el trabajo lo verifica, mientras que el equipo emplea un verificador adversarial en un contexto separado, basado en comandos.
- Paralelismo: el bucle funciona en un único contexto secuencial, mientras que el equipo ejecuta Workers en sesiones paralelas, con el líder que fusiona varias en una sola.
- Persistencia: el bucle mantiene una lista de mensajes hasta el momento de la resumen, mientras que el equipo conserva el estado de la sesión, los registros de revisión, los artefactos y la memoria de cada agente.
El Líder tampoco está limitado a llamadas puntuales que generan una subtarea y devuelven texto. En el diseño de MiniMax, este lee el estado en tiempo real del motor y puede enviar instrucciones adicionales a una Sesión que está en proceso de generación o verificación, lo que permite tanto envíos proactivos como consultas a demanda. Esto también resuelve el problema del silencio: el Líder responde en cuestión de segundos, y el usuario recibe informes en puntos clave como iniciado, bloqueado, se necesita una decisión o completado.
Base para el Verificador
El defecto de autoevaluación desaparece únicamente cuando el veredicto proviene de fuera de todos los modelos. En el caso del código, MiniMax rodea la ejecución con un marco de control. El trabajo se realiza en ramas, se ejecuta en entornos aislados, las ediciones se presentan como diferencias, las pruebas se pueden repetir, las revisiones dejan registros y los fallos se pueden reproducir. La frase más importante es esta: una condición de parada debe estar vinculada a algo determinista y observable desde el exterior, como las pruebas, los analizadores de código y el proceso de compilación, y nunca al sentido que tenga el modelo de haber terminado.
# grounded_verifier.py -- evidence over vibes.
# The verdict comes from exit codes and logs,
# never from the model's self-assessment.
import subprocess
import sys
PY = sys.executable
# pytest exit codes: 0 = all passed,
# 1 = failures, 5 = no tests collected
# (acceptable for a docs-only change).
PYTEST_OK = (0, 5)
CHECKS = [
("diff", ["git", "diff", "--stat"], (0,)),
("build", [PY, "-m", "compileall",
"-q", "."], (0,)),
("tests", [PY, "-m", "pytest", "-q",
"--maxfail", "1"], PYTEST_OK),
]
def run_check(name, cmd, ok_codes, cwd):
p = subprocess.run(
cmd, cwd=cwd,
capture_output=True, text=True)
tail = (p.stdout + p.stderr)[-2000:]
return {
"check": name,
"cmd": " ".join(cmd),
"exit_code": p.returncode,
"ok": p.returncode in ok_codes,
"output_tail": tail,
}
def verify(repo: str):
evidence = []
for name, cmd, ok_codes in CHECKS:
e = run_check(name, cmd, ok_codes, repo)
evidence.append(e)
if not e["ok"]:
# Reject with logs attached: the
# worker retries against real
# errors, not "please improve".
return False, evidence
return True, evidence
if __name__ == "__main__":
ok, ev = verify(".")
for e in ev:
print(f"{e['check']:>6} exit "
f"{e['exit_code']} ok={e['ok']}")
print("verdict:", "pass" if ok else "fail")
La lista de pruebas funciona también como registro de revisión, formando parte de un rastro reutilizable y verificable que distingue lo que realmente se puede entregar de lo que es solo plausible. MiniMax divide aún más el proceso de revisión. Un probador genera pruebas externas al ejecutar las pruebas, comprimir los registros de fallos y añadir una reproducción mínima. Un revisor plantea una pregunta diferente: no si la prueba pasó o no, sino si el código debería modificarse de esa manera, analizando los límites de abstracción, las nuevas dependencias, los permisos ampliados y los secretos que se filtran en los registros. Los revisores pueden trabajar en paralelo con especialidades como general, seguridad y dominio.
Qué cuesta un equipo y cuándo prescindir de él
Un equipo no es paralelismo gratuito. Aparecen tres costos tan pronto como los agentes colaboran, y una ventana de contexto más amplia no elimina ninguno de ellos.
Costo de transferencia: la misma información debe ser reorganizada entre los agentes, por lo que las páginas de investigación se convierten en un resumen para el escritor, el cual sirve como entrada para una verificación de formato. La solución es pasar archivos legibles mediante rutas específicas junto con resúmenes breves, en lugar de volcados de contexto sin procesar.
Costo de compartición: cada token compartido a nivel global es costeado por todos los trabajadores en cada ronda. Un diseño funcional mantiene la memoria privada para cada agente, permite que estos se comuniquen entre sí mediante interrupciones por CLI y ofrece una pizarra blanca compartida a la que los agentes acceden solo cuando lo necesitan.
El tercer costo es el de agregación. Recopilar diez borradores parciales es sencillo. Unirlos en un único archivo con datos, citas y tono coherentes es la tarea más difícil para el líder, y requiere un paso de fusión, no más personal.
El consenso también tiene un costo. El debate libre entre los agentes puede consumir varias veces más tokens que la autocorrección aislada y aún así no mejorar la precisión. El valor radica en la estructura: sesiones delimitadas, artefactos explícitos y un verificador autorizado para rechazar.
Omite el trabajo en equipo cuando la tarea es pequeña, local y económica de verificar, ya que un solo ciclo cuesta menos. Úsalo cuando el trabajo se divide claramente, los errores son costosos y una verificación independiente puede proporcionar pruebas de que el modelo no puede engañarse a sí mismo. Una buena prueba: añade un rol solo si cubre un modo de fallo que ningún otro rol abarca; de lo contrario, la sobrecarga de la colaboración no aporta nada.
Qué construir primero
Comience con la máquina de estados más pequeña en la que pueda confiar. Cada sesión cuenta con un objetivo, un artefacto, un registro de revisiones y un presupuesto para intentos adicionales, y cada transición está detallada claramente. Coloque al verificador detrás de un sistema cuyas respuestas provengan de pruebas, análisis de código, compilaciones o verificaciones mecánicas similares. Los trabajadores en paralelo se añaden posteriormente: el paralelismo es una optimización, mientras que los límites de corrección forman parte de la arquitectura. Deje visible el paso de fusión del líder, mantenga los artefactos de transferencia abiertos para inspección y registre por qué terminó cada sesión.
Lo que resulta no es una mente autónoma. Se trata de un flujo de trabajo controlado: los modelos proponen, las herramientas ejecutan las tareas y el código ordinario determina si el trabajo está completado. Construya el bucle básico a mano una vez antes de adoptar un framework. Espere que las tareas largas dañen su estructura debido a la ansiedad, la deriva, la autoevaluación y el silencio, algo que ninguna cantidad de instrucciones puede solucionar. Considere a los sistemas multiagente como un entorno de ejecución con un plano de control que gestiona el estado, los eventos, los permisos, la observabilidad y los artefactos recuperables, y no como un juego de roles en una instrucción. Además, asigne recursos para la transferencia de responsabilidades, el intercambio de información y la agregación de datos antes de formar un equipo, y nunca forme uno solo por un error tipográfico.
Leer más
- Notas prácticas: Claude Code Agent Teams: Un guía práctico para construir un equipo — Guía paso a paso de las Notas prácticas: Claude Code Agent Teams: Un guía práctico para construir un equipo: contratos, verificaciones y espacios para código integrable para equipos que implementan este patrón.
- Por qué los registros del agente no satisfarán el Artículo 12: Registros de decisiones, causalidad y procedencia — Las trazas no equivalen a responsabilidad. Cómo registrar las decisiones del agente como objetos duraderos con enlaces causales, procedencia en cadena de hash y reglas de política deterministas, utilizando Semantica.