Módulo 7: Versionado y rollout seguro
Comparando una versión nueva contra la vieja
Descripción
Con PROMPT_REGISTRY construido en la lección anterior, esta lección hace la comparación real: correr el mismo gate de regresión —el que el Módulo 5 construyó como un chequeo de forma, determinista, sin juez— contra v1 y contra v2, y ver, con evidencia, qué diferencia. Esta lección no reconstruye ese gate: lo reusa, tal como quedó en regression/harness.py y regression/golden_cases.json, sin tocar ni una línea. Lo único genuinamente nuevo aquí es la forma de usarlo para comparar dos versiones — la pieza que el propio Módulo 5 dejó preparada, a propósito, para este momento.
Al final de esta lección vas a tener dos números —PASS (5/5) y FAIL (4/5)— y, más importante, vas a saber exactamente en cuál caso divergen, con el mensaje exacto que el DISEÑO de esta guía exige citar.
Conexión con el módulo
Esta es la lección bisagra del módulo: la lección 02 mostró el peligro en abstracto (una pregunta, dos decisiones distintas); la lección 03 dio identidad a cada versión (el registro con hash); esta lección corre el chequeo real, de punta a punta, sobre los cinco casos del CASE_SET, no solo la pregunta aislada que ya viste. El resultado —v1 pasa, v2 no— es la evidencia que las lecciones 05, 06 y 07 van a usar para explicar, decidir, y actuar.
Analogía: la misma batería de pruebas, sobre dos pilotos
Retomando la analogía del pase médico de la introducción: el examen no cambia según quién lo rinde. Los mismos cinco chequeos, en el mismo orden, con los mismos umbrales, se aplican al piloto que lleva veinte años volando y al que se está certificando por primera vez. Esta lección es, con precisión, ese examen aplicado dos veces: una vez a v1, una vez a v2, sin cambiar ni un chequeo entre una corrida y la otra.
CASE_SET: retomado del Módulo 5, sin cambios
regression/golden_cases.json trae cinco casos fijos, cubriendo las cuatro tools de Reservo:
from regression.harness import load_case_set
CASE_SET = load_case_set("regression/golden_cases.json")
print("casos cargados:", len(CASE_SET))
for c in CASE_SET:
print(f"{c['name']:38} tools={c['expected_tools']}")
Qué esperar:
casos cargados: 5
quote_focus_pro_3h tools=['get_quote']
quote_focus_basic_3h tools=['get_quote']
book_focus_pro_3h_ana tools=['list_rooms', 'get_quote', 'book_room']
book_boardroom_pro_1h_sofia tools=['list_rooms', 'get_quote', 'book_room']
book_and_cancel_studio_basic_1h_diego tools=['book_room', 'cancel_booking']
Este módulo no declara un solo caso nuevo — usa exactamente el CASE_SET que el Módulo 5 fijó. La primera entrada, quote_focus_pro_3h, es la misma pregunta —"¿Cuánto cuesta Focus pro 3h?"— que ya viste en la lección 02, con la misma ancla de siempre: get_quote(Focus, pro, 3h) = 6000 centavos.
Modelando cada versión como un overrides: la pieza que el Módulo 5 dejó lista
run_regression_gate(case_set, overrides=None) acepta un diccionario opcional {nombre_del_caso: guion_sustituido}: para cualquier caso que no aparezca ahí, corre el model_script original del CASE_SET; para el que sí aparece, corre el guion sustituido, comparado contra el mismo expected_tools de siempre. La lección 05 del Módulo 5 documentó esta técnica, explícitamente, como "la pieza que el Módulo 7 va a reusar para comparar una versión vieja del agente contra una nueva" — este es, con precisión, ese momento.
Cada versión del registro se modela como un overrides: v1 es el CASE_SET sin ningún cambio ({}); v2 sustituye el guion de quote_focus_pro_3h por el que el system prompt "proactivo" produciría — el mismo guion regresivo que la lección 02 de este módulo ya adelantó, y que el Módulo 5 usó como su ejemplo canónico de FAIL:
# El guion que v2 produciría para "¿Cuánto cuesta Focus pro 3h?": salta la
# cotización y reserva directo -- el mismo guion regresivo del Módulo 5.
V2_REGRESSED_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."}]},
]
VERSION_OVERRIDES = {
"v1": {},
"v2": {"quote_focus_pro_3h": V2_REGRESSED_SCRIPT},
}
VERSION_OVERRIDES es el puente entre el registro de la lección 03 (qué versión existe, con qué hash) y el gate del Módulo 5 (qué comportamiento verificar): para cada version_id, declara qué casos del CASE_SET corren distinto bajo esa versión. v1, la versión original, no sustituye nada — corre el CASE_SET tal como el Módulo 5 lo dejó. v2 sustituye únicamente quote_focus_pro_3h — las otras cuatro preguntas nunca se tocan, porque la línea de proactividad que se agregó al prompt no afecta a ninguna de ellas.
compare_versions: correr el mismo gate contra dos configuraciones
Con VERSION_OVERRIDES en su lugar, la comparación completa cabe en una sola función: corre run_regression_gate dos veces —una por versión— y devuelve los dos GateReport, listos para que la lección 06 los compare con rollout_decision.
from regression.harness import run_regression_gate
def compare_versions(case_set, overrides_old, overrides_new):
"""Corre el MISMO gate del Módulo 5 contra dos configuraciones de
overrides -- 'la version vieja' y 'la version nueva' -- y devuelve
ambos GateReport, sin decidir nada todavía (eso es rollout_decision,
lección 06)."""
gate_old = run_regression_gate(case_set, overrides=overrides_old)
gate_new = run_regression_gate(case_set, overrides=overrides_new)
return gate_old, gate_new
compare_versions no agrega ningún chequeo nuevo — es, deliberadamente, una envoltura delgada sobre run_regression_gate, la misma disciplina de "envolver, no reconstruir" que ya viste en el Módulo 1 con run_and_observe. Su único trabajo es dejar explícito, con un nombre, el patrón que el resto de este módulo repite: dos corridas del mismo gate, una por versión, nunca una corrida mezclada.
Ejecutando la comparación: v1 contra v2
report_v1, report_v2 = compare_versions(CASE_SET, VERSION_OVERRIDES["v1"], VERSION_OVERRIDES["v2"])
print("GATE v1:", "PASS" if report_v1.passed else "FAIL",
f"({sum(c.passed for c in report_v1.cases)}/{len(report_v1.cases)})")
for c in report_v1.cases:
print(f" {c.name:38} {'PASS' if c.passed else 'FAIL'}")
Qué esperar:
GATE v1: 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
Ahora el reporte de v2, calculado en la misma llamada a compare_versions:
print("GATE v2:", "PASS" if report_v2.passed else "FAIL",
f"({sum(c.passed for c in report_v2.cases)}/{len(report_v2.cases)})")
for c in report_v2.cases:
status = "PASS" if c.passed else "FAIL"
line = f" {c.name:38} {status}"
if not c.passed:
line += f" -- tool_choice_ok={c.tool_choice_ok} actual_tools={c.actual_tools}"
print(line)
Qué esperar:
GATE v2: FAIL (4/5)
quote_focus_pro_3h FAIL -- tool_choice_ok=False actual_tools=['book_room']
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
Ahí está la evidencia completa: v1 resuelve los cinco casos exactamente como se esperaba; v2 resuelve cuatro, y falla en quote_focus_pro_3h — con el mismo mensaje que el DISEÑO de esta guía exige citar: tool esperada get_quote, obtenida book_room. v2 no solo eligió la tool equivocada: la ejecutó de verdad, y book_focus_pro_3h_ana —el caso que sí debía reservar— sigue pasando sin ningún efecto colateral, porque run_case reinicia el estado de Reservo antes de correr cada caso.
Confirmando que overrides no contamina al resto del CASE_SET
Vale la pena confirmar, con código, algo que la salida de arriba ya sugiere: book_focus_pro_3h_ana también llama a book_room —igual que la versión regresiva de quote_focus_pro_3h—, y aun así sigue en PASS bajo v2. Esto no es casualidad:
ana_case = next(c for c in report_v2.cases if c.name == "book_focus_pro_3h_ana")
print("book_focus_pro_3h_ana bajo v2 -- passed:", ana_case.passed)
print("actual_tools:", ana_case.actual_tools)
Qué esperar:
book_focus_pro_3h_ana bajo v2 -- passed: True
actual_tools: ['list_rooms', 'get_quote', 'book_room']
run_case llama a reset_reservo_state() al principio de cada caso —antes de quote_focus_pro_3h, y de nuevo antes de book_focus_pro_3h_ana—, así que la reserva que la regresión de quote_focus_pro_3h crea por error nunca se filtra al booking_id que el caso de Ana espera encontrar. Sin ese aislamiento —la disciplina que el Módulo 5, lección 03, estableció con precisión—, un FAIL real podría esconderse detrás de un booking_id contaminado, o un caso sano podría reportar un FAIL que no tiene nada que ver con ninguna regresión genuina.
Errores comunes
-
Correr el gate contra
v2sin haberlo corrido primero contrav1. Sin la línea base dev1(PASS 5/5), un resultado deFAIL 4/5env2no dice nada por sí solo — podría ser que elCASE_SETtenga un problema, no la versión nueva. Comparar siempre contra una línea base conocida es lo que le da sentido al4/5. -
Pensar que
overrides"reescribe" elCASE_SET. No lo hace —golden_cases.jsonen disco nunca cambia.overrideses un diccionario que vive solo en memoria, durante una corrida específica derun_regression_gate; la siguiente corrida sinoverridesvuelve a usar elmodel_scriptoriginal de cada caso, sin ningún rastro de la sustitución anterior. -
Construir
VERSION_OVERRIDES["v2"]con elmodel_scriptcompleto del caso, en vez de solo el guion sustituido.overridesespera{nombre_del_caso: guion_completo_alternativo}— un guion de turnos completo, con su propiostop_reasonycontent, no un fragmento ni un diccionario de diferencias. El guion sustituido reemplaza al original entero para ese caso. -
Suponer que la regresión de
v2afecta a los cinco casos por igual. Eloverridesde esta lección toca únicamentequote_focus_pro_3h— las otras cuatro preguntas delCASE_SET(quote_focus_basic_3h, las dos reservas completas, la cancelación) nunca aparecen enVERSION_OVERRIDES["v2"], así que corren con su guion original y pasan sin ningún cambio. Una regresión de prompt no tiene por qué romper todo lo que el agente hace — a menudo rompe una franja específica de comportamiento, y el gate, corrido sobre elCASE_SETcompleto, es lo que revela exactamente cuál. -
Interpretar
FAIL (4/5)como "el 80% del agente funciona bien". No es una medida de porcentaje de calidad — es un conteo exacto de casos que pasaron una comparación literal. Un solo caso roto puede ser, como en este ejemplo, la diferencia entre un agente seguro y uno que reserva sin permiso; el número no captura la severidad, solo la cantidad.
Ejercicios
Ejercicio 1: Confirma que book_focus_pro_3h_ana no se contamina, corriéndolo aislado (Fácil)
Usando run_case directamente (no el gate completo), corre book_focus_pro_3h_ana sin ningún overrides, dos veces seguidas en el mismo proceso. Confirma que ambas corridas producen exactamente el mismo CaseResult.
Ver solución
from regression.harness import run_case
case_ana = CASE_SET[2] # book_focus_pro_3h_ana
r1 = run_case(case_ana, 1)
r2 = run_case(case_ana, 2)
print("primera corrida :", r1.passed, r1.actual_tools)
print("segunda corrida :", r2.passed, r2.actual_tools)
print("resultados identicos:", r1.passed == r2.passed and r1.actual_tools == r2.actual_tools)
Salida esperada:
primera corrida : True ['list_rooms', 'get_quote', 'book_room']
segunda corrida : True ['list_rooms', 'get_quote', 'book_room']
resultados identicos: True
Explicación: run_case llama a reset_reservo_state() en su primera línea, antes de correr nada — así que no importa cuántas veces se corra el mismo caso, ni qué corrió antes en el mismo proceso, el resultado es siempre el mismo. Esta es la propiedad exacta que hace que comparar dos GateReport —el trabajo de las lecciones 05 y 06— sea confiable: ninguna diferencia entre v1 y v2 puede venir de un efecto de orden, solo de una diferencia real de comportamiento.
Ejercicio 2: Simula una v4 que rompe DOS casos a la vez (Medio)
Construye VERSION_OVERRIDES["v4"] combinando el guion regresivo de quote_focus_pro_3h con un segundo guion que reserva book_and_cancel_studio_basic_1h_diego pero nunca cancela (el mismo patrón "se detiene antes de completar la secuencia" del Módulo 5). Corre el gate y confirma el conteo y los dos casos rotos.
Ver solución
no_cancel_script = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Studio", "tier": "basic", "hours": 1, "member": "Diego"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Studio basic 1h para Diego."}]},
]
VERSION_OVERRIDES["v4"] = {
"quote_focus_pro_3h": V2_REGRESSED_SCRIPT,
"book_and_cancel_studio_basic_1h_diego": no_cancel_script,
}
report_v4 = run_regression_gate(CASE_SET, overrides=VERSION_OVERRIDES["v4"])
print("GATE v4:", "PASS" if report_v4.passed else "FAIL",
f"({sum(c.passed for c in report_v4.cases)}/{len(report_v4.cases)})")
for c in report_v4.cases:
if not c.passed:
print(" roto:", c.name, c.actual_tools)
Salida esperada:
GATE v4: FAIL (3/5)
roto: quote_focus_pro_3h ['book_room']
roto: book_and_cancel_studio_basic_1h_diego ['book_room']
Explicación: overrides no tiene ningún límite de cuántos casos puede sustituir a la vez — cada entrada del diccionario se aplica de forma independiente, y run_regression_gate agrega el veredicto global con all(...), sin importar si uno o varios casos fallaron. Esto confirma que el mecanismo escala igual de bien a una regresión acotada (un solo caso, como v2) que a una más amplia (dos casos, como esta v4 hipotética).
Ejercicio 3: Provoca un FAIL sin cambiar la tool final — solo el camino para llegar a ella (Difícil)
Construye VERSION_OVERRIDES["v5"] que sustituya book_boardroom_pro_1h_sofia por un guion que llama a get_quote dos veces seguidas, con los mismos argumentos exactos, antes de reservar (el agente "se arrepiente" y vuelve a cotizar). Corre el gate y confirma que, aunque la reserva final sería idéntica, el caso falla igual.
Ver solución
double_quote_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": "Boardroom", "tier": "pro", "hours": 1}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 1}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_04", "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."}]},
]
VERSION_OVERRIDES["v5"] = {"book_boardroom_pro_1h_sofia": double_quote_script}
report_v5 = run_regression_gate(CASE_SET, overrides=VERSION_OVERRIDES["v5"])
print("GATE v5:", "PASS" if report_v5.passed else "FAIL",
f"({sum(c.passed for c in report_v5.cases)}/{len(report_v5.cases)})")
broken = next(c for c in report_v5.cases if not c.passed)
print("roto:", broken.name, "obtenido:", broken.actual_tools, "tool_choice_ok:", broken.tool_choice_ok)
Salida esperada:
GATE v5: FAIL (4/5)
roto: book_boardroom_pro_1h_sofia obtenido: ['list_rooms', 'get_quote', 'get_quote', 'book_room'] tool_choice_ok: False
Explicación: check_tool_choice compara secuencias completas con ==, elemento por elemento — una lista de cuatro tools nunca es igual a una de tres, aunque las tres últimas coincidan con las esperadas y el resultado final de la reserva termine siendo idéntico. Esto confirma que el gate no solo detecta "la tool equivocada" (como v2), sino también un camino distinto para llegar al mismo resultado — una tool call redundante consume tokens y latencia reales (Módulos 3 y 4), aunque nunca cambie el desenlace de la conversación.
Resumen y siguiente paso
- Retomamos el
CASE_SETdel Módulo 5, sin cambios: cinco casos fijos, cubriendo las cuatro tools de Reservo. - Modelamos cada versión del registro como un
overridessobre eseCASE_SET:v1no sustituye nada;v2sustituye únicamentequote_focus_pro_3hpor el guion que la instrucción de proactividad produciría. - Construimos
compare_versions, la envoltura delgada que correrun_regression_gatedos veces —una por versión— y devuelve ambosGateReportjuntos. - Corrimos la comparación:
v1daPASS (5/5)— la línea base.v2, sin cambiar ni un chequeo del gate:FAIL (4/5), con el mensaje exacto delDISEÑO: tool esperadaget_quote, obtenidabook_room. - Confirmamos, ejecutando, que el aislamiento por caso (
reset_reservo_state()dentro derun_case) evita que la regresión de un caso contamine a otro que también usabook_room.
Siguiente lección: 05 — El gate como chequeo de rollout. Abrimos el CaseResult completo de quote_focus_pro_3h bajo v2 y confirmamos, con evidencia, cuáles de sus chequeos realmente detectaron la regresión — y por qué uno de ellos falla como un efecto colateral del otro.
Recursos adicionales
- Anthropic — Tool use (function calling) overview — El contrato
tool_use/tool_resultquecheck_tool_choicecompara, en su forma más literal. - Python — comparación de listas con
==— La base decheck_tool_choice: elemento por elemento, en orden, sensible a la longitud. - Python —
dict.getcon valor por defecto — La base deoverrides.get(case["name"])dentro derun_regression_gate, que devuelveNone(y por lo tanto "usa el guion original") para cualquier caso no sustituido. - Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.