Saltar al contenido principal

17 · Agentes y AI Engineering

Testing y evals para agentes

Modelos falsos, tests de arquitectura, golden files y suites de evals: testear un sistema cuyo núcleo no se puede leer como código.

12 minCaso de estudio: docket (se abre en una ventana nueva)

Si suma(2, 2) no devuelve 4, hay un bug: el test es un oráculo exacto. Un no funciona así. Entre la entrada y la salida hay decisiones de un modelo que no se pueden leer como código ni verificar con assert ==, y que cambian cuando cambias el , las herramientas o el modelo.

La salida no es renunciar al testing, sino separar el sistema en dos partes. Casi todo lo que rodea al modelo (el loop, el dispatcher, las políticas, los parsers, la persistencia) es determinista y se testea como cualquier software. Las decisiones del modelo se evalúan, con otras herramientas y otras expectativas. Veamos ambas mitades.

Tests y evals no son lo mismo

Test Eval
Pregunta ¿respeta un contrato conocido? ¿el comportamiento es suficientemente bueno?
Resultado pasa / falla tasa, puntaje, distribución
Oráculo exacto aproximado: métricas, rúbricas, jueces, humanos
Qué cubre el código el sistema con el modelo
Costo casi nulo llamadas reales al modelo

Los dos errores comunes son simétricos: testear todo con un modelo real y asserts exactos (tests lentos, caros y frágiles), o testear solo con mocks y no ver nunca el sistema con un modelo real (tests verdes, producto roto). Una suite de tests perfecta no prueba que el agente resuelva tareas; una buena no detecta que el dispatcher tiene un camino que salta la política.

El modelo falso

La técnica central es reemplazar el modelo por un backend guionado: un objeto que devuelve una secuencia fija de respuestas (incluidos tool calls) y registra lo que recibió. Así se provocan a propósito los casos que un modelo real produce rara vez: JSON roto, herramientas inexistentes, respuestas truncadas, pedidos peligrosos.

# Illustrative: script a model that tries to escape the workspace
backend = ScriptedBackend([
    tool_call_response("c1", "write", '{"path": "../escape.txt"}'),
    final_response("done"),
])
result = run_agent_turn(backend, registry, ctx, "session", "do it")
assert not (workspace.parent / "escape.txt").exists()

Esa frase es un buen criterio general: simula el modelo, no el protocolo. Si también falsificas la frontera HTTP, no detectarás el día en que el proveedor cambie un campo.

Tests que leen el código

Algunas garantías no son comportamientos, son invariantes de arquitectura: "toda tool call pasa por un único ". Un test de comportamiento prueba que hoy funciona; un test que lee el árbol de sintaxis prueba que nadie agregó un segundo camino.

# Excerpt, line-wrapped
call_sites = [
    node
    for node in ast.walk(tree)
    if isinstance(node, ast.Call)
    and isinstance(node.func, ast.Name)
    and node.func.id == "dispatch_tool"
]
assert len(call_sites) == 1, (
    "expected exactly one dispatch_tool(...) call site"
)

docket lleva la idea a una carpeta entera, tests/guards/: por ejemplo test_no_subprocess_in_core.py prohíbe subprocess dentro de core/, y test_function_span.py es un ratchet: ninguna función pasa de 150 líneas, y las que ya lo hacen, listadas en function_span_baseline.txt, solo pueden achicarse. El ratchet sirve para cualquier deuda técnica: no exige arreglar todo de golpe, prohíbe empeorar.

