Módulo 7: Versionado y rollout seguro
El gate como chequeo de rollout
Descripción
La lección anterior mostró el resultado: v2 falla FAIL (4/5), exactamente en quote_focus_pro_3h. Esta lección abre ese CaseResult con lupa. run_case (Módulo 5) no aplica un solo chequeo — aplica cinco, agrupados en tres preguntas: ¿la tool elegida coincide? (tool_choice_ok), ¿el resultado tiene la forma correcta? (schema_errors) y ¿coincide con el ancla fija del caso? (output_errors), ¿el costo y la latencia se mantuvieron bajo presupuesto? (cost_ok, latency_ok). Un caso pasa solo si los cinco son favorables. La pregunta de esta lección es precisa: de esos cinco, ¿cuáles fueron los que realmente atraparon la regresión de v2 — y cuáles ni siquiera llegaron a evaluarse?
Conexión con el módulo
Esta lección conecta el resultado ejecutado de la lección 04 con la frontera que el Módulo 5 traza con precisión desde su lección 04: un gate de forma nunca reemplaza a un juicio semántico sobre si la respuesta del agente es "buena". Aquí se ve, con evidencia numérica, exactamente qué tan lejos llega ese gate de forma — y un efecto colateral real que solo aparece al mirar el CaseResult completo, no solo el veredicto final.
El CaseResult completo de quote_focus_pro_3h bajo v2
from regression.harness import run_case
case = next(c for c in CASE_SET if c["name"] == "quote_focus_pro_3h")
result = run_case(case, 1, model_script=VERSION_OVERRIDES["v2"]["quote_focus_pro_3h"])
print("name :", result.name)
print("passed :", result.passed)
print()
print("tool_choice_ok:", result.tool_choice_ok, " actual_tools:", result.actual_tools)
print("schema_errors :", result.schema_errors)
print("output_errors :", result.output_errors)
print("cost_ok :", result.cost_ok, " cost_cents:", result.cost_cents)
print("latency_ok :", result.latency_ok, " latency_ms:", result.latency_ms)
Qué esperar:
name : quote_focus_pro_3h
passed : False
tool_choice_ok: False actual_tools: ['book_room']
schema_errors : []
output_errors : []
cost_ok : True cost_cents: 0
latency_ok : False latency_ms: 120
Dos hallazgos, ninguno obvio a primera vista. Primero: schema_errors y output_errors están vacíos — pero eso no significa que esos dos chequeos "pasaron". run_case los evalúa dentro de un if tool_choice_ok: (Módulo 5, lección 07): como la tool elegida ya está mal, no hay ningún resultado de get_quote que buscar dentro de history para validar contra su schema o su ancla — esos dos chequeos ni siquiera llegan a correr. Una lista vacía, en este contexto, significa "no se evaluó", no "pasó".
Segundo, y más sutil: latency_ok es False. quote_focus_pro_3h tiene un umbral de 100 ms —generoso para una cotización simple, una sola llamada a get_quote (25 ms modelados)—. Pero v2 no llamó a get_quote: llamó a book_room, que modela 120 ms — más lenta, y por encima del umbral que este caso específico declara. La regresión de elección de tool trajo consigo una segunda ruptura, de umbral, como efecto colateral: nadie diseñó el CASE_SET pensando en que este caso pudiera llamar a una tool distinta y más costosa, porque nunca debería hacerlo.
cost_ok sí pasa — y eso también es información
Vale la pena notar lo que no falló: cost_ok es True. El texto final que v2 produce ("Reservé Focus pro 3h para Ana.") no es más largo que el que v1 habría producido, así que el costo estimado se mantiene igual de bajo (0 centavos, la misma escala honesta de siempre). Esto confirma algo importante sobre el diseño del gate: no todos los chequeos fallan juntos automáticamente cuando algo se rompe. Cada uno mide una dimensión distinta, y una regresión concreta puede afectar a algunas sin afectar a las demás. Si el gate solo tuviera cost_ok y latency_ok, y no tool_choice_ok, este caso habría fallado igual —por latencia—, pero el mensaje habría sido mucho menos útil: "latencia por encima del umbral" no le dice a nadie que el problema real es que el agente reservó sin que se lo pidieran.
Por qué tool_choice_ok es el chequeo que hace legible el problema
tool_choice_ok es el único de los cinco que compara contra qué se esperaba de este caso específico, no contra una propiedad genérica del resultado. Los otros —schema, ancla, latencia— evalúan si lo que book_room devolvió es válido para book_room; tool_choice_ok es el único que pregunta si book_room era, para empezar, la tool que debía correr. Por eso el mensaje de FAIL de la lección 04 —tool esperada get_quote, obtenida book_room— es el que un equipo real necesita leer primero: nombra la causa raíz, no un síntoma derivado como el umbral de latencia cruzado.
Lo que este gate NO puede responder
Vale la pena ser explícito sobre el límite, porque es el límite exacto que traza la frontera con la guía hermana. Imagina una versión hipotética cuyo prompt sí llama a get_quote correctamente en quote_focus_pro_3h —pasa los cinco chequeos de este gate sin problema— pero cuya respuesta final en texto, la que el agente le muestra al usuario después del tool_result, dice algo confuso, agresivo, o simplemente mal redactado. El gate de esta guía no tiene ninguna forma de detectar eso. No lee el texto final del agente con ningún criterio de calidad; no le pide a ningún modelo que lo evalúe; no tiene ningún concepto de "tono" o "claridad".
# Un output que PASARIA los 5 chequeos de este gate, con una respuesta
# final pesima -- ninguno de los cinco inspecciona este string.
respuesta_final_hipotetica = "6000. eso es todo, no tengo mas para decir."
print("El gate de esta guia nunca inspecciona:", repr(respuesta_final_hipotetica))
Qué esperar:
El gate de esta guia nunca inspecciona: '6000. eso es todo, no tengo mas para decir.'
Esa es, con precisión, la frontera que el Módulo 5 traza en su lección 04, y que este módulo hereda sin modificarla: evaluar si una respuesta es semánticamente buena —clara, útil, con el tono correcto, fiel a la intención del usuario más allá de la acción técnica correcta— es el trabajo de evaluation-frameworks-guide, con su trajectory evaluation y su tool-call accuracy juzgada por un modelo. Ese otro tipo de evaluación necesita un LLM-as-judge o un dataset dorado con verdad "más o menos correcta" — exactamente lo que este gate se prohíbe usar, a propósito, porque un chequeo de forma determinista tiene una propiedad que un juez semántico nunca tiene: el mismo input produce, siempre, el mismo resultado, sin variar de una corrida a la siguiente. Cuando el alumno necesite responder "¿la respuesta del agente es buena, no solo tomó la acción correcta?", la guía a consultar es esa, nombrada aquí con precisión.
Errores comunes
-
Leer
schema_errors == []como "el schema pasó". Como muestra el ejemplo de esta lección, una lista vacía puede significar "no se llegó a evaluar" —porquetool_choice_okya eraFalse— en vez de "se evaluó y no encontró ningún problema". Revisar siempretool_choice_okprimero antes de interpretar cualquiera de los otros cuatro campos. -
Pensar que un gate que pasó
cost_okya es evidencia de que "no pasó nada grave". El ejemplo de esta lección lo desmiente con números:cost_ok=Truey, aun así, el caso completo debe fallar. Un gate de varios chequeos independientes solo es confiable si se exige que todos pasen, no una mayoría, y nunca se debe leer un solo campo en verde como "la señal principal". -
Confundir "el gate es de forma" con "el gate es débil". El gate de esta guía detectó, con total precisión, una regresión de comportamiento que ningún log de errores, ningún chequeo de latencia aislado, y ninguna inspección superficial ("¿el agente respondió algo coherente?") habría atrapado con el mismo nivel de detalle. Ser "de forma" no significa ser poco riguroso — significa que su rigor está acotado a comparaciones literales, no a juicios de calidad.
-
No darse cuenta de que
latency_ok=Falseaquí es un EFECTO, no una causa independiente. Si alguien solo miraralatency_oksin revisartool_choice_ok, podría concluir, equivocadamente, que el problema es "Reservo se puso lento" — cuando el problema real es que se llamó a una tool distinta, más lenta por diseño. Diagnosticar a partir del campo equivocado lleva a arreglar el síntoma (subir el umbral de latencia) en vez de la causa (corregir el prompt). -
Pensar que este gate puede reemplazar una revisión de la respuesta final del agente. Como muestra la sección anterior, el gate nunca inspecciona el texto que el agente le muestra al usuario. Un sistema real, en producción, necesita ambos: este gate para regresiones de comportamiento estructural, y algo como
evaluation-frameworks-guidepara calidad semántica de la respuesta.
Ejercicios
Ejercicio 1: Repite el desglose sobre un caso que SÍ pasa (Fácil)
Ejecuta run_case sobre book_focus_pro_3h_ana bajo v2 (sin ningún overrides para ese caso) y confirma que los cinco campos relevantes (tool_choice_ok, schema_errors, output_errors, cost_ok, latency_ok) indican, todos, que el caso pasó limpio.
Ver solución
case_ana = next(c for c in CASE_SET if c["name"] == "book_focus_pro_3h_ana")
result_ana = run_case(case_ana, 1)
print("tool_choice_ok:", result_ana.tool_choice_ok)
print("schema_errors :", result_ana.schema_errors)
print("output_errors :", result_ana.output_errors)
print("cost_ok :", result_ana.cost_ok)
print("latency_ok :", result_ana.latency_ok)
Salida esperada:
tool_choice_ok: True
schema_errors : []
output_errors : []
cost_ok : True
latency_ok : True
Explicación: aquí las listas vacías de schema_errors/output_errors sí significan "se evaluó, sin errores" —porque tool_choice_ok es True, run_case sí llegó a buscar el resultado de book_room y validarlo contra su schema y su ancla (booking_id: 1, confirmed: True)—. La diferencia con el ejemplo trabajado de esta lección es exactamente la que advirtió el error común 1: el significado de una lista vacía depende de si el chequeo anterior pasó.
Ejercicio 2: Diseña un umbral de latencia que ignore el efecto colateral (Medio)
Sin tocar golden_cases.json, construye una copia de quote_focus_pro_3h con latency_threshold_ms subido a 150 (en vez de 100) — un umbral que sí toleraría la latencia de book_room (120 ms). Corre run_case con el guion regresivo de v2 sobre esa copia, y confirma que latency_ok ahora es True, pero passed sigue siendo False.
Ver solución
case_lenient = {**case, "latency_threshold_ms": 150}
result_lenient = run_case(case_lenient, 1, model_script=VERSION_OVERRIDES["v2"]["quote_focus_pro_3h"])
print("latency_ok:", result_lenient.latency_ok, " latency_ms:", result_lenient.latency_ms)
print("tool_choice_ok:", result_lenient.tool_choice_ok)
print("passed:", result_lenient.passed)
Salida esperada:
latency_ok: True latency_ms: 120
tool_choice_ok: False
passed: False
Explicación: subir el umbral de latencia "arregla" el efecto colateral —120 <= 150 ahora es cierto—, pero no toca, ni puede tocar, la causa raíz: tool_choice_ok sigue siendo False, y passed es la conjunción de los cinco chequeos, así que el caso sigue fallando. Este ejercicio confirma, con código, la advertencia del error común 4: ajustar el umbral de latencia sería arreglar el síntoma, nunca la causa — el chequeo de elección de tool es el que de verdad protege este caso.
Ejercicio 3: Argumenta si un sexto chequeo ("¿el texto final menciona el precio correcto?") seguiría siendo "de forma" (Difícil)
Alguien en el equipo propone agregar un sexto chequeo al gate: check_final_text_contains_price(text, expected_price_cents), que confirma —con una búsqueda de substring, no con un modelo— que el texto final del agente contiene el número de precio esperado en dólares (por ejemplo, "$60.00" para price_cents=6000). Argumenta, en un párrafo, si este chequeo respeta la regla dura del gate ("de forma, determinista, sin juez") o si cruza la frontera hacia el territorio semántico de evaluation-frameworks-guide.
Ver solución
Este chequeo sí respeta la regla del gate, y vale la pena entender exactamente por qué, porque la línea es sutil. check_final_text_contains_price no le pide a ningún modelo que evalúe si el texto es claro, útil, o está bien redactado — hace una comparación de forma: ¿el string "$60.00" aparece, literalmente, dentro de otro string? Es determinista (el mismo texto de entrada siempre da el mismo resultado), no requiere ninguna llamada a un LLM, y compara contra un valor fijo derivado del mismo expected_output que check_expected_output ya usa (price_cents=6000 convertido a "$60.00"). La diferencia con "evaluar si la respuesta es buena" es la diferencia entre verificar un hecho puntual y verificable (el número correcto está presente) y juzgar una cualidad (el tono es apropiado, la redacción es clara, la respuesta responde exactamente lo que el usuario quería saber, ni más ni menos). Un chequeo de substring sobre un número es, con precisión, la misma familia que check_tool_choice o check_expected_output: comparación literal contra un valor fijo. Cruzaría la frontera hacia evaluation-frameworks-guide recién si el chequeo intentara algo como "¿el texto explica el precio de forma clara?" — ahí ya no hay ningún valor fijo contra el cual comparar, y la única forma de responder esa pregunta es con un juicio, humano o de un modelo.
Resumen y siguiente paso
CaseResultdesglosa cinco chequeos independientes: elección de tool, forma del resultado, ancla de valor, presupuesto de costo, presupuesto de latencia. Un caso pasa solo si los cinco son favorables.- Sobre
quote_focus_pro_3hdev2,schema_errorsyoutput_errorsquedan vacíos porque nunca llegaron a evaluarse —tool_choice_okya eraFalse—, no porque "pasaron".cost_oksí pasa de verdad. Ylatency_okfalla como un efecto colateral real de la tool equivocada:book_room(120ms) supera el umbral de100ms que este caso, pensado para una cotización simple, nunca contempló. tool_choice_okes el chequeo que hace legible la causa raíz — el mensaje tool esperadaget_quote, obtenidabook_roomdice, sin ambigüedad, qué se rompió, mientras que mirar sololatency_okhabría apuntado al síntoma equivocado.- El gate tiene un límite explícito: nunca inspecciona el texto final que el agente le muestra al usuario. Evaluar si esa respuesta es semánticamente buena es el trabajo de
evaluation-frameworks-guide, nombrada aquí con precisión como la frontera de esta guía.
Siguiente lección: 06 — GO o NO-GO. Construimos rollout_decision: la regla explícita que convierte el FAIL (4/5) de v2 en una decisión de una sola palabra — y por qué esa regla nunca permite que una versión nueva rompa un caso que la vieja ya resolvía bien.
Recursos adicionales
- Anthropic — Building effective agents — Sobre la diferencia entre verificar que un agente ejecutó la acción correcta y evaluar la calidad de su respuesta final.
- Anthropic — Tool use (function calling) overview — La forma exacta del contrato de una tool que
check_schemavalida contraOUTPUT_SCHEMAS. - Python — comparación de igualdad entre diccionarios — La base de
check_expected_output, usada dentro derun_casesolo cuandotool_choice_okya esTrue. - Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.