Módulo 5: Evals de regresión como gate de producción
Mini-proyecto: un gate de regresión para Reservo
Descripción
Siete lecciones construyeron, por separado, cada pieza: por qué hace falta un gate y qué frontera respeta (01), las tres preguntas que puede contestar (02), el CASE_SET fijo y su disciplina de aislamiento (03), la forma del resultado y la frontera con la calidad semántica (04), la elección literal de tool (05), los umbrales de costo y latencia (06), y el ensamble completo en run_case/run_regression_gate (07). Este mini-proyecto las junta todas en un solo directorio de trabajo —regression/harness.py y regression/golden_cases.json— y las corre sobre un escenario realista de punta a punta: el estado actual de Reservo, PASS; un cambio de prompt propuesto que introduce una regresión real, FAIL, detectado antes de llegar a producción; la corrección del prompt, PASS de nuevo, listo para desplegar.
Al final de esta lección vas a tener regression_report.json real, escrito a disco, con el veredicto completo de los cinco casos — el artefacto que el Módulo 7 va a reusar, sin modificarlo, para decidir GO/NO-GO en una comparación de versiones.
Conexión con el módulo
Esta es la síntesis del módulo completo. No hay ninguna pieza nueva de regression/harness.py — el mini-proyecto reusa run_case, run_regression_gate, CaseResult y GateReport exactamente como quedaron en la lección 07, y agrega una sola función nueva, print_gate_summary, que arma un reporte legible en consola a partir de un GateReport — la pieza que faltaba para que el gate sea útil a simple vista, no solo programáticamente.
Ejemplo trabajado: el flujo completo, de punta a punta
print_gate_summary: un reporte legible, con el motivo exacto de cada FAIL
def print_gate_summary(report):
"""Un reporte legible en consola: PASS/FAIL del lote, y para cada caso
roto, EXACTAMENTE cuál de los cuatro chequeos falló y con qué valores."""
passed_n = sum(c.passed for c in report.cases)
total_n = len(report.cases)
print(f"=== GATE: {'PASS' if report.passed else 'FAIL'} ({passed_n}/{total_n}) ===")
for c in report.cases:
status = "PASS" if c.passed else "FAIL"
line = f" {c.name:38} {status}"
if not c.passed:
reasons = []
if not c.tool_choice_ok:
reasons.append(f"tool_choice(esperaba distinto, obtuvo {c.actual_tools})")
if c.schema_errors:
reasons.append(f"schema{c.schema_errors}")
if c.output_errors:
reasons.append(f"output{c.output_errors}")
if not c.cost_ok:
reasons.append(f"cost({c.cost_cents}c)")
if not c.latency_ok:
reasons.append(f"latency({c.latency_ms}ms)")
line += " -- " + "; ".join(reasons)
print(line)
print_gate_summary no agrega ningún chequeo nuevo — solo lee los campos que CaseResult ya trae y arma una línea por caso, con el detalle completo únicamente para los que fallaron. Un caso que pasó no necesita explicación; uno que falló necesita decir, sin que nadie tenga que inspeccionar el objeto a mano, exactamente qué se rompió.
Paso 1: el estado actual de Reservo — PASS, listo para producción
CASE_SET = load_case_set("golden_cases.json")
print("--- 1. estado actual: PASS, listo para producción ---")
report = run_regression_gate(CASE_SET)
print_gate_summary(report)
Qué esperar:
--- 1. estado actual: PASS, listo para producción ---
=== GATE: PASS (5/5) ===
quote_focus_pro_3h PASS
quote_focus_basic_3h PASS
book_focus_pro_3h_ana PASS
book_boardroom_pro_1h_sofia PASS
book_and_cancel_studio_basic_1h_diego PASS
Este es el punto de partida: el agente de Reservo, tal como agent-fundamentals M8 lo entregó, pasa los cinco casos. Este veredicto es la línea base contra la que cualquier cambio futuro se compara.
Paso 2: se propone un cambio de prompt — el gate lo corre antes de desplegar
Imagina que alguien del equipo propone un ajuste al system prompt de Reservo, con la intención de que el agente sea "más directo" con usuarios que ya reservaron antes. El cambio, sin que nadie lo note en una revisión rápida de código (el prompt es texto libre, no código que un linter pueda analizar), hace que el modelo salte el paso de cotización para el caso más simple del set:
print("--- 2. antes de desplegar: se propone un cambio de prompt, se corre el gate contra él ---")
proposed_script = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro 3h para Ana."}]},
]
report_proposed = run_regression_gate(CASE_SET, overrides={"quote_focus_pro_3h": proposed_script})
print_gate_summary(report_proposed)
print()
print("decisión: NO-GO -- el cambio propuesto no se despliega hasta corregirlo.")
Qué esperar:
--- 2. antes de desplegar: se propone un cambio de prompt, se corre el gate contra él ---
=== GATE: FAIL (4/5) ===
quote_focus_pro_3h FAIL -- tool_choice(esperaba distinto, obtuvo ['book_room']); latency(120ms)
quote_focus_basic_3h PASS
book_focus_pro_3h_ana PASS
book_boardroom_pro_1h_sofia PASS
book_and_cancel_studio_basic_1h_diego PASS
decisión: NO-GO -- el cambio propuesto no se despliega hasta corregirlo.
Dos cosas para notar en este FAIL. Primero, el caso roto es exactamente el que se tocó —quote_focus_pro_3h—, y los otros cuatro, sin ningún cambio, siguen en PASS. Segundo, y esto es una consecuencia real del diseño de run_case: este caso falla por dos motivos a la vez, no solo uno. book_room (120 ms de latencia modelada) es una tool más lenta que get_quote (25 ms), y el umbral de latencia de este caso específico —100 ms, pensado para una cotización simple, de una sola tool rápida— nunca contempló que se llamara a una tool distinta y más costosa. La regresión de elección de tool trajo consigo una segunda ruptura, de umbral, como efecto colateral — exactamente el tipo de daño en cascada que un gate completo, con las cuatro preguntas evaluadas juntas, puede exponer y que un chequeo aislado (solo check_tool_choice, por ejemplo) habría dejado parcialmente oculto.
Paso 3: se corrige el prompt — el gate vuelve a PASS, listo para desplegar
Con el problema identificado con precisión —el prompt necesita seguir cotizando antes de reservar—, el equipo revierte el cambio. El gate se corre de nuevo, sobre el CASE_SET sin ningún overrides:
print("--- 3. tras corregir el prompt, se vuelve a correr el gate: PASS, ahora sí se despliega ---")
report_fixed = run_regression_gate(CASE_SET)
print_gate_summary(report_fixed)
Qué esperar:
--- 3. tras corregir el prompt, se vuelve a correr el gate: PASS, ahora sí se despliega ---
=== GATE: PASS (5/5) ===
quote_focus_pro_3h PASS
quote_focus_basic_3h PASS
book_focus_pro_3h_ana PASS
book_boardroom_pro_1h_sofia PASS
book_and_cancel_studio_basic_1h_diego PASS
Este es, de principio a fin, el ciclo completo que este módulo existe para hacer posible: proponer un cambio → correr el gate → decidir con evidencia, no con vibra → corregir si hace falta → confirmar de nuevo antes de desplegar. Ningún paso de este ciclo necesitó que un humano corriera el agente a mano y "revisara si se veía bien" — cada decisión se apoyó en un veredicto determinista, reproducible, con el motivo exacto de cada FAIL.
El entregable: regression_report.json
Con el estado final (Paso 3) confirmado en PASS, persiste el GateReport completo a disco — el mismo patrón de asdict + json.dump que ya usaste para cada entregable estructurado de esta guía:
import json
from dataclasses import asdict
with open("regression_report.json", "w", encoding="utf-8") as fh:
json.dump(asdict(report_fixed), fh, ensure_ascii=False, indent=2)
raw = open("regression_report.json", encoding="utf-8").read()
print("regression_report.json escrito:", len(raw), "bytes")
Qué esperar:
regression_report.json escrito: 1700 bytes
El archivo completo —los cinco CaseResult, con cada campo, más el passed global— queda disponible para cualquiera que necesite auditar la decisión sin volver a correr nada:
{
"cases": [
{
"name": "quote_focus_pro_3h",
"passed": true,
"actual_tools": ["get_quote"],
"tool_choice_ok": true,
"schema_errors": [],
"output_errors": [],
"cost_cents": 0,
"cost_ok": true,
"latency_ms": 25,
"latency_ok": true
}
],
"passed": true
}
(el archivo real contiene los cinco casos completos; se muestra el primero como referencia de formato). Este es, con precisión, el mismo tipo de artefacto que RUN_LOG.jsonl (Módulo 2) o un CostReport serializado (Módulo 3): texto plano, parseable, que sobrevive al proceso que lo generó — cualquiera puede abrirlo, sin volver a ejecutar ni una línea de Python, y confirmar exactamente qué veredicto produjo el gate y por qué.
🛑 Lo que este mini-proyecto demostró, y lo que deliberadamente nunca hizo
Vale la pena cerrar con la misma precisión con la que abrió el módulo. En las tres corridas de esta lección —PASS inicial, FAIL de la propuesta, PASS de la corrección—, en ningún momento se llamó a un modelo para calificar nada. Cada veredicto salió de cuatro comparaciones deterministas: ¿el resultado tiene la forma correcta?, ¿la tool elegida coincide, literalmente, con la esperada?, ¿el valor de salida coincide con el ancla fija del caso?, ¿el costo y la latencia modelados están bajo el umbral? El FAIL del Paso 2 no dijo "la respuesta del agente suena mal" — de hecho, "Reservé Focus pro 3h para Ana" es una respuesta perfectamente clara como texto. Dijo, con precisión mecánica, que la acción tomada —reservar en vez de cotizar— no era la esperada, y que la tool usada para tomarla excedía el presupuesto de tiempo de ese caso.
Si la pregunta, en algún punto de este mini-proyecto, hubiera sido "¿la confirmación que el agente le mostró a Ana es clara y profesional?" o "¿el agente entendió correctamente la intención del usuario?", ninguna función de este módulo podría contestarla. Esas preguntas —de calidad semántica, no de forma— pertenecen a evaluation-frameworks-guide: su módulo evaluating-agents (trajectory evaluation, tool-call accuracy juzgada, reasoning-quality) y su módulo evaluation-pipelines-in-production (evaluation CI/CD con datasets dorados, LLM-as-judge, A/B testing de calidad) cubren, con sus propias herramientas y su propio stack, exactamente ese territorio. Un sistema real, maduro, corre ambos tipos de gate —el de este módulo, determinista y rápido, en cada cambio; el de esa guía, más costoso y con juicio semántico, con menor frecuencia o sobre una muestra— sin confundir nunca cuál contesta cuál pregunta.
Errores comunes
-
Pensar que este mini-proyecto "ya resolvió" la confiabilidad completa del agente de Reservo. Resolvió una capa —regresiones de forma, detectables antes de producción—. Le sigue faltando resiliencia frente a fallos repetidos entre runs (Módulo 6) y una disciplina de versionado explícita para el prompt y las tools (Módulo 7). Cada uno de esos módulos se apoya en el gate que este mini-proyecto acaba de construir, no lo reemplaza.
-
Olvidar
reset_reservo_stateal reproducir este flujo con unCASE_SETpropio, más grande. Como advirtió la lección 03, sin ese reseteo, el orden de los casos puede producir un FAIL que no tiene nada que ver con ninguna regresión real — un riesgo que crece, no que disminuye, a medida que elCASE_SETcrece. -
Correr el gate una sola vez y no volver a correrlo tras "corregir" el problema. El Paso 3 de esta lección no es opcional — es la confirmación de que la corrección realmente funcionó, con la misma evidencia determinista que detectó el problema en primer lugar. "Creo que ya lo arreglé" no es un veredicto; un gate en PASS, sí.
-
Guardar
regression_report.jsonsolo cuando el gate falla, "para no llenar el disco de reportes de éxito". El valor de este archivo no es solo alertar sobre un FAIL — es dejar un rastro auditable de cada decisión, incluidas las que confirmaron que todo estaba bien. Un historial de reportes PASS es lo que permite, más adelante, confirmar cuándo exactamente empezó a fallar algo que hoy funciona. -
Confundir el
NO-GOdel Paso 2 con un juicio sobre si la idea del cambio de prompt era mala. La idea —"ser más directo con usuarios que ya reservaron antes"— podría ser perfectamente razonable; lo que el gate detectó fue que la implementación específica de esa idea, en el guion propuesto, rompió un comportamiento que elCASE_SETprotege (cotizar antes de reservar en el caso más simple). El gate no evalúa ideas — evalúa comportamiento observado, contra una expectativa fija.
Ejercicios
Ejercicio 1: Reproduce el ciclo completo con una regresión distinta (Fácil)
Repite el flujo de las tres partes de esta lección —PASS inicial, FAIL propuesto, PASS corregido— pero usando la regresión "por orden" de la lección 05 (list_rooms y get_quote intercambiados) sobre book_boardroom_pro_1h_sofia, en vez de la regresión de quote_focus_pro_3h usada en el ejemplo trabajado.
Ver solución
out_of_order_script = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 1}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "list_rooms", "input": {}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": "book_room",
"input": {"room": "Boardroom", "tier": "pro", "hours": 1, "member": "Sofía"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Reservé Boardroom pro 1h para Sofía."}]},
]
print("--- 1. estado actual ---")
print_gate_summary(run_regression_gate(CASE_SET))
print("--- 2. propuesta con orden alterado ---")
print_gate_summary(run_regression_gate(CASE_SET, overrides={"book_boardroom_pro_1h_sofia": out_of_order_script}))
print("--- 3. tras corregir ---")
print_gate_summary(run_regression_gate(CASE_SET))
Salida esperada (resumen):
--- 1. estado actual ---
=== GATE: PASS (5/5) ===
...
--- 2. propuesta con orden alterado ---
=== GATE: FAIL (4/5) ===
book_boardroom_pro_1h_sofia FAIL -- tool_choice(esperaba distinto, obtuvo ['get_quote', 'list_rooms', 'book_room'])
...
--- 3. tras corregir ---
=== GATE: PASS (5/5) ===
...
Explicación: el mismo ciclo de tres pasos, aplicado a un caso y una regresión distintos, produce exactamente la misma estructura de decisión — la evidencia de que el mecanismo del gate no depende de qué caso específico se rompió.
Ejercicio 2: Compara dos regression_report.json y detecta la diferencia (Medio)
Guarda el GateReport del Paso 2 (el FAIL) como regression_report_before.json, y el del Paso 3 (el PASS) como regression_report_after.json. Escribe código que cargue ambos archivos y reporte, por nombre de caso, cuáles cambiaron de passed entre uno y otro.
Ver solución
with open("regression_report_before.json", "w", encoding="utf-8") as fh:
json.dump(asdict(report_proposed), fh, ensure_ascii=False, indent=2)
with open("regression_report_after.json", "w", encoding="utf-8") as fh:
json.dump(asdict(report_fixed), fh, ensure_ascii=False, indent=2)
before = json.load(open("regression_report_before.json", encoding="utf-8"))
after = json.load(open("regression_report_after.json", encoding="utf-8"))
before_by_name = {c["name"]: c["passed"] for c in before["cases"]}
after_by_name = {c["name"]: c["passed"] for c in after["cases"]}
for name in before_by_name:
if before_by_name[name] != after_by_name[name]:
print(f"{name}: {before_by_name[name]} -> {after_by_name[name]}")
Salida esperada:
quote_focus_pro_3h: False -> True
Explicación: este es, con precisión, el mecanismo central de una comparación de rollout —el trabajo del Módulo 7—: dos regression_report.json, uno de "antes" y uno de "después", comparados caso por caso. Aquí se hace a mano, sobre dos archivos; el Módulo 7 lo formaliza como parte de la decisión GO/NO-GO de una versión nueva.
Ejercicio 3: Diseña un sexto caso que cubra un escenario no cubierto todavía (Difícil)
El CASE_SET actual no tiene ningún caso que ejercite un tier="basic" en una reserva completa (los dos casos de reserva usan pro). Diseña un sexto caso, book_focus_basic_2h_luis, que reserve Focus, basic, 2 horas, para "Luis" —con su question, model_script completo (list_rooms → get_quote → book_room → end_turn), expected_tools, expected_output (calcula price_cents a mano primero), y umbrales razonables—. Agrégalo al CASE_SET en memoria (sin modificar el archivo) y confirma que el gate de seis casos sigue en PASS.
Ver solución
Cálculo a mano: Focus basic 2h = 2500 * 2 = 5000 centavos (sin descuento, basic no lo aplica).
new_case = {
"name": "book_focus_basic_2h_luis",
"question": "Reserva Focus basic 2h para Luis",
"model_script": [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "list_rooms", "input": {}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Focus", "tier": "basic", "hours": 2}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": "book_room",
"input": {"room": "Focus", "tier": "basic", "hours": 2, "member": "Luis"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus basic por 2 horas para Luis. Total $50.00."}]},
],
"expected_tools": ["list_rooms", "get_quote", "book_room"],
"expected_output": {"booking_id": 1, "confirmed": True},
"cost_threshold_cents": 5,
"latency_threshold_ms": 250,
}
extended_case_set = CASE_SET + [new_case]
print_gate_summary(run_regression_gate(extended_case_set))
Salida esperada:
=== GATE: PASS (6/6) ===
quote_focus_pro_3h PASS
quote_focus_basic_3h PASS
book_focus_pro_3h_ana PASS
book_boardroom_pro_1h_sofia PASS
book_and_cancel_studio_basic_1h_diego PASS
book_focus_basic_2h_luis PASS
Explicación: run_regression_gate no tiene ningún límite hardcodeado de cinco casos — corre sobre cualquier lista que reciba, con la misma disciplina de reset_reservo_state por caso. Extender el CASE_SET con cobertura nueva (aquí, la combinación basic + reserva completa, que antes solo se probaba en una cotización simple) es exactamente el tipo de crecimiento sano que este módulo anticipa — siempre agregando casos fijos y explícitos, nunca generándolos al momento.
Resumen y siguiente paso
- Ensamblamos
regression/harness.pyyregression/golden_cases.jsoncompletos, y agregamosprint_gate_summary, la pieza final que hace legible cualquierGateReportde un vistazo. - Corrimos el ciclo completo de un cambio real: PASS (estado actual) → FAIL (una propuesta que introduce una regresión de elección de tool, con un efecto colateral real de umbral de latencia) → PASS (tras corregir) — con decisiones GO/NO-GO respaldadas por evidencia determinista en cada paso.
- Produjimos
regression_report.json, el entregable de este módulo: un archivo real, parseable, que documenta el veredicto completo de los cinco casos sin depender de que ningún proceso siga vivo. - Cerramos con la declaración que acompañó cada lección de este módulo: este gate verifica forma —schema, tool correcta, umbral— y coincidencia exacta contra anclas fijas, nunca calidad semántica. Esa evaluación —¿la respuesta es clara?, ¿el razonamiento fue sólido?— pertenece, con toda su propia disciplina y herramientas, a
evaluation-frameworks-guide.
Con esto se cierra el Módulo 5. Tienes un gate de regresión completo y ejecutado —regression/harness.py, regression/golden_cases.json, regression_report.json—, la frontera con la evaluación semántica declarada con precisión en cada punto donde importaba, y la evidencia de que un cambio real de comportamiento se puede detectar, diagnosticar y corregir sin necesidad de ningún juicio subjetivo de por medio.
Siguiente módulo: Módulo 6 — Fallos a escala: backoff, circuit breakers y rate limits. Con el gate de este módulo ya protegiendo contra regresiones de forma en cada cambio, ese módulo resuelve una pregunta distinta: ¿qué hace el sistema cuando una tool específica lleva fallando, de verdad, varios runs seguidos? Un CircuitBreaker por tool, con estado que persiste entre runs —nunca dentro de uno solo, eso ya lo resolvió agent-fundamentals M7—, va a reusar el mismo vocabulario de resilience-and-reliability-patterns-guide, aplicado, por primera vez, a la capa de tool-calls de un agente.
Recursos adicionales
- Anthropic — Tool use (function calling) overview — El protocolo completo que cada caso del
CASE_SETejercita, sin ninguna modificación respecto aagent-fundamentals. - Python —
json— La base completa deregression_report.json, y de la comparación entre dos reportes del Ejercicio 2. - Python —
dataclasses.asdict— La función que convierteGateReport(y cadaCaseResultanidado) en undictserializable, la misma técnica usada en cada entregable estructurado de esta guía. - Anthropic — Building effective agents — Sobre por qué un procedimiento de verificación repetible, corrido antes de cada cambio, es una de las prácticas que separa a un sistema agentic confiable de uno frágil.
- Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de este módulo, incluida la evidencia real del ciclo completo de esta lección.