English · Español
Break 00 — Agente sin terminación: elimina max_turns y max_tool_calls¶
🇪🇸 Le quitamos al bucle del agente las dos guardas de terminación (
max_turnsymax_tool_calls). En una entrada adversaria — "conjuga eat con cariño en el tiempo soñador" — el modelo emitetool_callindefinidamente porque el resultado no le satisface; el bucle nunca llega aanswer. CPU al 100% hasta que mates el proceso. La lección: los caps son obligatorios, no decorativos.
Este ejercicio /break apunta al invariante innegociable de terminación de los bucles de agente. El bug son dos condiciones borradas; el fallo es del tipo más espectacular — el agente corre para siempre.
Referencias: theory/01-react-and-planning.md, theory/05-agent-loop-architecture.md, .claude/commands/break.md.
Hipótesis¶
El aprendiz predice: "Sin max_turns ni max_tool_calls, la única terminación del bucle es que el LLM emita action: answer. En una entrada adversaria (o incluso en una entrada perfectamente normal donde el modelo está inseguro), el LLM seguirá eligiendo action: tool_call porque es la elección más segura. El bucle corre hasta que el reloj de pared o el OS lo maten. CPU al 100 %. Sin salida."
El break¶
En src/miniagent/loop.py:
def grammar_tutor(user_input: str,
- max_turns: int = 6,
- max_tool_calls: int = 4,
mcp_client: MCPClient) -> str:
scratchpad: list[Observation] = [Observation(role="user", text=user_input)]
- turns = 0
- tool_calls = 0
- while turns < max_turns:
- turns += 1
+ while True: # /break: no termination cap
action = llm_reason(scratchpad, schema=NEXT_ACTION_SCHEMA)
if action.kind == "answer":
return action.text
- if tool_calls >= max_tool_calls:
- return "I tried but could not resolve the grammar question in time."
- tool_calls += 1
try:
result = mcp_client.call(action.tool, action.args, timeout=2.0)
scratchpad.append(Observation(role="tool", text=result.text))
except MCPError as e:
scratchpad.append(Observation(role="tool", text=f"[error] {e}"))
- return "I tried but could not resolve the grammar question in time."
Cinco líneas borradas (los dos defaults de cap, los dos contadores, las dos comprobaciones y el return de fallback). La función ahora son ~12 líneas en vez de ~22 — y está incorrecta.
Predice, después ejecuta¶
Entrada de test adversaria: "Please conjugate the verb eat lovingly in the dreaming tense". El dreaming tense no está en el enum de §A13, así que:
- Turno 1: el LLM razona. Elige
tool_callparaconjugate(verb="eat", tense="???", person="???"). Como la entrada no especifica persona y "dreaming" no es un tiempo, el modelo emite su mejor adivinanza:tense="past simple", person="1st singular". La máscara fuerza alguna elección válida. - La herramienta devuelve
"ate". - Turno 2: el LLM razona. El scratchpad dice "el usuario preguntó sobre dreaming tense; la herramienta devolvió 'ate'". El modelo está inseguro; emite otro tool_call para verificar.
- La herramienta devuelve
"ate"otra vez (mismos args). - Turno 3: idéntico. Turno 4: idéntico. ... Turno N: idéntico.
Con caps, el bucle termina en el turno 6 con el mensaje de fallback. Sin caps, corre hasta que lo mates.
Predicciones¶
- Reloj-de-pared hasta "answer": nunca. El proceso debe matarse manualmente.
- Uso de CPU: 100 % de un núcleo durante todo el tiempo que corra el bucle.
- Crecimiento del scratchpad: lineal en el conteo de turnos; eventualmente excede el contexto de 64 tokens de Mini-GPT y empieza a truncar, pero el modelo sigue emitiendo
tool_callporque la última observación que ve sigue siendo ambigua. - Tool calls por minuto: ~6–8 en el i5-8250U de Borja (limitado por el decode de Mini-GPT + parse JSON). Tras 5 minutos has hecho 35 llamadas redundantes.
- Subproceso del servidor MCP: mantenido vivo por el agente; nunca se cierra.
Aún peor — en una entrada normal que debería terminar (p. ej., "conjugate work in past simple, 1st singular"), el bucle puede aún así fallar en terminar si el paso reason del LLM por casualidad favorece una llamada extra de verificación. Sin caps, la corrección del agente depende de la calibración de confianza del LLM, que es precisamente la propiedad para la que aún no estamos entrenados.
Escribe tus predicciones en learners/borja/phase-32/notes/breaks.md antes de ejecutar.
Observa¶
Ejecuta el agente roto con la entrada adversaria:
timeout 30 just exp 32-agent --variant no-cap --input "Conjugate eat lovingly in the dreaming tense"
El timeout 30 es tu red de seguridad — sin él, Borja debe hacer Ctrl-C. El reloj-de-pared de 30 s debería expirar sin salida del agente. Compara con:
Esto debería retornar en 2 s con el mensaje de fallback: "I tried but could not resolve the grammar question in time."
Diagnósticos:
- Número de líneas
tool_callen el log del agente para la ejecución rota vs la capped. Esperado: rota ~200, capped ~4. - Captura de
topohtopdurante la ejecución rota. Una CPU clavada al 100 %. - La cola del scratchpad tras 30 s — debería ser una lista larga de tool calls idénticas con respuestas idénticas.
Síntoma que Borja verá¶
- El proceso se cuelga, hay que matarlo con
timeouto Ctrl-C. - Una CPU al 100 %.
- El log muestra cientos de ciclos redundantes
tool_call → "ate". - Nunca se produce salida.
- Si
MCP_LOG_LEVEL=debug, el servidor loguea la misma tool call cada ~150 ms.
Causa oculta (una frase)¶
Eliminar las guardas max_turns y max_tool_calls despoja al bucle de toda condición de terminación excepto el LLM emitiendo voluntariamente action: answer, lo que en entradas adversarias o ambiguas puede no hacer nunca.
Cascada de pistas¶
- Mira el log del agente. ¿Cuántas líneas tool_call? ¿Cómo se compara con el default
max_tool_calls? - ¿Cuál es la condición de salida del bucle ahora? Traza el flujo de control en papel.
- Re-lee
theory/05-agent-loop-architecture.md§"Condiciones de terminación — tres capas". ¿Cuántas de esas tres capas están activas en tu código?
Diff del fix¶
def grammar_tutor(user_input: str,
+ max_turns: int = 6,
+ max_tool_calls: int = 4,
mcp_client: MCPClient) -> str:
scratchpad: list[Observation] = [Observation(role="user", text=user_input)]
+ turns = 0
+ tool_calls = 0
- while True:
+ while turns < max_turns:
+ turns += 1
action = llm_reason(scratchpad, schema=NEXT_ACTION_SCHEMA)
if action.kind == "answer":
return action.text
+ if tool_calls >= max_tool_calls:
+ return "I tried but could not resolve the grammar question in time."
+ tool_calls += 1
try:
result = mcp_client.call(action.tool, action.args, timeout=2.0)
scratchpad.append(Observation(role="tool", text=result.text))
except MCPError as e:
scratchpad.append(Observation(role="tool", text=f"[error] {e}"))
+ return "I tried but could not resolve the grammar question in time."
Cinco líneas restauradas. El agente ahora termina con como mucho 6 turnos y 4 tool calls.
Por qué esto enseña el concepto¶
La terminación es un contrato, no una heurística. Cada bucle en código de producción tiene una prueba de terminación; los bucles de agente no son la excepción. El modelo mental por defecto — "el LLM averiguará cuándo parar" — está mal. Los LLMs están entrenados para ser útiles, lo que en entradas inciertas se traduce en más tool calls, no menos. El bucle capped sacrifica una fracción pequeña de corrección en casos límite (donde una tool call más podría haber ayudado) por una cota superior dura en uso de recursos.
El tutor de gramática §A13 con caps alcanza ~85–90 % de precisión. Sin caps, sobre el mismo conjunto de eval, o bien se colgaría en algunas entradas o tendría la misma precisión con latencia ilimitada en algunas entradas. Ninguna es aceptable en producción.
Una lección más profunda: los presupuestos de fallo son first-class. Los agentes de producción declaran "este bucle usará ≤ N tool calls y ≤ T segundos, o devolverá un fallback". Esa declaración es lo que hace a los agentes schedulables, facturables y rate-limitables. Un agente sin cap es un bug, no una feature — incluso cuando por casualidad termina.
La pista de extensión X3 (docs/extension-track/X3-rlhf-dpo/) aborda la calidad de las decisiones dentro del cap (cuándo responder vs cuándo seguir invocando herramientas), pero nunca quita los caps en sí. El alineamiento ayuda al agente a rendirse antes; no elimina la necesidad estructural de rendirse.
Referencia¶
- Anthropic, Building effective agents (engineering blog, 2024-12-20) — explícito sobre los caps duros como propiedad innegociable del agente.
- Paper ReAct, §"Limitations" — discute el modo de fallo de divergencia del bucle y motiva heurísticas de presupuesto de pasos.
docs/phase-32-agents/lab/03-failure-mode-tour.md— reproduce este y otros tres bugs clásicos de agente en un lab controlado.
Siguiente: restaura los caps. Ejecuta lab/01-tutor-end-to-end.md sobre el conjunto de eval §A13 de 30 prompts para confirmar que el agente termina en cada entrada.