Artefacto 5 de 6 · Mes 4–5 Desplegado · con puerta de regresión

Ningún cambio de prompt entra sin un número que lo respalde.

No es una app nueva: es el mismo agente del Artefacto 4, pero con disciplina. Prompts versionados, un eval-set de bugs reales emparejados con casos de control, un juez LLM para lo que ningún if mide, structured outputs validados — y, desde el anexo, una puerta que corre sola en cada push y un canario que vigila el modelo.

85llamadas por ejecución
8casos · bug + control
0,875global · 5 veces igual
~8 ccoste por puerta

La idea

De «a ojo» a disciplina medida

El Artefacto 4 funcionaba, pero sus prompts se afinaron a ojo: escribir, probar una vez, y si colaba, a producción. Durante su construcción cacé tres bugs de prompt reales y los parcheé sobre la marcha metiendo frases. Funcionaban… pero nunca medí si funcionaban siempre. Este artefacto convierte ese «a ojo» en un proceso: esos tres bugs se vuelven el eval-set perfecto, precisamente porque son reales.

Definición de «hecho» Puedes señalar un cambio de prompt y enseñar el número de eval que lo justifica. «Parece mejor» no cuenta.
El instrumento

Una báscula que mide las dos caras

El eval-set va en parejas: cada bug (un fallo real) tiene su control (el daño de pasarse de frenada al arreglarlo). El bug mide si mejoras; el control, si rompes. Sin el control, la tabla te miente con buena cara.

Versiona prompts.py catálogo v1 · v2 · v3, con hash
→
Mide evaluar_prompts.py bugs + controles, N repeticiones
→
Registra historial.json append-only, con cerrojo

Cada fila guarda el hash del prompt que se usó de verdad: si alguien edita una versión publicada en vez de crear otra, el hash cambia y las ejecuciones viejas dejan de cuadrar. La regla «una versión no se edita» pasa de comentario a comprobable. Y guardar la salida cruda —no un resumen— tuvo premio diferido: cuando más tarde hubo que mover el listón, se pudieron re-puntuar las 13 ejecuciones de la historia sin gastar una sola llamada.

Por qué medir bien importa

La muestra pequeña casi invierte la verdad

Al comparar la carta v1 contra la nueva v3 con un juez LLM (un gpt-4o puntuando lo que ningún if ve: si la carta suena a persona o a plantilla), con 3 cartas el juez dijo que v3 era peor. Con 15, la conclusión se dio la vuelta entera.

juez de estilo · v1 vs v3N pequeño engaña
N =  3  → v3 parece PEOR en todo    (arranque 2,33 vs 3,33)   ✗ falso
N = 15  → v3 claramente MEJOR       (arranque 4,20 vs 2,80)   ✓
#         y v1 abre con plantilla 7 de 15 veces; v3, 0 de 15.

Aquellas 3 cartas pillaron por azar un 1 y un 2 que en 15 tiradas ni asoman. La muestra pequeña no fue solo imprecisa: invirtió el signo de la respuesta. Casi desplegamos la conclusión contraria a la verdad — la lección más cara del artefacto.

La forma, garantizada

Se acabó el json.loads a ciegas

Pedirle al modelo «devuelve JSON, por favor» no garantiza qué hay dentro: si falta una clave, el fallo aparece tres funciones más abajo como un None que nadie pidió. La última pieza del artefacto cambia la súplica por un molde Pydantic que el decodificador está obligado a rellenar.

herramientas.py · el contrato de formaconstriñe + valida
class Oferta(BaseModel):
    puesto: str
    empresa: str | None          # null explícito, no ausencia muda
    imprescindibles: list[str]
    ...

oferta = cliente.chat.completions.parse(
    response_format=Oferta).choices[0].message.parsed   # ya validado

Y el cierre bonito: cambiamos toda la fontanería de salida y re-corrimos la báscula. Como los prompts no se tocaron, la calidad tenía que aguantar — y el número lo demostró. Cambiar el cómo no cambió el qué.


Anexo · integración continua

Un eval que depende de que te acuerdes de lanzarlo no es una puerta

