English · Español
Lab 03 — Tour por los modos de fallo: induce cuatro bugs clásicos de agente¶
Lee
theory/01-react-and-planning.md,theory/02-memory.md. No consultessolutions/.
Objetivo¶
Induce deliberadamente cuatro modos de fallo clásicos de agente — iteración en bucle, herramientas alucinadas, fuga de scratchpad, agotamiento de presupuesto —, observa cada uno, documenta el síntoma y verifica que tu GrammarTutorAgent o bien previene cada uno o lo reporta con elegancia. Este es el lab de espacio negativo: un tour de lo que sale mal para que sepas contra qué defenderte.
Setup¶
Usa el agente del Lab 01. Construye variantes adversarias de Planner que disparen cada modo de fallo. No se necesita infraestructura nueva; solo subclases de MockPlanner que emitan los pasos equivocados.
Tareas¶
Modo de fallo 1 — iteración en bucle (misma acción repetida)¶
Construye un LoopingPlanner que emita el mismo ToolCall(tool="conjugate", args={"verb": "go", "person": "3sg", "tense": "past_simple"}) en cada paso.
Ejecuta el agente:
agent = GrammarTutorAgent(planner=LoopingPlanner(), ...)
result = agent.correct("He goed to school.")
Comportamiento esperado:
- El detector de acción duplicada del agente debería dispararse en el paso 2.
- El agente devuelve un
CorrectionResultconcorrected=None,rationale=["agent looped"],in_scopeposiblemente True (simplemente no pudimos decidir). - Pasos totales gastados: ≤ 2 (no los
max_stepscompletos).
Asserciones:
Si tu agente recorre los 8 pasos antes de detenerse, la detección de duplicados no está cableada.
Modo de fallo 2 — agotamiento de presupuesto (sin iteración en bucle, simplemente lento)¶
Construye un IndecisivePlanner que emita un ToolCall diferente en cada paso — nunca un FinalAnswer. Efectivamente el planificador no puede decidir.
Ejecuta el agente. Esperado: el agente recorre todos los max_steps (default 8) y devuelve un resultado budget_exhausted con un rationale claro.
Asserciones:
assert len(result.tool_trace) == 8 # full budget spent
assert "budget" in " ".join(result.rationale).lower()
assert result.corrected is None
Modo de fallo 3 — nombre de herramienta alucinado¶
Si tu planificador está implementado según el Lab 00 (con JSONSchemaMask), el planificador no puede emitir una herramienta desconocida — la máscara enum lo previene. Para testear la red de seguridad, construye un HallucinatingPlanner que eluda la máscara (devuelva un ToolCall(tool="this_tool_does_not_exist", args={}) construido a mano).
Ejecuta el agente. Esperado: el dispatcher (cliente MCP de la Fase 31) lanza un ToolNotFoundError limpio; el agente lo captura y o bien:
- (a) Reporta un fallo limpio (devuelve
CorrectionResultcon rationale "unknown tool: this_tool_does_not_exist"), O - (b) Re-prompta al planificador con el error como observación y vuelve a intentarlo.
Default: opción (a) para la Fase 32. La opción (b) es un patrón de retry que vale la pena conocer pero añade complejidad.
Asserciones:
assert result.corrected is None
assert any("unknown" in r.lower() or "does not exist" in r.lower() for r in result.rationale)
Modo de fallo 4 — fuga de scratchpad¶
Este es el que se esconde hasta producción: el scratchpad se comparte accidentalmente entre llamadas a correct(). Para testear:
agent = GrammarTutorAgent(planner=MockPlanner(scripts_for_two_sentences), ...)
result_a = agent.correct("He goed to school.")
result_b = agent.correct("I has a book.")
# The scratchpad for B should contain only B's steps.
# If the bug is present, B's trace will contain steps from A's correction.
for step in result_b.tool_trace:
assert step.args.get("verb") in {"have", "has", None}, \
f"scratchpad leaked: step references '{step.args.get('verb')}' from previous correction"
Esperado: el test pasa (el scratchpad es local a cada llamada correct()).
Si el test falla, el bug es el descrito en theory/02-memory.md §"Una trampa común: fuga de memoria entre correcciones" — arréglalo construyendo ScratchpadMemory() dentro de correct().
Tarea 5 — escribe una nota post-mortem corta¶
En learners/borja/phase-32/notes.md, escribe un resumen de 2–3 párrafos:
- ¿Cuál de los 4 modos de fallo manejaba ya tu implementación correctamente?
- ¿Cuál requirió un cambio de código?
- ¿Qué otros modos de fallo deberías estar testeando, pero aún no? (Posibles respuestas: planificador emitiendo args mal formados, herramienta devolviendo datos mal tipados, timeout MCP a mitad de llamada, estado del agente corrupto entre ejecuciones.)
Esta nota se convierte en parte de PHASE_32_REPORT.md.
Medidas a capturar¶
- Para cada uno de los 4 modos de fallo: assert pasa/falla, comportamiento del agente observado, tiempo gastado antes de detenerse.
- Tiempo total de ejecución del tour de modos de fallo (debería ser ~segundos, no minutos — la iteración en bucle debe terminar rápido).
Guarda en experiments/<date>-phase-32-failure-tour/results.json.
Aceptación¶
- Los 4 tests de modos de fallo escritos.
- El detector de iteración en bucle se dispara en el paso 2.
- El agotamiento de presupuesto se detiene limpiamente en el paso
max_steps. - La herramienta desconocida devuelve un resultado de fallo limpio.
- El scratchpad es local a cada llamada
correct()(sin fuga). - Nota post-mortem en
learners/borja/phase-32/notes.md. - Resultados del tour de modos de fallo guardados.
Trampas a esperar¶
- Umbrales de detección de iteración en bucle. Detectar "misma acción dos veces seguidas" captura los bucles más simples. Un detector más sofisticado busca ciclos de longitud 2+ (
A → B → A → B). La Fase 32 implementa solo longitud-1; los ciclos de mayor orden son una extensión. Documenta el límite. - Confusión del resultado de agotamiento de presupuesto. Si
corrected=Noneyin_scope=True, ¿qué ve el usuario? La política de la Fase 32: el rationale debería decir explícitamente "no pude decidir dentro del presupuesto" — no confundas esto con "no se necesita corrección". - La detección de fuga de scratchpad es frágil. El test del Modo de fallo 4 depende de que las Frases A y B tengan vocabulario disjunto. Si las frases de prueba comparten un verbo por casualidad, el test pasa por coincidencia. Usa verbos claramente distintos en el test (
gopara A,havepara B). HallucinatingPlannerdebería ser obviamente sintético. Márcalo con un docstring: "Solo para test; elude JSONSchemaMask para verificar la defensa del dispatcher". Si no, un mantenedor futuro podría confundirlo con una plantilla de planificador real.
Siguiente: Fase 33 — Inference Serving: From FastAPI to Continuous Batching (tras /quiz 32 y /phase-report 32).