Módulo 3: El eval como fitness function
6. El eval en CI: una compuerta sobre el deploy
Descripción
Al terminar esta lección vas a ver la compuerta de calidad dar su último paso: dejar de depender de que alguien se acuerde de correrla y volverse un paso automático del pipeline que bloquea el deploy cuando el score cae. En las lecciones 4 y 5 corriste el eval a mano y decidiste a mano. Eso funciona hasta el primer viernes con prisa, cuando alguien despliega sin correr el eval y una regresión llega a producción. La solución es la misma que el software clásico encontró hace décadas para los tests: ponerlos en integración continua (CI) para que corran solos en cada cambio, y hacer que un fallo detenga la entrega. Vas a ejecutar un paso de CI simulado que corre el eval, produce un exit code —0 si pasa, 1 si falla— y deja continuar o bloquea el deploy según ese código. Dos pull requests pasan por el pipeline: uno mejora el prompt (exit 0 → MERGE) y otro baja de modelo y regresa (exit 1 → BLOCK). El eval deja de ser una buena práctica opcional y se vuelve una barrera que el pipeline hace cumplir, igual que un test rojo bloquea un merge.
Esto importa porque una compuerta que depende de la disciplina humana no es una compuerta —es una recomendación—. Puedes tener el mejor eval-set del mundo, pero si correrlo es un paso manual que alguien puede saltarse, se saltará el día que más importa: bajo presión, con una fecha encima, "solo esta vez". La única forma de que una regla de calidad sea confiable es que el sistema la aplique sin pedir permiso ni depender de la memoria de nadie. Poner el eval en CI hace exactamente eso: convierte "deberíamos correr el eval antes de desplegar" en "no se puede desplegar sin que el eval pase". Es el mismo salto que hizo a los tests unitarios confiables —de "corre los tests antes de subir" a "el CI no mergea si los tests fallan"—, aplicado a la calidad probabilística. Y es lo que cierra el arco del módulo: el eval empezó como un score (lección 3), se volvió compuerta (lección 4), aprendió a atrapar regresiones (lección 5), y aquí se instala como guardián permanente del deploy.
Conexión con el módulo: esta lección automatiza lo que las anteriores construyeron a mano. La compuerta de la lección 4 y la comparación contra baseline de la lección 5 son exactamente lo que corre dentro del paso de CI —no hay mecanismo nuevo, hay posición nueva: la compuerta pasa de "algo que corres" a "algo que el pipeline corre por ti, siempre"—. Aquí se marca también una frontera: la mecánica de armar un pipeline de CI/CD serio (runners, etapas, ambientes, rollback) es de las guías de entrega; en esta lección el pipeline es el mínimo para mostrar el eval como el paso que gobierna el deploy. La lección 7, la última de tema, abre la caja del criterio de éxito. En una frase: aquí la compuerta se vuelve inevitable.
La analogía: el torniquete del metro
Piensa en el torniquete de entrada del metro. Para pasar al andén, tienes que apoyar tu tarjeta; si tiene saldo válido, la barrera se abre; si no, se queda cerrada y no pasas. Fíjate en tres cosas. Primera: no hay un humano decidiendo caso por caso quién pasa —el torniquete aplica la regla solo, con la misma vara para todos, sin cansarse ni hacer excepciones por prisa—. Segunda: está en el camino, no a un lado —para llegar al andén tienes que pasar por él, no puedes rodearlo—. Tercera: su veredicto es binario y ejecuta una acción física —abre o no abre—, no es una sugerencia que puedas ignorar. Un torniquete no te dice "sería bueno que tuvieras saldo"; simplemente no te deja pasar sin él.
El eval en CI es el torniquete de la calidad en el camino al deploy. El pipeline es el pasillo al andén: todo cambio tiene que recorrerlo para llegar a producción. El paso de eval es el torniquete: corre el eval-set, y si el score pasa el umbral, la barrera se abre (el deploy continúa); si no, se queda cerrada (el deploy se bloquea). Como el torniquete, no hay humano decidiendo en el momento —la regla está escrita en el umbral y el pipeline la aplica sola—; está en el camino —no puedes desplegar rodeando el eval—; y su veredicto ejecuta una acción —el deploy procede o se detiene—, no es un aviso opcional. Los desarrolladores ya conocen este torniquete en otra forma: el test unitario que se pone rojo y no deja mergear el pull request. El eval en CI es ese mismo torniquete, con una vara probabilística —un score contra un umbral en vez de un assert exacto— pero el mismo rol: barrera automática en el camino, que no se puede saltar.
Ejemplo trabajado: dos PRs contra el torniquete
Vamos a simular el paso de eval dentro de un pipeline de CI. El paso corre el eval-set, imprime lo que un log de CI imprimiría, y devuelve un exit code —la convención universal de CI: 0 significa éxito (el pipeline continúa) y cualquier cosa distinta de 0 significa fallo (el pipeline se detiene)—. Pasamos dos pull requests. El PR #841 mejora el prompt del agente (score 0.90). El PR #842 lo baja a un modelo más barato para ahorrar y, sin querer, regresa (score 0.60). El umbral es 0.80.
Primero, dónde vive el torniquete en el pipeline:
commit --> build --> unit tests --> [ EVAL GATE ] --> deploy
|
score < umbral?
/ \
no (0) si (1)
| |
continua BLOQUEADO
El eval gate es un paso más del pipeline, entre las pruebas y el deploy. Si el score pasa el umbral, el pipeline sigue hacia el deploy; si no, se detiene ahí, y el deploy nunca ocurre.
# Leccion 06 — el eval en CI: una compuerta sobre el deploy
# Todo SIMULADO. Cero red, cero API, cero claves. Salida determinista.
# (Reusa EVAL_SET, GOLD, POOR_ANSWER, make_agent, run_eval de las lecciones 3-5.)
EVAL_SET = [
{"id": "q1", "question": "donde esta mi pedido", "must_contain": "tracking"},
{"id": "q2", "question": "como devuelvo un producto", "must_contain": "devolucion"},
{"id": "q3", "question": "cuanto tarda el envio", "must_contain": "3 a 5 dias"},
{"id": "q4", "question": "puedo pagar en cuotas", "must_contain": "cuotas"},
{"id": "q5", "question": "el producto llego roto", "must_contain": "reembolso"},
{"id": "q6", "question": "como cambio mi direccion", "must_contain": "perfil"},
{"id": "q7", "question": "no recibi mi factura", "must_contain": "correo"},
{"id": "q8", "question": "quiero cancelar mi pedido", "must_contain": "cancelar"},
{"id": "q9", "question": "el cupon no funciona", "must_contain": "vigencia"},
{"id": "q10", "question": "como contacto a un vendedor", "must_contain": "mensajes"},
]
GOLD = {
"q1": "Puedes ver el tracking de tu pedido en tu perfil.",
"q2": "Para una devolucion, entra a tu pedido y pulsa Devolver.",
"q3": "El envio estandar tarda de 3 a 5 dias habiles.",
"q4": "Si, puedes pagar en cuotas sin interes con tarjeta.",
"q5": "Lamentamos eso; puedes pedir un reembolso desde el pedido.",
"q6": "Cambia tu direccion en la seccion Perfil, Direcciones.",
"q7": "Te reenviamos la factura al correo de tu cuenta.",
"q8": "Puedes cancelar el pedido si aun no fue enviado.",
"q9": "Revisa la vigencia del cupon; quiza ya expiro.",
"q10": "Escribe al vendedor desde la seccion Mensajes.",
}
POOR_ANSWER = "Lo siento, no tengo informacion sobre eso."
def make_agent(competent_ids):
def agent(question, case_id):
return GOLD[case_id] if case_id in competent_ids else POOR_ANSWER
return agent
def run_eval(agent):
passed = sum(
case["must_contain"] in agent(case["question"], case["id"]).lower()
for case in EVAL_SET
)
return passed / len(EVAL_SET)
# --- EL PASO DE EVAL EN CI: corre el eval y devuelve un exit code (0 ok, 1 falla) ---
def ci_eval_step(agent, threshold):
total = len(EVAL_SET)
score = run_eval(agent)
passed = score >= threshold
print(f" [ci] corriendo eval-set ({total} casos)...")
print(f" [ci] score = {score:.2f} umbral = {threshold:.2f}")
if passed:
print(f" [ci] EVAL PASSED -> deploy continua")
return 0 # exit 0: el pipeline sigue
else:
print(f" [ci] EVAL FAILED -> deploy BLOQUEADO (como un test rojo)")
return 1 # exit 1: el pipeline se detiene
THRESHOLD = 0.80
ALL_IDS = {c["id"] for c in EVAL_SET}
print("PR #841: 'mejorar el prompt del agente de soporte'")
code_a = ci_eval_step(make_agent(ALL_IDS - {"q9"}), THRESHOLD) # 0.90
print(f" exit code = {code_a}\n")
print("PR #842: 'bajar de modelo para ahorrar costo'")
code_b = ci_eval_step(make_agent({"q1","q2","q3","q4","q6","q8"}), THRESHOLD) # 0.60
print(f" exit code = {code_b}")
print(f"\nresumen CI: PR #841 {'MERGE' if code_a==0 else 'BLOCK'} | "
f"PR #842 {'MERGE' if code_b==0 else 'BLOCK'}")
Qué esperar. Al correrlo:
PR #841: 'mejorar el prompt del agente de soporte'
[ci] corriendo eval-set (10 casos)...
[ci] score = 0.90 umbral = 0.80
[ci] EVAL PASSED -> deploy continua
exit code = 0
PR #842: 'bajar de modelo para ahorrar costo'
[ci] corriendo eval-set (10 casos)...
[ci] score = 0.60 umbral = 0.80
[ci] EVAL FAILED -> deploy BLOQUEADO (como un test rojo)
exit code = 1
resumen CI: PR #841 MERGE | PR #842 BLOCK
Aquí está el torniquete en el camino al deploy. Léelo por PR.
El PR #841 pasa: exit code 0, MERGE. Alguien mejoró el prompt. El paso de eval corre solo dentro del pipeline —el desarrollador no lo invocó a mano, el CI lo disparó al abrir el PR—, mide un score de 0.90, ve que pasa el umbral, y devuelve exit code 0. En CI, exit 0 significa "este paso tuvo éxito", así que el pipeline continúa hacia el deploy: el PR se mergea. La barrera se abrió porque la tarjeta tenía saldo.
El PR #842 falla: exit code 1, BLOCK. Alguien bajó el agente a un modelo más barato para ahorrar. El paso de eval corre —otra vez, solo—, mide un score de 0.60, ve que no pasa el umbral, y devuelve exit code 1. En CI, cualquier exit distinto de 0 significa "este paso falló", y un paso fallido detiene el pipeline: el deploy se bloquea, el PR no se mergea. Y aquí está la fuerza de ponerlo en CI: nadie decidió bloquearlo. No hubo una reunión, ni un revisor que se diera cuenta, ni suerte de que alguien se acordara de correr el eval. El pipeline aplicó la regla solo, con la misma vara con la que dejó pasar al #841, y detuvo la regresión antes de que tocara a un solo cliente. El torniquete se quedó cerrado porque la tarjeta no tenía saldo, sin importar cuánta prisa tuviera quien intentó pasar.
El resumen lo dice todo: PR #841 MERGE | PR #842 BLOCK. Dos cambios, mismo pipeline, mismo umbral, veredictos opuestos, cero intervención humana en el momento de la decisión. Esto es lo que hace confiable a la compuerta: no depende de la disciplina de nadie. La regresión del PR #842 —exactamente la del cambio de modelo barato de la lección 5— habría llegado a producción en un proceso manual el día que alguien tuviera prisa; en CI, es imposible que llegue, porque el deploy no ocurre sin que el eval pase. La calidad de la IA dejó de ser una buena intención y se volvió una propiedad que el sistema garantiza.
Profundización: el exit code, el paralelo con los tests y la frontera
Por qué el exit code es la interfaz. Todo CI del mundo funciona con la misma convención primitiva: cada paso del pipeline es un comando que termina con un exit code, y 0 significa éxito mientras cualquier otro número significa fallo. Un paso que devuelve algo distinto de 0 detiene el pipeline. Esta convención es lo que hace que el eval encaje sin ceremonia en cualquier CI: no necesitas un plugin especial ni una integración exótica —envuelves el eval en un script que devuelve 0 si el score pasa el umbral y 1 si no, y el CI ya sabe qué hacer con eso—. Es exactamente cómo se integran los tests unitarios (el runner devuelve 0 si todos pasan, 1 si alguno falla). El eval no es un ciudadano de segunda en el pipeline: habla el mismo idioma que los tests, el exit code, y por eso el pipeline lo trata igual —un paso que puede detener el deploy—.
El paralelo exacto con el test rojo (y la única diferencia). Un desarrollador ya tiene el modelo mental correcto para esto, solo que aplicado a tests. Cuando escribes código y rompes un test unitario, el CI se pone rojo y no te deja mergear hasta que lo arregles. Nadie discute si "el test rojo es solo una sugerencia"; es una barrera dura. El eval en CI es idéntico en rol: se pone rojo (exit 1) cuando la calidad cae bajo el umbral y no deja desplegar. La única diferencia es la naturaleza de la prueba. Un test unitario es determinista y exacto: verifica una condición que es verdadera o falsa sin ambigüedad (assert suma(2,2) == 4). El eval es estadístico y probabilístico: verifica que un score agregado esté sobre un umbral (0.90 >= 0.80). Pero para el pipeline, los dos son lo mismo —un paso que devuelve 0 o 1—. Esta equivalencia es el corazón del módulo: el eval le da a un componente probabilístico el mismo tipo de red de seguridad automatizada que un test le da a código determinista.
Un matiz honesto: el eval es más lento y "ruidoso" que un test. Vale la pena nombrar dos diferencias prácticas con un test unitario, porque afectan cómo se pone el eval en CI. Primera, es más lento: correr un eval-set puede tomar segundos o minutos (en un sistema real, muchas llamadas al modelo), frente a los milisegundos de un test unitario —por eso a veces el eval corre en una etapa aparte, no en cada commit—. Segunda, con un modelo real puede ser ruidoso: como el componente es no determinista, el score puede variar un poco entre corridas del mismo código, así que el umbral debe tener algo de margen para no bloquear por ruido estadístico. (En nuestras simulaciones el stub es determinista, así que no hay ruido; en producción, esto se maneja con eval-sets suficientemente grandes y umbrales con holgura —y cómo hacerlo bien es diseño de evals, AI Engineering—.) Estas diferencias no cambian el rol del eval como gate; solo matizan cómo se opera en un pipeline real.
La frontera: aquí el eval como paso, no el pipeline a fondo. Habrás notado que el "pipeline" de esta lección es mínimo: un diagrama de cinco cajas y una función que devuelve un exit code. Es deliberado. Armar un pipeline de CI/CD de producción —elegir la herramienta, configurar runners, manejar ambientes de staging y producción, orquestar el rollback si algo sale mal, gestionar secretos, paralelizar etapas— es un tema entero, y es de las guías de entrega e infraestructura. Lo que esta lección enseña es dónde encaja la compuerta de calidad en ese pipeline —un paso entre las pruebas y el deploy, que puede detenerlo— y por qué su interfaz (el exit code) la hace encajar en cualquier CI. La mecánica del pipeline es de otra guía; el eval como el paso que gobierna el deploy es de esta.
Errores comunes
Dejar el eval como paso manual (de proceso). Qué pasa: el equipo tiene un buen eval-set y la costumbre de correrlo "antes de desplegar", pero es un paso manual. Funciona por un tiempo, hasta el día con prisa en que alguien despliega sin correrlo, una regresión llega a producción, y la investigación revela que "se nos olvidó correr el eval". Por qué pasa: un paso manual depende de la memoria y la disciplina, que fallan justo bajo presión —cuando más importa—. Cómo detectarlo: si correr el eval antes de un deploy depende de que alguien se acuerde, no es una compuerta, es una recomendación. Cómo corregirlo: mételo en CI como un paso que devuelve un exit code y que el pipeline obliga —igual que los tests unitarios—; que sea imposible desplegar sin que pase.
Poner el eval en CI pero como aviso, no como bloqueo (de configuración). Qué pasa: el equipo agrega el eval al pipeline, pero configurado para avisar sin detener el deploy —imprime un warning amarillo y continúa—. Con el tiempo, los warnings se vuelven ruido de fondo que nadie lee, y las regresiones pasan igual, ahora con un log que las anunció y que nadie miró. Por qué pasa: configurar el eval como bloqueante genera fricción (a veces frena deploys), y es tentador dejarlo como "informativo" para evitar quejas. Cómo detectarlo: si tu paso de eval nunca ha detenido un deploy porque está en modo aviso, es decorativo. Cómo corregirlo: haz que el exit code detenga el pipeline cuando el score cae bajo el umbral —un torniquete que avisa pero siempre abre no es un torniquete—.
Un umbral tan estricto (o un eval tan ruidoso) que bloquea todo (de calibración). Qué pasa: el equipo pone el umbral demasiado alto, o el eval-set es tan pequeño que el score varía mucho entre corridas, y el gate se pone rojo constantemente por cambios buenos o por puro ruido estadístico. Frustrados por los bloqueos injustos, terminan desactivando el gate —y se quedan sin protección—. Por qué pasa: un gate que da falsas alarmas es peor que molesto; erosiona la confianza hasta que alguien lo apaga. Cómo detectarlo: si tu eval gate bloquea deploys que en realidad estaban bien, o su veredicto cambia sin que el código cambie, está mal calibrado. Cómo corregirlo: calibra el umbral con algo de margen y usa un eval-set suficientemente grande para que el score sea estable (esto es diseño de evals, AI Engineering); un gate confiable bloquea las regresiones reales y deja pasar los cambios buenos, sin falsas alarmas que lo condenen a ser apagado.
Ejercicios
Ejercicio 1 — Lee el exit code. Un pipeline tiene el eval como paso antes del deploy, con umbral 0.85. Para cada corrida, di el exit code que devuelve el paso de eval y si el deploy procede o se bloquea. (a) score 0.91. (b) score 0.85. (c) score 0.79.
Ver solución
Con la regla passed = score >= 0.85, y exit 0 si pasa, 1 si falla:
- (a) 0.91: 0.91 ≥ 0.85 → passed → exit 0, el deploy procede. La barrera se abre.
- (b) 0.85: 0.85 ≥ 0.85 → passed (por la convención
>=) → exit 0, el deploy procede. Justo en el umbral, pasa. - (c) 0.79: 0.79 < 0.85 → no passed → exit 1, el deploy se bloquea. El pipeline se detiene en el paso de eval; el deploy nunca ocurre.
La moraleja: el exit code es la interfaz entre el eval y el pipeline. 0 abre el torniquete, cualquier otro número lo cierra. El pipeline no necesita entender de scores ni umbrales —solo mira el exit code y actúa—, que es exactamente por qué el eval encaja en cualquier CI sin ceremonia.
Ejercicio 2 — Aviso contra bloqueo. Dos equipos ponen el eval en CI. El equipo A lo configura para bloquear el deploy si el score cae bajo el umbral (exit 1 detiene el pipeline). El equipo B lo configura para avisar (imprime un warning pero el deploy continúa siempre). Ambos tienen el mismo eval-set y el mismo umbral. Un mes después, ¿cuál equipo tiene más probabilidad de haber desplegado una regresión, y por qué la configuración importa tanto como tener el eval?
Ver solución
El equipo B tiene mucha más probabilidad de haber desplegado una regresión, a pesar de tener exactamente el mismo eval-set y umbral que A. La diferencia no está en la calidad del eval, sino en lo que el pipeline hace con su resultado. En el equipo A, un score bajo el umbral detiene el deploy: la regresión es imposible de desplegar, el pipeline no lo permite. En el equipo B, un score bajo el umbral solo imprime un warning y el deploy continúa igual: la regresión se despliega, con un log amarillo que la anunció y que —como todos los warnings que no bloquean— nadie leyó.
Por qué la configuración importa tanto como tener el eval: un eval que mide pero no actúa es un termómetro, no una compuerta (lección 4). El equipo B tiene la medición pero no la barrera; es como tener un torniquete que registra si tienes saldo pero siempre te deja pasar —el registro es inútil si no cambia lo que ocurre—. La lección: poner el eval en CI no basta; tiene que estar configurado para bloquear, no solo avisar. Un warning que no detiene nada se vuelve ruido de fondo. La única versión útil del eval en CI es la que puede decir "no" y hacerlo cumplir.
Ejercicio 3 — ¿Del eval como gate o de la mecánica de CI/CD? Para cada tarea, di si es de este módulo (el eval como el paso que gobierna el deploy) o de la frontera (mecánica de CI/CD, otra guía), y por qué. (a) Envolver el eval en un script que devuelve exit 1 si el score cae bajo el umbral. (b) Configurar los runners y los ambientes de staging y producción del pipeline. (c) Colocar el paso de eval entre las pruebas y el deploy para que pueda detenerlo. (d) Diseñar la estrategia de rollback automático si el deploy falla en producción.
Ver solución
- (a) Envolver el eval en un script con exit code → este módulo. Es la interfaz entre el eval y el pipeline: cómo el score se convierte en una decisión de deploy (0 o 1). La esencia de esta lección.
- (b) Configurar runners y ambientes → frontera (CI/CD, otra guía). Es mecánica del pipeline: la infraestructura sobre la que corren los pasos. No es sobre el eval, es sobre el CI. Fuera de este módulo.
- (c) Colocar el eval entre pruebas y deploy para que lo detenga → este módulo. Es la posición arquitectónica de la compuerta: dónde vive el gate para gobernar el deploy. De esta lección.
- (d) Diseñar el rollback automático → frontera (CI/CD, otra guía). Es una técnica de entrega (qué hacer si un deploy sale mal) independiente del eval. Fuera de este módulo.
La regla que separa: si la tarea es sobre el eval como el paso que decide el deploy —su exit code, su posición en el flujo— (a, c), es de aquí; si es sobre la infraestructura del pipeline —runners, ambientes, rollback— (b, d), es de las guías de entrega. Este módulo pone la compuerta en el pipeline; no construye el pipeline.
Resumen y siguiente paso
En esta lección la compuerta de calidad dio su último paso: dejó de depender de la disciplina humana y se volvió un paso automático del pipeline que bloquea el deploy cuando el score cae. Con la analogía del torniquete del metro viste las tres propiedades que la hacen confiable: aplica la regla sola (sin un humano decidiendo en el momento), está en el camino (no se puede rodear), y su veredicto ejecuta una acción (abre o bloquea, no es un aviso). Ejecutaste un paso de eval en CI que devuelve un exit code —la interfaz universal del CI— y viste dos PRs recibir veredictos opuestos sin intervención humana: el #841 mejoró el prompt (exit 0 → MERGE) y el #842 bajó de modelo y regresó (exit 1 → BLOCK), la misma regresión de la lección 5, ahora imposible de desplegar. Entendiste el paralelo exacto con el test rojo que no deja mergear —misma barrera, vara probabilística en vez de exacta— y la frontera: aquí el eval como el paso que gobierna el deploy, no la mecánica del pipeline (otra guía).
Antes de avanzar deberías poder: explicar por qué una compuerta manual no es confiable y qué gana al estar en CI; describir cómo el exit code conecta el eval con cualquier pipeline; distinguir configurar el eval para bloquear de dejarlo como aviso; y separar el eval como gate de la mecánica de CI/CD.
Lo que sigue es abrir la caja que hasta ahora dimos por hecha: el criterio de éxito. En todo el módulo el criterio fue contains —¿la respuesta contiene la frase clave?—, el más simple. Pero hay más de un tipo, y la elección tiene consecuencias arquitectónicas. En la lección 7 vas a ejecutar cuatro criterios sobre los mismos casos —exact-match (demasiado estricto), contains (binario), LLM-as-judge (con crédito parcial) y el umbral estadístico— y vas a encontrarte con la advertencia más importante del módulo: el LLM-as-judge es otro componente de IA, con su propia latencia, costo y no-determinación, así que juzgar con él recursa todas las propiedades de esta guía. Es el paso de "sé usar el eval como gate" a "sé qué tipo de criterio alimenta ese gate y qué gobierna cada uno".
Recursos
- martinfowler.com — "Emerging Patterns in Building GenAI Applications", Bharani Subramaniam y Martin Fowler — el patrón de evals integrados en el pipeline de entrega como compuerta automática de calidad, en paralelo con los tests; el marco de arquitectura de esta lección.
- Anthropic — docs de Claude, evaluaciones en el flujo de desarrollo (conceptual) — la guía de correr evaluaciones de forma automatizada y repetible como parte del ciclo de desarrollo, no a mano; el respaldo de llevar el eval a CI, sin fijar versión.
- Chip Huyen — AI Engineering (O'Reilly), capítulos de evaluación y operación — el tratamiento de la evaluación como práctica continua y automatizada, con las diferencias prácticas (latencia, ruido) frente a los tests clásicos; la referencia para operar evals en un pipeline real.