Todo lo anterior tenía un agujero honesto, y estaba escrito en la propia bitácora: la báscula solo corría cuando yo me acordaba de correrla. El anexo lo cierra. Ahora hay dos vigilantes, y vigilan cosas distintas: la puerta protege el repositorio de mis cambios; el canario vigila que el modelo no se mueva bajo mis pies.

1 · La puerta: puerta-agente.yml

Cada push a main que toque código, casos, listones o el propio workflow levanta un runner, monta el entorno y corre las 85 llamadas de la báscula. Si un caso baja de su listón, la ejecución sale en rojo y el commit queda marcado. Tres decisiones que la hacen barata y honesta:

Filtro de rutasSolo dispara con **.py, casos.json, umbral.json, requirements.txt y el YAML. Empujar un HTML no gasta ni un céntimo — y se demostró en vivo: el commit del libro 14 no despertó la puerta.
Fail-closed por diseñoUn caso sin listón declarado en umbral.json bloquea. Añadir una prueba nueva y olvidarse de calibrarla no puede salir verde por descuido.
Sin juez en la puertaEl gpt-4o se queda fuera a propósito: caro y subjetivo. Su sitio es la ejecución a demanda y el canario, no el camino crítico de cada push.

El hallazgo que rediseñó la fase: el CI no tenía CV. Cinco de los ocho casos leen el currículum, y esa carpeta está gitignorada a propósito (lleva teléfono y dirección). En el runner sencillamente no existía. Se resolvió metiendo el CV como secreto — pero eso abrió un segundo filo más interesante: el repo es público y los logs de Actions también. GitHub enmascara el secreto, no lo derivado de él: una carta generada lleva dentro la firma del CV. Ahora, en modo --ci, la báscula se calla el detalle de cada intento y emite solo caso: nota ≥ listón, que es lo único que una máquina necesita. Verificado con una sonda de coste 0 € que inyectaba un teléfono cebo.

2 · Los tres diales

Calibrar el listón fue lo más fino del anexo, y salió de separar tres cosas que yo trataba como una. Pedir (lo que el prompt exige: una carta de 130–180 palabras) no es aceptar (lo que el assert da por bueno: 125, perdonando el roce) ni es juzgar (el listón de la puerta: carta_control ≥ 0,90). El argumento de fondo es lingüístico: bajar la petición hace que el modelo recentre la puntería y aterrice más abajo, y forzar las últimas cinco palabras mete relleno — la carta cumpliría el contrato y sería peor.

La calibración no fue por intuición sino por datos: agrupando el historial por configuración idéntica se ve que el ruido no está repartido —vive entero en el único caso a temperatura 0,7— y que más repeticiones lo estrechan. Re-puntuando la historia entera: aceptar en 125 bloquea la versión mala con margen; aceptar en 115 la deja pasar. Ese es el precio exacto de la ceguera.

3 · La prueba del rojo

Una puerta que solo has visto pasar es una puerta en la que confías por fe. Que el código diga sys.exit(1) no demuestra que GitHub pinte la ejecución en rojo. Así que la saboteé a propósito: subí el listón de un caso a 1,1 — inalcanzable, porque la nota máxima es 1,0 — y lo empujé junto al resto del trabajo. Una ejecución demuestra el bloqueo y transporta los cambios.

$ python evaluar_prompts.py --ci
   of_bug_gcp_aws .............. 1.0 ≥ 0.90   (5/5)
   of_bug_cefr_b2 .............. 1.0 ≥ 0.90   (5/5)
   of_bug_rango_python_o ....... 0.0  ·       (0/5)  fuera de la puerta
   carta_bug_numerologia ....... 1.0 ≥ 0.90  (20/20)
   carta_control ............... 1.0 ≥ 0.90  (20/20)
   ✗ of_control_compuesto_y ...... 1.0 < 1.1   (5/5)
   ────────────────────────────────────────────────
   bugs 0.8 · controles 1.0 · global 0.875
   PUERTA CERRADA — exit 1

falla El caso sacó 5 de 5 perfectos y la puerta lo bloqueó igual. Es exactamente lo que había que demostrar: quien decide es umbral.json, no el resultado. Revertido en el commit siguiente, la ejecución gemela salió verde — y las dos quedan una al lado de la otra en el historial público del repo.