Contratos, golden files y costuras

  • Contract tests. Cuando otro programa consume tu salida, el formato es un contrato. En docket, tests/integration/test_harness_contract.py regenera el JSON Schema de docs/contracts/harness-v1/schema.json desde los modelos Pydantic y exige igualdad byte a byte.
  • Golden tests. tests/golden/run.sh ejecuta la CLI real con un HOME sembrado y binarios falsos, normaliza fechas e ids con scrub.py y compara con diff contra 18 archivos .golden. La regla del proyecto (AGENTS.md): nunca regenerar un golden para ocultar un cambio de comportamiento no intencional.
  • Costuras. Dos componentes testeados por separado pueden fallar juntos. En docket, un cambio hizo que el loop escribiera una denegación como texto y otro, en paralelo, la parseara con un regex; cada lado pasaba sus tests y juntos vaciaban los campos del payload. La corrección fue una sola función dueña del formato (approval_unavailable_error en src/docket/core/agent_loop.py) y un test de ida y vuelta con el renderer y el parser reales (TestBlockedPayloadSurvivesTheDriverErrorRoundTrip).

Documentación que no puede mentir

Los números que un proyecto publica sobre sí mismo (tests, líneas, specs) se desactualizan en silencio. docket los calcula: scripts/metrics.py --check compara las cifras que cita CONTRIBUTING.md con el árbol y falla en CI si no coinciden (el paso se llama README drift guard en .github/workflows/ci.yml). El guard reemplazó a un script anterior que contaba rutas ya borradas y devolvía casi cero sin fallar, y tests/agent/truth/test_metrics_script.py existe para que eso no se repita.

Evals para agentes

Una eval de agente mide varias cosas a la vez:

  • Resultado: ¿el artefacto final es correcto? Para código: compila, pasan los tests del proyecto y tests ocultos que el agente no vio.
  • Trayectoria: ¿el camino fue razonable? ¿Tocó archivos prohibidos, intentó comandos peligrosos, modificó el test para que pasara?
  • Costo: turnos, medidos, duración.
  • Seguridad: casos adversariales y .
Agente A: grep → read → edit → pytest → final    5 pasos  12k tokens
Agente B: read ×14 → write test_login.py (borra test)
          → write auth.py → pytest → final      21 pasos  80k tokens

Una eval de resultado puntúa igual a los dos. Una de trayectoria descubre que B costó seis veces más y "aprobó" modificando el oráculo.

Esa eliminación es la lección: una eval que no puede fallar es peor que no tener eval, porque produce confianza falsa. Para evaluar modelos (held-out, baselines, ) ver Evaluar modelos.

Para CTOs

  • Exige las dos mitades: tests deterministas en cada PR y evals con modelo real a un ritmo que puedas pagar (subconjunto de regresión en CI, suite completa nocturna o antes de cambiar de modelo).
  • Pregunta por la trayectoria, no solo por la tasa de éxito.
  • Cada incidente de producción debe convertirse en un caso nuevo del dataset.
  • Desconfía de cualquier suite, eval o guard que nunca haya fallado: comprueba que puede ponerse en rojo.

Para llevar

  • Separa lo determinista (se testea) de las decisiones del modelo (se evalúan).
  • Simula el modelo con un backend guionado, pero conserva la frontera de protocolo real.
  • Los invariantes de arquitectura, como un único chokepoint, se protegen leyendo el código con AST.
  • Una eval de agente mira resultado, trayectoria y costo; un juez LLM necesita calibración.
  • Una eval o un guard que no puede fallar produce confianza falsa.

Comprueba lo aprendido

¿Qué aporta un test AST sobre el chokepoint que un test de comportamiento no aporta?

El test de comportamiento prueba que el camino actual pasa por dispatch_tool. El test AST prueba que no existe otro camino en el código, así que falla si alguien agrega una llamada directa a un handler.

¿Por qué una eval de resultado final puede aprobar a un mal agente de código?

Porque solo mira el artefacto. Un agente que modifica o borra el test que fallaba produce un resultado "verde"; solo una eval de trayectoria o una revisión del diff lo detecta.

¿Por qué docket eliminó `docket eval` en lugar de dejarlo?

Porque dependía de un daemon ya eliminado y se saltaba en silencio en vez de fallar, así que parecía cobertura sin medir nada. Una eval que no puede fallar genera confianza falsa.