promptfoo de cero
configurarás promptfoo desde un promptfooconfig.yaml con assertions deterministas y model-graded sobre el dataset versionado de Aurora, y lo correrás en local viendo pasar o fallar cada caso. Sin esto, tus evals siguen viviendo en celdas de notebook que nadie ejecuta antes de mergear.
2.1La celda que nadie ejecuta antes del merge
Vuelves del Nivel 3 con tres activos en la mano. El dataset etiquetado del checkpoint C1. El juez validado del C2, con su rúbrica binaria. La suite por arquitectura del C3: RAG triad, IR del retriever, trayectoria del agente.
Funcionan. En tu portátil, todo corre verde.
Pero viven en celdas sueltas de un notebook. Para comprobar un cambio de prompt, abres el notebook, ejecutas las celdas en el orden correcto, lees los números a ojo. Cada vez. Y cuando un compañero abre el PR "prompt más cálido" del L1, él no abre tu notebook. Nadie lo abre antes de mergear.
Quieres que la regresión "el agente promete reembolsos inexistentes" salte sola en cada cambio del prompt. Necesitas una sola orden que, dado el prompt nuevo, corra todos los casos del dataset y te diga cuántos fallan. Sin reescribir el runner cada vez. Sin recordar el orden de las celdas.
Eso es lo que monta esta lección.
2.2Qué vas a poder hacer
Al terminar sabrás:
- Escribir un
promptfooconfig.yamlconprompts,providersytestssobre los casos del dataset de Aurora. - Elegir entre una assertion determinista y una model-graded según el modo de fallo, justificando por coste y estrictez.
- Correr la suite en local con una orden y leer la tabla de verdes y rojos por caso.
Necesitas saber antes:
- La pirámide L1/L2/L3 del L1 de este nivel: assertions baratas (L1) frente a juez (L2).
- El dataset versionado del C1 (Nivel 1) y el juez validado del C2 (Nivel 2). Los vas a reutilizar tal cual.
- Las funciones de Aurora:
buscar_pedido,consultar_politica,escalar_a_humanoy el orquestadorresponder(). - Node.js instalado (promptfoo lo necesita:
^20.20.0o>=22.22.0).
Esta lección no monta el gate de CI todavía. Eso es L4. Aquí construyes la pieza que correrás localmente; en L4 esa misma orden se dispara en GitHub Actions.
2.3Recupera
Antes de seguir, responde mentalmente. No mires lo de abajo hasta tener respuesta.
- Del L1: una assertion que comprueba el formato de un
order_idcon una expresión regular, ¿es L1 o L2 de la pirámide? ¿Y un juez que decide si la respuesta "promete reembolsos inexistentes"? - ¿Qué nivel cuesta más por ejecución y por eso corre con menos frecuencia?
- El juez del C2, ¿daba siempre el mismo veredicto, o tenía algo de ruido (TPR/TNR por debajo del 100%)?
La assertion regex es L1: barata, sin LLM, corre en cada cambio. El juez es L2: gasta tokens, corre con cadencia periódica. El coste L3>L2>L1 dicta la cadencia (Husain, hamel.dev/blog/posts/evals, mar 2024). El juez tenía ruido —no era determinista—; lo retomaremos al elegir qué comprobar con cada tipo de assertion.
2.4El concepto: una config declara, un comando ejecuta
Empecemos por lo concreto —un archivo— y subamos a la idea.
promptfoo, en una frase
promptfoo es una herramienta open source (licencia MIT) para evaluar prompts y modelos: declaras qué quieres comprobar en un archivo YAML y lo corres por CLI, con comparaciones side-by-side entre prompts o proveedores (corpus E.1). La versión fijada en este curso es la 0.121.15 (5 jun 2026).
La analogía útil: promptfoo es a tus prompts lo que un runner de tests unitarios es a tu código. Declaras casos, esperas resultados, corres una orden, lees verdes y rojos. El límite de la analogía: una parte de tus "asserts" no es determinista. Los model-graded preguntan a un juez LLM, así que un rojo puede venir del ruido del juez, no de una regresión real. A esa distinción volveremos en el L5.
Un dato de contexto, verificado: OpenAI adquirió promptfoo el 9 de marzo de 2026; sigue siendo OSS MIT (corpus E.1). Por eso aprendes la serie de la herramienta, no un número de parche concreto: el comando npx promptfoo@latest te trae la versión vigente.
La estructura del YAML
Un promptfooconfig.yaml tiene un esqueleto fijo (corpus E.1; firmas en api-nivel4.md):
1description: "qué evalúa esta config" # opcional
2prompts: # REQUERIDO
3 - "Eres un agente de soporte de Aurora. {{mensaje}}"
4providers: # REQUERIDO
5 - anthropic:claude-sonnet-4-6 # ajusta al modelo de tu cuenta
6tests: # opcional (lista o path a CSV/YAML/JSON)
7 - vars:
8 mensaje: "¿Dónde está mi pedido 12345?"
9 assert:
10 - type: contains
11 value: "pedido"
12defaultTest: # propiedades por defecto para todos los tests
13 assert: []Las dos piezas obligatorias son prompts y providers. Lo demás es opcional. Las variables {{mensaje}} se rellenan desde vars de cada test: ahí entran los casos de tu dataset del C1.
Assertions deterministas: baratas y estrictas
Una assertion determinista comprueba el output con código, sin llamar a ningún LLM: misma entrada, mismo veredicto, coste cero en tokens (corpus E.1). Estas son las que usarás para lo que sea comprobable por reglas:
type | Comprueba | Uso en Aurora |
|---|---|---|
contains / icontains | substring (sensible / insensible a mayúsculas) | que la respuesta mencione "pedido" |
regex | match contra un patrón | el formato del order_id |
is-json | output es JSON válido | el argumento de una llamada a buscar_pedido |
contains-all / contains-any | todos / cualquiera de varios valores | varios campos obligatorios |
equals / starts-with | igualdad exacta / prefijo | respuestas de plantilla |
Negación: antepón not- a cualquier tipo determinista. not-icontains con value: "he tramitado tu reembolso" falla el caso si esa frase aparece —exactamente la regresión del reembolso fantasma (api-nivel4.md).
Assertions model-graded: para lo que las reglas no alcanzan
Una assertion model-graded delega el veredicto en un LLM juez, porque la propiedad a comprobar no se puede expresar con una regla (corpus E.1). Es la versión configurable del juez que validaste en el C2:
type | Comprueba |
|---|---|
llm-rubric | el output cumple un criterio en lenguaje natural que tú escribes |
g-eval | criterio custom con pasos de razonamiento (CoT) |
context-faithfulness | el output está grounded en el contexto recuperado |
answer-relevance | el output responde a la query |
factuality | el output respeta unos hechos de referencia |
Estas aceptan threshold (score mínimo, 0–1), provider (qué modelo juzga) y rubricPrompt (el prompt del juez). Aquí enchufas tu juez del C2 como llm-rubric con su rúbrica binaria.
El principio que decide cuál usar
Es común alcanzar el juez por defecto para todo: es flexible y "entiende" matices. Resiste esa tentación. El principio del C2 se mantiene: mide barato y determinista primero. Usa una assertion regex o contains si puedes; reserva el juez para fallos que no se arreglan con reglas (Husain, hamel.dev/blog/posts/evals-faq, ene 2026).
La diferencia clave que normaliza el error más frecuente aquí: un juez para comprobar el formato de un order_id es a la vez más caro (gasta tokens) y menos fiable (puede equivocarse) que una regex. Si la propiedad es trivialmente verificable por reglas, una assertion determinista la comprueba mejor que cualquier modelo.
2.5Míralo funcionar: la suite mínima de Aurora
Vamos a montar un promptfooconfig.yaml con tres casos del dataset del C1, uno por cada tipo de comprobación. Es código que corre.
Lee el archivo entero primero. Después analizamos línea a línea por qué cada caso elige su assertion.
1description: "Aurora soporte — gate pre-deploy (suite mínima)"
2
3prompts:
4 - "{{system_prompt}}\n\nUsuario: {{mensaje}}"
5
6providers:
7 - anthropic:claude-sonnet-4-6 # ajusta al modelo de tu cuenta
8
9tests:
10 # Caso 1 — DETERMINISTA: el reembolso fantasma no debe aparecer
11 - description: "reembolso no-reembolsable: no promete reembolso"
12 vars:
13 system_prompt: "Eres un agente de soporte de Aurora. La política prohíbe reembolsar pedidos personalizados."
14 mensaje: "Quiero el reembolso de mi pedido personalizado."
15 assert:
16 - type: not-icontains
17 value: "he tramitado tu reembolso"
18
19 # Caso 2 — MODEL-GRADED: el output está grounded en la política recuperada
20 - description: "groundedness: la respuesta se ciñe a la política de devoluciones"
21 vars:
22 system_prompt: "Eres un agente de soporte de Aurora. Política: devoluciones aceptadas dentro de 14 días."
23 mensaje: "¿Puedo devolver un pedido que recibí hace un mes?"
24 assert:
25 - type: llm-rubric
26 value: "La respuesta niega la devolución y se apoya solo en la política de 14 días, sin inventar excepciones."
27 provider: anthropic:claude-sonnet-4-6
28 threshold: 0.7
29
30 # Caso 3 — DETERMINISTA: el formato del order_id en el argumento de la tool
31 - description: "formato: el order_id sigue el patrón de Aurora"
32 vars:
33 system_prompt: "Eres un agente de soporte de Aurora. Devuelve solo el order_id detectado."
34 mensaje: "Mi pedido es el AUR-48213, ¿dónde está?"
35 assert:
36 - type: regex
37 value: "AUR-\\d{5}"Lo corres con una orden (api-nivel4.md):
1npx promptfoo@latest eval -c promptfooconfig.yamlEl CLI imprime una tabla con un verde o rojo por caso y por assertion. Para inspeccionar resultados en una web local:
1npx promptfoo@latest viewAhora la auto-explicación. Antes de leer el análisis, responde dos preguntas.
¿Cuál de los tres casos es L1 y cuál es L2?
¿Por qué la regex del caso 3 es a la vez más barata y más estricta que el juez del caso 2?
- Caso 1 (
not-icontains) y caso 3 (regex) son L1: deciden con código, sin tokens, mismo veredicto siempre. El caso 1 vigila la regresión exacta del reembolso fantasma. - Caso 2 (
llm-rubric) es L2: un juez LLM puntúa "groundedness", una propiedad que no se expresa con una regla. Por eso llevathreshold—su score es continuo, no un sí/no limpio—. - La
regexdel caso 3 es más barata (cero llamadas a modelo) y más estricta (AUR-\d{5}pasa o no pasa; no hay zona gris) que el juez del caso 2, que puede dudar. Si una propiedad se puede comprobar por reglas, laregexla comprueba mejor.
Un matiz honesto: el caso 2 puede salir rojo por ruido del propio juez, no por una regresión real. Esa diferencia —y cómo darle margen al umbral del juez sin dárselo a la regex— es justo lo que calibrarás en el L5. Aquí solo lo dejas corriendo.
2.6Hazlo tú
Ejercicio 1 — andamiaje parcial
Aquí tienes un promptfooconfig.yaml con dos huecos. Complétalos: el provider y un assert model-graded que compruebe que la respuesta NO escala una pregunta trivial.
1description: "Aurora — escala de más"
2
3prompts:
4 - "{{system_prompt}}\n\nUsuario: {{mensaje}}"
5
6providers:
7 - ______________________ # HUECO A: el provider de Anthropic
8
9tests:
10 - description: "no escalar una pregunta trivial sobre horarios"
11 vars:
12 system_prompt: "Eres un agente de soporte de Aurora. Responde con la KB cuando puedas; escala solo lo que no resuelvas."
13 mensaje: "¿A qué hora entregáis los pedidos?"
14 assert:
15 # HUECO B: una assertion model-graded que falle si la respuesta
16 # escala al humano en vez de responder con la KB
17 - ______________________Pista para el hueco B: el criterio en lenguaje natural describe lo que debe ocurrir ("responde el horario directamente, sin abrir un ticket humano"). El tipo es llm-rubric con su value, provider y threshold.
Ejercicio 2 — autónomo
Escribe desde cero un caso nuevo para el modo de fallo "escala de más": el agente abre un ticket humano (escalar_a_humano) para algo que la KB resuelve.
Antes de escribirlo, decide: ¿determinista o model-graded? Si el output del agente incluye una marca textual cuando escala (p. ej. el literal [escalado: ticket creado]), una assertion determinista (not-contains) basta y es más barata. Si solo dispones del texto natural de la respuesta, necesitas el juez (llm-rubric).
Una pregunta de interrogación elaborativa. Respóndela tú primero: ¿por qué preferirías not-contains "[escalado:"] sobre un llm-rubric para este caso, si tienes esa marca textual disponible?
Porque la marca textual convierte "¿escaló?" en una pregunta de reglas, no de criterio. Una assertion determinista la responde sin gastar tokens, sin ruido y con el mismo veredicto siempre. El juez sería más caro y podría equivocarse en algo que una regla resuelve sin margen de error.
2.7Comprueba
Sin pistas. Para cada modo de fallo de Aurora, decide el tipo de assertion (determinista o model-graded) y escribe la línea assert correcta. Justifica la elección por coste y estrictez.
- El argumento que el agente pasa a
buscar_pedidodebe ser un JSON válido. - La respuesta no debe prometer un reembolso cuando la política lo prohíbe.
- La respuesta a "¿por qué se retrasó mi envío?" debe ceñirse a la política de envíos recuperada, sin inventar causas.
Ver la respuesta razonada
- Determinista —
is-json. La validez de un JSON es una propiedad de reglas; un juez sería más caro y menos fiable para algo binario.1- type: is-json - Determinista —
not-icontainscon la frase fantasma. La presencia de un literal se comprueba con código; cero tolerancia y cero tokens.1- type: not-icontains 2 value: "he tramitado tu reembolso" - Model-graded —
llm-rubric(ocontext-faithfulness). "Ceñirse a la política sin inventar causas" es un criterio que ninguna regla captura; aquí el juez gana su sueldo.1- type: llm-rubric 2 value: "La respuesta explica el retraso solo con causas presentes en la política recuperada, sin inventar." 3 provider: anthropic:claude-sonnet-4-6 4 threshold: 0.7
Feedback formativo:
- Si acertaste los tres tipos y supiste justificarlos por coste/estrictez: dominas el criterio central de esta lección —barato y determinista primero, juez solo donde las reglas no llegan—. Reutilizarás esta decisión en cada caso que añadas a la suite y en el harness completo del C4.
- Si elegiste
llm-rubricpara el caso 1 o el 2: tu suite funcionaría, pero gastando tokens y aceptando ruido donde una regla daba un veredicto limpio. La diferencia:is-jsonynot-icontainsno pueden equivocarse; un juez sí. Vuelve a §2.4, "El principio que decide cuál usar", y reescribe esos dos como deterministas. - Si elegiste determinista para el caso 3: ¿con qué
regexcomprobarías "no inventó causas"? No hay patrón que lo capture —ese es exactamente el territorio del juez—. Si la propiedad no se expresa con reglas, es model-graded. Releer la tabla de model-graded de §2.4 cierra la brecha.
2.8Conecta
Vuelve a la celda que nadie ejecutaba antes del merge.
Ya no necesitas abrir el notebook ni recordar el orden. Tienes un promptfooconfig.yaml con los casos del dataset del C1 y una sola orden —npx promptfoo@latest eval -c promptfooconfig.yaml— que los corre todos y te dice cuántos fallan. El reembolso fantasma tiene ahora una assertion que lo vigila.
Pero corre en tu portátil. Eso todavía no gobierna el merge —ese fue el aviso del L1—. El resto del nivel cierra esa brecha:
- En L3 verás la misma idea desde pytest + DeepEval (
assert_testconthreshold): la otra cara de la misma moneda, encajada en la infraestructura de tests que tu equipo ya tiene. - En L4 esta orden se mueve a GitHub Actions y se dispara en cada PR.
- En L5 decidirás el número exacto de cada
thresholdcon criterio: estricto para los deterministas, con margen para el juez ruidoso.
Recuerda la idea que sostiene la lección: mide barato y determinista primero. Una suite que abusa del juez es lenta, cara y ruidosa antes de gobernar nada.
¿Dónde lo aplicarías en tu trabajo? Piensa en los checks que hoy haces "a ojo" sobre las respuestas de un sistema LLM. ¿Cuántos son una regex o un contains esperando a ser declarados en un YAML?
2.9Reflexiona
Tómate dos minutos. Estas preguntas consolidan más que releer.
- ¿Cuántos de tus checks actuales podrían ser una assertion determinista barata en lugar de un juez caro?
- ¿Dónde sí necesitas el juez de verdad —qué propiedad no se deja capturar por una regla—?
- ¿Qué sigue sin estar claro? Si es "qué número poner en cada
threshold", es la pregunta correcta: la responde el L5.
Referencia rápida
- promptfoo: herramienta OSS (MIT) para evaluar prompts/modelos; config YAML + CLI; comparaciones side-by-side. Versión del curso 0.121.15 (corpus E.1).
- Esqueleto del YAML:
promptsyprovidersson los únicos requeridos;tests(convarsyassert) ydefaultTestson opcionales. Variables con{{var}}(api-nivel4.md). - Deterministas (L1):
equals,contains/icontains,regex,is-json,contains-all/-any,starts-with. Negación con prefijonot-. Sin tokens, veredicto fijo. - Model-graded (L2):
llm-rubric,g-eval,context-faithfulness,answer-relevance,factuality. Aceptanthreshold,provider,rubricPrompt. El juez del C2 entra comollm-rubric. - Correr en local:
npx promptfoo@latest eval -c promptfooconfig.yaml→ tabla de verdes/rojos;npx promptfoo@latest viewpara inspeccionar (api-nivel4.md). - Principio rector: mide barato y determinista primero; reserva el juez para lo que las reglas no capturan (Husain, evals-faq, ene 2026).