4 · El canario: canario-del-modelo.yml

gpt-4o-mini no es una constante. OpenAI lo actualiza sin avisar, y los prompts se degradan con el mismo commit. Por eso hay un segundo workflow que corre solo, los lunes a las 06:00 UTC, sin que nadie haya tocado nada. Su valor está justo ahí: cuando no ha cambiado nada, cualquier movimiento tiene un único culpable. Aquí sí entra el juez (una carta puede pasar todos los asserts y dejar de sonar a mí; eso no lo caza una regla, lo caza un lector) e informa sin bloquear.

Semanal y no nocturno, por coste: 85 llamadas más 60 al juez son unos 12 minutos por ejecución; diario, en un repo que se toca dos veces por semana, es pagar siete veces por lo mismo. Y no se dejó en fe: se disparó a mano en vez de esperar al lunes, por el mismo argumento de la prueba del rojo.

carta_control — la buena, sin bug inducido
arranque4,05
suena a mí4,55
engancha4,40
carta_bug_numerologia
arranque3,35
suena a mí3,70
engancha3,70
carta_bug_rango_numerologia
arranque2,90
suena a mí3,45
engancha3,45

Primera línea base de estilo medida en CI · escala 1–5 · run 29849305745
El hallazgo: los tres casos de carta sacan 1,0 clavado en los asserts y el lector les pone 2,9–4,05 de arranque. Cumplen el contrato y empiezan flojas — que es exactamente lo que ninguna regla podía cazar, y el argumento entero de tener un juez. Si estos números caen un lunes sin haber tocado nada, el culpable tiene nombre.

5 · Lo que no se buscaba: estabilidad

Cinco ejecuciones consecutivas, en máquinas distintas, dieron bugs 0,8 · controles 1,0 · global 0,875 clavado. El contraste está en la propia bitácora: semanas antes, dos ejecuciones idénticas daban 0,833 y 0,9. La diferencia no es el modelo, son las repeticiones — de 5 a 20. Medir más veces no cambia el modelo: cambia lo que puedes afirmar sobre él.

Un cabo que sigue vivo, y lo digo aquí. El CV es la única entrada de la báscula que está fuera del control de versiones. Actualizar ese secreto no es un push, así que si un día sincronizo el currículum, las notas pueden moverse y nada avisa: el canario lo cazaría con hasta siete días de retraso. Los schedule de GitHub tienen además tres trampas conocidas — solo corren en la rama por defecto, se retrasan en horas punta y se desactivan solos tras 60 días sin actividad.


Documentación viva

Los dos cuadernos del anexo

Cada concepto nuevo del recorrido se escribe como un cuaderno propio, no como un comentario en el código. Estos dos son los del anexo; hay catorce en total.

Lo que se entregó

Los criterios, cumplidos

Prompts versionados y activablesUn catálogo v1/v2/v3 con la versión activa como una línea. Cada fila del historial guarda el hash: una versión publicada no se edita, y ahora es comprobable.
Eval-set de bugs reales con controlesCasos en parejas bug/control. Comprobaciones en Python, deterministas, con repeticiones — porque temperatura 0 tampoco lo es.
Mejora medida, no opinadaNumerología de 0,25 a 1,0 con few-shot y chain-of-thought; el juez confirmó el estilo. Cada versión ganó a la anterior con un número.
Structured outputs con esquema validadoLas tres herramientas devuelven moldes Pydantic constreñidos y validados, fail-closed; la báscula demostró que el cambio de fontanería no regresó la calidad.
La báscula corre sola, y se la ha visto fallarPuerta en cada push con listón calibrado sobre el ruido medido, canario semanal con juez, y una ejecución en rojo provocada a propósito para demostrar que el bloqueo es real.
prompt versioning eval-set bug/control LLM-as-judge gpt-4o few-shot + CoT structured outputs Pydantic GitHub Actions quality gate secretos + logs públicos Cloud Run
Por qué este artefacto importa

Escribir buenos prompts lo intenta cualquiera. Lo que distingue es versionarlos, medir cada cambio y haber visto fallar la puerta que los protege. No «tocar y rezar»: tocar y demostrar.