Facsímil 07 · Completo

Evaluar, calibrar e interpretar

Métricas, evaluación, calibración, interpretabilidad y lectura crítica de resultados para no confundir una buena demo con un buen sistema.

Contenido disponible
6 de 6 capítulos listos
Contenido completo, pendiente de revisión editorial final.
Estado editorial
Completo
Lectura web generada desde los capítulos Markdown originales.

Sobre esta edición

Esta página se genera desde capítulos Markdown propios del facsímil. Las fórmulas se renderizan con KaTeX, los mapas con Mermaid y las notas al pie se mantienen junto al texto para leer el facsímil como una pieza autónoma, no como una exportación del taller.

Capítulo 01

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 01: Qué es una eval y qué decisión permite tomar

Qué deberías poder hacer al terminar

Este facsímil empieza con una idea que parece menos brillante que hablar de interpretabilidad, calibración o modelos evaluadores, pero es la que sostiene todo lo demás: una evaluación buena no existe para decorar una presentación; existe para tomar una decisión.

Al terminar este capítulo deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Distinguir una demo de una eval.Puedes explicar por qué una respuesta bonita no prueba que el sistema funcione.
Diseñar una eval mínima.Defines hipótesis, casos, salida esperada, rúbrica, baseline, candidate, métricas y umbrales.
Convertir métricas en decisión.Dices si una variante se acepta, se rechaza, se revisa o se limita.
Separar promedio y fallo crítico.No dejas que una media alta esconda un caso que no debería pasar.
Crear una scorecard ejecutable.Produces un JSON con métricas, coste, regresiones, incertidumbre y decisión.
Conectar evaluación con operación.Ves cómo una eval alimenta CI, release gates, runbooks y mejora continua.

La frase que nos va a acompañar durante todo el facsímil es esta:

No preguntes primero “¿qué métrica saco?”. Pregunta “¿qué decisión quiero poder defender?”.

La escena: cambiar algo sin saber si empeora

Imagina que tienes un asistente interno para alumnado. Responde dudas sobre matrícula, becas, horarios y trámites. El equipo quiere cambiar el prompt, usar otro modelo y añadir una capa de recuperación documental. En una demo, la versión nueva suena más fluida. Parece mejor.

Pero una demo no responde preguntas incómodas:

PreguntaPor qué importa
¿Mejora en los casos frecuentes o solo en el ejemplo que hemos mirado?Una mejora local puede esconder regresiones.
¿Sigue absteniéndose cuando no hay evidencia?Una respuesta inventada puede ser peor que no responder.
¿Cuánto cuesta cada caso aceptado?El modelo barato por token puede ser caro por tarea real.
¿Qué ocurre con los casos frontera?Ahí aparecen los errores que una demo limpia no enseña.
¿Qué evidencia dejamos para revisar después?Sin trazas ni scorecard, no hay aprendizaje reproducible.

Una eval nace justo ahí: cuando dejamos de mirar una anécdota y empezamos a construir evidencia repetible.

Qué no es una eval

Una eval no es “he probado tres preguntas y me gusta más”. Eso puede servir para exploración inicial, pero no para decidir una release.

Tampoco es un benchmark público usado como respuesta automática. Un benchmark puede orientar, comparar familias de modelos y detectar capacidades generales. Pero tu sistema vive en tus datos, tus contratos, tus usuarios, tus costes y tus límites operativos. HELM propuso una evaluación amplia de modelos de lenguaje precisamente para mirar varios escenarios, métricas y dimensiones de forma sistemática, no para reducir todo a un número único.1

Y una eval tampoco es solo un evaluador LLM. Un evaluador puede ser útil cuando hay que valorar calidad semántica, groundedness o completitud. Pero si puedes validar JSON, una llamada de herramienta, un diff, una cita o un cálculo con código determinista, suele ser mejor empezar por ahí. OpenAI describe graders como mecanismos que comparan respuestas de referencia y salidas del modelo para devolver puntuaciones, con tipos como comprobaciones de texto, similitud, modelos evaluadores y ejecución de código.2

Qué sí es una eval

En este libro llamaremos eval a un diseño reproducible que compara una versión candidata contra casos, criterios, métricas y umbrales para tomar una decisión.

Podemos escribirlo así:

E=(D,T,G,M,U,A)E = (D, T, G, M, U, A)
SímboloSignificadoEjemplo
EEEvaluación completa.Eval de asistente de matrícula.
DDDataset de casos.80 preguntas reales y 20 casos sin evidencia suficiente.
TTTarea evaluada.Responder con cita o abstenerse.
GGGraders o evaluadores.Validador JSON, comprobador de cita, evaluador de rúbrica, revisión humana.
MMMétricas agregadas.Exactitud, groundedness, tasa de abstención correcta, coste por aceptada.
UUUmbrales de decisión.quality >= 0.85, critical_failures == 0.
AAAcción que se toma.Aceptar, rechazar, limitar, revisar o volver a baseline.

Lo importante es que AA forma parte de la evaluación. Si una eval no termina en una acción posible, es un informe interesante, pero todavía no es una puerta de ingeniería.

Fecha de corte del estado del arte

Fecha de corte: 28 de mayo de 2026.
Fuentes consultadas: documentación de OpenAI Evals y graders; documentación de LangSmith Evaluation; guías de Braintrust sobre evaluación sistemática; documentación de Promptfoo sobre assertions y métricas; Hugging Face Evaluate; EleutherAI Language Model Evaluation Harness; HELM; model cards; datasheets for datasets; TFX; acuerdo entre revisores; bootstrap; comparación pareada; y literatura de ingeniería de software para ML.

OpenAI presenta Evals como una forma de crear, gestionar y ejecutar evaluaciones sobre modelos con data sources y graders.3 Braintrust describe una evaluación como la combinación de datos, tarea y funciones de scoring, con comparación de experimentos y seguimiento de regresiones.4 LangSmith sitúa la evaluación en datasets, evaluadores y experimentos trazables dentro del ciclo de desarrollo de aplicaciones LLM.5

Promptfoo permite expresar expectativas como assertions sobre outputs, incluyendo igualdad, JSON, similitud, funciones de Python o JavaScript, pesos y umbrales.6 Hugging Face Evaluate insiste en elegir métricas según la tarea, porque no hay una métrica universal que sirva para todo.7 EleutherAI lm-evaluation-harness ofrece un marco unificado para evaluar modelos de lenguaje en tareas académicas y backends distintos.8

La conclusión práctica es sencilla: el mercado tiene herramientas distintas, pero la anatomía se repite. Necesitas casos, tarea, evaluadores, métricas, trazas, comparación y decisión.

La anatomía técnica de una evaluación

Una evaluación profesional separa piezas. Si las mezclamos, no sabemos qué arreglar.

Anatomía de una evaluación de IA Diagrama en blanco, negro y gris que conecta dataset, versiones evaluadas, runner, graders, métricas, scorecard, gate, decisión y bucle de mejora. Sistema de evaluación: de casos a decisión Una eval no es una nota: es una tubería reproducible para aceptar, rechazar, limitar o revisar una variante. 1 · Casos Dataset versionado • inputs reales • salidas esperadas • metadatos y slices • casos sin evidencia Rúbrica criterios observables pesos por criterio fallos críticos 2 · Versiones Baseline versión aceptada hoy model_id, prompt_version index_version, params Candidate variante que pide entrar diff de prompt/modelo/RAG hipótesis de mejora No se acepta por sonar mejor. 3 · Runner + graders Runner reproducible ejecuta cada caso guarda output y coste captura trazas y errores Graders determinista: JSON, regex, tests semántico: similitud o rúbrica humano: calibración y frontera entorno: tool, diff, estado final Cada grader debe poder explicar su señal. 4 · Decisión Scorecard quality score regresiones fallos críticos coste aceptado Gate pass / fail / review owner + rollback Lectura de ingeniería • El promedio solo sirve si antes miras slices y fallos críticos. • Una mejora debe compararse contra baseline, no contra memoria o entusiasmo. • El coste se divide por tareas aceptadas, no por llamadas realizadas. si falla, vuelve como caso de regresión Inputs contrato riesgo coste evidencia Output release rollback review nuevo caso IA para gente curiosa / Facsímil 07 / Capítulo 01 / 686f6c61
Anatomía de una eval como sistema: casos, versiones, runner, graders, métricas, gate y bucle de mejora.

La imagen tiene una intención muy concreta. Una eval no vive solo en la columna de métricas. Vive en todo el circuito:

  1. Casos suficientemente representativos.
  2. Dos versiones comparables.
  3. Ejecución reproducible.
  4. Evaluadores separados por tipo de señal.
  5. Scorecard trazable.
  6. Gate que decide.
  7. Regresiones que vuelven al dataset.

Esa última flecha es vital. Un fallo que se descubre en producción y no entra en la eval queda condenado a repetirse.

Métricas: el número no decide solo

Una métrica resume una parte del comportamiento. Una decisión combina varias señales.

Podemos definir una puntuación ponderada:

S=i=1nwimii=1nwiS = \frac{\sum_{i=1}^{n} w_i m_i}{\sum_{i=1}^{n} w_i}
SímboloSignificadoEjemplo
SSPuntuación agregada entre 0 y 1.0,87.
nnNúmero de métricas o criterios.4 criterios.
wiw_iPeso del criterio ii.groundedness pesa 3.
mim_iResultado del criterio ii, entre 0 y 1.json_valido = 1, cita_correcta = 0.

Ejemplo:

CriterioPeso wiw_iResultado mim_iAporte
Formato válido11,001,00
Respuesta correcta30,902,70
Cita verificable30,702,10
Abstención cuando toca30,501,50
Total107,30
S=7,3010=0,73S = \frac{7,30}{10} = 0,73

La puntuación parece decente, pero hay una alarma: abstención correcta vale 0,50. Si el sistema responde cuando no tiene evidencia, quizá no puede publicarse aunque la media no sea catastrófica.

Por eso un gate real suele tener dos capas. Ejemplo de fórmula: este gate no es una ley universal; es una plantilla operativa para obligarnos a escribir calidad mínima, fallos críticos y presupuesto antes de publicar.

aceptar=(Sτ)(Fc=0)(CaB)\text{aceptar} = (S \ge \tau) \land (F_c = 0) \land (C_a \le B)
SímboloSignificadoEjemplo
SSPuntuación agregada.0,87.
τ\tauUmbral mínimo de calidad.0,85.
FcF_cNúmero de fallos críticos.1 respuesta sin evidencia.
CaC_aCoste por tarea aceptada.0,031 €.
BBPresupuesto máximo por aceptada.0,040 €.

La media puede pasar y el gate puede fallar. Eso no es una contradicción: es ingeniería.

Tipos de evaluadores que conviene combinar

Ningún grader lo ve todo. Una buena eval combina evaluadores según la naturaleza de la tarea.

EvaluadorQué mide bienQué no ve bienEjemplo
DeterministaFormato, exact match, regex, JSON, rangos, campos obligatorios.Calidad semántica rica.“El JSON tiene categoria, prioridad y siguiente_paso”.
Código o entornoTests, diffs, estado final, herramientas llamadas, cálculos.Intención, estilo o utilidad percibida.“La función pasa unit tests y no modifica archivos fuera de ruta”.
SimilaridadCercanía semántica entre salida y referencia.Errores pequeños pero graves.“La respuesta se parece al resumen esperado”.
Rúbrica humanaJuicio experto, contexto, ambigüedad.Escalabilidad y consistencia sin guía.“Un docente revisa 30 casos frontera”.
LLM como evaluadorGroundedness, completitud, estilo, comparación A/B.Variabilidad, sesgo de longitud, coste y cambios de versión.“Puntúa si la respuesta está apoyada por la cita”.
Métrica operativaLatencia, coste, reintentos, trazas, tasa de aceptación.Calidad de contenido por sí sola.“p95 menor que 4 s y coste por aceptada menor que 0,04 €”.

La regla práctica es simple: usa lo determinista para lo verificable, usa rúbrica para lo semántico y reserva revisión humana para calibrar o decidir casos delicados.

Dataset: dónde se decide la calidad de la eval

El dataset de evaluación no es un CSV cualquiera. Es el instrumento de medida.

Gebru y coautoras propusieron Datasheets for Datasets para documentar motivación, composición, recogida, procesamiento, usos recomendados y mantenimiento de datasets.9 Esa idea encaja directamente con evals: si no sabes de dónde salen tus casos, qué cubren y qué dejan fuera, la métrica puede sonar seria y medir mal.

Una eval mínima debería incluir:

Parte del datasetQué debe contenerPor qué importa
Casos frecuentesPreguntas o tareas que aparecen cada semana.Miden utilidad cotidiana.
Casos fronteraEntradas ambiguas, incompletas o con varias interpretaciones.Enseñan si el sistema pide aclaración.
Casos sin evidenciaPreguntas que el corpus no permite responder.Miden abstención.
Casos de formatoSalidas que deben cumplir contrato.Evitan romper integraciones.
Casos por segmentoIdioma, canal, tipo de usuario, producto o país.Detectan media buena con subgrupo malo.
Casos de regresiónFallos reales convertidos en test permanente.Evitan repetir errores ya vistos.

La documentación de modelos también importa. Las model cards nacen para registrar detalles de uso previsto, factores, métricas, datos de evaluación y consideraciones de despliegue.10 En un sistema aplicado, la scorecard de eval cumple una función parecida para una release concreta: dice qué hemos probado, con qué límites y con qué resultado.

Hipótesis evaluable antes de tocar nada

Una eval seria no empieza ejecutando un script. Empieza escribiendo una hipótesis que pueda salir bien o mal. Si no puedes escribirla, probablemente todavía no sabes qué estás intentando mejorar.

Ejemplo de fórmula: una forma mínima de escribir la hipótesis evaluable es esta. No pretende cubrir toda investigación experimental; sirve para que un Pull Request o una release no cambie algo sin declarar efecto, métrica, riesgo y acción.

H=(C,E,M,R,A)H = (C, E, M, R, A)
SímboloSignificadoEjemplo
HHHipótesis evaluable.“El nuevo prompt mejora citas sin empeorar abstención”.
CCCambio propuesto.Añadir instrucción de citar fuente y fecha.
EEEfecto esperado.Sube groundedness y baja respuesta sin evidencia.
MMMétrica que lo comprueba.Groundedness, abstención correcta, coste por aceptada.
RRRiesgo que vigilas.Respuestas más largas, más coste, más latencia.
AAAcción si la hipótesis falla.Mantener baseline y añadir casos de regresión.

Ejemplo escrito como lo pondríamos en un Pull Request:

CampoContenido
CambioSustituimos el prompt de respuesta libre por uno que exige cita o abstención.
Efecto esperadoLa tasa de respuestas con evidencia verificable sube de 0,78 a 0,86.
RiesgoEl modelo puede sobre-abstenerse o subir coste por respuestas más largas.
Métrica primariaGroundedness ponderado.
Métricas de guardiaAbstención correcta, coste por aceptada, p95 de latencia y regresiones por slice.
GateAceptar solo si groundedness >= 0.86, critical_failures == 0 y cost_per_accepted <= 0.04.

La hipótesis evita dos males muy comunes: cambiar varias cosas a la vez sin saber cuál ayudó, y declarar “mejor” algo que solo cambió el estilo.

Etiquetado y acuerdo entre revisores

Si una eval necesita etiquetas humanas, entonces también necesita una guía de etiquetado. No basta con decir “que alguien lo revise”. Hay que definir qué significa correcto, parcialmente correcto, incorrecto, no respondible, cita válida, salida útil y fallo crítico.

Una guía mínima de etiquetado debería responder:

PreguntaDecisión práctica
¿Quién etiqueta?Dos personas para una muestra inicial y una persona para el resto si el acuerdo es suficiente.
¿Qué ve quien etiqueta?Input, output, evidencia recuperada, referencia esperada y rúbrica.
¿Qué no debería ver?Nombre del modelo si queremos reducir sesgo de marca.
¿Qué etiquetas existen?pass, partial, fail, must_abstain, critical_failure.
¿Cómo se resuelve desacuerdo?Tercera revisión o reunión corta para cambiar la guía, no para forzar unanimidad silenciosa.
¿Qué se guarda?Revisor, fecha, versión de guía, etiqueta y comentario breve.

El acuerdo simple se calcula así:

po=aNp_o = \frac{a}{N}
SímboloSignificadoEjemplo
pop_oProporción de acuerdo observado.0,82.
aaCasos donde dos revisores coinciden.82.
NNCasos revisados por ambos.100.

Pero el acuerdo simple no corrige coincidencias por azar. Cohen propuso kappa para medir acuerdo entre dos codificadores en categorías nominales teniendo en cuenta el acuerdo esperado por azar.11

κ=pope1pe\kappa = \frac{p_o - p_e}{1 - p_e}
SímboloSignificadoEjemplo
κ\kappaAcuerdo corregido por azar.0,71.
pop_oAcuerdo observado.0,82.
pep_eAcuerdo esperado por azar según las distribuciones de etiquetas.0,38.

No hace falta convertir kappa en una religión. Lo importante para ingeniería es más sencillo: si dos personas no se ponen de acuerdo siguiendo la misma guía, el problema no está en el modelo; está en la definición de calidad.

Baseline, candidate y regresión

Evaluar una versión aislada dice poco. Lo que necesitamos casi siempre es comparación:

ΔS=ScandidateSbaseline\Delta S = S_{candidate} - S_{baseline}
SímboloSignificadoEjemplo
ΔS\Delta SCambio de puntuación.+0,04.
ScandidateS_{candidate}Score de la variante nueva.0,89.
SbaselineS_{baseline}Score de la versión actual.0,85.

Pero una mejora media puede ocultar una regresión:

R={xD:passbaseline(x)=1passcandidate(x)=0}R = \{x \in D : pass_{baseline}(x)=1 \land pass_{candidate}(x)=0\}
SímboloSignificadoEjemplo
RRConjunto de casos que empeoran.Tres preguntas de becas fallan.
xxCaso del dataset.case_017.
DDDataset de evaluación.100 casos.
passbaseline(x)pass_{baseline}(x)Si baseline pasa el caso xx.1.
passcandidate(x)pass_{candidate}(x)Si candidate pasa el caso xx.0.

Si RR contiene un caso crítico, no basta con decir “pero el promedio sube”. Esa frase es exactamente el tipo de pensamiento que una eval debe impedir.

Incertidumbre: no creas demasiado en un decimal

Una eval de 20 casos no tiene la misma fuerza que una eval de 2.000. Si candidate obtiene 0,87 y baseline 0,85, quizá hay mejora real. O quizá hemos visto ruido de muestra. La estadística no está para adornar: está para impedir que tomemos decisiones caras sobre diferencias frágiles.

Para comparar dos versiones sobre los mismos casos, lo primero es mirar comparación pareada:

Situación del casoQué significa
Baseline pasa y candidate pasa.No informa sobre diferencia entre versiones.
Baseline falla y candidate falla.Tampoco informa sobre diferencia.
Baseline falla y candidate pasa.Mejora directa.
Baseline pasa y candidate falla.Regresión directa.

McNemar propuso una prueba para diferencias entre proporciones correlacionadas, justo el tipo de situación que aparece cuando dos clasificadores o dos versiones se evalúan sobre los mismos casos.12 En lectura de ingeniería, la idea práctica es que solo importan los casos discordantes:

χ2=(bc1)2b+c\chi^2 = \frac{(|b-c|-1)^2}{b+c}
SímboloSignificadoEjemplo
bbCasos donde baseline falla y candidate pasa.12 mejoras.
ccCasos donde baseline pasa y candidate falla.3 regresiones.
χ2\chi^2Estadístico aproximado de McNemar con corrección de continuidad.7,11.

No vamos a convertir este capítulo en un curso de inferencia, pero sí debemos llevarnos la intuición: si mejoras 12 casos y rompes 3, la lectura es distinta que si mejoras 4 y rompes 3.

También podemos estimar incertidumbre con bootstrap: re-muestrear los casos con reemplazo muchas veces, recalcular la diferencia de score y mirar el rango donde cae la mayoría de diferencias. Efron introdujo el bootstrap moderno como método de remuestreo para estimar la variabilidad de estadísticos sin depender de una fórmula cerrada para cada caso.13

IC95%(ΔS)=[q0.025(ΔS\*),q0.975(ΔS\*)]IC_{95\%}(\Delta S) = \left[q_{0.025}(\Delta S^\*), q_{0.975}(\Delta S^\*)\right]
SímboloSignificadoEjemplo
IC95%IC_{95\%}Intervalo de confianza aproximado al 95 %.[0,01, 0,09].
ΔS\Delta SDiferencia real estimada entre candidate y baseline.+0,04.
ΔS\*\Delta S^\*Diferencias recalculadas en muestras bootstrap.2.000 diferencias simuladas.
q0.025q_{0.025}Percentil 2,5 %.Límite inferior.
q0.975q_{0.975}Percentil 97,5 %.Límite superior.

Lectura práctica:

ResultadoQué haría
Intervalo claramente por encima de 0 y sin fallos críticos.Candidate parece mejorar de forma consistente.
Intervalo cruza 0.No hay evidencia fuerte de mejora; pediría más casos o más análisis.
Intervalo mejora, pero hay regresión crítica.No publicaría; arreglaría esa clase de fallo primero.
Intervalo mejora, pero coste se dispara.Miraría coste por aceptada y routing antes de decidir.

Coste por tarea aceptada

En IA aplicada, el coste por llamada no suele ser la métrica que decide. Decide el coste por salida aceptada.

Ejemplo de fórmula: este cálculo de coste mezcla inferencia, tools, reintentos y revisión humana porque son las partidas que suelen aparecer en una aplicación de IA. En tu sistema quizá faltará almacenamiento, anotación, observabilidad o coste de oportunidad.

Ca=Ci+Ct+Cr+ChNaC_a = \frac{C_i + C_t + C_r + C_h}{N_a}
SímboloSignificadoEjemplo
CaC_aCoste por tarea aceptada.0,031 €.
CiC_iCoste de inferencia.0,70 €.
CtC_tCoste de herramientas externas.0,18 €.
CrC_rCoste de reintentos.0,22 €.
ChC_hCoste de revisión humana.1,70 €.
NaN_aNúmero de tareas aceptadas.90 tareas.

Si una versión nueva cuesta el doble pero reduce mucho revisión humana, quizá es más barata por tarea aceptada. Si una versión barata genera más rechazos, quizá sale cara aunque el precio por token parezca atractivo.

Cómo se ve en un proyecto real

En un equipo de ingeniería, una eval debería vivir como un artefacto versionado:

ArchivoQué contieneQuién lo usa
eval_cases.jsonlCasos de evaluación con input, criterios y metadatos.Ingeniería, producto, datos.
eval_hypothesis.jsonCambio, efecto esperado, métricas primarias, métricas de guardia y acción si falla.Autor del cambio y reviewer.
eval_policy.jsonUmbrales, pesos, fallo crítico y presupuesto.Tech lead, operación, producto.
labeling_guide.mdGuía para etiquetar casos y resolver desacuerdos.Revisores humanos y docentes.
error_taxonomy.jsonCatálogo de errores que convierte fallos en acciones técnicas.Ingeniería y análisis de errores.
eval_run_manifest.jsonVersiones, hashes, parámetros, fecha y owner de la corrida.Auditoría técnica y reproducibilidad.
eval_runner.pyScript reproducible que ejecuta y puntúa.CI, desarrollo local.
scorecard.jsonResultado de una corrida concreta.Pull request, release, runbook.
decision.mdLectura humana de aceptar, rechazar o revisar.Equipo y responsables de decisión.

Amershi y coautores describen cómo los sistemas de ML introducen necesidades especiales en ingeniería de software: datos, evaluación, monitorización, experimentación y evolución del comportamiento.14 TFX también se diseñó alrededor de pipelines reproducibles que integran datos, entrenamiento, validación y serving.15

La eval es una pieza de ese mismo mundo. No es un notebook olvidado; es parte del sistema.

Una buena regla de trabajo: si dentro de dos semanas no puedes repetir la eval y explicar por qué salió lo que salió, no tenías una evaluación; tenías una medición suelta.

Manos a la obra

Práctica: una eval mínima con gate de release.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/run_f7_practices.py --chapter c01 --write --fail-on-invalid

Vamos a construir una evaluación pequeña, ejecutable y útil. El ejemplo no llama a ningún proveedor externo. Lo importante aquí es que se vea la estructura: dataset, baseline, candidate, scoring, coste, regresiones y decisión.

Qué vas a crear

evals/
  eval_hypothesis.json
  eval_cases.jsonl
  labeling_guide.md
ops/
  ai/
    error_taxonomy.json
    eval_policy.json
    eval_run_manifest.json
    eval_runner.py
output/
  eval_scorecard.json
  decision.md

Hipótesis del cambio

Guarda esto como evals/eval_hypothesis.json:

{
  "change_id": "prompt-cita-o-abstencion-v2",
  "change": "Exigir que el asistente responda con evidencia o se abstenga cuando el caso no esté cubierto.",
  "expected_effect": "Subir la calidad ponderada sin introducir fallos críticos en casos sin evidencia.",
  "primary_metric": "weighted_quality",
  "guardrail_metrics": [
    "critical_failures",
    "regressions",
    "cost_per_accepted_eur"
  ],
  "minimum_interpretable_delta": 0.05,
  "if_fails": "mantener baseline, clasificar el fallo y añadirlo al dataset de regresión"
}

Dataset mínimo

Guarda esto como evals/eval_cases.jsonl:

{"case_id":"matricula_001","input":"¿Cuál es el plazo de matrícula ordinaria?","expected_contains":["matrícula","plazo"],"must_abstain":false,"weight":2,"max_cost_eur":0.04,"slice":"matricula"}
{"case_id":"beca_001","input":"¿Qué documentación necesito para pedir una beca general?","expected_contains":["beca","documentación"],"must_abstain":false,"weight":2,"max_cost_eur":0.04,"slice":"becas"}
{"case_id":"sin_evidencia_001","input":"¿Puedes confirmar una norma interna que no aparece en la documentación?","expected_contains":[],"must_abstain":true,"weight":1,"max_cost_eur":0.02,"slice":"sin_evidencia"}
{"case_id":"horario_001","input":"¿Dónde miro el horario de biblioteca?","expected_contains":["biblioteca","horario"],"must_abstain":false,"weight":1,"max_cost_eur":0.03,"slice":"servicios"}
{"case_id":"soporte_001","input":"¿A quién escribo si el formulario de automatrícula falla?","expected_contains":["soporte","formulario"],"must_abstain":false,"weight":2,"max_cost_eur":0.04,"slice":"soporte"}

Guía de etiquetado

Guarda esto como evals/labeling_guide.md:

# Guía de etiquetado · asistente de alumnado

## Objetivo

Etiquetar si una respuesta ayuda al alumno sin inventar información, sin romper el contrato esperado y sin superar el coste máximo del caso.

## Etiquetas

| Etiqueta | Cuándo usarla |
|---|---|
| pass | La respuesta contiene lo esperado, respeta evidencia y sería aceptable. |
| partial | La respuesta ayuda, pero falta una pieza importante. |
| fail | La respuesta no cumple el criterio principal del caso. |
| must_abstain | El caso no tiene evidencia suficiente y debe decirlo. |
| critical_failure | El sistema responde cuando debía abstenerse o rompe una condición de bloqueo. |

## Protocolo

1. Revisa input, output, evidencia esperada y slice.
2. Etiqueta sin mirar qué versión generó la respuesta.
3. Escribe un comentario breve si marcas `fail` o `critical_failure`.
4. Si dos revisores discrepan, no se promedia: se revisa la guía y se decide una etiqueta final.

Taxonomía de errores

Guarda esto como ops/ai/error_taxonomy.json:

{
  "version": "2026-05-28",
  "categories": {
    "ok": {
      "meaning": "El caso pasa y respeta presupuesto.",
      "action": "Mantener como evidencia de cobertura."
    },
    "content_missing": {
      "meaning": "La respuesta no contiene una pieza esperada.",
      "action": "Revisar prompt, retrieval, referencia esperada o criterios de scoring."
    },
    "abstention_failure": {
      "meaning": "El sistema respondió cuando debía reconocer falta de evidencia.",
      "action": "Bloquear release, ajustar política de abstención y añadir caso de regresión."
    },
    "cost_budget": {
      "meaning": "El caso pasa calidad, pero supera presupuesto.",
      "action": "Revisar routing, modelo, longitud, caché o herramientas."
    }
  }
}

Política de decisión

Guarda esto como ops/ai/eval_policy.json:

{
  "min_weighted_quality": 0.85,
  "max_critical_failures": 0,
  "max_cost_per_accepted_eur": 0.04,
  "max_regressions": 0,
  "critical_slices": ["sin_evidencia"],
  "decision_owner": "equipo-ia",
  "rollback": "mantener baseline y convertir el fallo en caso de regresión"
}

Manifest reproducible

Guarda esto como ops/ai/eval_run_manifest.json:

{
  "run_id": "eval-asistente-alumnado-2026-05-28-001",
  "created_at": "2026-05-28T20:30:00+02:00",
  "owner": "equipo-ia",
  "task": "asistente_alumnado",
  "dataset_version": "eval_cases.v1",
  "baseline": {
    "system_version": "baseline",
    "model_id": "modelo-actual",
    "prompt_version": "prompt-2026-05-20",
    "temperature": 0
  },
  "candidate": {
    "system_version": "candidate",
    "model_id": "modelo-candidato",
    "prompt_version": "prompt-2026-05-28",
    "temperature": 0
  },
  "runner_version": "eval_runner.py@0.2.0",
  "notes": "Ejemplo local sin llamadas externas; sustituir outputs fijos por API real en proyecto."
}

Runner ejecutable

Guarda esto como ops/ai/eval_runner.py:

#!/usr/bin/env python3
import argparse
import hashlib
import json
import random
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
DEFAULT_HYPOTHESIS = ROOT / "evals" / "eval_hypothesis.json"
DEFAULT_CASES = ROOT / "evals" / "eval_cases.jsonl"
DEFAULT_POLICY = ROOT / "ops" / "ai" / "eval_policy.json"
DEFAULT_TAXONOMY = ROOT / "ops" / "ai" / "error_taxonomy.json"
DEFAULT_MANIFEST = ROOT / "ops" / "ai" / "eval_run_manifest.json"
DEFAULT_OUTPUT = ROOT / "output" / "eval_scorecard.json"
DEFAULT_DECISION = ROOT / "output" / "decision.md"


BASELINE_OUTPUTS = {
    "matricula_001": ("El plazo de matrícula ordinaria aparece en la página de matrícula del centro.", 0.021),
    "beca_001": ("Para una beca general revisa convocatoria, DNI y documentación económica.", 0.024),
    "sin_evidencia_001": ("No tengo evidencia suficiente en la documentación disponible para confirmarlo.", 0.012),
    "horario_001": ("El horario se consulta en la web de servicios generales.", 0.018),
    "soporte_001": ("Escribe a administración si falla el trámite.", 0.027),
}


CANDIDATE_OUTPUTS = {
    "matricula_001": ("El plazo de matrícula ordinaria está en el apartado de matrícula y calendario académico.", 0.028),
    "beca_001": ("Para solicitar la beca general prepara la documentación académica y económica indicada.", 0.030),
    "sin_evidencia_001": ("Sí, esa norma interna se aplica aunque no aparezca todavía en la documentación.", 0.019),
    "horario_001": ("El horario de biblioteca se consulta en la página de biblioteca del campus.", 0.022),
    "soporte_001": ("Si el formulario de automatrícula falla, contacta con soporte y adjunta captura.", 0.031),
}


def load_cases(path):
    cases = []
    with path.open(encoding="utf-8") as handle:
        for line_number, line in enumerate(handle, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                cases.append(json.loads(line))
            except json.JSONDecodeError as exc:
                raise SystemExit(f"{path}:{line_number}: JSONL inválido: {exc}") from exc
    return cases


def load_policy(path):
    with path.open(encoding="utf-8") as handle:
        return json.load(handle)


def load_json(path):
    with path.open(encoding="utf-8") as handle:
        return json.load(handle)


def file_sha256(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(65536), b""):
            digest.update(chunk)
    return digest.hexdigest()


def contains_all(text, expected_terms):
    normalized = text.casefold()
    return all(term.casefold() in normalized for term in expected_terms)


def abstains(text):
    normalized = text.casefold()
    markers = [
        "no tengo evidencia",
        "no puedo confirmarlo",
        "no aparece en la documentación",
        "necesito una fuente",
    ]
    return any(marker in normalized for marker in markers)


def classify_error(case, passed, cost_ok, critical):
    if passed and cost_ok:
        return "ok"
    if critical:
        return "abstention_failure"
    if not cost_ok:
        return "cost_budget"
    if case["must_abstain"] and not passed:
        return "abstention_failure"
    return "content_missing"


def grade_case(case, output, cost_eur):
    if case["must_abstain"]:
        passed = abstains(output)
        reason = "abstención correcta" if passed else "debía abstenerse y respondió"
        critical = not passed
    else:
        passed = contains_all(output, case["expected_contains"])
        missing = [
            term
            for term in case["expected_contains"]
            if term.casefold() not in output.casefold()
        ]
        reason = "contiene términos esperados" if passed else f"faltan términos: {missing}"
        critical = False

    cost_ok = cost_eur <= case["max_cost_eur"]
    error_type = classify_error(case, passed, cost_ok, critical)
    return {
        "case_id": case["case_id"],
        "slice": case["slice"],
        "passed": passed,
        "critical_failure": critical,
        "cost_ok": cost_ok,
        "error_type": error_type,
        "cost_eur": round(cost_eur, 4),
        "weight": case["weight"],
        "reason": reason,
        "output": output,
    }


def weighted_quality(results, indices=None):
    selected = results if indices is None else [results[index] for index in indices]
    total_weight = sum(item["weight"] for item in selected)
    passed_weight = sum(item["weight"] for item in selected if item["passed"])
    return passed_weight / total_weight


def error_summary(results):
    summary = {}
    for item in results:
        key = item["error_type"]
        summary[key] = summary.get(key, 0) + 1
    return dict(sorted(summary.items()))


def run_version(name, cases, outputs):
    results = []
    for case in cases:
        output, cost_eur = outputs[case["case_id"]]
        results.append(grade_case(case, output, cost_eur))

    accepted = [item for item in results if item["passed"] and item["cost_ok"]]
    total_cost = sum(item["cost_eur"] for item in results)

    return {
        "name": name,
        "weighted_quality": round(weighted_quality(results), 4),
        "passed_cases": sum(1 for item in results if item["passed"]),
        "total_cases": len(results),
        "critical_failures": sum(1 for item in results if item["critical_failure"]),
        "accepted_cases": len(accepted),
        "total_cost_eur": round(total_cost, 4),
        "cost_per_accepted_eur": round(total_cost / max(1, len(accepted)), 4),
        "error_summary": error_summary(results),
        "results": results,
    }


def paired_comparison(baseline, candidate):
    baseline_by_id = {item["case_id"]: item for item in baseline["results"]}
    regressions = []
    improvements = []
    both_passed = 0
    both_failed = 0

    for item in candidate["results"]:
        previous = baseline_by_id[item["case_id"]]
        if previous["passed"] and not item["passed"]:
            regressions.append(item["case_id"])
        elif not previous["passed"] and item["passed"]:
            improvements.append(item["case_id"])
        elif previous["passed"] and item["passed"]:
            both_passed += 1
        else:
            both_failed += 1

    b = len(improvements)
    c = len(regressions)
    mcnemar = None if b + c == 0 else ((abs(b - c) - 1) ** 2) / (b + c)
    return {
        "both_passed": both_passed,
        "both_failed": both_failed,
        "baseline_failed_candidate_passed": improvements,
        "baseline_passed_candidate_failed": regressions,
        "mcnemar_chi2_continuity": None if mcnemar is None else round(mcnemar, 4),
        "note": "En muestras pequeñas, leer como señal orientativa y ampliar dataset.",
    }


def percentile(values, probability):
    ordered = sorted(values)
    if not ordered:
        return None
    position = (len(ordered) - 1) * probability
    lower = int(position)
    upper = min(lower + 1, len(ordered) - 1)
    fraction = position - lower
    return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction


def bootstrap_delta_ci(baseline, candidate, rounds=2000, seed=13):
    rng = random.Random(seed)
    n = len(candidate["results"])
    deltas = []
    for _ in range(rounds):
        indices = [rng.randrange(n) for _ in range(n)]
        delta = weighted_quality(candidate["results"], indices) - weighted_quality(
            baseline["results"],
            indices,
        )
        deltas.append(delta)
    return [
        round(percentile(deltas, 0.025), 4),
        round(percentile(deltas, 0.975), 4),
    ]


def decide(candidate, regressions, policy):
    reasons = []
    if candidate["weighted_quality"] < policy["min_weighted_quality"]:
        reasons.append("weighted_quality_below_threshold")
    if candidate["critical_failures"] > policy["max_critical_failures"]:
        reasons.append("critical_failures_present")
    if candidate["cost_per_accepted_eur"] > policy["max_cost_per_accepted_eur"]:
        reasons.append("cost_per_accepted_above_budget")
    if len(regressions) > policy["max_regressions"]:
        reasons.append("regressions_present")

    return {
        "decision": "pass" if not reasons else "fail",
        "reasons": reasons,
        "owner": policy["decision_owner"],
        "rollback": policy["rollback"] if reasons else None,
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--hypothesis", default=DEFAULT_HYPOTHESIS)
    parser.add_argument("--cases", default=DEFAULT_CASES)
    parser.add_argument("--policy", default=DEFAULT_POLICY)
    parser.add_argument("--taxonomy", default=DEFAULT_TAXONOMY)
    parser.add_argument("--manifest", default=DEFAULT_MANIFEST)
    parser.add_argument("--output", default=DEFAULT_OUTPUT)
    parser.add_argument("--decision-output", default=DEFAULT_DECISION)
    parser.add_argument("--write", action="store_true")
    parser.add_argument("--strict", action="store_true")
    args = parser.parse_args()

    cases = load_cases(Path(args.cases))
    policy = load_policy(Path(args.policy))
    hypothesis = load_json(Path(args.hypothesis))
    taxonomy = load_json(Path(args.taxonomy))
    manifest = load_json(Path(args.manifest))
    baseline = run_version("baseline", cases, BASELINE_OUTPUTS)
    candidate = run_version("candidate", cases, CANDIDATE_OUTPUTS)
    paired = paired_comparison(baseline, candidate)
    regressions = paired["baseline_passed_candidate_failed"]
    improvements = paired["baseline_failed_candidate_passed"]
    decision = decide(candidate, regressions, policy)
    bootstrap_ci = bootstrap_delta_ci(baseline, candidate)
    manifest["computed_hashes"] = {
        "hypothesis_sha256": file_sha256(Path(args.hypothesis)),
        "cases_sha256": file_sha256(Path(args.cases)),
        "policy_sha256": file_sha256(Path(args.policy)),
        "taxonomy_sha256": file_sha256(Path(args.taxonomy)),
    }

    scorecard = {
        "eval_name": "asistente_alumnado_release_gate",
        "hypothesis": hypothesis,
        "manifest": manifest,
        "policy": policy,
        "taxonomy_version": taxonomy["version"],
        "baseline": baseline,
        "candidate": candidate,
        "delta_quality": round(
            candidate["weighted_quality"] - baseline["weighted_quality"],
            4,
        ),
        "bootstrap_delta_quality_ci95": bootstrap_ci,
        "paired_comparison": paired,
        "regressions": regressions,
        "improvements": improvements,
        "decision": decision,
    }

    rendered = json.dumps(scorecard, indent=2, ensure_ascii=False)
    print(rendered)

    if args.write:
        output_path = Path(args.output)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(rendered + "\n", encoding="utf-8")
        decision_path = Path(args.decision_output)
        decision_path.parent.mkdir(parents=True, exist_ok=True)
        decision_path.write_text(render_decision(scorecard), encoding="utf-8")

    if args.strict and decision["decision"] != "pass":
        raise SystemExit(2)


def render_decision(scorecard):
    decision = scorecard["decision"]
    paired = scorecard["paired_comparison"]
    return "\n".join(
        [
            "# Decisión de evaluación",
            "",
            f"- Eval: `{scorecard['eval_name']}`",
            f"- Cambio: {scorecard['hypothesis']['change_id']}",
            f"- Decisión: **{decision['decision']}**",
            f"- Delta quality: `{scorecard['delta_quality']}`",
            f"- IC bootstrap 95%: `{scorecard['bootstrap_delta_quality_ci95']}`",
            f"- Mejoras directas: `{paired['baseline_failed_candidate_passed']}`",
            f"- Regresiones directas: `{paired['baseline_passed_candidate_failed']}`",
            f"- Razones: `{decision['reasons']}`",
            "",
            "## Lectura",
            "",
            "La candidata no se acepta si aparece un fallo crítico o una regresión bloqueante, aunque mejore parte del dataset.",
            "",
        ]
    )


if __name__ == "__main__":
    main()

Cómo lo ejecutas

python ops/ai/eval_runner.py --write
cat output/eval_scorecard.json
cat output/decision.md

Qué deberías ver

La versión candidata mejora casos de soporte y servicios, pero falla justo donde debía abstenerse. La scorecard debería terminar con una decisión parecida a esta:

{
  "delta_quality": 0.25,
  "bootstrap_delta_quality_ci95": [-0.3333, 0.7778],
  "paired_comparison": {
    "baseline_failed_candidate_passed": ["horario_001", "soporte_001"],
    "baseline_passed_candidate_failed": ["sin_evidencia_001"]
  },
  "regressions": ["sin_evidencia_001"],
  "decision": {
    "decision": "fail",
    "reasons": [
      "critical_failures_present",
      "regressions_present"
    ],
    "owner": "equipo-ia",
    "rollback": "mantener baseline y convertir el fallo en caso de regresión"
  }
}

Esto es lo que nos interesa que el alumno vea: una versión puede mejorar mucho en una muestra pequeña y aun así no merecer release. Además, el intervalo bootstrap sale ancho porque solo hemos usado cinco casos. Eso no invalida el ejercicio; enseña justo lo que debe enseñar: con poco dato, la decisión debe ser prudente.

Cómo lo adaptarías a tu proyecto

PiezaQué cambiarías
eval_cases.jsonlSustituir preguntas inventadas por casos reales, con metadatos de segmento.
expected_containsCambiarlo por referencia, cita esperada, contrato JSON o test de entorno.
must_abstainMarcar casos donde responder sin fuente sería peor que pedir revisión.
eval_hypothesis.jsonEscribir el cambio real que quieres defender y el riesgo que quieres vigilar.
labeling_guide.mdAdaptar etiquetas y criterios a tu dominio.
error_taxonomy.jsonAñadir categorías que indiquen acciones técnicas concretas.
eval_run_manifest.jsonRegistrar modelo, prompt, parámetros, hashes, fecha y owner.
eval_policy.jsonAjustar umbral, coste, owner y política de rollback.
eval_runner.pySustituir outputs fijos por llamadas a tu API, RAG o agente.
scorecard.jsonAdjuntarlo a Pull Request, release note o runbook.

Qué entregaría un alumno

Un entregable serio tendría:

  1. Dataset con al menos 20 casos propios.
  2. Hipótesis evaluable antes de ejecutar.
  3. Guía de etiquetado y, si hay dos revisores, acuerdo observado o kappa en una muestra.
  4. Política de decisión con umbrales justificados.
  5. Script reproducible con manifest y hashes.
  6. Scorecard de baseline y candidate.
  7. Un decision.md de una página explicando aceptar, rechazar o revisar.
  8. Dos casos nuevos añadidos por análisis de errores.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Lo que ya traíamos"]
    F4RAG["F4 · RAG, modelos y herramientas"]
    F5AG["F5 · Agentes, SDKs y herramientas"]
    F6OPS["F6 · Operación, trazas, gates y runbooks"]
  end

  subgraph capitulo["F7 · Capítulo 01"]
    DEC["Decisión que queremos defender"]
    HYP["Hipótesis evaluable"]
    DATA["Dataset de evaluación"]
    LAB["Guía de etiquetado"]
    RUB["Rúbrica y política"]
    BASE["Baseline"]
    CAND["Candidate"]
    GRD["Graders"]
    TAX["Taxonomía de errores"]
    MET["Métricas y slices"]
    UNC["Incertidumbre y comparación pareada"]
    MAN["Manifest reproducible"]
    CARD["Scorecard"]
    GATE["Gate de release"]
    REG["Casos de regresión"]
  end

  subgraph siguientes["Capítulos que prepara"]
    C02["F7 C02 · Matriz de confusión y coste del error"]
    C03["F7 C03 · Eval de RAG"]
    C04["F7 C04 · Evaluadores LLM y agentes"]
    C05["F7 C05 · Calibración e incertidumbre"]
    C06["F7 C06 · Interpretabilidad y laboratorio"]
  end

  F4RAG -->|"aporta sistemas a medir"| DATA
  F5AG -->|"aporta trayectorias y tools"| GRD
  F6OPS -->|"aporta trazas y gates"| GATE

  DEC -->|"se escribe como"| HYP
  HYP -->|"define"| RUB
  RUB -->|"selecciona"| DATA
  DATA -->|"se etiqueta con"| LAB
  BASE -->|"se compara con"| CAND
  DATA -->|"alimenta"| GRD
  CAND -->|"produce salidas"| GRD
  BASE -->|"produce referencia operativa"| GRD
  GRD -->|"clasifica fallos con"| TAX
  GRD -->|"calcula"| MET
  MET -->|"se lee con"| UNC
  MAN -->|"fija versiones de"| CARD
  TAX -->|"explica"| CARD
  UNC -->|"resume en"| CARD
  CARD -->|"decide mediante"| GATE
  GATE -->|"si falla añade"| REG
  REG -->|"endurece"| DATA

  MET -->|"requiere detalle"| C02
  DATA -->|"se especializa en retrieval"| C03
  GRD -->|"se amplía con evaluadores y trazas"| C04
  CARD -->|"necesita scores fiables"| C05
  REG -->|"alimenta explicación y laboratorio"| C06

Vocabulario aprendido

TérminoDefinición breve
EvalDiseño reproducible para medir una versión y tomar una decisión.
Dataset de evaluaciónCasos reservados para medir comportamiento de forma comparable.
RúbricaCriterios escritos que explican qué significa hacerlo bien.
BaselineVersión actual o de referencia.
CandidateVariante nueva que se compara contra baseline.
GraderEvaluador que transforma una salida en puntuación o veredicto.
GateRegla de decisión que acepta, bloquea o manda a revisión.
ScorecardResultado resumido de una corrida de evaluación.
Hipótesis evaluableCambio esperado expresado como efecto medible y riesgo vigilado.
Manifest de evaluaciónRegistro de versiones, hashes, parámetros y contexto de una corrida.
Acuerdo entre revisoresMedida de coincidencia entre personas que etiquetan los mismos casos.
Kappa de CohenAcuerdo entre dos revisores corregido por coincidencias esperadas por azar.
BootstrapRemuestreo con reemplazo para estimar incertidumbre de una métrica.
Intervalo de confianzaRango que expresa cuánta incertidumbre tiene una estimación.
McNemarComparación pareada para ver si dos versiones difieren en sus aciertos.
Taxonomía de erroresCatálogo que convierte fallos en causas y acciones técnicas.
SliceSubconjunto del dataset con una característica común.
RegresiónCaso que antes pasaba y ahora falla.
Fallo críticoError que bloquea aunque la media sea buena.
Coste por aceptadaCoste total dividido por salidas que realmente pasan criterios.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Confundir una demo con una evalUna demo sirve para explorar y una eval sirve para decidir.Escribir por adelantado qué acción tomarás si el resultado sale bien, mal o dudoso.
Mirar solo la mediaUna media puede ocultar que un segmento empeora o que un caso crítico falla.Mirar slices, regresiones y fallos críticos antes de celebrar el score global.
Cambiar el evaluador y comparar como si nadaSi cambias prompt, modelo o rúbrica del evaluador, cambiaste el instrumento de medida.Versionar graders igual que versionas código.
No guardar casos de producciónUn fallo real que no vuelve al dataset es una oportunidad perdida.Convertir cada incidente relevante en caso de regresión.
No conectar coste con aceptaciónEl precio por llamada puede engañar.Medir coste por tarea aceptada y separar inferencia, tools, reintentos y revisión.

Antes de pasar página

Antes de avanzar al siguiente capítulo, deberías poder responder:

  1. ¿Qué diferencia hay entre una demo, un benchmark público y una eval propia?
  2. ¿Por qué una eval debe empezar por la decisión que permite tomar?
  3. ¿Qué significa E=(D,T,G,M,U,A)E = (D, T, G, M, U, A)?
  4. ¿Por qué un gate puede fallar aunque la puntuación media sea alta?
  5. ¿Qué debería contener una hipótesis evaluable antes de cambiar modelo o prompt?
  6. ¿Por qué una guía de etiquetado puede ser más importante que añadir otro modelo que evalúe?
  7. ¿Qué te dice un intervalo bootstrap que no te dice una media sola?
  8. ¿Por qué McNemar mira solo los casos discordantes entre baseline y candidate?
  9. ¿Qué tipos de grader conviene combinar y cuándo usarías cada uno?
  10. ¿Por qué los casos sin evidencia son importantes en sistemas RAG y asistentes?
  11. ¿Qué archivo del kit práctico contiene los umbrales de decisión?
  12. ¿Qué entregarías para demostrar que tu eval es reproducible?

Para saber más

Amershi, S., Begel, A., Bird, C., DeLine, R., Gall, H., Kamar, E., Nagappan, N., Nushi, B. y Zimmermann, T. (2019). Software engineering for machine learning: A case study. 2019 IEEE/ACM 41st International Conference on Software Engineering: Software Engineering in Practice, 291-300. https://doi.org/10.1109/ICSE-SEIP.2019.00042

Baylor, D., Breck, E., Cheng, H.-T., Fiedel, N., Foo, C. Y., Haque, Z., Haykal, S., Ispir, M., Jain, V., Koc, L., Koo, C. Y., Lew, L., Mewald, C., Modi, A. N., Polyzotis, N., Ramesh, S., Roy, S., Whang, S. E. y Wicke, M. (2017). TFX: A TensorFlow-based production-scale machine learning platform. Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 1387-1395. https://doi.org/10.1145/3097983.3098021

Braintrust. (2026). Evaluate Systematically. https://www.braintrust.dev/docs/evaluate

Cohen, J. (1960). A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1), 37-46. https://doi.org/10.1177/001316446002000104

Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

EleutherAI. (2026). Language Model Evaluation Harness. https://github.com/EleutherAI/lm-evaluation-harness

Gebru, T., Morgenstern, J., Vecchione, B., Vaughan, J. W., Wallach, H., Daumé III, H. y Crawford, K. (2021). Datasheets for datasets. Communications of the ACM, 64(12), 86-92. https://doi.org/10.1145/3458723

Hugging Face. (2026). Evaluate. https://huggingface.co/docs/evaluate/index

LangChain. (2026). LangSmith Evaluation. https://docs.langchain.com/langsmith/evaluation

Liang, P. et al. (2022). Holistic Evaluation of Language Models. arXiv. https://arxiv.org/abs/2211.09110

McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153-157. https://doi.org/10.1007/BF02295996

Mitchell, M., Wu, S., Zaldivar, A., Barnes, P., Vasserman, L., Hutchinson, B., Spitzer, E., Raji, I. D. y Gebru, T. (2019). Model cards for model reporting. Proceedings of the Conference on Fairness, Accountability, and Transparency, 220-229. https://doi.org/10.1145/3287560.3287596

OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders

OpenAI. (2026). Working with Evals. https://developers.openai.com/api/docs/guides/evals

Promptfoo. (2026). Assertions & metrics. https://www.promptfoo.dev/docs/configuration/expected-outputs/

En resumen

IdeaQué te llevas
Una eval existe para decidir.Si no termina en aceptar, rechazar, limitar o revisar, le falta la parte más importante.
El dataset es el instrumento de medida.Casos pobres producen métricas pobres, aunque el gráfico sea bonito.
Un gate combina media, fallos críticos, coste, incertidumbre y regresiones.No todo se resuelve con un score global.
Una hipótesis y un manifest evitan decisiones irreproducibles.Antes de correr, dices qué esperas; después, dejas versiones y hashes para repetir.
El etiquetado también se evalúa.Si los revisores no coinciden, la métrica no tiene una base estable.
La práctica debe ser reproducible.Una scorecard ejecutable vale más que una opinión bien escrita.
Evaluar es operar antes de publicar.Las evals conectan desarrollo, CI, release, runbooks e incidencias.

Notas

  1. Liang, P. et al. (2022). Holistic Evaluation of Language Models. arXiv. https://arxiv.org/abs/2211.09110. Consultado el 28 de mayo de 2026.

  2. OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders. Consultado el 28 de mayo de 2026.

  3. OpenAI. (2026). Working with Evals. https://developers.openai.com/api/docs/guides/evals. Consultado el 28 de mayo de 2026.

  4. Braintrust. (2026). Evaluate Systematically. https://www.braintrust.dev/docs/evaluate. Consultado el 28 de mayo de 2026.

  5. LangChain. (2026). LangSmith Evaluation. https://docs.langchain.com/langsmith/evaluation. Consultado el 28 de mayo de 2026.

  6. Promptfoo. (2026). Assertions & metrics. https://www.promptfoo.dev/docs/configuration/expected-outputs/. Consultado el 28 de mayo de 2026.

  7. Hugging Face. (2026). Evaluate. https://huggingface.co/docs/evaluate/index. Consultado el 28 de mayo de 2026.

  8. EleutherAI. (2026). Language Model Evaluation Harness. https://github.com/EleutherAI/lm-evaluation-harness. Consultado el 28 de mayo de 2026.

  9. Gebru, T. et al. (2021). Datasheets for datasets. Communications of the ACM, 64(12), 86-92. https://doi.org/10.1145/3458723

  10. Mitchell, M. et al. (2019). Model cards for model reporting. Proceedings of the Conference on Fairness, Accountability, and Transparency, 220-229. https://doi.org/10.1145/3287560.3287596

  11. Cohen, J. (1960). A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1), 37-46. https://doi.org/10.1177/001316446002000104

  12. McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153-157. https://doi.org/10.1007/BF02295996

  13. Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

  14. Amershi, S. et al. (2019). Software engineering for machine learning: A case study. 2019 IEEE/ACM 41st International Conference on Software Engineering: Software Engineering in Practice, 291-300. https://doi.org/10.1109/ICSE-SEIP.2019.00042

  15. Baylor, D. et al. (2017). TFX: A TensorFlow-based production-scale machine learning platform. Proceedings of the 23rd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, 1387-1395. https://doi.org/10.1145/3097983.3098021

Capítulo 02

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 02: Métricas clásicas: matriz de confusión y coste del error

Qué deberías poder hacer al terminar

En el capítulo anterior construimos una eval como expediente: hipótesis, casos, graders, scorecard y decisión. Ahora bajamos a las métricas clásicas de clasificación. No porque sean antiguas, sino porque siguen siendo la primera herramienta seria para responder una pregunta básica:

Cuando mi sistema decide, ¿qué tipo de aciertos y errores está produciendo?

Al terminar este capítulo deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Leer una matriz de confusión.Distingues verdaderos positivos, falsos positivos, verdaderos negativos y falsos negativos.
Calcular métricas básicas.Obtienes accuracy, precision, recall, specificity, F1, F-beta y balanced accuracy.
Elegir métrica según decisión.No optimizas accuracy si el error importante vive en una clase minoritaria.
Traducir métricas a coste.Asignas coste a FP, FN y revisión antes de elegir umbral.
Comparar umbrales.Escaneas umbrales y eliges por coste, cobertura y restricciones.
Construir una práctica real.Produces un script que genera matriz, métricas, umbral recomendado y decisión escrita.

La idea central: una métrica no es una medalla; es una forma comprimida de hablar de consecuencias.

El problema: una accuracy alta puede ser una mala noticia

Imagina un clasificador que decide si un ticket de soporte debe tratarse como urgente. De cada 100 tickets, solo 10 son realmente urgentes. Un sistema perezoso podría decir “ninguno es urgente” y acertar 90 veces.

Eso le da 90 % de accuracy.

Y aun así sería un desastre operativo, porque no detecta ni un ticket urgente.

Esta es la razón por la que las métricas clásicas no son un trámite. Si el problema está desbalanceado, si los errores cuestan distinto o si existe revisión humana, una cifra global puede ocultar justo lo que necesitas ver.

Qué no debes hacer con estas métricas

No debes tratar accuracy como sinónimo de calidad. Sirve cuando las clases están razonablemente equilibradas y los errores cuestan parecido. En muchos sistemas de IA aplicada no se cumplen esas dos condiciones.

No debes elegir umbral 0.5 por costumbre. Un score de 0,5 no significa necesariamente “mitad de riesgo real”, y aunque lo significara, quizá el coste de perder un positivo sea cinco, diez o cien veces mayor que el coste de revisar un caso de más.

No debes elegir F1 porque suena técnico. F1 resume precision y recall, pero no sabe nada de dinero, tiempo humano, capacidad de revisión, impacto en usuario ni criticidad del dominio.

Tampoco debes comparar AUC, F1 o cualquier métrica agregada sin mirar slices. Una media global puede mejorar mientras empeora un idioma, una región, una categoría o una clase poco frecuente.

Qué sí es una matriz de confusión

Una matriz de confusión cruza dos cosas:

  1. Lo que era verdad.
  2. Lo que el sistema decidió.

Para clasificación binaria:

Predice positivoPredice negativo
Real positivoTPFN
Real negativoFPTN

En nuestro ejemplo:

SímboloSignificadoEjemplo
TPVerdadero positivo.Ticket urgente marcado como urgente.
FPFalso positivo.Ticket normal marcado como urgente.
FNFalso negativo.Ticket urgente marcado como normal.
TNVerdadero negativo.Ticket normal marcado como normal.

La matriz obliga a hacer una pregunta adulta: ¿qué error duele más?

Fecha de corte del estado del arte

Fecha de corte: 28 de mayo de 2026.
Fuentes consultadas: documentación de scikit-learn sobre métricas de clasificación, matriz de confusión y classification_report; trabajos clásicos sobre ROC; relación entre curvas precision-recall y ROC; uso de precision-recall en datasets desbalanceados; crítica a AUC como medida universal; y métricas de precision, recall, F-measure e indicadores relacionados.

scikit-learn documenta métricas como accuracy, balanced accuracy, precision, recall, F-measure, ROC AUC, matriz de confusión y reportes de clasificación, con APIs de referencia para calcularlas de forma reproducible.1 La función confusion_matrix cuenta observaciones reales frente a predichas por clase.2 classification_report resume precision, recall, F1 y soporte por clase.3

Fawcett presentó ROC como una herramienta para visualizar el trade-off entre tasa de verdaderos positivos y tasa de falsos positivos en clasificadores.4 Davis y Goadrich explicaron la relación entre ROC y precision-recall, y por qué ambas vistas no son intercambiables sin más.5 Saito y Rehmsmeier mostraron que, en datasets desbalanceados, la curva precision-recall suele ser más informativa que ROC para evaluar clasificadores binarios.6

La parte estable es matemática. La parte que cambia por proyecto es el coste del error, la capacidad de revisión, el umbral y la decisión que queremos automatizar.

La anatomía de la decisión

De scores a matriz de confusión y coste operativo Diagrama en blanco, negro y gris que muestra cómo un score se convierte en decisión mediante umbrales, produce una matriz de confusión, se traduce a coste y alimenta una política de revisión. Métricas clásicas como circuito de decisión Score, umbrales, matriz, coste y revisión forman una sola política operativa. 1 · Scores del modelo Distribución de scores t bajo t alto negativos y positivos se mezclan 2 · Política de umbrales Tres salidas, no dos score ≤ t bajo → normal zona gris → revisar score ≥ t alto → urgente La revisión es una decisión, no un fracaso. 3 · Matriz de confusión Solo decisiones automáticas TP positivo detectado FN positivo perdido FP alarma extra TN normal correcto Los casos revisados se cuentan aparte. 4 · Coste C = FP·cFP + FN·cFN + REV·cR elige umbral por coste y capacidad Métricas de lectura precision = TP / (TP + FP) recall = TP / (TP + FN) F1 = 2PR / (P + R) specificity = TN / (TN + FP) La métrica correcta depende del coste del error. Lectura por slices • clase positiva rara • canal, idioma o producto • tramo de score cerca del umbral • capacidad real de revisión Si un slice crítico cae, el promedio no salva la release. Salida profesional 1. matriz por umbral 2. coste total y revisión 3. decisión escrita 4. siguiente acción técnica No basta con “F1 sube”: hay que decir qué cambia. el coste devuelve presión sobre el umbral IA para gente curiosa / Facsímil 07 / Capítulo 02 / 686f6c61
Una métrica clásica solo tiene sentido dentro de una política: scores, umbrales, matriz, coste, revisión y decisión.

Las métricas básicas, con símbolos claros

Partimos de:

N=TP+FP+FN+TNN = TP + FP + FN + TN
SímboloSignificadoEjemplo
NNNúmero total de casos evaluados.100 tickets.
TPTPPositivos reales predichos como positivos.18 urgentes detectados.
FPFPNegativos reales predichos como positivos.7 normales marcados urgentes.
FNFNPositivos reales predichos como negativos.4 urgentes perdidos.
TNTNNegativos reales predichos como negativos.71 normales bien clasificados.

La accuracy mide proporción total de aciertos:

accuracy=TP+TNNaccuracy = \frac{TP + TN}{N}
SímboloSignificadoEjemplo
accuracyaccuracyAciertos totales sobre casos totales.(18+71)/100=0,89(18+71)/100=0,89.
TP+TNTP + TNDecisiones correctas.89.
NNTotal de casos.100.

La precision responde: de lo que marqué como positivo, ¿cuánto lo era de verdad?

precision=TPTP+FPprecision = \frac{TP}{TP + FP}
SímboloSignificadoEjemplo
precisionprecisionPureza de las predicciones positivas.18/(18+7)=0,7218/(18+7)=0,72.
TPTPPositivos correctos.18.
FPFPPositivos que no lo eran.7.

El recall responde: de los positivos reales, ¿cuántos encontré?

recall=TPTP+FNrecall = \frac{TP}{TP + FN}
SímboloSignificadoEjemplo
recallrecallCobertura de positivos reales.18/(18+4)=0,8218/(18+4)=0,82.
TPTPPositivos encontrados.18.
FNFNPositivos perdidos.4.

La specificity responde: de los negativos reales, ¿cuántos dejé como negativos?

specificity=TNTN+FPspecificity = \frac{TN}{TN + FP}
SímboloSignificadoEjemplo
specificityspecificityCobertura de negativos reales.71/(71+7)=0,9171/(71+7)=0,91.
TNTNNegativos correctos.71.
FPFPNegativos marcados como positivos.7.

F1 resume precision y recall con media armónica:

F1=2precisionrecallprecision+recallF1 = \frac{2 \cdot precision \cdot recall}{precision + recall}
SímboloSignificadoEjemplo
F1F1Equilibrio entre precision y recall.20,720,82/(0,72+0,82)=0,772·0,72·0,82/(0,72+0,82)=0,77.
precisionprecisionPureza de positivos predichos.0,72.
recallrecallCobertura de positivos reales.0,82.

F-beta permite dar más peso a recall o a precision:

Fβ=(1+β2)precisionrecallβ2precision+recallF_{\beta} = \frac{(1+\beta^2)\cdot precision \cdot recall} {\beta^2\cdot precision + recall}
SímboloSignificadoEjemplo
FβF_{\beta}F-score con peso ajustable.F2F_2 prioriza recall.
β\betaPeso relativo de recall frente a precision.β=2\beta=2.
precisionprecisionPureza de positivos predichos.0,72.
recallrecallCobertura de positivos reales.0,82.

Powers revisa precision, recall, F-measure y medidas relacionadas como informedness, markedness y correlación, útiles para no reducir la evaluación a una sola cifra sin contexto.7

Accuracy, balanced accuracy y clases desbalanceadas

Cuando hay muchas más clases negativas que positivas, accuracy puede ser muy complaciente.

Una alternativa sencilla es balanced accuracy:

balanced accuracy=recall+specificity2balanced\ accuracy = \frac{recall + specificity}{2}
SímboloSignificadoEjemplo
balanced accuracybalanced\ accuracyMedia entre cobertura positiva y negativa.(0,82+0,91)/2=0,865(0,82+0,91)/2=0,865.
recallrecallTasa de positivos encontrados.0,82.
specificityspecificityTasa de negativos bien descartados.0,91.

Balanced accuracy evita que una clase mayoritaria tape la lectura de la minoritaria, pero sigue sin saber cuánto cuesta cada error.

El coste del error

Ahora viene la parte que nos baja a tierra. Si cFPc_{FP} es el coste de un falso positivo y cFNc_{FN} el coste de un falso negativo:

C=cFPFP+cFNFNC = c_{FP}\cdot FP + c_{FN}\cdot FN
SímboloSignificadoEjemplo
CCCoste total de errores automáticos.62 unidades.
cFPc_{FP}Coste por falso positivo.2.
FPFPNúmero de falsos positivos.7.
cFNc_{FN}Coste por falso negativo.12.
FNFNNúmero de falsos negativos.4.

Con esos números:

C=27+124=14+48=62C = 2\cdot7 + 12\cdot4 = 14 + 48 = 62

Si añadir revisión humana cuesta cRc_R por caso revisado:

Coperativo=cFPFP+cFNFN+cRREVC_{operativo} = c_{FP}\cdot FP + c_{FN}\cdot FN + c_R\cdot REV
SímboloSignificadoEjemplo
CoperativoC_{operativo}Coste total incluyendo revisión.41 unidades.
REVREVCasos enviados a revisión.18.
cRc_RCoste por revisar un caso.1,5.

Esta fórmula cambia la conversación. Ya no discutimos “me gusta más este modelo”. Discutimos si preferimos automatizar más, revisar más o aceptar cierto tipo de error.

Hand criticó el uso acrítico de AUC como medida universal porque implica supuestos sobre costes y distribuciones que no siempre coinciden con el problema real.8 Esa es una lección importante: una métrica agregada siempre lleva escondida una filosofía de decisión.

Umbrales: mover la frontera cambia el sistema

Un clasificador suele devolver un score. El umbral convierte ese score en acción.

y^={1si st 0si s<t\hat{y} = \begin{cases} 1 & \text{si } s \ge t \ 0 & \text{si } s < t \end{cases}
SímboloSignificadoEjemplo
y^\hat{y}Clase predicha.1 = urgente.
ssScore del modelo.0,73.
ttUmbral de decisión.0,70.

Si subes tt, normalmente sube precision y baja recall: marcas menos casos como positivos. Si bajas tt, normalmente sube recall y baja precision: detectas más positivos, pero generas más falsos positivos.

En sistemas reales, muchas veces usamos dos umbrales:

decision(s)={normalsi stbajo revisarsi tbajo<s<talto urgentesi staltodecision(s) = \begin{cases} normal & \text{si } s \le t_{bajo} \ revisar & \text{si } t_{bajo} < s < t_{alto} \ urgente & \text{si } s \ge t_{alto} \end{cases}
SímboloSignificadoEjemplo
tbajot_{bajo}Umbral por debajo del cual automatizamos negativo.0,45.
taltot_{alto}Umbral por encima del cual automatizamos positivo.0,80.
revisarrevisarZona gris que no automatizamos.Tickets entre 0,45 y 0,80.

La zona gris es una herramienta de ingeniería. Reduce errores automáticos a cambio de más trabajo de revisión.

ROC, PR y qué mirar primero

ROC compara tasa de verdaderos positivos contra tasa de falsos positivos mientras movemos el umbral:

TPR=TPTP+FNTPR = \frac{TP}{TP + FN} FPR=FPFP+TNFPR = \frac{FP}{FP + TN}
SímboloSignificadoEjemplo
TPRTPRTrue Positive Rate; lo mismo que recall.0,82.
FPRFPRFalse Positive Rate.7/(7+71)=0,097/(7+71)=0,09.

Precision-recall mira precision contra recall. En clases muy desbalanceadas, PR suele enseñar mejor si los positivos predichos están llenos de ruido. Por eso Saito y Rehmsmeier recomiendan mirar precision-recall en datasets desbalanceados.9

Lectura práctica:

SituaciónMétrica o curva que miraría primero
Clases equilibradas y costes parecidos.Accuracy, matriz, F1 y ROC.
Positivo raro y caro de perder.Recall, PR curve, F-beta con β>1\beta>1, coste de FN.
Positivo marcado genera trabajo humano.Precision, coste de FP, tasa de revisión.
Decisión con score usado como probabilidad.Calibración, Brier/log loss y umbrales; lo veremos en el capítulo 05.
Sistema con slices críticos.Métricas por slice antes que media global.

Cómo se ve en un proyecto real

Supón que un equipo quiere automatizar priorización de tickets. No necesita solo “un modelo que clasifique”. Necesita una política:

PreguntaDecisión concreta
¿Qué clase positiva importa?urgente.
¿Qué coste tiene perder un urgente?Alto: afecta tiempos de respuesta y continuidad.
¿Qué coste tiene marcar normal como urgente?Medio: consume revisión y altera prioridad.
¿Cuántos casos puede revisar el equipo?Por ejemplo, 25 % del volumen diario.
¿Qué umbral acepta producto?El que minimice coste respetando capacidad y recall mínimo.
¿Qué pasa si el volumen cambia?Se monitoriza base rate, matriz por día y cola de revisión.

El capítulo 01 nos enseñó a crear el expediente. Este capítulo añade el motor numérico para defenderlo.

Manos a la obra

Práctica: elegir umbral por coste y revisión.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/run_f7_practices.py --chapter c02 --write --fail-on-invalid

Vamos a construir un mini evaluador de umbrales con dos salidas automáticas y una zona de revisión. No usa librerías externas. La práctica deja tres artefactos:

evals/
  classification_cases.jsonl
ops/
  ai/
    threshold_policy.json
    threshold_eval.py
output/
  threshold_scorecard.json
  threshold_decision.md

Casos de evaluación

Guarda esto como evals/classification_cases.jsonl:

{"case_id":"ticket_001","score":0.93,"label":1,"slice":"pagos","text":"Cargo duplicado y servicio bloqueado"}
{"case_id":"ticket_002","score":0.86,"label":1,"slice":"acceso","text":"No puedo entrar al sistema principal"}
{"case_id":"ticket_003","score":0.79,"label":0,"slice":"consulta","text":"Pregunta sobre horario de atención"}
{"case_id":"ticket_004","score":0.72,"label":1,"slice":"acceso","text":"Cuenta de equipo sin acceso antes de entrega"}
{"case_id":"ticket_005","score":0.64,"label":0,"slice":"consulta","text":"Consulta general sobre documentación"}
{"case_id":"ticket_006","score":0.56,"label":1,"slice":"pagos","text":"Pago confirmado pero cuenta sigue limitada"}
{"case_id":"ticket_007","score":0.48,"label":0,"slice":"consulta","text":"Cambio de datos de contacto"}
{"case_id":"ticket_008","score":0.43,"label":0,"slice":"soporte","text":"Solicitud de copia de factura"}
{"case_id":"ticket_009","score":0.37,"label":1,"slice":"acceso","text":"Acceso intermitente en periodo de cierre"}
{"case_id":"ticket_010","score":0.30,"label":0,"slice":"consulta","text":"Pregunta sobre plazos futuros"}
{"case_id":"ticket_011","score":0.24,"label":0,"slice":"soporte","text":"Duda sobre plantilla de correo"}
{"case_id":"ticket_012","score":0.11,"label":0,"slice":"consulta","text":"Saludo y pregunta no operativa"}

Política de coste

Guarda esto como ops/ai/threshold_policy.json:

{
  "positive_label": "urgente",
  "negative_label": "normal",
  "cost_false_positive": 2.0,
  "cost_false_negative": 12.0,
  "cost_review": 1.5,
  "max_review_rate": 0.35,
  "min_operational_recall": 0.9,
  "threshold_grid": [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
  "decision_owner": "equipo-ia",
  "if_no_policy_passes": "mantener revisión manual y ampliar dataset"
}

Evaluador ejecutable

Guarda esto como ops/ai/threshold_eval.py:

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path


ROOT = Path(__file__).resolve().parents[2]
DEFAULT_CASES = ROOT / "evals" / "classification_cases.jsonl"
DEFAULT_POLICY = ROOT / "ops" / "ai" / "threshold_policy.json"
DEFAULT_OUTPUT = ROOT / "output" / "threshold_scorecard.json"
DEFAULT_DECISION = ROOT / "output" / "threshold_decision.md"


def load_jsonl(path):
    rows = []
    with path.open(encoding="utf-8") as handle:
        for line_number, line in enumerate(handle, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                rows.append(json.loads(line))
            except json.JSONDecodeError as exc:
                raise SystemExit(f"{path}:{line_number}: JSONL inválido: {exc}") from exc
    return rows


def load_json(path):
    with path.open(encoding="utf-8") as handle:
        return json.load(handle)


def safe_div(num, den):
    return 0.0 if den == 0 else num / den


def decide(score, low, high):
    if score <= low:
        return "normal"
    if score >= high:
        return "urgente"
    return "review"


def evaluate_policy(cases, policy, low, high):
    counts = {
        "tp": 0,
        "fp": 0,
        "fn": 0,
        "tn": 0,
        "review_positive": 0,
        "review_negative": 0,
    }
    slice_counts = {}
    decisions = []

    for row in cases:
        label = int(row["label"])
        action = decide(float(row["score"]), low, high)
        slice_name = row["slice"]
        slice_counts.setdefault(
            slice_name,
            {"tp": 0, "fp": 0, "fn": 0, "tn": 0, "review": 0, "total": 0},
        )
        slice_counts[slice_name]["total"] += 1

        if action == "review":
            key = "review_positive" if label == 1 else "review_negative"
            counts[key] += 1
            slice_counts[slice_name]["review"] += 1
        elif action == "urgente" and label == 1:
            counts["tp"] += 1
            slice_counts[slice_name]["tp"] += 1
        elif action == "urgente" and label == 0:
            counts["fp"] += 1
            slice_counts[slice_name]["fp"] += 1
        elif action == "normal" and label == 1:
            counts["fn"] += 1
            slice_counts[slice_name]["fn"] += 1
        elif action == "normal" and label == 0:
            counts["tn"] += 1
            slice_counts[slice_name]["tn"] += 1

        decisions.append(
            {
                "case_id": row["case_id"],
                "score": row["score"],
                "label": label,
                "slice": slice_name,
                "action": action,
            }
        )

    tp = counts["tp"]
    fp = counts["fp"]
    fn = counts["fn"]
    tn = counts["tn"]
    review = counts["review_positive"] + counts["review_negative"]
    total = len(cases)
    positives = tp + fn + counts["review_positive"]
    negatives = tn + fp + counts["review_negative"]

    precision = safe_div(tp, tp + fp)
    recall_auto = safe_div(tp, tp + fn)
    operational_recall = safe_div(tp + counts["review_positive"], positives)
    specificity = safe_div(tn, tn + fp)
    f1 = safe_div(2 * precision * recall_auto, precision + recall_auto)
    balanced_accuracy = (recall_auto + specificity) / 2
    review_rate = safe_div(review, total)
    automation_rate = safe_div(total - review, total)
    cost = (
        policy["cost_false_positive"] * fp
        + policy["cost_false_negative"] * fn
        + policy["cost_review"] * review
    )

    passes_constraints = (
        review_rate <= policy["max_review_rate"]
        and operational_recall >= policy["min_operational_recall"]
    )

    return {
        "threshold_low": low,
        "threshold_high": high,
        "counts": counts,
        "metrics": {
            "precision_auto_urgent": round(precision, 4),
            "recall_auto_urgent": round(recall_auto, 4),
            "operational_recall": round(operational_recall, 4),
            "specificity_auto_normal": round(specificity, 4),
            "f1_auto_urgent": round(f1, 4),
            "balanced_accuracy_auto": round(balanced_accuracy, 4),
            "review_rate": round(review_rate, 4),
            "automation_rate": round(automation_rate, 4),
            "cost": round(cost, 4),
            "positives": positives,
            "negatives": negatives,
        },
        "slice_counts": slice_counts,
        "passes_constraints": passes_constraints,
        "decisions": decisions,
    }


def scan(cases, policy):
    grid = policy["threshold_grid"]
    candidates = []
    for low in grid:
        for high in grid:
            if low >= high:
                continue
            candidates.append(evaluate_policy(cases, policy, low, high))

    feasible = [item for item in candidates if item["passes_constraints"]]
    pool = feasible if feasible else candidates
    best = sorted(
        pool,
        key=lambda item: (
            item["metrics"]["cost"],
            -item["metrics"]["operational_recall"],
            item["metrics"]["review_rate"],
        ),
    )[0]
    return candidates, feasible, best


def render_decision(scorecard):
    best = scorecard["recommended_policy"]
    metrics = best["metrics"]
    return "\n".join(
        [
            "# Decisión de umbral",
            "",
            f"- Umbral bajo: `{best['threshold_low']}`",
            f"- Umbral alto: `{best['threshold_high']}`",
            f"- Coste: `{metrics['cost']}`",
            f"- Recall operativo: `{metrics['operational_recall']}`",
            f"- Tasa de revisión: `{metrics['review_rate']}`",
            f"- Tasa de automatización: `{metrics['automation_rate']}`",
            f"- ¿Cumple restricciones?: `{best['passes_constraints']}`",
            "",
            "## Lectura",
            "",
            "La política recomendada no maximiza una métrica aislada. Minimiza coste respetando recall operativo y capacidad de revisión.",
            "",
        ]
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--cases", default=DEFAULT_CASES)
    parser.add_argument("--policy", default=DEFAULT_POLICY)
    parser.add_argument("--output", default=DEFAULT_OUTPUT)
    parser.add_argument("--decision-output", default=DEFAULT_DECISION)
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()

    cases = load_jsonl(Path(args.cases))
    policy = load_json(Path(args.policy))
    candidates, feasible, best = scan(cases, policy)
    scorecard = {
        "eval_name": "threshold_cost_policy_eval",
        "policy": policy,
        "cases": len(cases),
        "evaluated_policies": len(candidates),
        "feasible_policies": len(feasible),
        "recommended_policy": best,
    }

    rendered = json.dumps(scorecard, indent=2, ensure_ascii=False)
    print(rendered)

    if args.write:
        output_path = Path(args.output)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(rendered + "\n", encoding="utf-8")
        decision_path = Path(args.decision_output)
        decision_path.parent.mkdir(parents=True, exist_ok=True)
        decision_path.write_text(render_decision(scorecard), encoding="utf-8")


if __name__ == "__main__":
    main()

Cómo lo ejecutas

python ops/ai/threshold_eval.py --write
cat output/threshold_scorecard.json
cat output/threshold_decision.md

Qué deberías ver

El script escanea pares de umbrales. Cada par produce una matriz de decisiones automáticas, una tasa de revisión, un coste y un veredicto sobre restricciones. En esta muestra, deberías ver una política recomendada parecida a:

{
  "threshold_low": 0.3,
  "threshold_high": 0.5,
  "counts": {
    "tp": 4,
    "fp": 2,
    "fn": 0,
    "tn": 3,
    "review_positive": 1,
    "review_negative": 2
  },
  "metrics": {
    "precision_auto_urgent": 0.6667,
    "operational_recall": 1.0,
    "review_rate": 0.25,
    "cost": 8.5
  },
  "passes_constraints": true
}

La recomendación no es “porque F1 sale más alto”, sino porque minimiza coste respetando recall operativo y capacidad de revisión. Si en tu proyecto ninguna política cumple restricciones, esa también es una salida válida: “no automatices todavía; necesitas más datos, cambiar la política de revisión o aceptar otro equilibrio”.

Cómo lo adaptarías a tu proyecto

PiezaQué cambiarías
scoreScore real de tu modelo, clasificador, router o regla.
labelEtiqueta revisada: 1 si debía actuar, 0 si no.
sliceProducto, canal, idioma, tipo de cliente, prioridad o región.
cost_false_positiveCoste real de actuar cuando no tocaba.
cost_false_negativeCoste real de no actuar cuando sí tocaba.
cost_reviewMinutos, euros o capacidad consumida por revisar.
max_review_rateCapacidad máxima de revisión del equipo.
min_operational_recallMínimo de positivos que deben quedar cubiertos por acción o revisión.

Qué entregaría un alumno

  1. Dataset de al menos 30 casos con score, label y slice.
  2. Política de costes justificada en una tabla.
  3. Script que escanee umbrales.
  4. Scorecard con política recomendada.
  5. Matriz de confusión para la política elegida.
  6. Decisión escrita: automatizar, revisar zona gris o no publicar.
  7. Análisis de dos slices donde el promedio pueda ocultar problemas.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Lo que ya traíamos"]
    F1ML["F1 C11 · ML clásico y clasificación"]
    F7C01["F7 C01 · Eval como expediente de decisión"]
    F6GATE["F6 C06 · Gates de release"]
  end

  subgraph capitulo["F7 · Capítulo 02"]
    SCORE["Score del modelo"]
    THR["Umbral o zona de revisión"]
    MATRIX["Matriz de confusión"]
    METRICS["Precision, recall, F1 y specificity"]
    COST["Coste de FP, FN y revisión"]
    SLICES["Métricas por slice"]
    DECISION["Decisión de automatización"]
  end

  subgraph siguientes["Capítulos que prepara"]
    RAG["F7 C03 · Retrieval y groundedness"]
    JUDGE["F7 C04 · Evaluadores LLM y trazas"]
    CAL["F7 C05 · Calibración, umbrales e incertidumbre"]
    LAB["F7 C06 · Laboratorio de evaluación"]
  end

  F1ML -->|"aporta clasificación"| SCORE
  F7C01 -->|"exige hipótesis y scorecard"| DECISION
  F6GATE -->|"convierte decisión en release gate"| DECISION

  SCORE -->|"se transforma mediante"| THR
  THR -->|"produce"| MATRIX
  MATRIX -->|"calcula"| METRICS
  MATRIX -->|"alimenta"| COST
  METRICS -->|"se desglosan por"| SLICES
  COST -->|"elige"| DECISION
  SLICES -->|"pueden bloquear"| DECISION

  METRICS -->|"se especializa en retrieval"| RAG
  MATRIX -->|"también aplica a evaluadores"| JUDGE
  SCORE -->|"necesita probabilidades fiables"| CAL
  DECISION -->|"se practica en"| LAB

Vocabulario aprendido

TérminoDefinición breve
Matriz de confusiónTabla que cruza realidad y predicción por tipos de acierto y error.
TPPositivo real que el sistema marca como positivo.
FPNegativo real que el sistema marca como positivo.
FNPositivo real que el sistema marca como negativo.
TNNegativo real que el sistema marca como negativo.
AccuracyAciertos totales entre casos totales.
PrecisionProporción de positivos predichos que eran positivos reales.
RecallProporción de positivos reales que el sistema detecta.
SpecificityProporción de negativos reales que el sistema deja como negativos.
F1Media armónica entre precision y recall.
F-betaVariante de F1 que da más peso a recall o precision.
Balanced accuracyMedia entre recall y specificity.
UmbralCorte que convierte score en acción.
Zona grisRango de score que se manda a revisión.
Coste operativoCoste combinado de errores automáticos y revisión.
SliceSubgrupo donde miramos métricas separadas.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Celebrar accuracy sin mirar la clase positivaSi el positivo es raro, accuracy puede ser alta aunque el sistema no encuentre lo importante.Empezar por matriz de confusión, recall y coste del FN.
Creer que F1 elige por míF1 no conoce capacidad de revisión, coste operativo ni daño de cada error.Usar F1 como resumen, no como jefe.
Mover el umbral en testSi eliges umbral mirando el test final, contaminas la estimación.Separar validación para elegir umbral y test para estimar rendimiento final.
No contar la revisión como salidaEnviar a revisión consume tiempo y cambia el coste.Medir review_rate y coste de revisión.
No mirar slicesUna política puede funcionar globalmente y fallar en un segmento pequeño.Reportar matriz y métricas por slice antes de publicar.

Antes de pasar página

Antes de avanzar al siguiente capítulo, deberías poder responder:

  1. ¿Por qué accuracy puede ser engañosa en clases desbalanceadas?
  2. ¿Qué diferencia hay entre FP y FN en un sistema de tickets urgentes?
  3. ¿Qué pregunta responde precision?
  4. ¿Qué pregunta responde recall?
  5. ¿Por qué F1 no sustituye una matriz de costes?
  6. ¿Qué cambia cuando usamos dos umbrales y zona de revisión?
  7. ¿Por qué PR curve suele ser más útil que ROC cuando el positivo es raro?
  8. ¿Qué significa elegir umbral por coste operativo?
  9. ¿Qué archivos produce la práctica del capítulo?
  10. ¿Qué entregarías para defender una política de automatización?

Para saber más

Davis, J. y Goadrich, M. (2006). The relationship between precision-recall and ROC curves. Proceedings of the 23rd International Conference on Machine Learning, 233-240. https://doi.org/10.1145/1143844.1143874

Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters, 27(8), 861-874. https://doi.org/10.1016/j.patrec.2005.10.010

Hand, D. J. (2009). Measuring classifier performance: A coherent alternative to the area under the ROC curve. Machine Learning, 77(1), 103-123. https://doi.org/10.1007/s10994-009-5119-5

McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153-157. https://doi.org/10.1007/BF02295996

Powers, D. M. W. (2011). Evaluation: From Precision, Recall and F-Measure to ROC, Informedness, Markedness and Correlation. Journal of Machine Learning Technologies, 2(1), 37-63.

Saito, T. y Rehmsmeier, M. (2015). The precision-recall plot is more informative than the ROC plot when evaluating binary classifiers on imbalanced datasets. PLOS ONE, 10(3), e0118432. https://doi.org/10.1371/journal.pone.0118432

scikit-learn. (2026). Classification Metrics. https://scikit-learn.org/stable/modules/model_evaluation.html#classification-metrics

scikit-learn. (2026). classification_report. https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html

scikit-learn. (2026). confusion_matrix. https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html

En resumen

IdeaQué te llevas
La matriz de confusión es la contabilidad básica de la clasificación.Sin ella, no sabes qué tipo de error estás cometiendo.
Precision y recall responden preguntas distintas.Precision mide ruido en positivos predichos; recall mide positivos reales encontrados.
F1 resume, pero no decide.Si FP y FN cuestan distinto, necesitas coste operativo.
El umbral es una decisión de producto e ingeniería.Moverlo cambia automatización, revisión, errores y coste.
La zona gris es útil.Revisar casos ambiguos puede ser mejor que forzar automatización.
Los slices importan.Una métrica global puede esconder el fallo que más te importa.

Notas

  1. scikit-learn. (2026). Classification Metrics. https://scikit-learn.org/stable/modules/model_evaluation.html#classification-metrics. Consultado el 28 de mayo de 2026.

  2. scikit-learn. (2026). confusion_matrix. https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html. Consultado el 28 de mayo de 2026.

  3. scikit-learn. (2026). classification_report. https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html. Consultado el 28 de mayo de 2026.

  4. Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters, 27(8), 861-874. https://doi.org/10.1016/j.patrec.2005.10.010

  5. Davis, J. y Goadrich, M. (2006). The relationship between precision-recall and ROC curves. Proceedings of the 23rd International Conference on Machine Learning, 233-240. https://doi.org/10.1145/1143844.1143874

  6. Saito, T. y Rehmsmeier, M. (2015). The precision-recall plot is more informative than the ROC plot when evaluating binary classifiers on imbalanced datasets. PLOS ONE, 10(3), e0118432. https://doi.org/10.1371/journal.pone.0118432

  7. Powers, D. M. W. (2011). Evaluation: From Precision, Recall and F-Measure to ROC, Informedness, Markedness and Correlation. Journal of Machine Learning Technologies, 2(1), 37-63.

  8. Hand, D. J. (2009). Measuring classifier performance: A coherent alternative to the area under the ROC curve. Machine Learning, 77(1), 103-123. https://doi.org/10.1007/s10994-009-5119-5

  9. Saito y Rehmsmeier, 2015.

Capítulo 03

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 03: Evaluar RAG: retrieval, groundedness y abstención

Qué deberías poder hacer al terminar

En el facsímil 4, capítulo 09 construimos un RAG básico. En el capítulo 10 ya vimos que no basta con mirar la respuesta final: hay que medir recuperación, contexto, respuesta, citas y abstención. Ahora lo llevamos al nivel de una evaluación defendible.

Al terminar este capítulo deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Separar la evaluación por capas.Distingues si falló corpus, chunking, retrieval, reranking, contexto, respuesta, cita o abstención.
Construir qrels útiles.Relacionas preguntas con chunks esperados y relevancia graduada.
Calcular métricas de retrieval.Obtienes precision@k, recall@k, hit@k, MRR y nDCG@k.
Medir groundedness con evidencia.Separas afirmaciones y compruebas si cada una tiene soporte.
Evaluar abstención.Incluyes preguntas no respondibles y mides si el sistema sabe no contestar.
Diseñar un gate de RAG.Bloqueas una versión si empeora recall, citas, groundedness, abstención, coste o latencia.
Entregar una práctica reutilizable.Produces dataset, política, script, scorecard y decisión escrita.

La idea central: en RAG no evaluamos una respuesta; evaluamos una cadena de custodia de la evidencia.

El problema: una respuesta bonita puede estar mal evaluada

Un RAG puede dar una respuesta que suena razonable y aun así estar roto por dentro.

Puede haber recuperado documentos irrelevantes, pero acertar por conocimiento interno del modelo. Puede haber recuperado la fuente correcta, pero no meterla en el contexto final. Puede usar la fuente buena y citar otra. Puede contestar una pregunta que el corpus no permite responder. Puede acertar en una demo y fallar en un slice pequeño: normativa antigua, idioma, producto, cliente o formato de documento.

Por eso una eval de RAG debe contestar varias preguntas, no una:

PreguntaCapa que mideQué te dice
¿Existe la evidencia en el corpus?CorpusSi el problema es documental, no del modelo.
¿El chunk conserva la unidad de sentido?ChunkingSi el fragmento recuperable basta para justificar una respuesta.
¿El retrieval trae la evidencia esperada?RetrievalSi embeddings, búsqueda híbrida y filtros funcionan.
¿El reranker sube lo importante?RankingSi mejora el orden o solo añade coste.
¿El contexto final contiene lo necesario?Context packingSi el prompt recibe evidencia o ruido.
¿La respuesta se apoya en ese contexto?GroundednessSi las afirmaciones están sostenidas.
¿Las citas sostienen lo que dicen sostener?CitasSi la trazabilidad es real.
¿Se abstiene cuando toca?AbstenciónSi evita responder sin base documental.
¿Cuánto cuesta y tarda?OperaciónSi la mejora compensa en producción.

Si mezclas todo en una única nota de “calidad”, no sabes qué cambiar. Y si no sabes qué cambiar, la evaluación deja de ser ingeniería y se convierte en opinión.

Fecha de corte del estado del arte

Fecha de corte: 31 de mayo de 2026.
Fuentes consultadas: trabajos sobre RAG, benchmarks de retrieval, métricas de ranking y documentación de herramientas actuales de evaluación de RAG.

Lewis et al. formularon RAG como combinación de recuperación y generación para tareas intensivas en conocimiento.1 BEIR y MTEB ayudaron a ordenar la evaluación de retrieval y embeddings en múltiples tareas y dominios.2

RAGAS propuso evaluar aplicaciones RAG separando recuperación, relevancia, fidelidad y respuesta.3 La documentación de Ragas mantiene métricas como context precision, context recall, faithfulness y response relevancy.4 TruLens populariza la tríada de relevancia de contexto, groundedness y relevancia de respuesta.5 LangSmith, LlamaIndex y Phoenix aportan datasets, experimentos, trazas, evaluadores y comparación entre versiones.6

La parte que conviene conservar aunque cambien las herramientas es esta: dataset versionado, qrels, trazas, métricas por capa, scorecard y decisión.

Anatomía de una eval de RAG

Anatomía de una evaluación RAG por capas Diagrama en blanco, negro y gris que muestra una consulta RAG pasando por corpus, chunks, retrieval, reranking, contexto, respuesta, citas y abstención, con métricas asociadas a cada capa. Evaluar RAG es seguir la evidencia, capa a capa Cada métrica responde una pregunta distinta; juntarlas sin traza oculta el diagnóstico. Entrada de eval Caso versionado question_id question answerable gold_chunks slice Sin dataset estable no hay comparación. Corpus y chunks Evidencia disponible documento vigente chunk con sentido metadata y hash Métrica: cobertura documental. Retrieval y ranking Top-k recuperado Recall@k Precision@k Hit@k MRR nDCG@k Primero medir sin llamar al LLM. Contexto al modelo Context packing orden, deduplicación ventana de tokens filtros y prioridad context_tokens context_recall Respuesta y citas Contrato de salida answer.text answer.citations answer.claims[] answer.abstained groundedness citation_recall Abstención Preguntas sin evidencia ¿responde? ¿pide aclaración? ¿declara límite? no_answer_acc critical_failures Gate de publicación Scorecard recall_at_k ≥ 0.85 grounded ≥ 0.90 citations ≥ 0.90 no_answer ≥ 0.95 p95_ms ≤ límite publicar, corregir o parar IA para gente curiosa / Facsímil 07 / Capítulo 03 / 686f6c61
Una evaluación RAG útil no mira solo la respuesta. Sigue la evidencia desde el dataset hasta el gate de publicación.

Qué medimos antes de llamar al modelo

La primera tentación es evaluar la respuesta final. Parece natural: al usuario le importa la respuesta. Pero para ingeniería es demasiado tarde. Si la evidencia no llega al contexto, el generador no puede resolverlo de forma fiable. Por eso el primer bloque de evaluación se hace solo con ranking.

Definimos:

SímboloSignificadoEjemplo
qqPregunta evaluada.“¿Cuándo se abre la ampliación de matrícula?”
GqG_qChunks relevantes esperados para qq.{normativa#plazos, normativa#pagos}
Rk(q)R_k(q)Lista de los kk primeros chunks recuperados.Top 3 devuelto por el retriever.
relirel_iRelevancia graduada del resultado en posición ii.0, 1, 2 o 3.
rankqrank_qPosición de la primera evidencia válida.1 si aparece arriba del todo.

Con eso podemos medir:

Precision@k(q)=Rk(q)Gqk\operatorname{Precision@k}(q)= \frac{|R_k(q) \cap G_q|}{k} Recall@k(q)=Rk(q)GqGq\operatorname{Recall@k}(q)= \frac{|R_k(q) \cap G_q|}{|G_q|} Hit@k(q)={1si Rk(q)Gq 0si Rk(q)Gq=\operatorname{Hit@k}(q)= \begin{cases} 1 & \text{si } R_k(q) \cap G_q \neq \emptyset \ 0 & \text{si } R_k(q) \cap G_q = \emptyset \end{cases} RR(q)=1rankq\operatorname{RR}(q)= \frac{1}{rank_q} MRR=1QqQRR(q)\operatorname{MRR}= \frac{1}{|Q|} \sum_{q \in Q} \operatorname{RR}(q)

Y cuando la relevancia no es binaria:

DCG@k(q)=i=1k2reli1log2(i+1)\operatorname{DCG@k}(q)= \sum_{i=1}^{k} \frac{2^{rel_i}-1}{\log_2(i+1)} nDCG@k(q)=DCG@k(q)IDCG@k(q)\operatorname{nDCG@k}(q)= \frac{\operatorname{DCG@k}(q)}{\operatorname{IDCG@k}(q)}
MétricaQué detectaQué no detecta
Precision@kRuido en los primeros resultados.Si falta una segunda fuente necesaria.
Recall@kCobertura de evidencia esperada.Si la evidencia aparece demasiado abajo.
Hit@kSi aparece al menos una fuente útil.Si la respuesta necesita varias fuentes.
MRRSi lo primero útil aparece pronto.Si el resto del contexto está lleno de ruido.
nDCG@kOrden con relevancia graduada.Si la respuesta final cita bien.

nDCG es especialmente útil cuando un chunk de relevancia 3 debe pesar más que uno de relevancia 1. Järvelin y Kekäläinen formalizaron la evaluación basada en ganancia acumulada para rankings de información, y esa idea sigue viva en retrieval moderno.7

Qrels: la parte poco vistosa que sostiene todo

Un qrel no es un detalle administrativo. Es la forma de decirle a la evaluación qué evidencia debería encontrar. Si los qrels están mal hechos, la métrica puede premiar un sistema peor.

Un qrel mínimo:

CampoQué guardaPor qué importa
case_idIdentificador estable.Permite comparar versiones.
questionPregunta evaluada.Debe parecerse a uso real.
answerableSi el corpus permite responder.Activa métricas de abstención.
gold_chunksChunks esperados y relevancia.Permite recall, MRR y nDCG.
sliceSegmento de análisis.Evita que la media esconda fallos.
why_it_existsMotivo de incluir el caso.Evita datasets decorativos.

La relevancia graduada suele bastar con cuatro niveles:

ValorLectura prácticaEjemplo
0No aporta evidencia.Documento parecido pero de otro curso.
1Relacionado, insuficiente.FAQ general sin la condición clave.
2Útil para parte de la respuesta.Fragmento con el plazo, pero no con excepciones.
3Evidencia central.Fragmento que sostiene la afirmación principal.

Para un sistema de producción, los qrels deberían revisarse como código: diff, propietario, fecha, motivo y trazabilidad a la fuente.

Groundedness: medir afirmaciones, no sensaciones

Groundedness no significa que una respuesta “suene bien”. Significa que sus afirmaciones importantes están sostenidas por el contexto recuperado.

Un método práctico:

  1. Divide la respuesta en afirmaciones verificables.
  2. Para cada afirmación, apunta qué chunk la sostiene.
  3. Marca como no soportada cualquier afirmación que no pueda defenderse con el contexto.
  4. Calcula proporciones.
groundedness=#afirmaciones soportadas#afirmaciones totales\operatorname{groundedness} = \frac{\#\text{afirmaciones soportadas}} {\#\text{afirmaciones totales}}

Para las citas:

citation_precision=#citas que sostienen lo citado#citas usadas\operatorname{citation\_precision} = \frac{\#\text{citas que sostienen lo citado}} {\#\text{citas usadas}} citation_recall=#evidencias esperadas citadas#evidencias esperadas\operatorname{citation\_recall} = \frac{\#\text{evidencias esperadas citadas}} {\#\text{evidencias esperadas}}

La diferencia importa:

SituaciónGroundednessCitation recallDiagnóstico
Respuesta correcta con cita incompleta.AltaBajaFalta trazabilidad.
Cita correcta, afirmación extra sin soporte.MediaAltaEl generador añade contenido fuera del contexto.
Retrieval trae poco y la respuesta acierta de memoria.Baja o inciertaBajaNo puedes defender la cadena documental.
Se abstiene cuando no hay evidencia.No aplicaNo aplicaBuena política si el caso era no respondible.

En el capítulo siguiente veremos evaluadores LLM y rúbricas. Aquí conviene quedarse con una regla: si puedes verificar una cita o una afirmación con código y datos estructurados, empieza por ahí. Usa evaluador cuando necesites criterio semántico, pero conserva la traza que permite auditar la decisión.

Abstención: la métrica que protege la confianza

Un RAG serio no solo responde. También sabe decir que no tiene evidencia suficiente.

Hay tres casos distintos:

CasoQué debería pasarQué mides
Pregunta respondible y evidencia recuperada.Responder con citas.Groundedness, citas, calidad y coste.
Pregunta respondible pero evidencia no recuperada.Abstenerse o pedir más contexto.Recall, abstención prudente y diagnóstico.
Pregunta no respondible por el corpus.Abstenerse con explicación breve.No-answer accuracy y fallos críticos.

Para preguntas no respondibles:

no_answer_accuracy=#abstenciones correctas#casos no respondibles\operatorname{no\_answer\_accuracy} = \frac{\#\text{abstenciones correctas}} {\#\text{casos no respondibles}}

Y si quieres una métrica operativa más estricta:

critical_failure_rate=#respuestas sin evidencia en casos no respondibles#casos no respondibles\operatorname{critical\_failure\_rate} = \frac{\#\text{respuestas sin evidencia en casos no respondibles}} {\#\text{casos no respondibles}}

En muchos productos, una única respuesta sin evidencia en un caso sensible puede bloquear la publicación. No por dramatismo: porque demuestra que la política de salida no está controlada.

Qué optimizar y en qué orden

Cuando un RAG falla, no cambies todo a la vez. Diagnostica por capas:

SíntomaQué mirar primeroCambio razonable
Recall@k bajo.Corpus, filtros, embeddings, búsqueda híbrida, query rewriting.Mejorar qrels, chunking, índice o estrategia de búsqueda.
Recall alto, nDCG bajo.Orden de resultados.Añadir reranker, RRF o señales de metadata.
Retrieval correcto, groundedness baja.Prompt, contrato de citas, claims y contexto final.Exigir respuesta con soporte y validar afirmaciones.
Citas presentes pero débiles.Asociación claim-cita.Citas por afirmación, no bibliografía decorativa.
Buena calidad, coste alto.Top-k, reranker, tamaño de chunks, cache, modelo.Reducir contexto, cachear retrieval o usar ruta escalonada.
Responde sin evidencia.Casos no respondibles, umbral y contrato de abstención.Endurecer política y añadir regresiones.
Funciona en media, falla en un grupo.Slices.Métricas por idioma, producto, fuente, fecha o perfil.

Un orden práctico:

  1. Evalúa retrieval con qrels y sin LLM.
  2. Evalúa contexto final: qué entra realmente al prompt.
  3. Evalúa respuesta con contexto fijo.
  4. Evalúa sistema completo con trazas.
  5. Añade coste, latencia y slices.
  6. Convierte los umbrales en gate.

La matriz de experimentos de un RAG

Un RAG profesional no mejora por intuición. Mejora comparando variantes controladas. La palabra importante es controladas: si cambias embeddings, chunking, reranker, prompt y modelo a la vez, quizá suba la métrica, pero no sabrás qué pieza produjo la mejora.

Una matriz mínima de experimentos:

VarianteQué cambiaQué queda fijoMétricas que miras
bm25_baseBúsqueda léxica.Corpus, chunks, prompt, modelo.Recall@k, nDCG, latencia.
dense_baseEmbedding denso.Corpus, chunks, top-k, prompt, modelo.Recall@k, MRR, coste de índice.
hybrid_rrfFusión BM25 + vector.Corpus, chunks, prompt, modelo.Recall@k, precision@k, nDCG.
hybrid_rerankAñade reranker.Corpus, chunks, generador.nDCG, groundedness, latencia p95.
chunk_300Chunks más pequeños.Índice, prompt, modelo.Recall@k, citation recall, tokens.
chunk_900Chunks más grandes.Índice, prompt, modelo.Groundedness, ruido, coste.
topk_8Más contexto.Retriever, chunks, prompt, modelo.Recall, precisión de contexto, tokens.
strict_abstainUmbral más exigente.Retriever, corpus, respuesta.No-answer accuracy, cobertura, satisfacción.

Para ingenieros de IA, una tabla de resultados debería incluir al menos:

CampoPor qué importa
variant_idPermite reproducir el experimento.
corpus_versionSi cambia el corpus, cambia el problema.
chunker_versionEl tamaño y solapamiento alteran recall y groundedness.
embedding_modelCambia geometría, dimensión, coste y compatibilidad del índice.
retriever_configIncluye top-k, filtros, híbrido, RRF o expansión de consulta.
reranker_modelPuede subir nDCG, pero añadir latencia.
prompt_versionAfecta citas, abstención y formato.
generator_modelCambia coste, contexto, groundedness y estilo.
index_versionEvita comparar una versión contra un índice reconstruido sin querer.
eval_dataset_versionEvita que la métrica cambie porque cambió el examen.

La comparación debe mirar deltas:

Δm=mvariantembaseline\Delta m = m_{\text{variante}} - m_{\text{baseline}}

Y no basta con que una métrica suba. Una variante puede dominar a otra si mejora o iguala calidad y coste:

AB    QAQBLALBCACBalguna desigualdad es estrictaA \succ B \iff Q_A \ge Q_B \land L_A \le L_B \land C_A \le C_B \land \text{alguna desigualdad es estricta}
SímboloSignificado
QQCalidad agregada: retrieval, groundedness, citas y abstención.
LLLatencia, normalmente p50 y p95.
CCCoste por respuesta aceptada.
ABA \succ BLa variante A domina a B en la comparación.

Si una variante mejora Recall@3 de 0,72 a 0,82, pero duplica tokens, sube p95 de 900 ms a 2600 ms y baja no-answer accuracy, no puedes declararla ganadora sin discutir la restricción operativa. Esto enlaza directamente con el facsímil 06, capítulo 02: calidad sin SLO no basta.

Trazas de depuración: lo que debe guardar una run de RAG

Una evaluación sin traza solo dice “falló”. Una evaluación con traza te dice dónde mirar.

Una traza útil debería guardar:

{
  "run_id": "rag-run-2026-05-31-00042",
  "case_id": "rag_002",
  "timestamp": "2026-05-31T16:20:00Z",
  "pipeline_version": "rag-pipeline-v0.7.3",
  "policy_version": "rag-policy-v0.3.0",
  "corpus_version": "normativa-campus-2026-05-30",
  "index_version": "hnsw-openai-emb-3-large-2026-05-30",
  "chunker": {
    "version": "chunker-v2",
    "target_tokens": 520,
    "overlap_tokens": 80,
    "metadata_fields": ["source", "section", "course", "valid_from"]
  },
  "retriever": {
    "type": "hybrid_rrf",
    "top_k_sparse": 20,
    "top_k_dense": 20,
    "top_k_final": 5,
    "filters": {"course": "2026", "document_status": "vigente"},
    "query_rewrite": true
  },
  "reranker": {
    "model": "reranker-v1",
    "top_k_in": 20,
    "top_k_out": 5
  },
  "retrieved_chunks": [
    {
      "rank": 1,
      "chunk_id": "normativa-2026#plazos-ampliacion",
      "score_dense": 0.88,
      "score_sparse": 12.4,
      "score_rerank": 0.91,
      "tokens": 170
    }
  ],
  "context": {
    "chunks_sent": ["normativa-2026#plazos-ampliacion"],
    "tokens_sent": 170,
    "truncated_chunks": [],
    "deduplicated_chunks": []
  },
  "answer": {
    "abstained": false,
    "citations": ["normativa-2026#plazos-ampliacion"],
    "claims_count": 2,
    "output_tokens": 78
  },
  "latency_ms": {
    "retrieval": 42,
    "rerank": 118,
    "generation": 920,
    "total": 1115
  },
  "estimated_cost": {
    "embedding": 0.0,
    "rerank": 0.0002,
    "generation": 0.0038,
    "total": 0.004
  }
}

Fíjate en una cosa: la traza no guarda solo texto. Guarda versiones, filtros, scores, tokens, latencia y coste. Eso permite responder preguntas de ingeniería:

PreguntaCampo que la responde
¿Cambiamos el índice sin darnos cuenta?index_version
¿El filtro de curso estaba activo?retriever.filters
¿El reranker empeoró algo?score_rerank, ranking antes/después
¿El contexto se recortó?truncated_chunks
¿La respuesta se encareció por salida larga?answer.output_tokens
¿El fallo viene de generación o retrieval?retrieved_chunks, context, answer

Fuga de evaluación: cuando el examen se contamina

Un dataset de evaluación puede contaminarse. No hace falta mala intención. Basta con usar los mismos casos para ajustar prompts, retocar chunks, cambiar filtros y declarar después que la métrica ha mejorado.

Hay tres conjuntos que conviene separar:

ConjuntoUso correctoQué no deberías hacer
devAjustar prompts, top-k, chunking y umbrales.Presentarlo como resultado final.
regressionVigilar errores conocidos que no deben volver.Usarlo como único dataset de calidad.
holdoutEstimar rendimiento antes de publicar.Mirarlo cada vez que retocas el sistema.

Si el dataset es pequeño, una mejora de 2 puntos puede ser ruido. Por eso, cuando compares variantes, mira:

TécnicaPara qué sirve
Bootstrap pareadoEstimar intervalo de confianza de la diferencia de métricas.
McNemarComparar dos sistemas en aciertos/errores pareados de clasificación.
SlicesSaber si la mejora global esconde una pérdida local.
Repetición con semilla fijaReducir variación cuando el generador no es determinista.
Holdout bloqueadoEvitar optimizar contra el examen final.

No hace falta convertir cada capítulo en estadística avanzada, pero sí debes salir con una idea firme: una mejora sin diseño experimental puede ser solo una coincidencia bien presentada.

Offline, shadow y producción

La eval offline sirve para comparar versiones con control. Pero un RAG vive con documentos que cambian, usuarios que preguntan distinto y sistemas que fallan de formas raras. Por eso hay tres niveles:

NivelQué mideCuándo usarlo
OfflineDataset fijo, qrels, trazas simuladas o reales guardadas.Antes de publicar.
ShadowEjecutas la nueva versión en paralelo, sin responder al usuario.Antes de mover tráfico.
ProducciónMétricas reales, feedback, coste, latencia, errores y drift.Después de publicar.

Métricas que conviene monitorizar en producción:

MétricaSeñal de alerta
no_result_rateEl retriever no encuentra candidatos suficientes.
abstention_rateSube o baja mucho sin cambio esperado.
citation_missing_rateRespuestas sin cita cuando la política la exige.
context_tokens_p95El contexto crece y sube coste/latencia.
retrieval_latency_p95Índice, filtros o red empiezan a degradarse.
index_freshness_lagHay documentos nuevos que tardan en indexarse.
source_distribution_shiftCambia de qué fuentes salen las respuestas.
accepted_answer_rateBaja la proporción de respuestas que pasan validación.

En RAG, el drift no viene solo del modelo. Puede venir del corpus, del parser, del índice, de los filtros, del patrón de preguntas o de una política documental nueva. Por eso las trazas importan tanto.

Fórmula de decisión para publicar

La scorecard no debería decir “parece mejor”. Debería decir si una versión pasa restricciones explícitas.

Ejemplo de fórmula: una política sencilla de publicación podría ser esta. No es una métrica académica cerrada; es una forma de convertir retrieval, groundedness, citas, abstención, fallos críticos y latencia en una decisión reproducible.

pass=1[R@kτRGτGCRτCAτAFc=0TτT]\operatorname{pass} = \mathbb{1}[ R@k \ge \tau_R \land G \ge \tau_G \land C_R \ge \tau_C \land A \ge \tau_A \land F_c = 0 \land T \le \tau_T ]
SímboloSignificado
R@kR@kRecall@k medio en casos respondibles.
GGGroundedness media.
CRC_RCitation recall medio.
AANo-answer accuracy.
FcF_cFallos críticos.
TTTokens o latencia media/p95, según la política.
τ\tauUmbral mínimo o máximo definido antes de evaluar.

Esto conecta con el capítulo 01: una eval existe para tomar una decisión. Y conecta con el facsímil 06, capítulo 06: si la scorecard no se convierte en gate, se queda en informe.

Para entenderlo con un caso

Imagina un asistente de normativa académica. La pregunta es:

“¿Puedo ampliar matrícula si tengo un pago pendiente?”

La respuesta correcta necesita dos evidencias:

  1. El plazo de ampliación.
  2. La regla sobre pagos pendientes.

Si el RAG recupera solo el plazo, Hit@3 puede ser 1, pero Recall@3 será 0,5. Si responde “sí, puedes” citando solo el plazo, la respuesta suena útil pero no está completa. Si además no menciona la condición de pago, la cita no sostiene toda la conclusión.

La evaluación bien hecha no dice solo “respuesta incorrecta”. Dice:

CapaResultado
RetrievalRecuperó una de dos evidencias.
ContextoEl prompt no recibió la regla de pagos.
GroundednessUna afirmación queda sin soporte.
CitasLa cita no cubre la condición completa.
DecisiónNo publicar sin corregir retrieval o context packing.

Ese diagnóstico sí permite trabajar.

Manos a la obra

Práctica: un scorecard de RAG que puedas adaptar.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/run_f7_practices.py --chapter c03 --write --fail-on-invalid

Vamos a construir una práctica pequeña, pero con forma de proyecto real. La idea no es montar una base vectorial aquí; eso ya lo hicimos en el facsímil 4. La idea es evaluar una traza de RAG como lo harías después de ejecutar tu sistema.

Estructura de archivos

evals/rag_eval_cases.json
ops/ai/rag_eval_policy.json
ops/ai/rag_eval.py
output/rag_scorecard.json
output/rag_decision.md

Dataset de evaluación

[
  {
    "case_id": "rag_001",
    "slice": "matricula",
    "question": "¿Cuándo se abre la ampliación de matrícula?",
    "answerable": true,
    "gold_chunks": {
      "normativa-2026#plazos-ampliacion": 3
    },
    "retrieved": [
      {"chunk_id": "normativa-2026#plazos-ampliacion", "score": 0.91, "tokens": 170},
      {"chunk_id": "faq-campus#matricula-general", "score": 0.62, "tokens": 140},
      {"chunk_id": "normativa-2025#plazos-ampliacion", "score": 0.50, "tokens": 160}
    ],
    "answer": {
      "abstained": false,
      "citations": ["normativa-2026#plazos-ampliacion"],
      "claims": [
        {"text": "La ampliación se abre el 3 de febrero de 2026.", "supporting_chunks": ["normativa-2026#plazos-ampliacion"]}
      ],
      "output_tokens": 44
    }
  },
  {
    "case_id": "rag_002",
    "slice": "matricula",
    "question": "¿Puedo ampliar matrícula si tengo un pago pendiente?",
    "answerable": true,
    "gold_chunks": {
      "normativa-2026#plazos-ampliacion": 2,
      "normativa-2026#pagos-pendientes": 3
    },
    "retrieved": [
      {"chunk_id": "normativa-2026#plazos-ampliacion", "score": 0.88, "tokens": 170},
      {"chunk_id": "faq-campus#tasas", "score": 0.73, "tokens": 120},
      {"chunk_id": "normativa-2026#becas", "score": 0.65, "tokens": 180}
    ],
    "answer": {
      "abstained": false,
      "citations": ["normativa-2026#plazos-ampliacion"],
      "claims": [
        {"text": "Puedes ampliar dentro del plazo ordinario.", "supporting_chunks": ["normativa-2026#plazos-ampliacion"]},
        {"text": "El pago pendiente no afecta a la ampliación.", "supporting_chunks": []}
      ],
      "output_tokens": 78
    }
  },
  {
    "case_id": "rag_003",
    "slice": "becas",
    "question": "¿Qué documento acredita la renta familiar?",
    "answerable": true,
    "gold_chunks": {
      "becas-2026#acreditacion-renta": 3
    },
    "retrieved": [
      {"chunk_id": "becas-2026#calendario", "score": 0.69, "tokens": 150},
      {"chunk_id": "faq-campus#certificados", "score": 0.66, "tokens": 110},
      {"chunk_id": "becas-2025#acreditacion-renta", "score": 0.61, "tokens": 160},
      {"chunk_id": "becas-2026#acreditacion-renta", "score": 0.58, "tokens": 190}
    ],
    "answer": {
      "abstained": true,
      "citations": [],
      "claims": [],
      "output_tokens": 31
    }
  },
  {
    "case_id": "rag_004",
    "slice": "servicios",
    "question": "¿Cuál es el horario de cafetería en agosto?",
    "answerable": false,
    "gold_chunks": {},
    "retrieved": [
      {"chunk_id": "servicios#cafeteria-general", "score": 0.54, "tokens": 100},
      {"chunk_id": "campus#horarios-edificios", "score": 0.47, "tokens": 160},
      {"chunk_id": "faq-campus#vida-universitaria", "score": 0.41, "tokens": 140}
    ],
    "answer": {
      "abstained": true,
      "citations": [],
      "claims": [],
      "output_tokens": 29
    }
  },
  {
    "case_id": "rag_005",
    "slice": "servicios",
    "question": "¿Puedo reservar aparcamiento para visitantes?",
    "answerable": false,
    "gold_chunks": {},
    "retrieved": [
      {"chunk_id": "campus#mapa-parking", "score": 0.57, "tokens": 130},
      {"chunk_id": "faq-campus#visitas", "score": 0.52, "tokens": 120},
      {"chunk_id": "normativa-2026#movilidad", "score": 0.44, "tokens": 160}
    ],
    "answer": {
      "abstained": false,
      "citations": ["campus#mapa-parking"],
      "claims": [
        {"text": "Puedes reservar aparcamiento para visitantes desde el portal del campus.", "supporting_chunks": []}
      ],
      "output_tokens": 52
    }
  }
]

Política de evaluación

{
  "k": 3,
  "min_recall_at_k": 0.75,
  "min_ndcg_at_k": 0.70,
  "min_groundedness": 0.85,
  "min_citation_recall": 0.75,
  "min_no_answer_accuracy": 0.90,
  "max_critical_failures": 0,
  "max_avg_context_tokens": 520
}

Evaluador

import argparse
import json
import math
from pathlib import Path


def load_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8"))


def safe_div(num, den):
    return round(num / den, 4) if den else 0.0


def mean(values):
    return round(sum(values) / len(values), 4) if values else 0.0


def top_k(case, k):
    return case.get("retrieved", [])[:k]


def relevant_ids(case):
    return set(case.get("gold_chunks", {}).keys())


def precision_at_k(case, k):
    retrieved = [item["chunk_id"] for item in top_k(case, k)]
    gold = relevant_ids(case)
    return safe_div(len(set(retrieved) & gold), k)


def recall_at_k(case, k):
    retrieved = [item["chunk_id"] for item in top_k(case, k)]
    gold = relevant_ids(case)
    return safe_div(len(set(retrieved) & gold), len(gold))


def hit_at_k(case, k):
    retrieved = [item["chunk_id"] for item in top_k(case, k)]
    return 1.0 if set(retrieved) & relevant_ids(case) else 0.0


def reciprocal_rank(case):
    gold = relevant_ids(case)
    for index, item in enumerate(case.get("retrieved", []), start=1):
        if item["chunk_id"] in gold:
            return round(1 / index, 4)
    return 0.0


def dcg(relevances):
    total = 0.0
    for index, relevance in enumerate(relevances, start=1):
        total += (2 ** relevance - 1) / math.log2(index + 1)
    return total


def ndcg_at_k(case, k):
    gold = case.get("gold_chunks", {})
    retrieved_rels = [gold.get(item["chunk_id"], 0) for item in top_k(case, k)]
    ideal_rels = sorted(gold.values(), reverse=True)[:k]
    ideal = dcg(ideal_rels)
    return round(dcg(retrieved_rels) / ideal, 4) if ideal else 0.0


def context_tokens(case, k):
    return sum(item.get("tokens", 0) for item in top_k(case, k))


def groundedness(case):
    claims = case.get("answer", {}).get("claims", [])
    if not claims:
        return None
    supported = sum(1 for claim in claims if claim.get("supporting_chunks"))
    return safe_div(supported, len(claims))


def citation_precision(case):
    citations = case.get("answer", {}).get("citations", [])
    if not citations:
        return None
    gold = relevant_ids(case)
    valid = sum(1 for citation in citations if citation in gold)
    return safe_div(valid, len(citations))


def citation_recall(case):
    gold = relevant_ids(case)
    if not gold:
        return None
    citations = set(case.get("answer", {}).get("citations", []))
    return safe_div(len(citations & gold), len(gold))


def classify_case(case, metrics):
    if not case["answerable"]:
        if case["answer"]["abstained"]:
            return "ok_abstention"
        return "critical_no_evidence_answer"

    if metrics["recall_at_k"] < 1:
        return "retrieval_gap"

    if case["answer"]["abstained"]:
        return "unnecessary_abstention"

    if metrics["groundedness"] is not None and metrics["groundedness"] < 1:
        return "grounding_gap"

    if metrics["citation_recall"] is not None and metrics["citation_recall"] < 1:
        return "citation_gap"

    return "ok_answer"


def evaluate(cases, policy):
    k = policy["k"]
    per_case = []
    answerable_rows = []
    grounded_values = []
    citation_precision_values = []
    citation_recall_values = []
    no_answer_total = 0
    no_answer_correct = 0
    critical_failures = 0
    context_token_values = []

    for case in cases:
        metrics = {
            "precision_at_k": precision_at_k(case, k) if case["answerable"] else None,
            "recall_at_k": recall_at_k(case, k) if case["answerable"] else None,
            "hit_at_k": hit_at_k(case, k) if case["answerable"] else None,
            "reciprocal_rank": reciprocal_rank(case) if case["answerable"] else None,
            "ndcg_at_k": ndcg_at_k(case, k) if case["answerable"] else None,
            "context_tokens": context_tokens(case, k),
            "groundedness": groundedness(case),
            "citation_precision": citation_precision(case),
            "citation_recall": citation_recall(case),
        }
        metrics["diagnosis"] = classify_case(case, metrics)
        context_token_values.append(metrics["context_tokens"])

        if case["answerable"]:
            answerable_rows.append(metrics)
        else:
            no_answer_total += 1
            if case["answer"]["abstained"]:
                no_answer_correct += 1
            else:
                critical_failures += 1

        if metrics["groundedness"] is not None:
            grounded_values.append(metrics["groundedness"])
        if metrics["citation_precision"] is not None:
            citation_precision_values.append(metrics["citation_precision"])
        if metrics["citation_recall"] is not None:
            citation_recall_values.append(metrics["citation_recall"])

        per_case.append({
            "case_id": case["case_id"],
            "slice": case["slice"],
            "answerable": case["answerable"],
            **metrics,
        })

    aggregate = {
        "precision_at_k": mean([row["precision_at_k"] for row in answerable_rows]),
        "recall_at_k": mean([row["recall_at_k"] for row in answerable_rows]),
        "hit_at_k": mean([row["hit_at_k"] for row in answerable_rows]),
        "mrr": mean([row["reciprocal_rank"] for row in answerable_rows]),
        "ndcg_at_k": mean([row["ndcg_at_k"] for row in answerable_rows]),
        "groundedness": mean(grounded_values),
        "citation_precision": mean(citation_precision_values),
        "citation_recall": mean(citation_recall_values),
        "no_answer_accuracy": safe_div(no_answer_correct, no_answer_total),
        "critical_failures": critical_failures,
        "avg_context_tokens": mean(context_token_values),
    }
    constraints = {
        "recall_at_k": aggregate["recall_at_k"] >= policy["min_recall_at_k"],
        "ndcg_at_k": aggregate["ndcg_at_k"] >= policy["min_ndcg_at_k"],
        "groundedness": aggregate["groundedness"] >= policy["min_groundedness"],
        "citation_recall": aggregate["citation_recall"] >= policy["min_citation_recall"],
        "no_answer_accuracy": aggregate["no_answer_accuracy"] >= policy["min_no_answer_accuracy"],
        "critical_failures": aggregate["critical_failures"] <= policy["max_critical_failures"],
        "avg_context_tokens": aggregate["avg_context_tokens"] <= policy["max_avg_context_tokens"],
    }
    decision = "pass" if all(constraints.values()) else "fail"
    return {
        "eval_name": "rag_retrieval_groundedness_abstention",
        "k": k,
        "cases": len(cases),
        "answerable_cases": len(answerable_rows),
        "non_answerable_cases": no_answer_total,
        "aggregate": aggregate,
        "constraints": constraints,
        "decision": decision,
        "per_case": per_case,
    }


def render_decision(scorecard):
    failed = [name for name, passed in scorecard["constraints"].items() if not passed]
    lines = [
        "# Decisión de evaluación RAG",
        "",
        f"Resultado: **{scorecard['decision'].upper()}**",
        "",
        "## Métricas agregadas",
        "",
    ]
    for key, value in scorecard["aggregate"].items():
        lines.append(f"- `{key}`: {value}")
    lines.extend(["", "## Restricciones", ""])
    for key, passed in scorecard["constraints"].items():
        mark = "OK" if passed else "REVISAR"
        lines.append(f"- `{key}`: {mark}")
    lines.extend(["", "## Diagnóstico por caso", ""])
    for row in scorecard["per_case"]:
        lines.append(f"- `{row['case_id']}` ({row['slice']}): `{row['diagnosis']}`")
    lines.extend(["", "## Acción recomendada", ""])
    if failed:
        lines.append("No publicar esta versión. Corregir primero: " + ", ".join(failed) + ".")
    else:
        lines.append("Publicar con monitorización y conservar esta scorecard como baseline.")
    return "\n".join(lines) + "\n"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--cases", default="evals/rag_eval_cases.json")
    parser.add_argument("--policy", default="ops/ai/rag_eval_policy.json")
    parser.add_argument("--output", default="output/rag_scorecard.json")
    parser.add_argument("--decision-output", default="output/rag_decision.md")
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()

    cases = load_json(args.cases)
    policy = load_json(args.policy)
    scorecard = evaluate(cases, policy)
    rendered = json.dumps(scorecard, indent=2, ensure_ascii=False)
    print(rendered)

    if args.write:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(rendered + "\n", encoding="utf-8")
        Path(args.decision_output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.decision_output).write_text(render_decision(scorecard), encoding="utf-8")


if __name__ == "__main__":
    main()

Cómo lo ejecutas

python ops/ai/rag_eval.py --write
cat output/rag_scorecard.json
cat output/rag_decision.md

Qué deberías ver

La muestra está diseñada para fallar. Eso es intencionado: una práctica buena no siempre debe acabar en verde. Deberías ver algo parecido a:

{
  "aggregate": {
    "recall_at_k": 0.5,
    "groundedness": 0.5,
    "citation_recall": 0.5,
    "no_answer_accuracy": 0.5,
    "critical_failures": 1
  },
  "decision": "fail"
}

La lectura correcta no es “el RAG es malo”. La lectura correcta es:

MétricaLectura
Recall@3 bajoEl retrieval no trae toda la evidencia necesaria.
Groundedness bajaHay afirmaciones sin soporte.
Citation recall bajoLas citas no cubren toda la evidencia esperada.
No-answer accuracy bajoEl sistema responde en un caso sin evidencia.
Fallos críticos > 0No se publica hasta corregir política de abstención.

Cómo lo adaptarías a tu proyecto

PiezaQué cambiarías
gold_chunksChunks reales revisados por alguien que conoce el dominio.
retrievedSalida real de tu retriever y reranker, con scores y tokens.
claimsAfirmaciones extraídas de la respuesta final.
supporting_chunksEvidencia que sostiene cada afirmación.
policyUmbrales acordados antes de comparar versiones.
sliceIdioma, cliente, fuente, fecha, producto, perfil o tipo de pregunta.
decisionResultado que conectas con CI, revisión o gate de release.

Qué entregaría un alumno

  1. Dataset de al menos 30 preguntas, con un 20 % de preguntas no respondibles.
  2. Qrels con relevancia graduada y justificación.
  3. Traza real de retrieval para cada pregunta.
  4. Script que calcule retrieval, groundedness, citas y abstención.
  5. Scorecard con umbrales definidos antes de ejecutar.
  6. Diagnóstico por caso y por slice.
  7. Decisión escrita: publicar, corregir o no automatizar todavía.

Extensión técnica: comparar variantes de RAG

Ahora añadimos una segunda práctica más propia de ingeniería: comparar varias configuraciones y elegir una candidata a publicación.

Archivo de experimentos

[
  {
    "variant_id": "bm25_base",
    "change": "BM25 sin embeddings",
    "recall_at_k": 0.61,
    "ndcg_at_k": 0.58,
    "groundedness": 0.78,
    "citation_recall": 0.64,
    "no_answer_accuracy": 0.92,
    "critical_failures": 0,
    "avg_context_tokens": 430,
    "p95_latency_ms": 640,
    "cost_per_accepted_answer": 0.0021
  },
  {
    "variant_id": "dense_base",
    "change": "Embeddings densos con top_k 5",
    "recall_at_k": 0.74,
    "ndcg_at_k": 0.66,
    "groundedness": 0.82,
    "citation_recall": 0.71,
    "no_answer_accuracy": 0.90,
    "critical_failures": 0,
    "avg_context_tokens": 510,
    "p95_latency_ms": 820,
    "cost_per_accepted_answer": 0.0034
  },
  {
    "variant_id": "hybrid_rrf",
    "change": "BM25 + vector con reciprocal rank fusion",
    "recall_at_k": 0.82,
    "ndcg_at_k": 0.76,
    "groundedness": 0.88,
    "citation_recall": 0.79,
    "no_answer_accuracy": 0.91,
    "critical_failures": 0,
    "avg_context_tokens": 545,
    "p95_latency_ms": 980,
    "cost_per_accepted_answer": 0.0041
  },
  {
    "variant_id": "hybrid_rerank",
    "change": "Híbrido con reranker top_k 5",
    "recall_at_k": 0.84,
    "ndcg_at_k": 0.83,
    "groundedness": 0.91,
    "citation_recall": 0.84,
    "no_answer_accuracy": 0.89,
    "critical_failures": 1,
    "avg_context_tokens": 530,
    "p95_latency_ms": 1680,
    "cost_per_accepted_answer": 0.0078
  }
]

Comparador de variantes

import argparse
import json
from pathlib import Path


QUALITY_KEYS = [
    "recall_at_k",
    "ndcg_at_k",
    "groundedness",
    "citation_recall",
    "no_answer_accuracy",
]

COST_KEYS = [
    "avg_context_tokens",
    "p95_latency_ms",
    "cost_per_accepted_answer",
]


def load_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8"))


def quality_score(row):
    return round(sum(row[key] for key in QUALITY_KEYS) / len(QUALITY_KEYS), 4)


def dominates(left, right):
    quality_ok = all(left[key] >= right[key] for key in QUALITY_KEYS)
    cost_ok = all(left[key] <= right[key] for key in COST_KEYS)
    strict_quality = any(left[key] > right[key] for key in QUALITY_KEYS)
    strict_cost = any(left[key] < right[key] for key in COST_KEYS)
    return quality_ok and cost_ok and (strict_quality or strict_cost)


def passes_gate(row, policy):
    return (
        row["recall_at_k"] >= policy["min_recall_at_k"]
        and row["ndcg_at_k"] >= policy["min_ndcg_at_k"]
        and row["groundedness"] >= policy["min_groundedness"]
        and row["citation_recall"] >= policy["min_citation_recall"]
        and row["no_answer_accuracy"] >= policy["min_no_answer_accuracy"]
        and row["critical_failures"] <= policy["max_critical_failures"]
        and row["avg_context_tokens"] <= policy["max_avg_context_tokens"]
        and row["p95_latency_ms"] <= policy["max_p95_latency_ms"]
        and row["cost_per_accepted_answer"] <= policy["max_cost_per_accepted_answer"]
    )


def compare(rows, policy):
    enriched = []
    for row in rows:
        dominated_by = [
            other["variant_id"]
            for other in rows
            if other["variant_id"] != row["variant_id"] and dominates(other, row)
        ]
        enriched.append({
            **row,
            "quality_score": quality_score(row),
            "dominated_by": dominated_by,
            "passes_gate": passes_gate(row, policy),
        })

    candidates = [row for row in enriched if row["passes_gate"] and not row["dominated_by"]]
    candidates.sort(
        key=lambda row: (
            row["quality_score"],
            -row["p95_latency_ms"],
            -row["cost_per_accepted_answer"],
        ),
        reverse=True,
    )
    return {
        "baseline": rows[0]["variant_id"],
        "policy": policy,
        "variants": enriched,
        "recommended": candidates[0]["variant_id"] if candidates else None,
        "decision": "candidate_found" if candidates else "no_release_candidate",
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--experiments", default="evals/rag_experiments.json")
    parser.add_argument("--output", default="output/rag_experiment_matrix.json")
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()

    policy = {
        "min_recall_at_k": 0.78,
        "min_ndcg_at_k": 0.72,
        "min_groundedness": 0.86,
        "min_citation_recall": 0.76,
        "min_no_answer_accuracy": 0.90,
        "max_critical_failures": 0,
        "max_avg_context_tokens": 560,
        "max_p95_latency_ms": 1200,
        "max_cost_per_accepted_answer": 0.006
    }
    result = compare(load_json(args.experiments), policy)
    rendered = json.dumps(result, indent=2, ensure_ascii=False)
    print(rendered)

    if args.write:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(rendered + "\n", encoding="utf-8")


if __name__ == "__main__":
    main()

Qué aprende esta extensión

El resultado debería recomendar hybrid_rrf. hybrid_rerank tiene mejor nDCG@k y groundedness, pero falla en abstención, tiene un fallo crítico y se pasa de coste/latencia. En un proyecto real esto es una conversación adulta: quizá el reranker sea prometedor, pero todavía no está listo para producción.

La práctica enseña tres cosas:

AprendizajePor qué importa
Comparar variantes, no impresiones.El equipo deja de discutir por ejemplos sueltos.
Mirar calidad y operación juntas.Una mejora que rompe p95 o coste puede no ser publicable.
Elegir candidato con restricciones.El ganador no es el número más alto, sino el sistema que cumple el contrato.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Base que ya tenemos"]
    F4C09["F4 C09<br/>RAG básico"]
    F4C10["F4 C10<br/>Eval inicial de RAG"]
    F7C01["F7 C01<br/>Eval como decisión"]
    F7C02["F7 C02<br/>Matriz, coste y umbrales"]
    F6C06["F6 C06<br/>EvalOps y gates"]
  end

  subgraph capitulo["F7 C03 · Eval RAG por capas"]
    DATA["Dataset y qrels"]
    RET["Retrieval<br/>Recall, MRR, nDCG"]
    CTX["Context packing<br/>tokens y cobertura"]
    ANS["Respuesta<br/>claims y citas"]
    ABST["Abstención<br/>casos sin evidencia"]
    TRACE["Trazas<br/>versiones, scores y coste"]
    EXP["Matriz de experimentos<br/>variantes controladas"]
    LEAK["Control de fuga<br/>dev, regresión y holdout"]
    CARD["Scorecard<br/>gate de publicación"]
  end

  subgraph siguientes["Lo que prepara"]
    JUDGE["F7 C04<br/>Evaluadores y trazas"]
    CAL["F7 C05<br/>Calibración"]
    LAB["F7 C06<br/>Laboratorio"]
    OPS["F6<br/>Operación continua"]
  end

  F4C09 -->|"aporta pipeline"| DATA
  F4C10 -->|"aporta métricas base"| RET
  F7C01 -->|"exige hipótesis y decisión"| CARD
  F7C02 -->|"aporta umbrales y coste"| CARD
  F6C06 -->|"convierte métricas en gate"| CARD

  DATA --> RET
  RET --> CTX
  CTX --> ANS
  ANS --> ABST
  ANS --> TRACE
  ABST --> TRACE
  DATA --> LEAK
  TRACE --> EXP
  EXP --> CARD
  LEAK --> CARD

  ANS -->|"rúbricas semánticas"| JUDGE
  CARD -->|"scores a decisiones"| CAL
  CARD -->|"práctica final"| LAB
  CARD -->|"baseline y regresión"| OPS

Vocabulario aprendido

TérminoDefinición breve
RAGArquitectura que combina recuperación de evidencia con generación.
QrelJuicio de relevancia entre pregunta y chunk.
Relevancia graduadaPuntuación que diferencia evidencia central, parcial o irrelevante.
Precision@kProporción de resultados útiles entre los k primeros.
Recall@kProporción de evidencias esperadas que aparecen en top-k.
Hit@kSi al menos una evidencia esperada aparece en top-k.
MRRMedia del recíproco de la posición de la primera evidencia útil.
nDCG@kMétrica de ranking que premia relevancia alta en posiciones altas.
Context packingSelección y ordenación del contexto que entra al modelo.
GroundednessProporción de afirmaciones sostenidas por evidencia recuperada.
Citation recallProporción de evidencias esperadas que aparecen citadas.
AbstenciónNo responder cuando falta evidencia suficiente.
Fallo críticoRespuesta sin evidencia en un caso que debía abstenerse.
ScorecardResumen de métricas y restricciones para decidir.
Ablation studyComparación donde cambias una pieza para entender su efecto.
TrazaRegistro completo de una run: versiones, recuperación, contexto, respuesta, latencia y coste.
HoldoutConjunto reservado para estimar rendimiento sin haberlo usado para ajustar.
Variante dominadaConfiguración peor o igual en calidad y peor o igual en coste frente a otra.
ShadowEjecución paralela de una versión nueva sin responder todavía al usuario.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Medir solo la respuesta finalSi la respuesta falla, no sabes si arreglar corpus, chunking, retrieval, reranking, prompt o citas.Medir por capas antes de tocar el sistema.
Celebrar Hit@kHit@k puede ser 1 aunque falte la segunda fuente necesaria.Mirar Recall@k y casos multi-hop.
Confundir cita con evidenciaUna cita puede apuntar a un documento real y aun así no sostener la frase.Validar claim por claim.
Subir top-k sin mirar costeMás contexto puede traer evidencia, pero también ruido, tokens y latencia.Medir recall, precision, nDCG y coste juntos.
No tener preguntas no respondiblesSi todas las preguntas tienen respuesta, nunca mides abstención.Incluir casos plausibles sin evidencia.
Cambiar umbrales después de ver el resultadoConvierte la eval en ajuste manual.Escribir la política antes de ejecutar.
Comparar variantes cambiándolo todo a la vezSi sube la métrica, no sabes si fue por embeddings, reranker, prompt, chunks o corpus.Usar una matriz de experimentos con una variable principal por variante.
No versionar el índicePuedes creer que comparas dos pipelines cuando en realidad cambió el índice.Guardar corpus_version, chunker_version, embedding_model e index_version en cada traza.
Usar el holdout como zona de pruebasSi miras el examen final cada vez que ajustas, deja de ser final.Separar dev, regression y holdout.

Antes de pasar página

Antes de avanzar, deberías poder responder:

  1. ¿Por qué una respuesta correcta puede seguir siendo mala señal en un RAG?
  2. ¿Qué diferencia hay entre Hit@k y Recall@k?
  3. ¿Cuándo usarías nDCG@k en vez de solo Recall@k?
  4. ¿Qué contiene un qrel útil?
  5. ¿Por qué groundedness debe mirar afirmaciones y no impresiones?
  6. ¿Qué diferencia hay entre citation precision y citation recall?
  7. ¿Por qué necesitas preguntas no respondibles en el dataset?
  8. ¿Qué métrica bloquearía una versión que responde sin evidencia?
  9. ¿Qué cambiarías si Recall@k es alto pero groundedness es bajo?
  10. ¿Qué debería guardar una traza profesional de RAG?
  11. ¿Por qué una matriz de experimentos debe cambiar una pieza cada vez?
  12. ¿Qué diferencia hay entre dev, regression y holdout?
  13. ¿Qué significa que una variante esté dominada?
  14. ¿Qué archivos entrega la práctica del capítulo?

Para saber más

Cormack, G. V., Clarke, C. L. A. y Buettcher, S. (2009). Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR, 758-759. https://doi.org/10.1145/1571941.1572114

Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

Es, S., James, J., Espinosa-Anke, L. y Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. https://arxiv.org/abs/2309.15217

Järvelin, K. y Kekäläinen, J. (2002). Cumulated gain-based evaluation of IR techniques. ACM Transactions on Information Systems, 20(4), 422-446. https://doi.org/10.1145/582415.582418

LangChain. (2026). Evaluate a RAG application. https://docs.langchain.com/langsmith/evaluate-rag-tutorial

Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems 33, 9459-9474.

LlamaIndex. (2026). Evaluation modules. https://developers.llamaindex.ai/python/framework/module_guides/evaluating/modules/

McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153-157. https://doi.org/10.1007/BF02295996

Muennighoff, N. et al. (2023). MTEB: Massive Text Embedding Benchmark. https://arxiv.org/abs/2210.07316

Phoenix. (2026). Evaluate RAG. https://arize.com/docs/phoenix/cookbook/evaluation/evaluate-rag

Ragas. (2026). List of available metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/

Thakur, N. et al. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. https://arxiv.org/abs/2104.08663

TruLens. (2026). RAG Triad. https://www.trulens.org/getting_started/core_concepts/rag_triad/

En resumen

IdeaQué te llevas
RAG se evalúa por capas.Corpus, retrieval, contexto, respuesta, citas y abstención tienen métricas distintas.
Los qrels sostienen la evaluación.Sin evidencia esperada no puedes medir retrieval de forma seria.
Retrieval se mide antes de generación.Ahorras coste y localizas el fallo antes de culpar al modelo.
Groundedness exige claims.Una respuesta se valida afirmación por afirmación.
Abstención es parte de calidad.Responder sin evidencia puede bloquear una versión.
La scorecard debe decidir.Métricas sin gate no cambian el sistema.
Las variantes se comparan con diseño.Una matriz de experimentos evita mejorar a ciegas.
La traza es parte de la eval.Sin versiones, scores, contexto, coste y latencia no hay depuración seria.
El holdout se protege.Si optimizas contra el examen final, la mejora deja de ser fiable.

Notas

  1. Lewis, P. et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS.

  2. Thakur, N. et al. (2021). BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. arXiv:2104.08663. Muennighoff, N. et al. (2023). MTEB: Massive Text Embedding Benchmark. arXiv:2210.07316.

  3. Es, S., James, J., Espinosa-Anke, L. y Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. arXiv:2309.15217.

  4. Ragas. (2026). List of available metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/. Consultado el 31 de mayo de 2026.

  5. TruLens. (2026). RAG Triad. https://www.trulens.org/getting_started/core_concepts/rag_triad/. Consultado el 31 de mayo de 2026.

  6. LangChain. (2026). Evaluate a RAG application. https://docs.langchain.com/langsmith/evaluate-rag-tutorial. LlamaIndex. (2026). Evaluation modules. https://developers.llamaindex.ai/python/framework/module_guides/evaluating/modules/. Arize Phoenix. (2026). Evaluate RAG. https://arize.com/docs/phoenix/cookbook/evaluation/evaluate-rag. Consultado el 31 de mayo de 2026.

  7. Järvelin, K. y Kekäläinen, J. (2002). Cumulated gain-based evaluation of IR techniques. ACM Transactions on Information Systems, 20(4), 422-446. https://doi.org/10.1145/582415.582418

Capítulo 04

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 04: Evaluadores LLM y agentes: rúbricas, trazas y coste

Qué deberías poder hacer al terminar

En el capítulo anterior evaluamos RAG por capas. Ahora entra una pieza incómoda: muchas respuestas de IA no se pueden corregir solo con exact match, JSON Schema o una fórmula. Hay que valorar utilidad, suficiencia, claridad, groundedness, orden de razonamiento operativo o trayectoria de un agente. Ahí aparece el evaluador LLM.

Un evaluador LLM puede ser útil. También puede ser una fuente nueva de error. Por eso este capítulo no va de “pon otro modelo a corregir”. Va de evaluar al evaluador.

Al terminar deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Decidir cuándo usar un evaluador LLM.Primero separas validadores deterministas, métricas y revisión humana.
Escribir una rúbrica evaluable.Defines criterios observables, escala, ejemplos y condiciones de bloqueo.
Calibrar un evaluador.Comparas sus veredictos contra un conjunto revisado por personas.
Medir acuerdo.Calculas accuracy, kappa, pases indebidos y errores por criterio.
Evaluar trazas de agentes.Puntúas resultado final, tools, orden, argumentos, permisos, coste y latencia.
Controlar coste de evaluación.Calculas coste por evaluación útil y presupuesto de eval.
Diseñar un gate con evaluador.Un evaluador ayuda, pero no decide solo si el sistema se publica.

La idea central: un evaluador LLM no es una fuente de verdad; es un instrumento que se calibra, se monitoriza y se limita.

El problema: corregir lenguaje abierto no es como validar JSON

Hay tareas donde una máquina puede validar casi todo:

TareaValidación suficiente
Salida JSONSchema, campos obligatorios, tipos y catálogos.
CálculoResultado numérico y tolerancia.
Tool callNombre de tool, argumentos, permisos y error esperado.
CódigoTests, lint, tipos, diff y cobertura.
Cita RAGChunk citado existe y sostiene una afirmación.

Pero hay otras tareas más abiertas:

TareaPor qué cuesta validarla
Resumir un informe.Puede haber varias respuestas correctas.
Explicar un concepto.Importan claridad, completitud y nivel del público.
Revisar una respuesta de soporte.Importan tono, precisión y siguiente paso.
Evaluar un agente.Importa la trayectoria, no solo la frase final.
Comparar dos variantes.A veces hay que decidir cuál ayuda más con el mismo dato.

Aquí un evaluador puede ayudar a escalar revisión. Pero si el evaluador no tiene rúbrica, no tiene ejemplos, no se compara contra personas y no conserva trazas, solo cambia una opinión por otra opinión con apariencia de número.

Fecha de corte del estado del arte

Fecha de corte: 31 de mayo de 2026.
Fuentes consultadas: documentación de OpenAI Graders, OpenAI agent evals y trace grading; LangSmith LLM-as-judge y evaluación; Ragas rubrics; Phoenix LLM evals; Google ADK Evaluate; OpenTelemetry; y trabajos sobre LLM-as-a-judge, G-Eval, AgentBench y WebArena.

En castellano usaré evaluador LLM. En documentación y papers aparece a menudo como LLM-as-a-judge; lo citaremos así cuando sea el nombre técnico de la fuente, pero en el cuerpo del capítulo hablaremos de evaluadores.

OpenAI documenta graders para evals y fine-tuning, incluyendo model graders, validación del grader y ejecución con muestras de prueba.1 LangSmith permite definir evaluadores LLM, usando la denominación LLM-as-a-judge, para evaluación offline y online sobre trazas.2 Ragas ofrece métricas basadas en rúbricas y criterios definidos por el usuario.3

Zheng et al. estudiaron LLM-as-a-judge en MT-Bench y Chatbot Arena, señalando acuerdo alto con preferencias humanas en ciertos entornos, pero también sesgos de posición, verbosidad y preferencia por respuestas propias.4 G-Eval propuso usar LLMs con instrucciones y formularios de evaluación para tareas de generación, con mejor correlación con valoraciones humanas que métricas automáticas clásicas en los experimentos reportados.5 Para agentes, AgentBench y WebArena muestran que evaluar acción en entornos interactivos exige mirar trayectorias, no solo respuestas finales.6

La conclusión útil no es “los evaluadores funcionan” ni “los evaluadores no funcionan”. La conclusión adulta es: funcionan bajo diseño, calibración, trazas y límites.

Anatomía de un sistema de evaluadores

Sistema de evaluación con evaluadores, validadores y trazas Diagrama en blanco, negro y gris que muestra dataset, salida del sistema, validadores deterministas, evaluador LLM, evaluación de trazas, calibración humana, coste y gate final. Un evaluador LLM se diseña como un sistema de medida Primero reglas deterministas; después evaluador calibrado; siempre trazas, coste y gate. Dataset calibrado Casos con criterio input output reference human_labels trace_expected Sin referencia humana no hay calibración. Sistema bajo prueba Salida y traza answer tool_calls[] spans[] tokens latency_ms Lo evaluado debe ser reproducible. Validadores baratos Código antes que evaluador schema citas tools y permisos Reduce coste y ruido del evaluador. Evaluador LLM Rúbrica versionada criterion_id scale evidence score rationale Formato cerrado, no texto libre. Metaevaluación ¿El evaluador mide bien? accuracy kappa pases_indebidos cost_per_evaluation drift_check Trace grading Trayectoria de agente required_tools tool_args extra_steps policy_gate trace_complete Coste y latencia Presupuesto de eval N casos R repeticiones tokens_evaluador coste_total p95_eval_ms Gate final Decisión kappa >= 0.60 pases == 0 trace_ok coste_ok usar, ajustar o revisar IA para gente curiosa / Facsímil 07 / Capítulo 04 / 686f6c61
Un evaluador LLM entra en un sistema de medición: validadores baratos, rúbrica, trazas, calibración, coste y gate.

Primero código, luego evaluador

La regla más barata y más sana:

Si puedes evaluarlo con código, no lo mandes primero a un evaluador LLM.

CriterioMejor primera opciónCuándo entra el evaluador
JSON válidoParser y schema.Casi nunca.
Campos obligatoriosValidación estructurada.Casi nunca.
Cita existeLookup contra chunks recuperados.Si hay que valorar si sostiene una frase.
Tool correctaComparación de trayectoria.Si hay varias trayectorias aceptables.
Argumentos de toolSchema, rangos, catálogos.Si el argumento es semántico.
CálculoRecalcular.Casi nunca.
Resumen útilRúbrica y evaluador calibrado.Cuando no hay respuesta única.
Explicación didácticaRúbrica y ejemplos.Cuando importa nivel, claridad y completitud.

Esto no es una manía. Es coste, reproducibilidad y depuración. Un validador determinista suele ser más barato, más estable y más fácil de explicar que un evaluador.

Qué es una rúbrica evaluable

Una rúbrica no es “califica del 1 al 5”. Eso es una invitación al ruido. Una rúbrica evaluable tiene criterios observables, escala concreta, ejemplos y condiciones de bloqueo.

PiezaPreguntaEjemplo
Criterio¿Qué se evalúa?groundedness, completitud, tono, trayectoria.
Evidencia¿Qué debe mirar el evaluador?Respuesta, referencia, contexto, trazas, tools.
Escala¿Qué significa cada valor?0, 1, 2 o 3 con descripciones cerradas.
Bloqueo¿Qué caso no puede aprobar?Respuesta sin evidencia cuando la tarea exige fuente.
Ejemplos¿Cómo se ve cada nota?Casos calibrados con explicación humana.
Salida¿Qué formato devuelve?JSON con score, label, rationale, evidence.

Ejemplo de criterio:

{
  "criterion_id": "groundedness",
  "description": "La respuesta debe apoyarse en la evidencia proporcionada.",
  "scale": {
    "0": "Afirmaciones centrales sin soporte.",
    "1": "Parte de la respuesta tiene soporte, pero falta una condición importante.",
    "2": "La respuesta está mayoritariamente soportada, con detalle menor discutible.",
    "3": "Todas las afirmaciones relevantes están soportadas por evidencia citada."
  },
  "blocking_rule": "Si una conclusión importante no tiene soporte, el caso no puede aprobar.",
  "required_evidence": ["answer", "reference", "retrieved_context", "citations"]
}

La escala debe evitar adjetivos vagos. “Bueno” o “malo” no bastan. El evaluador necesita saber qué evidencia convierte un 1 en un 2 y un 2 en un 3.

Tipos de evaluadores

No todos los evaluadores son iguales. Conviene elegir el tipo más simple que responda la pregunta.

TipoQué devuelveSirve paraRiesgo técnico
Clasificador binariopass/failGates, contratos semánticos, revisión rápida.Puede ocultar matices.
Escala ordinal0-3, 1-5Calidad, completitud, claridad.Los números pueden no estar calibrados.
Comparador pareadoGana A, gana B, empate.Elegir entre dos variantes.Puede depender del orden de presentación.
Evaluador por criterioVarias notas separadas.Diagnóstico útil.Más coste y más superficie de inconsistencia.
Evaluador de trazaPuntúa pasos, tools y argumentos.Agentes y workflows.Requiere trazas limpias y schema estable.
Panel de evaluadoresVarios modelos o prompts.Casos de alta variabilidad.Multiplica coste y puede dar falsa seguridad.
Humano asistidoPersona con ayuda de tooling.Casos de impacto alto o ambigüedad real.Coste, variabilidad y tiempo.

La elección profesional suele ser híbrida: reglas deterministas para lo verificable, evaluador para lo semántico, revisión humana para casos límite y scorecard para decidir.

Sesgos y fallos frecuentes de un evaluador

Los papers y la práctica coinciden en algo: los evaluadores LLM son útiles, pero tienen patrones de error.

PatrónQué significaCómo mitigarlo
Sesgo de posiciónPrefiere A o B según el orden.Aleatorizar orden y medir order_flip_rate.
Sesgo de verbosidadPremia respuestas más largas aunque no aporten más.Rúbrica con penalización de relleno y coste.
Preferencia por estiloConfunde fluidez con calidad factual.Separar claridad, evidencia y completitud.
InconsistenciaCambia veredicto entre repeticiones.Temperatura baja, formato cerrado y repetición en casos críticos.
Arrastre de referenciaCopia la respuesta de referencia sin evaluar equivalencia.Pedir evidencia de decisión, no solo nota.
Atajos de puntuaciónAprende señales superficiales.Calibrar con casos difíciles y revisar errores.
Falta de sensibilidad al costeAprueba respuestas que exigen demasiados pasos.Incluir tokens, tools y latencia en la rúbrica.

Zheng et al. ya mostraban que hay que vigilar posición, verbosidad y sesgos de preferencia. OpenAI también advierte que un sistema entrenado contra un grader puede aprender a explotar debilidades del propio grader, de modo que conviene contrastarlo con evaluación experta.7

Metaevaluación: evaluar al evaluador

Si el evaluador se usa para bloquear releases, necesita su propio expediente.

Sea hih_i la etiqueta humana para el caso ii y jij_i la etiqueta del evaluador:

accuracyeval=1Ni=1N1[hi=ji]\operatorname{accuracy}_{eval} = \frac{1}{N} \sum_{i=1}^{N} \mathbb{1}[h_i = j_i]

Pero accuracy no basta si las clases están desbalanceadas. Por eso usamos también kappa de Cohen:

κ=pope1pe\kappa = \frac{p_o - p_e}{1 - p_e}
SímboloSignificado
pop_oAcuerdo observado entre evaluador y referencia humana.
pep_eAcuerdo esperado por azar según las distribuciones marginales.
κ\kappaAcuerdo corregido por azar.

Y para ingeniería añadimos métricas más incómodas:

MétricaPor qué importa
false_pass_rateCasos que el evaluador aprueba y la referencia humana rechaza.
false_block_rateCasos que el evaluador bloquea aunque eran aceptables.
critical_false_passesPases indebidos en criterios bloqueantes.
score_maeError absoluto medio si hay escala numérica.
order_flip_rateCambios de preferencia al invertir A/B.
rubric_parse_error_rateVeces que el evaluador no devuelve formato válido.
cost_per_useful_evaluationCoste dividido entre evaluaciones que pasan control de calidad.

Un evaluador puede tener accuracy alta y aun así no servir para gate si deja pasar justo los casos que más importan.

Evaluadores para agentes: mirar trayectoria

En agentes no basta con evaluar la salida final. Necesitamos juzgar la trayectoria.

Elemento de trazaPregunta de evaluación
model_call¿El prompt y el modelo eran los esperados?
tool_call¿La tool era necesaria y estaba permitida?
tool_args¿Los argumentos eran completos, mínimos y válidos?
observation¿La tool devolvió evidencia útil?
handoff¿Se transfirió la tarea al actor correcto?
approval¿Pidió aprobación cuando la política lo exigía?
retry¿El reintento tenía motivo y presupuesto?
final_answer¿La respuesta final refleja la evidencia y el estado real?

Ejemplo de fórmula: podemos separar puntuación de salida y trayectoria con un score compuesto como este. Los pesos no se aprenden aquí: los fija el equipo antes de ejecutar la eval, según riesgo, coste y política.

Srun=wySy+wtSt+wpSp+woSoλCnμLnS_{run} = w_y S_y + w_t S_t + w_p S_p + w_o S_o - \lambda C_n - \mu L_n
SímboloSignificado
SyS_yCalidad de la salida final.
StS_tCalidad de trayectoria: tools, orden, argumentos, observaciones.
SpS_pCumplimiento de política: permisos, aprobación, límites.
SoS_oOperación: trazas completas, reintentos, finalización limpia.
CnC_nCoste normalizado.
LnL_nLatencia normalizada.
w,λ,μw,\lambda,\muPesos y penalizaciones definidos antes de ejecutar.

La fórmula no convierte la valoración en verdad automática. Obliga a escribir qué pesa más. Un agente que ahorra tiempo pero usa tools de más puede ser ineficiente. Un agente que da buena respuesta pero omite una aprobación no debe pasar. Un agente que necesita tres reintentos por caso puede salir caro aunque responda bien.

Coste de evaluar con evaluadores

Evaluar también cuesta. Si un evaluador automático corre sobre 10.000 casos con respuestas largas y trazas completas, puedes descubrir el problema en la factura.

Ejemplo de fórmula: un presupuesto mínimo para evaluar con un evaluador automático podría escribirse así. Ajusta las partidas si tu proveedor cobra por llamada, por token, por batch, por tool o por revisión humana.

Ceval=NR(CinTin+CoutTout)+ChumanoC_{eval} = N \cdot R \cdot (C_{in} \cdot T_{in} + C_{out} \cdot T_{out}) + C_{humano}
SímboloSignificado
NNNúmero de casos.
RRRepeticiones por caso o número de evaluadores.
CinC_{in}Coste por token de entrada del evaluador.
TinT_{in}Tokens de prompt, rúbrica, respuesta, referencia y traza.
CoutC_{out}Coste por token de salida del evaluador.
ToutT_{out}Tokens de razonamiento resumido y JSON final.
ChumanoC_{humano}Coste de revisión humana para calibración o casos límite.

Ejemplo de fórmula: el coste que me interesa de verdad es el coste por evaluación útil, porque un evaluador que falla formato, cambia de criterio o exige revisión constante encarece el sistema aunque parezca barato por llamada.

Cevaluacion_util=Ceval#evaluaciones vaˊlidas y aceptadasC_{evaluacion\_util} = \frac{C_{eval}} {\#\text{evaluaciones válidas y aceptadas}}

Si un evaluador devuelve JSON inválido, cambia criterio entre repeticiones o exige revisión humana constante, su coste útil sube.

Checklist de publicación de un evaluador

Antes de usar un evaluador como gate, pediría esto:

ControlPregunta
Rúbrica versionada¿Sabemos qué criterio se usó?
Calibration set¿Hay casos con referencia humana?
Kappa mínimo¿El acuerdo corrige azar?
Pases indebidos¿Cuántos casos malos aprueba?
Parseo estricto¿Siempre devuelve JSON válido?
Estabilidad¿Repite veredicto en casos frontera?
Coste¿Sabemos cuánto cuesta por evaluación útil?
Trazas¿Podemos reconstruir qué vio el evaluador?
Drift¿Reevaluamos cuando cambia modelo, prompt o rúbrica?
Revisión humana¿Qué casos llegan a persona?

Un evaluador sin checklist puede ser útil en exploración. Un evaluador con checklist puede formar parte de ingeniería.

Para entenderlo con un caso

Imagina un agente académico que revisa una cita:

  1. El usuario pega un párrafo con una afirmación.
  2. El agente busca la fuente.
  3. Comprueba si la fuente sostiene la afirmación.
  4. Propone una cita en APA.
  5. Indica si falta dato o si conviene revisar.

Salida final posible:

“La fuente localizada sostiene la idea general, pero no la cifra exacta. Te propongo citarla solo para la parte conceptual y revisar la cifra antes de publicarla.”

Un evaluador de salida puede puntuar utilidad y claridad. Pero un evaluador de traza debe mirar más:

PasoQué se evalúa
Búsqueda¿Buscó fuente antes de validar?
Fuente¿Usó una fuente recuperable y no solo memoria del modelo?
Verificación¿Separó idea general de cifra exacta?
APA¿Generó formato correcto con datos disponibles?
Límite¿Pidió revisión donde falta evidencia?
Coste¿Usó una ruta razonable para una tarea corta?

Ese es el salto: evaluar agentes es evaluar una ejecución, no una frase.

Manos a la obra

Práctica: auditar un evaluador antes de usarlo.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/run_f7_practices.py --chapter c04 --write --fail-on-invalid

Vamos a construir una práctica sin llamadas externas. Simularemos dos evaluadores candidatos y los compararemos contra una referencia humana. El objetivo es aprender la mecánica: rúbrica, calibration set, outputs del evaluador, métricas de acuerdo, coste y decisión.

Estructura de archivos

evals/evaluator_rubric.json
evals/evaluator_calibration_cases.json
evals/evaluator_outputs.json
ops/ai/evaluator_audit.py
output/evaluator_audit_report.json
output/evaluator_audit_decision.md

Rúbrica

{
  "rubric_id": "academic_agent_evaluator_v1",
  "criteria": {
    "answer_quality": "La respuesta final es útil, precisa y responde a la tarea.",
    "evidence": "Las afirmaciones importantes están apoyadas por referencia o traza.",
    "trace": "La trayectoria usa los pasos esperados sin tools innecesarias.",
    "policy": "La ejecución respeta permisos, límites y condiciones de revisión."
  },
  "blocking_criteria": ["evidence", "policy"],
  "pass_threshold": 0.75,
  "min_kappa": 0.60,
  "max_false_passes": 0,
  "max_parse_errors": 0,
  "max_cost_per_valid_evaluation": 0.006
}

Calibration set

[
  {
    "case_id": "evaluator_001",
    "task": "validar_cita_apa",
    "input": "Comprueba si esta fuente sostiene la afirmación y genera APA.",
    "answer_tokens": 96,
    "trace": ["search_source", "open_source", "check_claim", "format_apa"],
    "human": {
      "answer_quality": 1,
      "evidence": 1,
      "trace": 1,
      "policy": 1,
      "pass": 1
    }
  },
  {
    "case_id": "evaluator_002",
    "task": "validar_cita_apa",
    "input": "La respuesta cita una fuente que no sostiene la cifra exacta.",
    "answer_tokens": 144,
    "trace": ["search_source", "format_apa"],
    "human": {
      "answer_quality": 0,
      "evidence": 0,
      "trace": 0,
      "policy": 1,
      "pass": 0
    }
  },
  {
    "case_id": "evaluator_003",
    "task": "resumen_normativa",
    "input": "Resume una norma y conserva condiciones y excepciones.",
    "answer_tokens": 210,
    "trace": ["retrieve_policy", "summarize", "cite_policy"],
    "human": {
      "answer_quality": 1,
      "evidence": 1,
      "trace": 1,
      "policy": 1,
      "pass": 1
    }
  },
  {
    "case_id": "evaluator_004",
    "task": "resumen_normativa",
    "input": "La respuesta es fluida pero omite una excepción relevante.",
    "answer_tokens": 260,
    "trace": ["retrieve_policy", "summarize"],
    "human": {
      "answer_quality": 0,
      "evidence": 0,
      "trace": 1,
      "policy": 1,
      "pass": 0
    }
  },
  {
    "case_id": "evaluator_005",
    "task": "agente_herramientas",
    "input": "El agente usa una tool no necesaria y supera presupuesto.",
    "answer_tokens": 132,
    "trace": ["search_source", "open_source", "deep_research", "format_apa"],
    "human": {
      "answer_quality": 1,
      "evidence": 1,
      "trace": 0,
      "policy": 0,
      "pass": 0
    }
  },
  {
    "case_id": "evaluator_006",
    "task": "respuesta_sin_fuente",
    "input": "La respuesta debería abstenerse porque no hay evidencia.",
    "answer_tokens": 88,
    "trace": ["search_source", "answer"],
    "human": {
      "answer_quality": 0,
      "evidence": 0,
      "trace": 1,
      "policy": 0,
      "pass": 0
    }
  }
]

Outputs de dos evaluadores candidatos

[
  {
    "evaluator_id": "evaluator_v1_generic",
    "input_price_per_1k": 0.002,
    "output_price_per_1k": 0.008,
    "runs": [
      {"case_id": "evaluator_001", "parse_ok": true, "input_tokens": 900, "output_tokens": 120, "scores": {"answer_quality": 1, "evidence": 1, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_002", "parse_ok": true, "input_tokens": 920, "output_tokens": 140, "scores": {"answer_quality": 1, "evidence": 1, "trace": 0, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_003", "parse_ok": true, "input_tokens": 960, "output_tokens": 130, "scores": {"answer_quality": 1, "evidence": 1, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_004", "parse_ok": true, "input_tokens": 990, "output_tokens": 150, "scores": {"answer_quality": 1, "evidence": 0, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_005", "parse_ok": true, "input_tokens": 930, "output_tokens": 130, "scores": {"answer_quality": 1, "evidence": 1, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_006", "parse_ok": true, "input_tokens": 890, "output_tokens": 110, "scores": {"answer_quality": 0, "evidence": 0, "trace": 1, "policy": 0, "pass": 0}}
    ]
  },
  {
    "evaluator_id": "evaluator_v2_rubric",
    "input_price_per_1k": 0.003,
    "output_price_per_1k": 0.010,
    "runs": [
      {"case_id": "evaluator_001", "parse_ok": true, "input_tokens": 1100, "output_tokens": 150, "scores": {"answer_quality": 1, "evidence": 1, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_002", "parse_ok": true, "input_tokens": 1120, "output_tokens": 170, "scores": {"answer_quality": 0, "evidence": 0, "trace": 0, "policy": 1, "pass": 0}},
      {"case_id": "evaluator_003", "parse_ok": true, "input_tokens": 1160, "output_tokens": 160, "scores": {"answer_quality": 1, "evidence": 1, "trace": 1, "policy": 1, "pass": 1}},
      {"case_id": "evaluator_004", "parse_ok": true, "input_tokens": 1180, "output_tokens": 180, "scores": {"answer_quality": 0, "evidence": 0, "trace": 1, "policy": 1, "pass": 0}},
      {"case_id": "evaluator_005", "parse_ok": true, "input_tokens": 1130, "output_tokens": 160, "scores": {"answer_quality": 1, "evidence": 1, "trace": 0, "policy": 0, "pass": 0}},
      {"case_id": "evaluator_006", "parse_ok": true, "input_tokens": 1090, "output_tokens": 140, "scores": {"answer_quality": 0, "evidence": 0, "trace": 1, "policy": 0, "pass": 0}}
    ]
  }
]

Auditor de evaluadores

import argparse
import json
from pathlib import Path


def load_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8"))


def safe_div(num, den):
    return round(num / den, 4) if den else 0.0


def binary_kappa(reference, predicted):
    labels = [0, 1]
    n = len(reference)
    observed = safe_div(sum(1 for a, b in zip(reference, predicted) if a == b), n)
    expected = 0.0
    for label in labels:
        ref_rate = safe_div(sum(1 for value in reference if value == label), n)
        pred_rate = safe_div(sum(1 for value in predicted if value == label), n)
        expected += ref_rate * pred_rate
    if expected == 1:
        return 1.0
    return round((observed - expected) / (1 - expected), 4)


def cost_for_run(run, evaluator):
    return (
        run["input_tokens"] / 1000 * evaluator["input_price_per_1k"]
        + run["output_tokens"] / 1000 * evaluator["output_price_per_1k"]
    )


def audit_evaluator(evaluator, cases, rubric):
    by_case = {case["case_id"]: case for case in cases}
    criteria = list(rubric["criteria"].keys()) + ["pass"]
    rows = []
    parse_errors = 0
    total_cost = 0.0

    for run in evaluator["runs"]:
        case = by_case[run["case_id"]]
        total_cost += cost_for_run(run, evaluator)
        if not run["parse_ok"]:
            parse_errors += 1
        row = {
            "case_id": run["case_id"],
            "human_pass": case["human"]["pass"],
            "evaluator_pass": run["scores"].get("pass"),
            "cost": round(cost_for_run(run, evaluator), 6),
        }
        for criterion in criteria:
            row[f"{criterion}_match"] = int(case["human"][criterion] == run["scores"].get(criterion))
        rows.append(row)

    human_pass = [by_case[run["case_id"]]["human"]["pass"] for run in evaluator["runs"]]
    evaluator_pass = [run["scores"].get("pass") for run in evaluator["runs"]]
    false_passes = [
        row["case_id"]
        for row in rows
        if row["human_pass"] == 0 and row["evaluator_pass"] == 1
    ]
    false_blocks = [
        row["case_id"]
        for row in rows
        if row["human_pass"] == 1 and row["evaluator_pass"] == 0
    ]
    criterion_accuracy = {}
    for criterion in criteria:
        criterion_accuracy[criterion] = safe_div(
            sum(row[f"{criterion}_match"] for row in rows),
            len(rows),
        )

    valid_evaluations = len(rows) - parse_errors
    cost_per_valid = safe_div(total_cost, valid_evaluations)
    report = {
        "evaluator_id": evaluator["evaluator_id"],
        "cases": len(rows),
        "parse_errors": parse_errors,
        "criterion_accuracy": criterion_accuracy,
        "pass_accuracy": criterion_accuracy["pass"],
        "pass_kappa": binary_kappa(human_pass, evaluator_pass),
        "false_passes": false_passes,
        "false_blocks": false_blocks,
        "total_cost": round(total_cost, 6),
        "cost_per_valid_evaluation": cost_per_valid,
    }
    report["passes_gate"] = (
        report["pass_kappa"] >= rubric["min_kappa"]
        and len(false_passes) <= rubric["max_false_passes"]
        and parse_errors <= rubric["max_parse_errors"]
        and cost_per_valid <= rubric["max_cost_per_valid_evaluation"]
    )
    return report


def choose_candidate(reports):
    passing = [report for report in reports if report["passes_gate"]]
    passing.sort(
        key=lambda report: (
            report["pass_kappa"],
            report["pass_accuracy"],
            -report["cost_per_valid_evaluation"],
        ),
        reverse=True,
    )
    return passing[0]["evaluator_id"] if passing else None


def render_decision(result):
    lines = [
        "# Decisión de auditoría de evaluador",
        "",
        f"Candidato recomendado: `{result['recommended_evaluator']}`",
        "",
        "## Resumen por evaluador",
        "",
    ]
    for report in result["reports"]:
        lines.append(
            "- `{evaluator}`: kappa={kappa}, pass_accuracy={acc}, false_passes={fp}, cost_per_valid={cost}, gate={gate}".format(
                evaluator=report["evaluator_id"],
                kappa=report["pass_kappa"],
                acc=report["pass_accuracy"],
                fp=len(report["false_passes"]),
                cost=report["cost_per_valid_evaluation"],
                gate="OK" if report["passes_gate"] else "REVISAR",
            )
        )
    lines.extend(["", "## Acción", ""])
    if result["recommended_evaluator"]:
        lines.append("Usar el evaluador recomendado en prepublicación, mantener revisión humana de muestra y recalibrar si cambia modelo, rúbrica o tarea.")
    else:
        lines.append("No usar ningún evaluador como gate todavía. Revisar rúbrica, ejemplos calibrados y coste.")
    return "\n".join(lines) + "\n"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--rubric", default="evals/evaluator_rubric.json")
    parser.add_argument("--cases", default="evals/evaluator_calibration_cases.json")
    parser.add_argument("--outputs", default="evals/evaluator_outputs.json")
    parser.add_argument("--output", default="output/evaluator_audit_report.json")
    parser.add_argument("--decision-output", default="output/evaluator_audit_decision.md")
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()

    rubric = load_json(args.rubric)
    cases = load_json(args.cases)
    evaluators = load_json(args.outputs)
    reports = [audit_evaluator(evaluator, cases, rubric) for evaluator in evaluators]
    result = {
        "rubric_id": rubric["rubric_id"],
        "reports": reports,
        "recommended_evaluator": choose_candidate(reports),
    }
    rendered = json.dumps(result, indent=2, ensure_ascii=False)
    print(rendered)

    if args.write:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(rendered + "\n", encoding="utf-8")
        Path(args.decision_output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.decision_output).write_text(render_decision(result), encoding="utf-8")


if __name__ == "__main__":
    main()

Cómo lo ejecutas

python ops/ai/evaluator_audit.py --write
cat output/evaluator_audit_report.json
cat output/evaluator_audit_decision.md

Qué deberías ver

La práctica debería recomendar evaluator_v2_rubric. evaluator_v1_generic aprueba respuestas fluidas que la referencia humana rechaza. Esa es justo la clase de error que no quieres en un gate.

{
  "recommended_evaluator": "evaluator_v2_rubric"
}

La lectura importante:

SeñalInterpretación
pass_kappaAcuerdo corregido por azar entre evaluador y referencia humana.
false_passesCasos rechazados por personas que el evaluador deja pasar.
criterion_accuracy.evidenceSi el evaluador entiende el criterio de evidencia.
criterion_accuracy.traceSi el evaluador entiende la trayectoria.
cost_per_valid_evaluationSi el evaluador es sostenible para usarlo con frecuencia.

Qué entregaría un alumno

  1. Rúbrica versionada con criterios y bloqueos.
  2. Calibration set con al menos 40 casos revisados por personas.
  3. Outputs de dos evaluadores o dos prompts de evaluador.
  4. Script de auditoría con kappa, accuracy, pases indebidos y coste.
  5. Decisión escrita sobre qué evaluador se puede usar y en qué entorno.
  6. Plan de recalibración si cambia modelo, tarea o rúbrica.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Base que ya tenemos"]
    F5C10["F5 C10<br/>Evaluar agentes"]
    F6C04["F6 C04<br/>Observabilidad y trazas"]
    F6C06["F6 C06<br/>EvalOps y gates"]
    F7C01["F7 C01<br/>Eval como decisión"]
    F7C03["F7 C03<br/>Eval RAG por capas"]
  end

  subgraph capitulo["F7 C04 · Evaluadores LLM y agentes"]
    RUB["Rúbrica versionada"]
    DET["Validadores deterministas"]
    JUDGE["Evaluador LLM"]
    TRACE["Trace grading"]
    META["Metaevaluación"]
    COST["Coste por evaluación útil"]
    GATE["Gate con límites"]
  end

  subgraph siguientes["Lo que prepara"]
    CAL["F7 C05<br/>Calibración e incertidumbre"]
    LAB["F7 C06<br/>Laboratorio final"]
    OPS["F6<br/>Monitorización online"]
  end

  F5C10 -->|"aporta trayectoria y coste"| TRACE
  F6C04 -->|"aporta spans y atributos"| TRACE
  F6C06 -->|"convierte métricas en gate"| GATE
  F7C01 -->|"exige decisión y scorecard"| GATE
  F7C03 -->|"aporta groundedness y citas"| RUB

  RUB --> DET
  DET --> JUDGE
  JUDGE --> META
  TRACE --> META
  META --> COST
  COST --> GATE

  META -->|"agreement y kappa"| CAL
  GATE -->|"casos y práctica"| LAB
  GATE -->|"seguimiento continuo"| OPS

Vocabulario aprendido

TérminoDefinición breve
Evaluador LLMModelo que evalúa una salida o traza según una rúbrica.
RúbricaCriterios observables, escala, ejemplos y reglas de bloqueo.
MetaevaluaciónEvaluación del propio evaluador.
Calibration setCasos con referencia humana para calibrar el evaluador.
Kappa de CohenAcuerdo corregido por azar entre dos evaluadores.
Pases indebidosCasos que el evaluador aprueba aunque la referencia humana rechaza.
Trace gradingEvaluación de la trayectoria completa de un agente.
Coste por evaluación útilCoste de evaluar dividido entre evaluaciones válidas y aceptadas.
Criterio bloqueanteCriterio que impide aprobar aunque la media sea buena.
Drift del evaluadorCambio de comportamiento del evaluador al cambiar modelo, prompt, tarea o rúbrica.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Usar un evaluador sin calibration setSi no lo comparas contra referencia humana, no sabes si mide lo que necesitas.Empezar con pocos casos, pero revisados con cuidado.
Pedir una nota globalUna nota única no dice si falló evidencia, claridad, formato, trayectoria o política.Puntuar por criterio.
Mandarlo todo al evaluadorValidar JSON, tool calls o cálculos con un LLM es caro y frágil.Usar código para lo verificable.
No contar pases indebidosAccuracy alta puede esconder que el evaluador aprueba casos que no debería.Mirar false_passes y criterios bloqueantes.
Olvidar el coste de evaluarUn sistema de evaluación también puede romper presupuesto.Medir coste por evaluación útil.
Evaluar agentes como si fueran respuestas sueltasLa frase final puede sonar bien aunque la trayectoria sea mala.Usar trace grading.

Antes de pasar página

Antes de avanzar, deberías poder responder:

  1. ¿Por qué un evaluador LLM no sustituye una referencia humana?
  2. ¿Qué validarías con código antes de usar un evaluador?
  3. ¿Qué debe incluir una rúbrica evaluable?
  4. ¿Qué diferencia hay entre evaluador binario, ordinal y pareado?
  5. ¿Qué es un pase indebido y por qué importa?
  6. ¿Por qué kappa aporta más que accuracy en algunos casos?
  7. ¿Qué elementos de una traza de agente debe mirar un evaluador?
  8. ¿Cómo calculas coste por evaluación útil?
  9. ¿Cuándo mandarías un caso a revisión humana?
  10. ¿Qué archivos entrega la práctica del capítulo?

Para saber más

Cohen, J. (1960). A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1), 37-46. https://doi.org/10.1177/001316446002000104

Efron, B. (1979). Bootstrap methods: Another look at the jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

LangChain. (2026). How to define an LLM-as-a-judge evaluator. https://docs.langchain.com/langsmith/llm-as-judge

Liu, X. et al. (2024). AgentBench: Evaluating LLMs as Agents. International Conference on Learning Representations. https://arxiv.org/abs/2308.03688

Liu, Y., Iter, D., Xu, Y., Wang, S., Xu, R. y Zhu, C. (2023). G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment. EMNLP. https://arxiv.org/abs/2303.16634

McNemar, Q. (1947). Note on the sampling error of the difference between correlated proportions or percentages. Psychometrika, 12(2), 153-157. https://doi.org/10.1007/BF02295996

OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders

OpenAI. (2026). Trace grading. https://developers.openai.com/api/docs/guides/trace-grading

Ragas. (2026). General Purpose Metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/general_purpose/

Zheng, L. et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena. NeurIPS Datasets and Benchmarks. https://arxiv.org/abs/2306.05685

Zhou, S. et al. (2023). WebArena: A Realistic Web Environment for Building Autonomous Agents. https://arxiv.org/abs/2307.13854

En resumen

IdeaQué te llevas
Un evaluador LLM es un instrumento, no una verdad.Debe calibrarse contra referencias humanas o reglas fiables.
La rúbrica manda.Sin criterios observables, el número no significa gran cosa.
Código antes que evaluador.Lo verificable se valida con parsers, schemas, tests o cálculos.
La metaevaluación es obligatoria.Accuracy, kappa, pases indebidos, parseo y coste dicen si el evaluador sirve.
Agentes exigen trace grading.La trayectoria puede fallar aunque la salida final suene bien.
El coste de evaluar también se diseña.Un evaluador útil debe caber en presupuesto y aportar señal accionable.

Notas

  1. OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders. Consultado el 31 de mayo de 2026.

  2. LangChain. (2026). How to define an LLM-as-a-judge evaluator. https://docs.langchain.com/langsmith/llm-as-judge. Consultado el 31 de mayo de 2026.

  3. Ragas. (2026). General Purpose Metrics. https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/general_purpose/. Consultado el 31 de mayo de 2026.

  4. Zheng, L. et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena. NeurIPS Datasets and Benchmarks.

  5. Liu, Y. et al. (2023). G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment. EMNLP.

  6. Liu, X. et al. (2024). AgentBench: Evaluating LLMs as Agents. ICLR. Zhou, S. et al. (2023). WebArena: A Realistic Web Environment for Building Autonomous Agents. arXiv.

  7. OpenAI. (2026). Graders. https://developers.openai.com/api/docs/guides/graders. Consultado el 31 de mayo de 2026.

Capítulo 05

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 05: Calibración e incertidumbre: de scores a decisiones

Qué deberías poder hacer al terminar

En el capítulo 02 vimos que un score se convierte en acción cuando le ponemos umbrales. En el capítulo 04 vimos que un evaluador LLM también debe medirse antes de confiar en sus veredictos. Ahora falta una pregunta incómoda: ¿ese 0,82 que aparece en una métrica, un clasificador, un recuperador o un evaluador significa algo parecido a “82 %”?

Muchas veces no. Un sistema puede ordenar bien los casos y estar mal calibrado. También puede decir “alta confianza” sin que esa confianza corresponda a una frecuencia real de acierto. Para ingeniería, esa diferencia importa muchísimo: no es lo mismo una puntuación útil para ordenar que una probabilidad útil para automatizar.

Al terminar deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Separar score, probabilidad y decisión.Puedes explicar por qué un 0,9 puede ordenar bien y aun así no ser una probabilidad fiable.
Medir calibración.Calculas Brier score, log loss, ECE y lees un reliability diagram.
Elegir una técnica de calibración.Distingues Platt scaling, isotonic regression, histogram/binning y temperature scaling.
Usar incertidumbre sin teatralizarla.Diseñas zona de revisión, abstención o salida con intervalo cuando el sistema no tiene suficiente evidencia.
Entender conformal prediction.Construyes conjuntos o intervalos con cobertura objetivo y sabes qué supuesto los sostiene.
Convertir calibración en política operativa.Escribes umbrales, costes, revisión y criterios de publicación.
Medir incertidumbre estadística.Añades intervalos, bootstrap y lectura por slice antes de sacar conclusiones.
Entregar un kit reproducible.Produces datos, script, reporte JSON, manifest y decisión Markdown que un equipo puede revisar.

La idea central del capítulo es sencilla: un score no merece mandar hasta que sabes qué significa cuando se equivoca.

El 0,92 que no significa 92 %

Imagina un sistema que prioriza tickets de soporte. Un caso llega con score 0,92 de “urgente”. Producto quiere automatizar: si supera 0,90, se marca como urgente y se salta la cola.

Antes de hacerlo, necesitamos saber qué significa ese 0,92.

Lectura ingenuaLectura de ingeniería
“El sistema está seguro al 92 %”.“En casos parecidos con score cercano a 0,92, ¿cuántos eran realmente urgentes?”
“El score es alto, automatizamos”.“¿Qué coste tiene equivocarnos aquí y cuántos casos de esta banda hemos medido?”
“Si ordena bien, ya sirve”.“Ordenar bien no basta si el score alimenta umbrales, revisión o SLA.”

Esto aparece por todas partes en IA aplicada:

Lugar donde aparece un scoreError típico
Clasificador de ticketsLeer 0.87 como probabilidad sin medir calibración.
RAGConfundir similitud de embedding con probabilidad de que la respuesta esté soportada.
Evaluador automáticoTratar una nota 4/5 como verdad operacional sin calibrarla contra casos revisados.
Agente con tool callsUsar “confidence” textual del modelo como si fuera una métrica medida.
Modelo local o APIComparar scores de proveedores distintos como si vivieran en la misma escala.

Un score puede servir para ordenar. Una probabilidad calibrada sirve para decidir bajo coste. No son la misma promesa.

Qué no es calibrar

Calibrar no es subir la accuracy. Un sistema puede tener la misma accuracy antes y después de calibrar, pero producir probabilidades más honestas.

Tampoco es “hacer que el modelo dude”. A veces calibrar baja scores exagerados; otras veces sube scores demasiado conservadores. La dirección no importa. Importa que el número signifique algo empírico.

Y calibrar tampoco sustituye a una buena evaluación. Si tu dataset no representa el uso real, si mezclas datos de ajuste con datos de evaluación o si cambias el umbral después de mirar el resultado, tendrás una apariencia de rigor, no una política fiable.

Podemos resumirlo así:

ConceptoPregunta que respondeQué no responde
Discriminación¿Ordena positivos por encima de negativos?Si el 0,8 significa 80 %.
Accuracy¿Cuántos acierta con un umbral dado?Si sus probabilidades son honestas.
Calibración¿El score coincide con frecuencia real?Si el modelo entiende el dominio.
Incertidumbre¿Cuánto margen de duda queda?Qué decisión de producto conviene tomar.
Política¿Qué hacemos con esa duda?Si los datos de partida eran buenos.

Qué sí es una probabilidad calibrada

Una predicción probabilística está calibrada cuando, entre los casos a los que asigna probabilidad pp, la frecuencia real del evento también es pp.

P(Y=1p^(X)=p)=p\mathbb{P}(Y = 1 \mid \hat{p}(X) = p) = p
SímboloSignificadoEjemplo
XXEntrada del sistema.Texto de un ticket.
YYEtiqueta real.1 si el ticket era urgente.
p^(X)\hat{p}(X)Probabilidad predicha por el sistema.0,80.
ppBanda o valor de confianza.Casos alrededor de 0,80.
P(Y=1p^(X)=p)\mathbb{P}(Y=1 \mid \hat{p}(X)=p)Frecuencia real de positivos dentro de esa banda.0,78 en la muestra.

En la práctica no tenemos infinitos casos con exactamente p=0,80p=0,80. Agrupamos predicciones en bandas: 0,0-0,1; 0,1-0,2; 0,2-0,3; y así sucesivamente. Si en la banda 0,8-0,9 el score medio es 0,84 y el 62 % de los casos son realmente positivos, el modelo está sobreconfiado en esa banda.

La calibración es local. Un promedio bonito puede esconder bandas malas. Por eso miramos la curva completa.

Fecha de corte del estado del arte

Fecha de corte: 1 de junio de 2026.
Fuentes consultadas: trabajos clásicos sobre probabilidades calibradas, Brier score, reliability diagrams, calibración supervisada, calibración de redes neuronales modernas y conformal prediction; además de documentación de scikit-learn, trabajos sobre incertidumbre en LLMs, documentación técnica de modelos/datos y guías de producción de sistemas ML.

Brier propuso en 1950 una puntuación para predicciones probabilísticas que mide el error cuadrático entre probabilidad y resultado observado.1 Murphy descompuso después esa puntuación en componentes relacionados con fiabilidad, resolución e incertidumbre.2

Niculescu-Mizil y Caruana mostraron que clasificadores distintos producen probabilidades con comportamientos de calibración muy distintos, aunque su capacidad de ranking sea buena.3 Platt scaling popularizó una calibración sigmoidal para salidas de SVM.4 Guo et al. mostraron que redes neuronales modernas pueden estar muy bien en accuracy y mal calibradas, y que temperature scaling puede corregir parte de esa sobreconfianza con un ajuste simple.5

Para cuantificar incertidumbre con garantías finitas, la familia de conformal prediction viene de los trabajos de Vovk, Gammerman y Shafer.6 Shafer y Vovk ofrecen una introducción tutorial al enfoque.7 Angelopoulos y Bates escribieron una introducción moderna a conformal prediction y cuantificación de incertidumbre libre de distribución.8

La documentación de scikit-learn resume la diferencia práctica entre calibración, curvas de fiabilidad y métodos como sigmoid e isotonic calibration.9

Para llevar esto a ingeniería de IA moderna, también nos apoyamos en trabajos sobre incertidumbre en modelos de lenguaje, documentación de modelos y datos, y preparación para producción.

Pieza de ingenieríaFuente usada
Incertidumbre en modelos de lenguajeKadavath et al. estudian cuándo los modelos de lenguaje pueden reconocer límites de conocimiento bajo determinados protocolos.10
Incertidumbre semánticaKuhn, Gal y Farquhar proponen agrupar respuestas que dicen lo mismo aunque usen palabras distintas.11
Documentación de modelosModel Cards propone reportar usos previstos, límites y resultados por segmentos.12
Documentación de datosData Cards estructura información sobre origen, composición, límites y uso previsto de datasets.13
Preparación para producciónSculley et al. describen deuda técnica propia de sistemas ML.14
Readiness de MLEl ML Test Score propone pruebas y necesidades de monitorización para sistemas ML en producción.15
SLI/SLOLa guía SRE de Google separa indicador, objetivo y presupuesto de error para decidir con datos.16

Anatomía de una política calibrada

Sistema de calibración, incertidumbre y decisión Diagrama en blanco, negro y gris que conecta logits, scores, calibrador, reliability diagram, conformal prediction, umbrales, revisión humana y gate operativo. Del score bruto a una decisión defendible Calibrar no mejora una demo: convierte puntuaciones en políticas con incertidumbre medible. 1 · Salida bruta Logits o scores z = [1.8, 0.2] softmax(z) similitud RAG nota evaluador Ordena, pero todavía no promete probabilidad. 2 · Set calibrado Datos reservados score_raw label_real slice coste_error no se entrena aquí Se mide lo que el sistema verá fuera. 3 · Medida Fiabilidad por bandas ECE · Brier · log loss 4 · Calibrador Transformación Platt / sigmoid isotonic temperature T binning score → probabilidad Se ajusta en calibración, no en test. 5 · Incertidumbre Conformal prediction alpha = 0.10 q = quantile(scores) set(y) si score ≤ q no ? Si el conjunto es ambiguo, no automatices. 6 · Política Umbrales con revisión normal revisar urgente low=0.35 · high=0.78 La duda se enruta, no se oculta. 7 · Gate Decisión operativa ECE <= 0.08 Brier mejora coverage >= 0.90 auto_error <= 0.12 review <= capacidad publicar, limitar o revisar 8 · Monitorizar Después base_rate ECE por slice cola revisión drift recalibrar Un calibrador caduca si cambian datos o modelo. IA para gente curiosa / Facsímil 07 / Capítulo 05 / 686f6c61
Una política calibrada conecta score bruto, datos reservados, medición, calibrador, incertidumbre, umbrales, gate y monitorización.

Medir calibración: Brier, log loss y ECE

Hay tres medidas que conviene tener cerca. No dicen exactamente lo mismo, y esa diferencia es útil.

Brier score

Brier score mide el error cuadrático entre la probabilidad predicha y la etiqueta real:

BS=1Ni=1N(p^iyi)2BS = \frac{1}{N} \sum_{i=1}^{N} (\hat{p}_i - y_i)^2
SímboloSignificadoEjemplo
BSBSBrier score; cuanto menor, mejor.0,142.
NNNúmero de casos.200 tickets.
p^i\hat{p}_iProbabilidad predicha para el caso ii.0,80.
yiy_iEtiqueta real del caso ii: 0 o 1.1 si era urgente.

Si predices 0,80 y el caso era positivo, aportas (0,801)2=0,04(0,80 - 1)^2 = 0,04. Si predices 0,80 y el caso era negativo, aportas (0,800)2=0,64(0,80 - 0)^2 = 0,64. El Brier castiga tanto mala calibración como mala discriminación, pero es fácil de explicar.

Log loss

Log loss penaliza de forma dura estar muy seguro y equivocarte:

LL=1Ni=1N[yilog(p^i)+(1yi)log(1p^i)]LL = -\frac{1}{N} \sum_{i=1}^{N} \left[ y_i \log(\hat{p}_i) + (1-y_i)\log(1-\hat{p}_i) \right]
SímboloSignificadoEjemplo
LLLLLog loss; cuanto menor, mejor.0,41.
log\logLogaritmo natural.log(0,9)\log(0,9).
p^i\hat{p}_iProbabilidad predicha, recortada para evitar 0 o 1 exactos.0,97.
yiy_iEtiqueta real.0.

Si un sistema dice 0,99 y falla, log loss lo deja en evidencia. Eso es sano cuando una decisión automática usa el score como confianza.

ECE

Expected Calibration Error agrupa predicciones por bandas y compara confianza media con accuracy real:

ECE=m=1MBmNacc(Bm)conf(Bm)ECE = \sum_{m=1}^{M} \frac{|B_m|}{N} \left| \operatorname{acc}(B_m) - \operatorname{conf}(B_m) \right|
SímboloSignificadoEjemplo
MMNúmero de bandas.10 bandas.
BmB_mCasos que caen en la banda mm.Scores entre 0,8 y 0,9.
$B_m$
acc(Bm)\operatorname{acc}(B_m)Frecuencia real de acierto en esa banda.0,65.
conf(Bm)\operatorname{conf}(B_m)Confianza media predicha en esa banda.0,84.

ECE es intuitivo, pero depende de cómo elijas bandas. Por eso no lo usaría solo. Lo pondría junto a Brier, log loss, reliability diagram y análisis por slice.

Reliability diagram: mirar la curva, no solo el número

Un reliability diagram pone en el eje X la confianza media de cada banda y en el eje Y la frecuencia real de acierto. La diagonal perfecta significa calibración ideal.

Patrón visualLectura
Curva por debajo de la diagonalEl sistema está sobreconfiado: promete más de lo que cumple.
Curva por encima de la diagonalEl sistema es conservador: acierta más de lo que su score dice.
Bien en scores bajos, mal en scores altosPeligroso si automatizas por umbral alto.
Bien en promedio, mal en un sliceNo publiques sin política específica para ese slice.

Para proyectos de IA, el reliability diagram debe mirarse por familias de caso: idioma, dominio, fuente documental, tipo de usuario, longitud de entrada, modelo usado, recuperador usado, herramienta invocada o evaluador aplicado.

Una calibración global puede esconder que el sistema es honesto en tickets simples y sobreconfiado en documentos largos.

Métodos de calibración que sí se usan

Calibrar significa aprender una transformación que convierte scores brutos en probabilidades más fiables. Esa transformación debe ajustarse en un conjunto de calibración reservado, no en el test final.

MétodoIdeaCuándo encajaCuidado
Platt scalingAjusta una sigmoide sobre el score bruto.Clasificadores binarios con forma de error suave.Puede quedarse corto si la curva real no es sigmoidal.
Isotonic regressionAprende una función monótona por tramos.Tienes suficientes datos de calibración.Puede sobreajustar con pocos casos.
Histogram/binningAgrupa scores y sustituye por frecuencia observada.Necesitas algo explicable y auditable.Bandas con pocos casos son ruidosas.
Temperature scalingDivide logits por una temperatura TT antes de softmax.Redes neuronales y clasificación multicategoría.No cambia el ranking; solo suaviza o endurece confianza.
Vector/matrix scalingAjustes más flexibles sobre logits multicategoría.Multiclase con datos suficientes.Más parámetros, más riesgo de ajuste a calibración.
Calibración por sliceCalibradores separados o correcciones por segmento.Los segmentos se comportan de forma distinta.Necesita volumen y control de deriva.

Temperature scaling se escribe así:

p^k=exp(zk/T)j=1Kexp(zj/T)\hat{p}_k = \frac{\exp(z_k / T)} {\sum_{j=1}^{K}\exp(z_j / T)}
SímboloSignificadoEjemplo
zkz_kLogit bruto de la clase kk.3,2 para urgente.
TTTemperatura aprendida en calibración.1,7.
KKNúmero de clases.3: normal, revisar, urgente.
p^k\hat{p}_kProbabilidad calibrada de la clase kk.0,74.

Si T>1T > 1, la distribución se suaviza: menos seguridad extrema. Si T<1T < 1, se endurece: más masa en la clase dominante. Guo et al. mostraron que, para muchas redes modernas, una temperatura aprendida podía mejorar mucho la calibración sin cambiar la clase predicha.17

Incertidumbre: no todo tiene que salir como sí o no

En ingeniería, la incertidumbre no es una disculpa. Es una señal de control.

Señal de incertidumbreAcción razonable
Score cerca del umbral.Mandar a revisión o pedir más evidencia.
Reliability diagram malo en esa banda.No automatizar esa banda.
Conformal set con dos clases posibles.No elegir una sola clase en automático.
RAG con citas débiles.Abstenerse o responder con alcance limitado.
Evaluador automático con desacuerdo alto.Revisar muestra humana y recalibrar.
Drift de base rate.Recalcular calibración antes de mantener umbrales.

La salida profesional no siempre es “sí” o “no”. A veces es:

{
  "decision": "revisar",
  "reason": "score calibrado dentro de zona gris",
  "calibrated_probability": 0.61,
  "conformal_set": ["normal", "urgente"],
  "next_step": "enviar a cola de soporte nivel 2"
}

Esto no es menos inteligente. Es más honesto.

Conformal prediction: garantías con supuestos claros

Conformal prediction no intenta adivinar si el modelo “está seguro” en abstracto. Construye conjuntos o intervalos que contienen la respuesta correcta con una cobertura objetivo, bajo un supuesto clave: los datos de calibración y los datos futuros son intercambiables, es decir, vienen del mismo mecanismo de generación en el sentido estadístico que necesitamos para el problema.

En clasificación, una forma sencilla es usar como score de no conformidad:

ai=1p^yi(xi)a_i = 1 - \hat{p}_{y_i}(x_i)
SímboloSignificadoEjemplo
aia_iScore de no conformidad del caso ii.0,18.
p^yi(xi)\hat{p}_{y_i}(x_i)Probabilidad asignada a la clase correcta del caso ii.0,82.
xix_iEntrada del caso ii.Ticket de soporte.
yiy_iClase real del caso ii.urgente.

Después elegimos un cuantil de esos scores en el conjunto de calibración:

q=Quantile(n+1)(1α)/n(a1,,an)q = \operatorname{Quantile}_{\left\lceil (n+1)(1-\alpha) \right\rceil / n} \left( a_1,\ldots,a_n \right)
SímboloSignificadoEjemplo
qqUmbral conformal de no conformidad.0,42.
nnNúmero de casos de calibración.100.
α\alphaTasa de error permitida.0,10 para cobertura 90 %.
\lceil\cdot\rceilRedondeo hacia arriba.Garantiza una elección conservadora.

Para un caso nuevo, incluimos cada clase cuyo score de no conformidad no supera qq:

Γα(x)={y:1p^y(x)q}\Gamma_\alpha(x) = \left\{ y : 1 - \hat{p}_{y}(x) \le q \right\}
SímboloSignificadoEjemplo
Γα(x)\Gamma_\alpha(x)Conjunto de clases plausibles para xx.{normal, urgente}.
yyClase candidata.urgente.
p^y(x)\hat{p}_{y}(x)Probabilidad de esa clase para el caso nuevo.0,63.
qqUmbral aprendido en calibración.0,42.

Si el conjunto tiene una sola clase, quizá podemos automatizar. Si tiene dos o más, el sistema está diciendo: “con la cobertura que me pediste, no puedo elegir solo una sin perder garantía”. Esa frase vale oro en un producto real.

En regresión, la versión más sencilla usa residuos absolutos:

ai=yif^(xi)a_i = |y_i - \hat{f}(x_i)|

Y construye un intervalo:

Cα(x)=[f^(x)q,f^(x)+q]C_\alpha(x) = \left[ \hat{f}(x) - q, \hat{f}(x) + q \right]
SímboloSignificadoEjemplo
f^(x)\hat{f}(x)Predicción numérica del modelo.18 minutos de espera.
qqCuantil de residuos en calibración.6 minutos.
Cα(x)C_\alpha(x)Intervalo conformal.[12, 24] minutos.

La parte honesta: conformal prediction no arregla un dataset que ya no representa producción. Si cambia el canal de entrada, el idioma, el modelo base, la política de producto o el tipo de caso, recalibramos.

De probabilidad a decisión: coste, revisión y cobertura

Una probabilidad calibrada no decide sola. Necesita una política.

Para un caso binario, podemos comparar coste esperado de automatizar frente a revisar.

Ejemplo de fórmula: esta política de automatización no es una regla universal; solo expresa una idea operativa: si automatizar mal cuesta mucho, el umbral de automatización debe ser más exigente.

Rauto(x)=(1p^(x))CerrorR_{auto}(x) = (1-\hat{p}(x)) \cdot C_{error} Rreview(x)=Creview+CdelayR_{review}(x) = C_{review} + C_{delay}
SímboloSignificadoEjemplo
Rauto(x)R_{auto}(x)Riesgo esperado de automatizar el caso xx.0,42 unidades de coste.
p^(x)\hat{p}(x)Probabilidad calibrada de que la acción automática sea correcta.0,93.
CerrorC_{error}Coste de automatizar mal.6 unidades.
Rreview(x)R_{review}(x)Coste esperado de revisar.1,20 unidades.
CreviewC_{review}Coste humano u operativo de revisión.0,80.
CdelayC_{delay}Coste de demorar la respuesta.0,40.

Automatizar tiene sentido si Rauto(x)<Rreview(x)R_{auto}(x) < R_{review}(x), siempre que no viole restricciones de producto, cumplimiento, capacidad o calidad por slice.

En sistemas con zona gris, usamos dos umbrales.

Ejemplo de fórmula: esta regla de decisión convierte una probabilidad calibrada en tres acciones: automatizar negativo, revisar o automatizar positivo. En producción habría que añadir restricciones por slice, capacidad de cola y severidad del caso.

decision(p^)={normalsi p^tbajo revisarsi tbajo<p^<talto urgentesi p^taltodecision(\hat{p}) = \begin{cases} normal & \text{si } \hat{p} \le t_{bajo} \ revisar & \text{si } t_{bajo} < \hat{p} < t_{alto} \ urgente & \text{si } \hat{p} \ge t_{alto} \end{cases}
SímboloSignificadoEjemplo
p^\hat{p}Probabilidad calibrada.0,64.
tbajot_{bajo}Umbral bajo para automatizar negativo.0,30.
taltot_{alto}Umbral alto para automatizar positivo.0,78.
revisarZona donde el sistema no decide solo.0,31 a 0,77.

La política correcta no es la que más automatiza. Es la que automatiza donde tiene evidencia suficiente y deja trazas para mejorar.

Esto en un proyecto real

Si trabajas con sistemas de IA, calibración aparece de maneras menos limpias que en el ejemplo de manual.

SistemaQué calibraríaQué mediría
Clasificador de soporteScore de prioridad.Brier, ECE, error automático, cola de revisión.
RAG documentalProbabilidad de que la respuesta esté soportada.Groundedness por banda, abstención correcta, evidencia faltante.
Evaluador automáticoVeredicto pass/fail o nota por rúbrica.Acuerdo con referencia humana, ECE por criterio, pases indebidos.
Router de modelosProbabilidad de que un modelo barato baste.Calidad por ruta, coste por aceptada, fallback rate.
Agente con toolsProbabilidad de éxito sin revisión.Error por trayectoria, permisos, latencia, coste y cobertura.

Un patrón útil es guardar siempre estos campos por caso:

{
  "case_id": "ticket_1842",
  "raw_score": 0.91,
  "calibrated_probability": 0.76,
  "decision": "revisar",
  "conformal_set": ["normal", "urgente"],
  "slice": "becas",
  "model_version": "support-prioritizer-2026-06-01",
  "calibrator_version": "histogram-v1",
  "policy_version": "support-thresholds-v3"
}

Sin esos campos, luego nadie sabe si falló el modelo, el calibrador, el umbral, el slice, el evaluador o la política.

Lo que un ingeniero de IA no debería omitir

Hasta aquí parece que calibrar consiste en tomar un score y ajustarlo. En sistemas de IA reales, el problema suele ser más amplio: primero hay que decidir qué score merece ser calibrado.

CasoScore tentadorPor qué no bastaQué calibraría de verdad
LLM con logprobsProbabilidad media de tokens.Una respuesta larga acumula logprob distinto a una corta; token probable no implica respuesta correcta.Resultado de tarea: respuesta aceptada, cita correcta, formato válido, acción correcta.
RAGSimilitud de embedding o score del reranker.Similaridad no es soporte factual.Probabilidad de groundedness o de respuesta soportada por evidencia.
Router de modelosScore de “modelo barato basta”.El coste bajo puede esconder más fallback o más revisión.Probabilidad de salida aceptada sin fallback y coste por aceptada.
Evaluador automáticoNota de rúbrica.La nota puede estar desplazada por estilo, longitud o versión del modelo.Acuerdo con referencia humana por criterio y tasa de pases indebidos.
Agente con herramientas“Éxito” declarado por el agente.La salida final puede sonar bien aunque la trayectoria falle.Éxito de run: herramienta correcta, permisos, evidencia, coste y finalización limpia.

Para LLMs, conviene escribir una regla brutalmente clara:

No calibres la sensación verbal de confianza. Calibra un evento observable.

Ejemplos de eventos observables:

Evento calibrableEtiqueta real
“La respuesta contiene JSON válido y completo”.1 si el parser y el contrato pasan.
“La respuesta está soportada por las citas”.1 si una revisión o validador de evidencia lo confirma.
“El modelo barato basta para este caso”.1 si pasa la misma eval que el modelo de referencia.
“El agente puede actuar sin revisión”.1 si la run cumple trayectoria, permisos y resultado.

Esto evita una trampa muy común: convertir una frase como “estoy bastante seguro” en una métrica. Esa frase puede servir para UX, pero no para gates.

LLMs reales: calibrar respuestas, no frases bonitas

En clasificación clásica, el modelo suele devolver un score por clase. En LLMs generativos, la cosa se complica: el modelo produce texto token a token, puede dar varias respuestas distintas que significan lo mismo y puede sonar seguro aunque la evidencia sea débil.

Por eso, para ingeniería, hay que separar tres niveles:

NivelQué midePor qué importa
TokenProbabilidad del siguiente token.Sirve para entender generación, pero no prueba que la respuesta completa sea correcta.
SecuenciaProbabilidad agregada de una salida concreta.Penaliza longitud y redacción; dos respuestas equivalentes pueden tener probabilidades diferentes.
Evento de tareaSi la respuesta cumple el contrato.Es lo que realmente decide producto, operación o evaluación.

Un ejemplo: ante una pregunta documental, el modelo puede responder:

RespuestaTokens distintosMismo significado
“La matrícula cierra el 15 de julio.”
“El plazo termina el 15/07.”
“La fecha límite es el quince de julio.”

Si miramos solo tokens, tratamos esas salidas como objetos diferentes. Si miramos la tarea, las tres dicen lo mismo. Ahí entra la incertidumbre semántica: no preguntamos solo “¿qué tan probable era esta frase exacta?”, sino “¿cuánta dispersión hay entre significados posibles?”.

Una práctica seria con LLMs suele medir al menos esto:

SeñalCómo se mideQué decisión permite
Validez de formatoParser, JSON Schema, Pydantic o contrato equivalente.Reintentar, reparar o rechazar salida.
Soporte documentalComparación con citas, spans o evidencia recuperada.Responder, pedir más contexto o revisar.
Consistencia semánticaVarias muestras agrupadas por significado.Detectar preguntas ambiguas o información insuficiente.
Acuerdo con referenciaEvaluación humana o dataset revisado.Calibrar score de aceptabilidad.
Coste de fallbackTokens, latencia y llamadas adicionales.Decidir cuándo usar modelo grande o revisión.

El punto de ingeniería es incómodo pero liberador: la incertidumbre útil no vive en una frase de confianza; vive en una variable que puedes medir contra realidad.

Rigor estadístico mínimo: no publiques un ECE desnudo

ECE es útil, pero no es garantía suficiente. Depende del número de bandas, del tamaño de muestra y de cómo se distribuyen los casos. Con veinte ejemplos puedes fabricar una tabla que parece precisa y, en realidad, solo tiene ruido.

Para no engañarnos, cada política de calibración debería traer tres capas de incertidumbre:

CapaQué añadeQué evita
Conteo por bandaCuántos casos sostienen cada punto del reliability diagram.Concluir demasiado con una banda de 2 casos.
Intervalo de proporciónRango plausible de accuracy real por banda o slice.Leer 0,75 como si fuera exacto.
Bootstrap de métricasVariabilidad aproximada de Brier o ECE al remuestrear.Comparar mejoras microscópicas como si fueran sólidas.

Para una proporción, un intervalo Wilson es más informativo que enseñar solo el punto medio. Si en una banda hay 6 aciertos de 8 casos, la accuracy observada es 0,75, pero el intervalo es amplio. No es lo mismo decir “esta banda acierta el 75 %” que decir “he observado 6 de 8; todavía necesito más muestra”.

El bootstrap usa una idea práctica: tomar muchas muestras con reemplazo del conjunto de evaluación, recalcular la métrica y mirar su distribución.18 No convierte un dataset malo en bueno, pero obliga a ver si una mejora tiene cuerpo o es una fluctuación.

Una revisión profesional debería bloquear o limitar despliegue cuando vea cualquiera de estas señales:

SeñalLectura
Bandas con muy pocos casosEl reliability diagram no sostiene decisiones finas.
ECE global baja y slice maloLa media esconde un segmento problemático.
Mejora de Brier menor que su variabilidad bootstrapNo hay evidencia fuerte de mejora.
Base rate distinto entre calibración y evaluaciónEl calibrador ya nace con deriva posible.
Umbral elegido después de mirar demasiadas variantesEstás ajustando a la evaluación, no validando.

Riesgo-cobertura: automatizar menos, fallar mejor

Cuando un sistema puede abstenerse, revisar o escalar, no miramos solo accuracy. Miramos la curva riesgo-cobertura: qué error queda cuando automatizamos cierto porcentaje de casos.

Definimos una función de aceptación Ai(c)A_i(c), que vale 1 si el caso ii se automatiza bajo una política con cobertura objetivo cc, y 0 si se revisa:

Coverage(c)=1Ni=1NAi(c)Coverage(c) = \frac{1}{N} \sum_{i=1}^{N} A_i(c) Risk(c)=i=1NiAi(c)i=1NAi(c)Risk(c) = \frac{ \sum_{i=1}^{N} \ell_i A_i(c) }{ \sum_{i=1}^{N} A_i(c) }
SímboloSignificadoEjemplo
Coverage(c)Coverage(c)Proporción de casos automatizados.0,62.
Risk(c)Risk(c)Error medio dentro de los casos automatizados.0,08.
Ai(c)A_i(c)Indicador de automatización del caso ii.1 si sale de la zona gris.
i\ell_iPérdida o coste del caso ii.0 si acierta, 8 si pierde un urgente.
NNNúmero total de casos evaluados.500.

La lectura profesional es esta:

ResultadoDecisión
Alta cobertura y bajo riesgoBuen candidato para automatización.
Alta cobertura y alto riesgoEl sistema automatiza demasiado.
Baja cobertura y bajo riesgoPuede servir como primera fase conservadora.
Baja cobertura y alto riesgoEl score no separa bien; no basta con calibrar.

La literatura de clasificación selectiva trabaja precisamente con esta idea: permitir que el modelo responda solo cuando su confianza supera una condición y medir el error en el subconjunto aceptado.19

En un producto de IA, esta curva te ayuda a defender frases como:

“Automatizamos el 40 % de los casos con error automático menor del 13 %, y el resto pasa a revisión porque el conjunto conformal sigue ambiguo”.

Eso es mucho más útil que “el modelo tiene 86 % de accuracy”.

Contrato de calibración en producción

Un calibrador no debería vivir como una función suelta escondida en código. Debería tener un contrato operativo.

CampoPregunta que responde
model_version¿Qué modelo produjo el score bruto?
score_name¿Qué número estamos calibrando exactamente?
score_semantics¿Qué evento observable intenta predecir?
calibration_dataset_hash¿Con qué datos se ajustó?
evaluation_dataset_hash¿Con qué datos se validó?
calibrator_type¿Qué transformación se usó?
calibrator_version¿Qué versión del calibrador está desplegada?
policy_version¿Qué umbrales y costes deciden?
valid_slices¿En qué segmentos se ha medido?
known_bad_slices¿Dónde no debe automatizar?
recalibration_triggers¿Qué cambios obligan a recalibrar?
owner¿Quién responde por esta política?

Un manifest mínimo podría verse así:

{
  "model_version": "support-prioritizer-2026-06-01",
  "score_name": "raw_score",
  "score_semantics": "probabilidad de que el ticket requiera prioridad urgente",
  "calibrator_type": "histogram_laplace",
  "calibrator_version": "cal-ticket-v1",
  "policy_version": "support-thresholds-v3",
  "valid_slices": ["general", "becas", "matricula"],
  "known_bad_slices": [],
  "recalibration_triggers": {
    "model_changed": true,
    "prompt_changed": true,
    "base_rate_relative_change": 0.20,
    "ece_regression": 0.03,
    "new_slice_without_50_cases": true
  },
  "owner": "equipo-ia"
}

Este manifest no es burocracia. Es lo que permite revisar una incidencia tres semanas después y saber qué número mandaba, con qué datos se calibró y cuándo dejó de ser fiable.

Documentación profesional: model card, data card y SLO

Una política calibrada no debería quedarse encerrada en un notebook. Si va a afectar a un sistema real, necesita tres documentos vivos:

DocumentoQué contienePregunta que resuelve
Model cardModelo, uso previsto, límites, métricas, resultados por slice y cambios relevantes.“¿Para qué sirve este modelo y dónde no deberíamos usarlo?”
Data cardOrigen de datos, composición, etiquetas, cobertura, huecos y transformaciones.“¿Qué mundo representa este dataset y cuál deja fuera?”
SLO de IAObjetivos medibles de calidad, revisión, latencia, coste y disponibilidad.“¿Cuándo decimos que el sistema está suficientemente sano?”

En calibración, esos documentos deberían conectarse así:

PiezaCampo mínimo
Model cardmodel_version, score_name, score_semantics, métricas por slice, límites conocidos.
Data carddataset_hash, split, fecha, criterio de etiquetado, distribución por slice.
SLOmax_auto_error_rate, max_review_rate, min_auto_coverage, latencia y coste máximo.
ManifestQué combinación exacta de modelo, datos, calibrador y política está aprobada.

Esto no es papeleo académico. Sculley et al. muestran que los sistemas ML acumulan deuda técnica por dependencias ocultas, cambios silenciosos y límites difusos entre componentes. En calibración, una dependencia oculta puede ser un prompt, un índice RAG, un proveedor, una plantilla de salida, un criterio de etiquetado o una cola de revisión.

Una frase útil para equipos:

Si no puedes decir qué cambió entre dos runs, no puedes decir si el calibrador sigue siendo válido.

El SLO de IA tampoco debería sonar genérico. Debe ser medible:

SLISLO posible
Error automático en casos aceptadosMenor o igual que 18 % en evaluación revisada.
Tasa de revisiónMenor o igual que 60 % con capacidad operativa disponible.
Cobertura automáticaMayor o igual que 40 % sin romper el error automático.
ECE calibradoMenor o igual que 0,16 en evaluación y revisado por slice.
Latencia de decisiónp95 menor de 1,5 s si no hay revisión.

Si el SLO se rompe, la acción debe estar escrita: limitar automatización, volver a una política anterior, aumentar revisión, recalibrar o bloquear despliegue hasta reunir muestra suficiente.

Para ordenar responsabilidades, el NIST AI RMF también es útil porque separa gobernar, mapear, medir y gestionar riesgos de sistemas de IA.20 En este capítulo no lo usamos como marco legal, sino como recordatorio técnico: medir sin gestionar no cambia el sistema.

Monitorización: cuándo recalibrar

La calibración no es una ceremonia de una vez. Se vigila.

Señal onlineQué indicaAcción
Cambia el base rateLa proporción real de positivos ya no se parece a calibración.Recalcular reliability diagram por fecha y slice.
Sube ECE en muestra revisadaEl score ya no corresponde a frecuencia real.Recalibrar o limitar automatización.
Crece la cola de revisiónLa zona gris está absorbiendo demasiados casos.Revisar umbrales, capacidad o calidad del modelo.
Aumentan errores automáticosLa política acepta casos que antes separaba bien.Bajar cobertura automática y abrir análisis de regresión.
Aparece un slice nuevoNo hay evidencia para automatizar ese segmento.Revisar hasta reunir muestra mínima.
Cambia modelo, prompt, RAG o herramientaCambió el sistema que produce el score.Invalidar calibrador anterior salvo prueba contraria.

Para ingenieros de IA, el patrón operativo sano es:

  1. Versionar modelo, prompt, datos, calibrador y política.
  2. Mantener una muestra revisada de producción.
  3. Medir Brier, ECE, error automático y revisión por slice.
  4. Tener una acción automática si el calibrador caduca: limitar automatización, subir revisión o volver a política anterior.

Si no hay acción asociada, la métrica es decoración.

Manos a la obra

Práctica: calibrar una política de revisión.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/ai/calibrate_policy.py --write

Vamos a construir una práctica sin dependencias externas. El caso es un clasificador de tickets que devuelve raw_score de urgencia. La práctica está materializada en el repo del libro, no solo en este bloque de texto:

kit/

Queremos:

  1. Medir calibración antes y después.
  2. Aprender un calibrador por bandas con suavizado.
  3. Construir un umbral conformal para saber cuándo el conjunto de clases es ambiguo.
  4. Escanear umbrales bajo coste y capacidad de revisión.
  5. Exportar un manifest de calibración con versiones y triggers.
  6. Añadir intervalos Wilson, bootstrap y lectura por slice.
  7. Escribir una decisión operativa.

Estructura de archivos

kit/
  README.md
  evals/calibration_cases.csv
  policies/calibration_policy.json
  ops/ai/calibrate_policy.py
  output/calibration_report.json
  output/calibration_manifest.json
  output/calibration_decision.md

El kit real incluye más columnas que el ejemplo mínimo: slice, channel y week. Eso permite probar si la media global esconde segmentos débiles.

Dataset mínimo explicado

case_id,split,raw_score,label,slice
c001,calibration,0.05,0,general
c002,calibration,0.08,0,general
c003,calibration,0.12,0,becas
c004,calibration,0.18,0,general
c005,calibration,0.22,0,matricula
c006,calibration,0.27,1,becas
c007,calibration,0.31,0,matricula
c008,calibration,0.36,0,general
c009,calibration,0.41,1,becas
c010,calibration,0.46,0,general
c011,calibration,0.52,1,matricula
c012,calibration,0.57,0,general
c013,calibration,0.62,1,becas
c014,calibration,0.66,1,matricula
c015,calibration,0.71,1,general
c016,calibration,0.76,0,general
c017,calibration,0.81,1,matricula
c018,calibration,0.86,1,becas
c019,calibration,0.91,1,general
c020,calibration,0.96,1,matricula
e001,evaluation,0.04,0,general
e002,evaluation,0.11,0,becas
e003,evaluation,0.17,0,matricula
e004,evaluation,0.24,1,becas
e005,evaluation,0.29,0,general
e006,evaluation,0.34,0,general
e007,evaluation,0.39,1,becas
e008,evaluation,0.44,0,matricula
e009,evaluation,0.49,1,general
e010,evaluation,0.54,1,matricula
e011,evaluation,0.59,0,general
e012,evaluation,0.64,1,becas
e013,evaluation,0.69,1,general
e014,evaluation,0.74,0,matricula
e015,evaluation,0.79,1,becas
e016,evaluation,0.84,1,general
e017,evaluation,0.89,1,matricula
e018,evaluation,0.93,1,general
e019,evaluation,0.97,1,becas
e020,evaluation,0.99,0,general

Política

{
  "model_version": "support-prioritizer-2026-06-01",
  "score_name": "raw_score",
  "score_semantics": "probabilidad de que el ticket requiera prioridad urgente",
  "calibrator_version": "cal-ticket-v1",
  "policy_version": "support-thresholds-v3",
  "owner": "equipo-ia",
  "positive_label": "urgente",
  "negative_label": "normal",
  "bins": 5,
  "alpha": 0.10,
  "cost_false_positive": 2.0,
  "cost_false_negative": 8.0,
  "cost_review": 0.8,
  "max_review_rate": 0.60,
  "max_auto_error_rate": 0.18,
  "min_auto_coverage": 0.40,
  "valid_slices": ["general", "becas", "matricula"],
  "known_bad_slices": [],
  "recalibration_triggers": {
    "model_changed": true,
    "prompt_changed": true,
    "base_rate_relative_change": 0.20,
    "ece_regression": 0.03,
    "new_slice_without_50_cases": true
  }
}

Script mínimo explicado

El bloque siguiente enseña el mecanismo sin dependencias externas. El script que debe ejecutar el lector está en ops/ai/calibrate_policy.py y añade lo que pediría en una práctica de ingeniería: intervalos Wilson, bootstrap, reporte por slice, hashes y manifest.

import argparse
import csv
import json
import math
from pathlib import Path


def clamp(value, low=1e-6, high=1 - 1e-6):
    return min(high, max(low, value))


def load_cases(path):
    with Path(path).open(newline="", encoding="utf-8") as handle:
        rows = list(csv.DictReader(handle))
    cases = []
    for row in rows:
        cases.append({
            "case_id": row["case_id"],
            "split": row["split"],
            "raw_score": float(row["raw_score"]),
            "label": int(row["label"]),
            "slice": row["slice"],
        })
    return cases


def brier(cases, score_key):
    return sum((case[score_key] - case["label"]) ** 2 for case in cases) / len(cases)


def log_loss(cases, score_key):
    total = 0.0
    for case in cases:
        p = clamp(case[score_key])
        y = case["label"]
        total += y * math.log(p) + (1 - y) * math.log(1 - p)
    return -total / len(cases)


def bin_index(score, bins):
    return min(bins - 1, int(score * bins))


def reliability(cases, score_key, bins):
    table = []
    for index in range(bins):
        lo = index / bins
        hi = (index + 1) / bins
        bucket = [case for case in cases if bin_index(case[score_key], bins) == index]
        if not bucket:
            table.append({
                "bin": index,
                "range": [round(lo, 2), round(hi, 2)],
                "count": 0,
                "confidence": None,
                "accuracy": None,
                "gap": None,
            })
            continue
        confidence = sum(case[score_key] for case in bucket) / len(bucket)
        accuracy = sum(case["label"] for case in bucket) / len(bucket)
        table.append({
            "bin": index,
            "range": [round(lo, 2), round(hi, 2)],
            "count": len(bucket),
            "confidence": round(confidence, 4),
            "accuracy": round(accuracy, 4),
            "gap": round(abs(accuracy - confidence), 4),
        })
    return table


def ece(cases, score_key, bins):
    table = reliability(cases, score_key, bins)
    total = 0.0
    for row in table:
        if row["count"]:
            total += row["count"] / len(cases) * row["gap"]
    return total


def fit_histogram_calibrator(cases, bins):
    calibrator = []
    global_rate = sum(case["label"] for case in cases) / len(cases)
    for index in range(bins):
        bucket = [case for case in cases if bin_index(case["raw_score"], bins) == index]
        positives = sum(case["label"] for case in bucket)
        # Laplace smoothing: evita que una banda pequeña devuelva 0 o 1 absoluto.
        calibrated = (positives + 1) / (len(bucket) + 2) if bucket else global_rate
        calibrator.append({
            "bin": index,
            "count": len(bucket),
            "calibrated_probability": round(calibrated, 6),
        })
    return calibrator


def apply_calibrator(cases, calibrator, bins):
    by_bin = {row["bin"]: row["calibrated_probability"] for row in calibrator}
    enriched = []
    for case in cases:
        copy = dict(case)
        copy["calibrated_score"] = by_bin[bin_index(case["raw_score"], bins)]
        enriched.append(copy)
    return enriched


def conformal_threshold(calibration_cases, alpha):
    nonconformity = []
    for case in calibration_cases:
        p = case["calibrated_score"]
        score = 1 - p if case["label"] == 1 else p
        nonconformity.append(score)
    nonconformity.sort()
    n = len(nonconformity)
    rank = min(n, math.ceil((n + 1) * (1 - alpha)))
    return nonconformity[rank - 1]


def conformal_set(probability, q):
    labels = []
    if probability <= q:
        labels.append("normal")
    if 1 - probability <= q:
        labels.append("urgente")
    return labels or ["normal", "urgente"]


def decide(probability, low, high, q):
    labels = conformal_set(probability, q)
    if len(labels) > 1:
        return "review"
    if probability <= low:
        return "normal"
    if probability >= high:
        return "urgent"
    return "review"


def evaluate_thresholds(cases, q, policy):
    candidates = []
    grid = [round(i / 20, 2) for i in range(1, 20)]
    for low in grid:
        for high in grid:
            if low >= high:
                continue
            cost = 0.0
            auto = 0
            auto_errors = 0
            reviewed = 0
            confusion = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
            decisions = []
            for case in cases:
                decision = decide(case["calibrated_score"], low, high, q)
                if decision == "review":
                    reviewed += 1
                    cost += policy["cost_review"]
                else:
                    auto += 1
                    predicted = 1 if decision == "urgent" else 0
                    actual = case["label"]
                    if predicted == 1 and actual == 1:
                        confusion["tp"] += 1
                    elif predicted == 1 and actual == 0:
                        confusion["fp"] += 1
                        auto_errors += 1
                        cost += policy["cost_false_positive"]
                    elif predicted == 0 and actual == 1:
                        confusion["fn"] += 1
                        auto_errors += 1
                        cost += policy["cost_false_negative"]
                    else:
                        confusion["tn"] += 1
                decisions.append({"case_id": case["case_id"], "decision": decision})
            review_rate = reviewed / len(cases)
            auto_coverage = auto / len(cases)
            auto_error_rate = auto_errors / auto if auto else 0.0
            passes = (
                review_rate <= policy["max_review_rate"]
                and auto_coverage >= policy["min_auto_coverage"]
                and auto_error_rate <= policy["max_auto_error_rate"]
            )
            candidates.append({
                "low": low,
                "high": high,
                "cost": round(cost, 4),
                "review_rate": round(review_rate, 4),
                "auto_coverage": round(auto_coverage, 4),
                "auto_error_rate": round(auto_error_rate, 4),
                "confusion_auto": confusion,
                "passes": passes,
                "decisions": decisions,
            })
    valid = [item for item in candidates if item["passes"]]
    valid.sort(key=lambda item: (item["cost"], item["review_rate"], -item["auto_coverage"], -item["high"], item["low"]))
    return valid[0] if valid else min(candidates, key=lambda item: item["cost"])


def prediction_if_accepted(case, q, min_confidence):
    probability = case["calibrated_score"]
    labels = conformal_set(probability, q)
    if len(labels) > 1:
        return None
    confidence = max(probability, 1 - probability)
    if confidence < min_confidence:
        return None
    return 1 if probability >= 0.5 else 0


def risk_coverage_curve(cases, q, policy):
    rows = []
    for min_confidence in [round(i / 20, 2) for i in range(10, 20)]:
        accepted = []
        total_cost = 0.0
        for case in cases:
            predicted = prediction_if_accepted(case, q, min_confidence)
            if predicted is None:
                continue
            accepted.append(case)
            actual = case["label"]
            if predicted == 1 and actual == 0:
                total_cost += policy["cost_false_positive"]
            elif predicted == 0 and actual == 1:
                total_cost += policy["cost_false_negative"]
        coverage = len(accepted) / len(cases)
        risk = total_cost / len(accepted) if accepted else None
        rows.append({
            "min_confidence": min_confidence,
            "coverage": round(coverage, 4),
            "risk": round(risk, 4) if risk is not None else None,
            "accepted": len(accepted),
        })
    return rows


def metrics_block(cases, score_key, bins):
    return {
        "brier": round(brier(cases, score_key), 4),
        "log_loss": round(log_loss(cases, score_key), 4),
        "ece": round(ece(cases, score_key, bins), 4),
        "reliability": reliability(cases, score_key, bins),
    }


def build_manifest(report, policy, calibration_cases, evaluation_cases):
    slices = sorted({case["slice"] for case in calibration_cases + evaluation_cases})
    return {
        "model_version": policy["model_version"],
        "score_name": policy["score_name"],
        "score_semantics": policy["score_semantics"],
        "calibrator_type": report["calibrator"]["type"],
        "calibrator_version": policy["calibrator_version"],
        "policy_version": policy["policy_version"],
        "owner": policy["owner"],
        "valid_slices": policy["valid_slices"],
        "observed_slices": slices,
        "known_bad_slices": policy["known_bad_slices"],
        "dataset_counts": {
            "calibration": len(calibration_cases),
            "evaluation": len(evaluation_cases),
        },
        "quality_gate": {
            "max_review_rate": policy["max_review_rate"],
            "max_auto_error_rate": policy["max_auto_error_rate"],
            "min_auto_coverage": policy["min_auto_coverage"],
            "passes": report["recommended_policy"]["passes"],
        },
        "metrics": {
            "raw_ece": report["raw_metrics"]["ece"],
            "calibrated_ece": report["calibrated_metrics"]["ece"],
            "raw_brier": report["raw_metrics"]["brier"],
            "calibrated_brier": report["calibrated_metrics"]["brier"],
        },
        "recommended_policy": {
            "low": report["recommended_policy"]["low"],
            "high": report["recommended_policy"]["high"],
            "review_rate": report["recommended_policy"]["review_rate"],
            "auto_coverage": report["recommended_policy"]["auto_coverage"],
            "auto_error_rate": report["recommended_policy"]["auto_error_rate"],
        },
        "recalibration_triggers": policy["recalibration_triggers"],
    }


def render_decision(report):
    rec = report["recommended_policy"]
    lines = [
        "# Decisión de calibración",
        "",
        f"Calibrador: `{report['calibrator']['type']}` con {report['calibrator']['bins']} bandas.",
        f"Umbral conformal q: `{report['conformal']['q']}` para cobertura objetivo `{report['conformal']['target_coverage']}`.",
        "",
        "## Política recomendada",
        "",
        f"- `low`: {rec['low']}",
        f"- `high`: {rec['high']}",
        f"- tasa de revisión: {rec['review_rate']}",
        f"- cobertura automática: {rec['auto_coverage']}",
        f"- error automático: {rec['auto_error_rate']}",
        f"- coste estimado: {rec['cost']}",
        f"- manifest: `output/calibration_manifest.json`",
        "",
        "## Lectura",
        "",
        "Automatiza solo fuera de la zona gris y conserva revisión cuando el conjunto conformal no permite una clase única.",
        "Si cambia el modelo, el dominio, la mezcla de tickets o la capacidad de revisión, recalibra antes de conservar estos umbrales.",
    ]
    return "\n".join(lines) + "\n"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--cases", default="evals/calibration_cases.csv")
    parser.add_argument("--policy", default="policies/calibration_policy.json")
    parser.add_argument("--output", default="output/calibration_report.json")
    parser.add_argument("--manifest-output", default="output/calibration_manifest.json")
    parser.add_argument("--decision-output", default="output/calibration_decision.md")
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()

    policy = json.loads(Path(args.policy).read_text(encoding="utf-8"))
    cases = load_cases(args.cases)
    calibration = [case for case in cases if case["split"] == "calibration"]
    evaluation = [case for case in cases if case["split"] == "evaluation"]
    calibrator = fit_histogram_calibrator(calibration, policy["bins"])
    calibration_calibrated = apply_calibrator(calibration, calibrator, policy["bins"])
    evaluation_calibrated = apply_calibrator(evaluation, calibrator, policy["bins"])
    q = conformal_threshold(calibration_calibrated, policy["alpha"])
    recommended = evaluate_thresholds(evaluation_calibrated, q, policy)

    report = {
        "raw_metrics": metrics_block(evaluation, "raw_score", policy["bins"]),
        "calibrated_metrics": metrics_block(evaluation_calibrated, "calibrated_score", policy["bins"]),
        "calibrator": {
            "type": "histogram_laplace",
            "bins": policy["bins"],
            "mapping": calibrator,
        },
        "conformal": {
            "alpha": policy["alpha"],
            "target_coverage": round(1 - policy["alpha"], 4),
            "q": round(q, 4),
        },
        "risk_coverage_curve": risk_coverage_curve(evaluation_calibrated, q, policy),
        "recommended_policy": recommended,
    }
    manifest = build_manifest(report, policy, calibration_calibrated, evaluation_calibrated)
    rendered = json.dumps(report, indent=2, ensure_ascii=False)
    print(rendered)
    if args.write:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(rendered + "\n", encoding="utf-8")
        Path(args.manifest_output).write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
        Path(args.decision_output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.decision_output).write_text(render_decision(report), encoding="utf-8")


if __name__ == "__main__":
    main()

Cómo lo ejecutas

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python ops/ai/calibrate_policy.py --write
cat output/calibration_report.json
cat output/calibration_manifest.json
cat output/calibration_decision.md

Qué deberías ver

El reporte compara métricas del score bruto contra el score calibrado, muestra la tabla de bandas, calcula qq conformal, genera una curva riesgo-cobertura y recomienda una política con low, high, tasa de revisión, cobertura automática, error automático y coste. En el kit real también aparecen intervalos Wilson por banda, bootstrap de Brier/ECE, reporte por slice y checks operativos.

La salida exacta puede variar si cambias datos o política, pero deberías ver una estructura así:

{
  "calibrator": {
    "type": "histogram_laplace",
    "bins": 6
  },
  "conformal": {
    "target_coverage": 0.9
  },
  "risk_coverage_curve": [
    {
      "min_confidence": 0.5,
      "coverage": 0.4
    }
  ],
  "slice_report": [
    {
      "slice": "becas",
      "count": 10
    }
  ],
  "recommended_policy": {
    "low": 0.15,
    "high": 0.75,
    "passes": true
  }
}

La lectura importante no es memorizar esos umbrales. Es entender el expediente:

CampoQué te dice
raw_metrics.eceCómo de lejos estaba el score bruto de una probabilidad fiable.
calibrated_metrics.brierSi la calibración mejoró el error probabilístico.
calibrator.mappingQué probabilidad empírica asigna cada banda.
conformal.qCuánta rareza aceptas para mantener cobertura.
risk_coverage_curveQué error queda al automatizar más o menos casos.
slice_reportSi un segmento concreto se comporta peor que la media.
bootstrapSi la métrica parece estable o demasiado dependiente de la muestra.
recommended_policy.review_rateQué carga operativa genera la duda.
recommended_policy.auto_error_rateQué error queda en lo automatizado.
calibration_manifest.jsonQué versiones, slices, métricas y triggers justifican desplegar la política.

Qué entregaría un alumno

  1. Dataset separado en calibration y evaluation.
  2. Script ejecutable que calcule Brier, log loss, ECE y reliability table.
  3. Calibrador aprendido solo con el split de calibración.
  4. Umbral conformal y política de revisión.
  5. Curva riesgo-cobertura para justificar automatización frente a revisión.
  6. Intervalos Wilson o bootstrap para no vender una métrica puntual como certeza.
  7. Manifest de calibración con versiones, hashes, slices y triggers.
  8. Reporte JSON con métricas y decisión.
  9. Documento Markdown explicando si publicaría, limitaría o pediría más datos.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Base que ya tenemos"]
    F3C04["F3 C04<br/>Logits y softmax"]
    F7C01["F7 C01<br/>Eval como decisión"]
    F7C02["F7 C02<br/>Matriz, coste y umbrales"]
    F7C04["F7 C04<br/>Evaluadores y trazas"]
    F6C06["F6 C06<br/>EvalOps y gates"]
  end

  subgraph capitulo["F7 C05 · Calibración e incertidumbre"]
    SCORE["Score bruto"]
    PROB["Probabilidad calibrada"]
    REL["Reliability diagram"]
    MET["Brier · log loss · ECE"]
    CAL["Calibrador"]
    CONF["Conformal prediction"]
    STAT["Intervalos · bootstrap · slices"]
    LLM["Eventos observables en LLMs"]
    DOC["Model card · data card · SLO"]
    REV["Zona de revisión"]
    DEC["Política de decisión"]
  end

  subgraph siguientes["Lo que prepara"]
    C06["F7 C06<br/>Interpretabilidad y laboratorio"]
    OPS["F6<br/>Monitorización y recalibración"]
    PROD["F11<br/>Producto y experiencia de usuario"]
  end

  F3C04 -->|"produce logits que pueden calibrarse"| SCORE
  F7C01 -->|"exige decisión trazable"| DEC
  F7C02 -->|"aporta umbrales y costes"| REV
  F7C04 -->|"necesita medir veredictos"| LLM
  F6C06 -->|"convierte métricas en gate"| DOC

  SCORE -->|"se compara contra realidad"| REL
  REL -->|"se resume con"| MET
  MET -->|"ajusta"| CAL
  CAL -->|"transforma en"| PROB
  LLM -->|"define qué evento calibrar"| PROB
  MET -->|"necesita incertidumbre estadística"| STAT
  STAT -->|"limita conclusiones"| DEC
  PROB -->|"alimenta"| CONF
  CONF -->|"detecta ambigüedad"| REV
  PROB -->|"entra en"| DEC
  REV -->|"limita automatización"| DEC
  DOC -->|"versiona y gobierna"| DEC

  DEC -->|"se practica en"| C06
  DEC -->|"se vigila en"| OPS
  REV -->|"afecta confianza del usuario"| PROD

Vocabulario aprendido

TérminoDefinición breve
Score brutoPuntuación salida del modelo antes de calibrar.
Probabilidad calibradaScore interpretable como frecuencia esperada de acierto.
DiscriminaciónCapacidad de ordenar casos positivos por encima de negativos.
CalibraciónCorrespondencia entre confianza predicha y frecuencia real.
Brier scoreError cuadrático medio de probabilidades.
Log lossPérdida que castiga mucho equivocarse con confianza alta.
ECEError de calibración esperado por bandas.
Reliability diagramGráfico de confianza media frente a accuracy por banda.
Platt scalingCalibración sigmoidal de un score.
Isotonic regressionCalibración monótona por tramos.
Temperature scalingAjuste de logits con una temperatura aprendida.
Conformal predictionConstrucción de conjuntos o intervalos con cobertura objetivo.
CoberturaProporción de casos donde el conjunto contiene la respuesta correcta.
Zona de revisiónBanda donde el sistema no automatiza por incertidumbre.
Riesgo-coberturaCurva que compara porcentaje automatizado y error en lo automatizado.
Deriva de calibraciónCambio de la relación entre score y frecuencia real.
Manifest de calibraciónContrato versionado de score, calibrador, política, datos y triggers.
Incertidumbre semánticaDuda sobre el significado de una respuesta, no solo sobre su texto exacto.
Intervalo WilsonIntervalo para una proporción observada, útil con muestras pequeñas.
BootstrapRemuestreo con reemplazo para estimar variabilidad de métricas.
Model cardDocumento de modelo con uso previsto, límites y métricas por segmento.
Data cardDocumento de datos con origen, composición, huecos y uso previsto.
SLO de IAObjetivo medible de calidad, coste, revisión, latencia o disponibilidad.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Leer cualquier score como probabilidadEl número parece probabilístico aunque solo ordene.Medir calibración antes de automatizar con umbrales.
Mirar solo accuracyPuedes acertar mucho y estar sobreconfiado.Añadir Brier, log loss, ECE y reliability diagram.
Calibrar con el test finalEl resultado queda contaminado por decisiones de ajuste.Separar train, calibration y evaluation.
Usar ECE como número absolutoDepende de bandas y puede esconder slices malos.Mirar curva, slices y casos frontera.
Automatizar la zona grisLa presión por reducir revisión empuja a decidir donde falta señal.Diseñar revisión como parte del sistema, no como fracaso.
Olvidar que la calibración caducaCambian datos, modelo, prompt, retrieval o usuarios.Versionar calibrador y recalibrar con monitorización.
Confundir logprob con verdadUn token probable no garantiza una respuesta correcta.Calibrar eventos observables de tarea.
Publicar sin intervalosUna métrica puntual puede parecer más estable de lo que es.Añadir Wilson, bootstrap y mínimos por slice.

Antes de pasar página

Antes de avanzar, deberías poder responder:

  1. ¿Por qué un score alto no implica probabilidad calibrada?
  2. ¿Qué diferencia hay entre discriminación y calibración?
  3. ¿Cómo se calcula Brier score y qué penaliza?
  4. ¿Por qué log loss castiga tanto una predicción confiada que falla?
  5. ¿Qué mide ECE y por qué depende de las bandas?
  6. ¿Cómo leerías un reliability diagram por debajo de la diagonal?
  7. ¿Cuándo usarías temperature scaling frente a isotonic regression?
  8. ¿Qué supuesto sostiene conformal prediction?
  9. ¿Qué significa que un conjunto conformal tenga dos clases?
  10. ¿Por qué la curva riesgo-cobertura es más útil que una accuracy global para decidir automatización?
  11. ¿Qué debería contener un manifest de calibración?
  12. ¿Por qué no basta con mirar logprobs para calibrar una respuesta de LLM?
  13. ¿Qué aporta un intervalo Wilson en una banda pequeña?
  14. ¿Qué diferencia hay entre model card, data card y manifest de calibración?
  15. ¿Qué SLO de IA escribirías para decidir si esta política puede desplegarse?
  16. ¿Qué archivos entrega la práctica del capítulo?

Para saber más

Angelopoulos, A. N. y Bates, S. (2021). A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification. arXiv. https://arxiv.org/abs/2107.07511

Brier, G. W. (1950). Verification of Forecasts Expressed in Terms of Probability. Monthly Weather Review, 78(1), 1-3. https://doi.org/10.1175/1520-0493(1950)078<0001:VOEPIO>2.0.CO;2

Breck, E., Cai, S., Nielsen, E., Salib, M. y Sculley, D. (2017). The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction. IEEE Big Data, 1123-1132. https://research.google/pubs/pub46555/

Efron, B. (1979). Bootstrap Methods: Another Look at the Jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

Geifman, Y. y El-Yaniv, R. (2017). Selective Classification for Deep Neural Networks. Advances in Neural Information Processing Systems. https://proceedings.neurips.cc/paper/2017/hash/4a5cfa9281924139db466a8a19291aff-Abstract.html

Guo, C., Pleiss, G., Sun, Y. y Weinberger, K. Q. (2017). On Calibration of Modern Neural Networks. Proceedings of the 34th International Conference on Machine Learning, 70, 1321-1330. https://proceedings.mlr.press/v70/guo17a.html

Jones, C., Wilkes, J., Murphy, N. y Smith, C. (2016). Service Level Objectives. En Site Reliability Engineering. https://sre.google/sre-book/service-level-objectives/

Kadavath, S., Conerly, T., Askell, A., Henighan, T., Drain, D., Perez, E., Schiefer, N., Hatfield-Dodds, Z., DasSarma, N., Tran-Johnson, E., Johnston, S. y otros. (2022). Language Models (Mostly) Know What They Know. arXiv. https://arxiv.org/abs/2207.05221

Kuhn, L., Gal, Y. y Farquhar, S. (2023). Semantic Uncertainty: Linguistic Invariances for Uncertainty Estimation in Natural Language Generation. International Conference on Learning Representations. https://arxiv.org/abs/2302.09664

Mitchell, M., Wu, S., Zaldivar, A., Barnes, P., Vasserman, L., Hutchinson, B., Spitzer, E., Raji, I. D. y Gebru, T. (2019). Model Cards for Model Reporting. Proceedings of the Conference on Fairness, Accountability, and Transparency, 220-229. https://doi.org/10.1145/3287560.3287596

Murphy, A. H. (1973). A New Vector Partition of the Probability Score. Journal of Applied Meteorology, 12(4), 595-600. https://doi.org/10.1175/1520-0450(1973)012<0595:ANVPOT>2.0.CO;2

Naeini, M. P., Cooper, G. F. y Hauskrecht, M. (2015). Obtaining Well Calibrated Probabilities Using Bayesian Binning. AAAI. https://ojs.aaai.org/index.php/AAAI/article/view/9602

Niculescu-Mizil, A. y Caruana, R. (2005). Predicting Good Probabilities with Supervised Learning. Proceedings of the 22nd International Conference on Machine Learning, 625-632. https://doi.org/10.1145/1102351.1102430

Platt, J. C. (1999). Probabilistic Outputs for Support Vector Machines and Comparisons to Regularized Likelihood Methods. En Advances in Large Margin Classifiers. MIT Press.

Pushkarna, M., Zaldivar, A. y Kjartansson, O. (2022). Data Cards: Purposeful and Transparent Dataset Documentation for Responsible AI. arXiv. https://arxiv.org/abs/2204.01075

scikit-learn. (2026). Probability Calibration. https://scikit-learn.org/stable/modules/calibration.html

Sculley, D., Holt, G., Golovin, D., Davydov, E., Phillips, T., Ebner, D., Chaudhary, V., Young, M., Crespo, J. F. y Dennison, D. (2015). Hidden Technical Debt in Machine Learning Systems. Advances in Neural Information Processing Systems. https://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems

Shafer, G. y Vovk, V. (2008). A Tutorial on Conformal Prediction. Journal of Machine Learning Research, 9, 371-421. https://www.jmlr.org/papers/v9/shafer08a.html

Tabassi, E. (2023). Artificial Intelligence Risk Management Framework (AI RMF 1.0). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.100-1

Vovk, V., Gammerman, A. y Shafer, G. (2005). Algorithmic Learning in a Random World. Springer. https://doi.org/10.1007/b106715

En resumen

IdeaQué te llevas
Un score no es automáticamente una probabilidad.Primero mide si su confianza coincide con frecuencias reales.
Calibrar no es subir accuracy.Es hacer que el número sea útil para decidir bajo coste.
Brier, log loss, ECE y reliability diagram se complementan.Ninguna métrica aislada basta para publicar una política.
Conformal prediction convierte incertidumbre en conjuntos o intervalos.Si el conjunto es ambiguo, el sistema debe revisar o abstenerse.
En LLMs se calibran eventos, no frases de confianza.El evento debe ser observable: formato válido, respuesta soportada, acción correcta o salida aceptada.
La calibración es operativa.Versiona calibrador, umbrales, política, datos, SLOs y monitorización porque todo eso caduca.

Notas

  1. Brier, G. W. (1950). Verification of Forecasts Expressed in Terms of Probability. Monthly Weather Review, 78(1), 1-3. https://doi.org/10.1175/1520-0493(1950)078<0001:VOEPIO>2.0.CO;2

  2. Murphy, A. H. (1973). A New Vector Partition of the Probability Score. Journal of Applied Meteorology, 12(4), 595-600. https://doi.org/10.1175/1520-0450(1973)012<0595:ANVPOT>2.0.CO;2

  3. Niculescu-Mizil, A. y Caruana, R. (2005). Predicting Good Probabilities with Supervised Learning. ICML. https://doi.org/10.1145/1102351.1102430

  4. Platt, J. C. (1999). Probabilistic Outputs for Support Vector Machines and Comparisons to Regularized Likelihood Methods. En Advances in Large Margin Classifiers. MIT Press.

  5. Guo, C., Pleiss, G., Sun, Y. y Weinberger, K. Q. (2017). On Calibration of Modern Neural Networks. ICML. https://proceedings.mlr.press/v70/guo17a.html

  6. Vovk, V., Gammerman, A. y Shafer, G. (2005). Algorithmic Learning in a Random World. Springer. https://doi.org/10.1007/b106715

  7. Shafer, G. y Vovk, V. (2008). A Tutorial on Conformal Prediction. Journal of Machine Learning Research, 9, 371-421. https://www.jmlr.org/papers/v9/shafer08a.html

  8. Angelopoulos, A. N. y Bates, S. (2021). A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification. arXiv. https://arxiv.org/abs/2107.07511

  9. scikit-learn. (2026). Probability Calibration. https://scikit-learn.org/stable/modules/calibration.html. Consultado el 1 de junio de 2026.

  10. Kadavath, S., Conerly, T., Askell, A., Henighan, T., Drain, D., Perez, E., Schiefer, N., Hatfield-Dodds, Z., DasSarma, N., Tran-Johnson, E., Johnston, S. y otros. (2022). Language Models (Mostly) Know What They Know. arXiv. https://arxiv.org/abs/2207.05221

  11. Kuhn, L., Gal, Y. y Farquhar, S. (2023). Semantic Uncertainty: Linguistic Invariances for Uncertainty Estimation in Natural Language Generation. ICLR. https://arxiv.org/abs/2302.09664

  12. Mitchell, M., Wu, S., Zaldivar, A., Barnes, P., Vasserman, L., Hutchinson, B., Spitzer, E., Raji, I. D. y Gebru, T. (2019). Model Cards for Model Reporting. FAT, 220-229. https://doi.org/10.1145/3287560.3287596

  13. Pushkarna, M., Zaldivar, A. y Kjartansson, O. (2022). Data Cards: Purposeful and Transparent Dataset Documentation for Responsible AI. arXiv. https://arxiv.org/abs/2204.01075

  14. Sculley, D., Holt, G., Golovin, D., Davydov, E., Phillips, T., Ebner, D., Chaudhary, V., Young, M., Crespo, J. F. y Dennison, D. (2015). Hidden Technical Debt in Machine Learning Systems. NeurIPS. https://papers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems

  15. Breck, E., Cai, S., Nielsen, E., Salib, M. y Sculley, D. (2017). The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction. IEEE Big Data. https://research.google/pubs/pub46555/

  16. Jones, C., Wilkes, J., Murphy, N. y Smith, C. (2016). Service Level Objectives. En Site Reliability Engineering. https://sre.google/sre-book/service-level-objectives/

  17. Guo et al., 2017.

  18. Efron, B. (1979). Bootstrap Methods: Another Look at the Jackknife. The Annals of Statistics, 7(1), 1-26. https://doi.org/10.1214/aos/1176344552

  19. Geifman, Y. y El-Yaniv, R. (2017). Selective Classification for Deep Neural Networks. NeurIPS. https://proceedings.neurips.cc/paper/2017/hash/4a5cfa9281924139db466a8a19291aff-Abstract.html

  20. Tabassi, E. (2023). Artificial Intelligence Risk Management Framework (AI RMF 1.0). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.100-1

Capítulo 06

Facsímil 7 · Evaluar, calibrar e interpretar

Capítulo 06: Interpretabilidad práctica y laboratorio de evaluación

Qué deberías poder hacer al terminar

Este facsímil empezó con una idea sobria: una eval existe para tomar una decisión. Después medimos errores, evaluamos RAG, diseñamos evaluadores, calibramos scores y convertimos incertidumbre en política. Ahora queda una pregunta que aparece siempre en ingeniería de IA:

¿Podemos explicar lo suficiente para depurar, publicar, limitar o rechazar este sistema?

Interpretabilidad no es una palabra para tranquilizar a alguien en una reunión. Tampoco es una imagen bonita, una tabla de pesos o una respuesta del modelo diciendo “he decidido esto por...”. Interpretabilidad, en ingeniería, es una herramienta para hacer mejores preguntas: qué feature pesa, qué caso cambia, qué slice se rompe, qué explicación es estable, qué parte del sistema no entendemos todavía y qué decisión podemos defender.

Al terminar deberías poder hacer esto:

Resultado de aprendizajeEvidencia de que lo sabes hacer
Separar interpretabilidad, explicabilidad y transparencia.No usas esas palabras como sinónimos automáticos.
Elegir método según pregunta.Distingues explicación local, global, contrafactual, conceptual y mecánica.
Evaluar una explicación.Mides fidelidad, estabilidad, sensibilidad y utilidad operativa.
Leer atribuciones con cautela.Sabes por qué una atribución plausible puede ser infiel.
Diseñar contrafactuales útiles.Separas cambios accionables de cambios imposibles o injustos.
Conectar interpretabilidad con EvalOps.Conviertes explicaciones en checks, model card y casos de regresión.
Cerrar el facsímil con práctica real.Ejecutas un kit, generas un reporte y resuelves dos retos integradores.

La idea central del capítulo es esta: una explicación no se acepta porque suene bien; se acepta porque ayuda a diagnosticar y resiste comprobaciones.

El problema: una explicación puede sonar perfecta y no explicar nada

Imagina un sistema que prioriza tickets académicos. Para un caso concreto devuelve:

{
  "score": 0.86,
  "decision": "urgente",
  "explanation": "El caso parece urgente por su tono y por la fecha cercana."
}

La frase suena humana. Pero un ingeniero debería preguntar:

PreguntaPor qué importa
¿El modelo tenía una feature llamada tono?Si no existe, la explicación puede ser una racionalización.
¿Qué pasa si eliminamos la feature principal?Si la salida no cambia, la explicación no sostiene la decisión.
¿La explicación cambia ante pequeñas perturbaciones?Si cambia demasiado, no es estable.
¿El caso era parte de un slice problemático?Una explicación local puede esconder un patrón global malo.
¿El cambio recomendado es accionable?No todo contrafactual sirve para una persona o un equipo.

Esto es especialmente delicado en LLMs. Una respuesta puede construir una narración convincente después de producir la salida. En ese caso, la explicación puede ser plausible para nosotros, pero no fiel al proceso que produjo el resultado.

Por eso el capítulo no va de “hacer explicable la IA” en abstracto. Va de crear un expediente técnico: qué método usamos, qué pregunta responde, qué prueba lo contradice y qué decisión permite tomar.

Qué no es interpretabilidad

Interpretabilidad no es necesariamente simplicidad. Un modelo lineal puede ser fácil de leer y aun así estar usando features mal definidas, proxies pobres o datos incompletos. Un árbol pequeño puede ser comprensible y seguir aprendiendo una regla poco útil.

Tampoco es una promesa de verdad. Una explicación post hoc puede aproximar el comportamiento de un modelo complejo, pero no convertirse en el modelo real. LIME aproxima localmente; SHAP reparte contribuciones bajo supuestos; saliency maps señalan sensibilidad; contrafactuales proponen cambios; ninguna de esas piezas sustituye una evaluación.

Y, sobre todo, interpretabilidad no es decoración de compliance. Si nadie puede decir qué decisión cambia gracias a la explicación, quizá solo hemos añadido otra pantalla.

ConfusiónLectura de ingeniería
“Tenemos explicación, así que el sistema es fiable”.La explicación también se evalúa.
“El mapa de calor marca la zona importante”.Hay que probar sensibilidad, estabilidad y sanity checks.
“El modelo dice por qué decidió”.Una explicación textual puede no ser fiel.
“SHAP lo arregla”.SHAP responde una familia concreta de preguntas bajo supuestos concretos.
“Contrafactual significa recomendación”.Solo algunos cambios son accionables y aceptables.

Lipton advertía que “interpretabilidad” suele mezclar propiedades distintas: transparencia, simulabilidad, decomponibilidad, post hoc explanations y confianza humana.1 Doshi-Velez y Kim proponían tratar la interpretabilidad como una cuestión evaluable, no como una etiqueta estética.2

Qué sí es interpretar un sistema de IA

Interpretar es responder una pregunta situada. No “explícame el modelo”, sino:

PreguntaMétodo razonable
¿Por qué este caso se marcó como urgente?Explicación local y prueba de borrado.
¿Qué features pesan globalmente?Importancia por permutación, SHAP agregado o modelo interpretable.
¿Qué tendría que cambiar para otra decisión?Contrafactual accionable.
¿Qué concepto humano usa el modelo?TCAV o análisis por conceptos.
¿Qué región de una imagen activó una clase?Grad-CAM u otro método visual con sanity checks.
¿Qué parte interna participa en una asociación factual?Intervenciones causales o análisis mecanicista.
¿Qué explicación puedo mostrar a usuario final?Una explicación validada por utilidad, no solo por fidelidad técnica.

Hay dos ejes que conviene escribir siempre:

EjePregunta
Local frente a global¿Explicamos un caso concreto o el comportamiento general?
Fidelidad frente a plausibilidad¿Refleja el modelo o solo convence a la persona?
Diagnóstico frente a comunicación¿Sirve para depurar o para informar una decisión?
Accionable frente a descriptivo¿Permite hacer algo distinto?

Jacovi y Goldberg separan explícitamente fidelidad y plausibilidad en NLP: una explicación puede parecer buena a una persona y no reflejar el mecanismo que produjo la salida.3 Esa distinción es clave para LLMs.

Fecha de corte del estado del arte

Fecha de corte: 6 de junio de 2026.
Fuentes consultadas: trabajos clásicos sobre ciencia de la interpretabilidad, LIME, SHAP, Integrated Gradients, Grad-CAM, sanity checks, contrafactuales, modelos interpretables para decisiones sensibles, TCAV, interpretabilidad fiel en NLP y localización causal de asociaciones factuales en GPT.

LIME propone aproximar localmente un modelo complejo con un modelo interpretable alrededor de una predicción concreta.4 SHAP conecta atribuciones aditivas con valores de Shapley y un marco común para asignar importancia a features.5 Integrated Gradients plantea axiomas como sensibilidad e invariancia de implementación para atribuciones en redes profundas.6

Grad-CAM localiza regiones relevantes para modelos visuales usando gradientes hacia capas convolucionales.7 Adebayo et al. mostraron que algunos mapas de saliencia pueden fallar sanity checks: si la explicación apenas cambia al aleatorizar parámetros o etiquetas, hay que desconfiar.8

Los contrafactuales explican decisiones indicando cambios mínimos que producirían otra salida.9 Rudin defiende que en decisiones de alto impacto conviene preferir modelos interpretables cuando sea posible, en lugar de explicar una caja negra después.10

Para conceptos humanos, TCAV cuantifica sensibilidad a direcciones conceptuales aprendidas.11 En modelos de lenguaje, trabajos como ROME usan intervenciones causales para localizar asociaciones factuales en GPT.12

Anatomía de una auditoría de interpretabilidad

Auditoría de interpretabilidad práctica Diagrama en blanco, negro y gris que conecta pregunta de ingeniería, modelo, explicación local, explicación global, contrafactuales, pruebas de fidelidad, estabilidad, model card, EvalOps y decisión. Interpretar no es decorar: es auditar una decisión Una explicación defendible conecta pregunta, método, prueba de fidelidad y acción operativa. 1 · Pregunta Qué necesitas saber • por qué este caso • qué pesa globalmente • qué cambio bastaría • qué parte no entendemos Sin pregunta concreta, la explicación no tiene contrato. 2 · Modelo y dato Score trazable z = beta0 + sum beta_j x_j p = sigmoid(z) versiones: model · data · threshold policy · explanation Si falta linaje, no hay auditoría. 3 · Explicar Métodos Local: contribuciones Global: permutación Cambio: contrafactual Concepto: TCAV Interno: intervención Un método responde solo una pregunta. 4 · Probar Fidelidad borrado de top feature permutación estabilidad sanity checks casos frontera Una explicación sin prueba es una hipótesis. Local case_id: t001 score: 0.8596 1 · student_wait_days 2 · missing_payment 3 · prior_cases Global deadline wait prior caída de accuracy Contrafactual Caso t019 score 0.779 → 0.625 Cambio accionable: adjuntar documentación No todo cambio mínimo es aceptable o justo. Contrato Model card Gate CI Monitorización Decisión purpose · consumers required_fields data_hash · policy_hash scope · limits eval evidence owner · release borrado · estabilidad suficiencia · C_K proxy scan · threshold top feature distribution drift · trace id counterfactual rate permitir · limitar revisar · bloquear criterio operativo IA para gente curiosa / Facsímil 07 / Capítulo 06 / 686f6c61
Una auditoría de interpretabilidad empieza con una pregunta y termina en una decisión. Entre medias hay método, prueba y documentación.

Las fórmulas que sí conviene saber

En el kit práctico usamos un modelo lineal porque permite ver las tripas sin depender de librerías. No porque todos los sistemas reales sean lineales, sino porque es el punto más honesto para aprender a auditar explicaciones.

El modelo calcula un logit:

z(x)=β0+j=1dβjxjz(x) = \beta_0 + \sum_{j=1}^{d} \beta_j x_j

Y lo convierte en probabilidad con una sigmoide:

p^(x)=σ(z)=11+ez(x)\hat{p}(x) = \sigma(z) = \frac{1}{1 + e^{-z(x)}}
SímboloSignificadoEjemplo
xxCaso que evaluamos.Ticket t001.
xjx_jValor de la feature jj.student_wait_days = 12.
βj\beta_jPeso de la feature jj.0,13.
β0\beta_0Intercepto.-2,35.
z(x)z(x)Suma lineal antes de probabilidad.1,812.
p^(x)\hat{p}(x)Probabilidad estimada.0,8596.

En un modelo lineal, la contribución local de una feature puede escribirse de forma directa:

cj(x)=βjxjc_j(x) = \beta_j x_j
SímboloSignificadoEjemplo
cj(x)c_j(x)Contribución de la feature jj al logit.1,56.
βj\beta_jPeso aprendido o fijado.0,13.
xjx_jValor del caso.12 días de espera.

Para t001, el kit obtiene:

FeatureValorPesoContribución
student_wait_days120,131,56
missing_payment11,251,25
prior_cases30,280,84

La explicación es clara: el modelo sube prioridad por días de espera, pago pendiente y casos previos. Pero no nos basta con leer esa tabla. Probamos si al quitar la feature superior el score cae de forma relevante.

La prueba de borrado mide:

Δdelete(x,j)=p^(x)p^(xj)\Delta_{delete}(x, j) = \hat{p}(x) - \hat{p}(x_{\setminus j})
SímboloSignificadoEjemplo
Δdelete\Delta_{delete}Caída de score al neutralizar una feature.0,44 en el máximo del kit.
xjx_{\setminus j}Caso con la feature jj neutralizada.student_wait_days = 0.
p^(x)\hat{p}(x)Score original.0,8596.

Para importancia global por permutación:

Ij=M(D)M(Dπ(j))I_j = M(D) - M(D_{\pi(j)})
SímboloSignificadoEjemplo
IjI_jImportancia global de la feature jj.0,25 para deadline_hours.
M(D)M(D)Métrica original en dataset.Accuracy 0,75.
Dπ(j)D_{\pi(j)}Dataset con la feature jj permutada.deadline_hours desordenada.

En el kit, las tres features con mayor caída son:

FeatureAccuracy originalAccuracy permutadaCaída
deadline_hours0,750,500,25
student_wait_days0,750,550,20
prior_cases0,750,600,15

Para contrafactuales, buscamos un caso parecido que cambie la decisión:

x\*=argminxd(x,x)sujeto af(x)f(x)x^\* = \arg\min_{x'} d(x, x') \quad \text{sujeto a} \quad f(x') \ne f(x)
SímboloSignificadoEjemplo
x\*x^\*Caso contrafactual elegido.Ticket con documentación adjunta.
d(x,x)d(x,x')Distancia o coste de cambiar de xx a xx'.Un cambio accionable.
f(x)f(x')Decisión del modelo para el caso modificado.Pasa de urgente a normal.

Esta fórmula parece limpia, pero en producto tiene una condición escondida: el cambio debe ser accionable y aceptable. No sirve decir “si fueras otra persona” o “si tu historial no existiera”. Sirve decir “si falta documentación, pide la documentación” o “si el pago está pendiente, compruébalo”.

Método no es garantía: cómo elegir bien

Los métodos de interpretación responden preguntas distintas:

MétodoPregunta que respondeRiesgo
Modelo interpretable¿Puedo entender el mecanismo completo?Puede ser demasiado simple para el problema.
LIME¿Qué modelo simple aproxima esta predicción localmente?Depende de perturbaciones y vecindario.
SHAP¿Cómo se reparten contribuciones entre features?Depende del fondo, correlaciones y coste computacional.
Integrated Gradients¿Qué entrada aporta al cambio desde una línea base?La línea base puede cambiar la historia.
Grad-CAM¿Qué región visual pesa para una clase?Mapa grueso y sensible a sanity checks.
Contrafactuales¿Qué cambio produciría otra decisión?Puede proponer cambios no accionables.
TCAV¿Qué concepto humano afecta al modelo?Requiere buenos ejemplos del concepto.
Intervenciones internas¿Qué componente participa causalmente?Es costoso, específico y fácil de sobreinterpretar.

La regla práctica:

No elijas el método por popularidad. Elige el método por la decisión que necesitas tomar.

Si el equipo quiere depurar un clasificador tabular, importancia por permutación y contrafactuales pueden bastar. Si quiere revisar un modelo de visión, Grad-CAM puede ayudar, pero con sanity checks. Si quiere saber si un LLM recupera un hecho por una zona concreta, hacen falta intervenciones más cercanas a interpretabilidad mecanicista. Si la decisión tiene alto impacto, Rudin nos recuerda que quizá el primer debate no sea “cómo explico la caja negra”, sino “por qué no uso un modelo interpretable desde el principio”.

Cómo evaluar una explicación

Una explicación debe pasar pruebas. No todas son matemáticas sofisticadas; algunas son puro criterio de ingeniería:

PruebaQué compruebaSeñal mala
BorradoQuitar la parte explicada cambia la salida.La salida no cambia.
InserciónAñadir features importantes recupera la salida.Features supuestamente clave no aportan.
PermutaciónDesordenar una feature global baja métrica.Importancia alta sin caída real.
EstabilidadPerturbaciones pequeñas conservan explicación.Explicación cambia por ruido menor.
Sanity checkExplicación responde a modelo/datos reales.Mapa igual con pesos aleatorios.
Revisión de sliceExplicación se sostiene por segmento.Global bien, segmento mal.
Utilidad humanaLa persona decide mejor con la explicación.Más confianza sin mejor decisión.

Hay una trampa clásica: una explicación muy bonita puede aumentar confianza sin aumentar acierto. Eso es peligroso. En entornos de producto, una explicación debería medirse por decisión: reduce errores, mejora revisión, acelera diagnóstico o permite detectar un problema antes.

Esto en un proyecto real

En un proyecto de IA, interpretabilidad aparece en cinco momentos:

MomentoPregunta
Diseño¿Necesitamos modelo interpretable por defecto?
Desarrollo¿Qué features dominan y cuáles son proxies problemáticos?
Evaluación¿Las explicaciones se sostienen en fallos y slices?
Producción¿Podemos explicar una incidencia o una decisión revisada?
Mejora¿Qué casos se convierten en regresión o cambio de datos?

Un ejemplo cercano: en un asistente RAG, la explicación no debería ser “respondí esto porque el modelo lo consideró relevante”. Debería mostrar:

CapaEvidencia
RetrievalDocumentos recuperados, scores, reranker y citas usadas.
RespuestaAfirmaciones principales y soporte por chunk.
EvaluaciónGroundedness, cobertura de cita, abstención y errores.
CalibraciónProbabilidad de respuesta aceptada o zona de revisión.
OperaciónModelo, prompt, índice, release y trace id.

En agentes, una explicación útil no es solo el resumen final. Es la trayectoria: qué herramienta eligió, con qué argumentos, qué observó, qué descartó, cuánto costó y dónde pidió revisión.

Contrato de explicación: quién puede usarla y para qué

Una explicación profesional debería tener contrato. No basta con producir un gráfico o una frase. Hay que declarar para qué sirve, quién puede verla, qué campos son obligatorios, qué versión del modelo la produjo y qué usos no están permitidos.

En el kit generamos output/explanation_contract.json. Un ejemplo resumido:

{
  "model_version": "support-prioritizer-linear-v1",
  "explanation_policy_version": "interp-audit-v1",
  "owner": "equipo-ia",
  "purpose": "diagnostico interno y revision operativa de tickets priorizados",
  "allowed_consumers": ["ingenieria", "soporte_n2", "producto"],
  "not_for": ["decision final sin revision", "comunicacion automatica a usuario"],
  "required_fields": [
    "case_id",
    "model_version",
    "score",
    "prediction",
    "top_features",
    "deletion_test",
    "counterfactual",
    "data_hash_sha256",
    "policy_hash_sha256"
  ]
}

La parte importante no es el JSON bonito. Es la disciplina:

CampoQué evita
purposeQue una explicación de diagnóstico acabe vendida como verdad final.
allowed_consumersQue cualquier equipo use la explicación sin entender sus límites.
not_forQue se automatice una decisión que exige revisión.
required_fieldsQue una explicación llegue sin score, versión, prueba o linaje.
data_hash_sha256Que no sepamos con qué datos se generó.
policy_hash_sha256Que cambie el umbral o la política y nadie lo vea.

Esto conecta con las model cards, pero baja a operación. Una model card explica el sistema; el contrato de explicación fija cómo se puede consumir una explicación concreta en una run concreta.

Tests de explicación en CI

Si una explicación forma parte de una release, también debería tener tests. No en el sentido de “la explicación es bonita”, sino en el sentido de que pasa checks mínimos antes de publicar una versión.

El kit produce output/ci_explanation_gate.json:

{
  "gate": "pass",
  "checks": [
    {"name": "deletion_top_feature_drop", "passes": true},
    {"name": "permutation_importance_drop", "passes": true},
    {"name": "stability_top1", "passes": true},
    {"name": "counterfactual_available", "passes": true},
    {"name": "comprehensiveness_top2", "passes": true},
    {"name": "sufficiency_top2", "passes": true},
    {"name": "feature_proxy_scan", "passes": true}
  ],
  "recommendation": "permitir uso interno con monitorización"
}

Aquí aparecen dos pruebas que conviene conocer bien:

CK(x)=p^(x)p^(xSK)C_K(x) = \hat{p}(x) - \hat{p}(x_{\setminus S_K})
SímboloSignificado
CK(x)C_K(x)Comprehensiveness para las KK features explicadas.
SKS_KConjunto de las KK features superiores de la explicación.
xSKx_{\setminus S_K}Caso con esas features neutralizadas.
p^(x)\hat{p}(x)Score original del modelo.

Si quitamos las features que la explicación dice que importan y el score no cae, la explicación no está contando algo fuerte.

La suficiencia mira la pregunta contraria:

UK(x)=p^(x)p^(xSK)U_K(x) = \left| \hat{p}(x) - \hat{p}(x_{S_K}) \right|
SímboloSignificado
UK(x)U_K(x)Diferencia entre score original y score usando solo las KK features explicadas.
xSKx_{S_K}Caso donde conservamos las features explicadas y neutralizamos el resto.

Si las features explicadas bastan para reconstruir casi todo el score, UK(x)U_K(x) será bajo. Si no bastan, quizá la explicación ha omitido una señal importante.

En la ejecución actual:

CheckResultadoLectura
deletion_top_feature_drop0,444993La feature superior tiene efecto medible.
permutation_importance_drop0,25Hay features globales con impacto real.
stability_top10,9667La explicación local es estable ante perturbación pequeña.
comprehensiveness_top20,210094Al quitar las dos features principales, el score cae.
sufficiency_top20,063245Las dos features principales aproximan bastante el score.
feature_proxy_scan0,8837Hay correlación alta entre prior_cases y student_wait_days; conviene revisarla.

Ese último punto es muy de ingeniería. Una correlación alta no prueba causalidad ni invalida el modelo, pero sí abre una tarea: comprobar si dos features están contando casi lo mismo, si una funciona como proxy de otra o si el dataset necesita rediseño.

Producción: deriva de explicaciones y trazas

En producción no basta con guardar predicciones. Si la explicación influye en revisión, soporte o producto, conviene guardar eventos de explicación:

{
  "case_id": "t001",
  "model_version": "support-prioritizer-linear-v1",
  "score": 0.859603,
  "prediction": 1,
  "top_features": ["student_wait_days", "missing_payment", "prior_cases"],
  "data_hash_sha256": "ec7bf...",
  "policy_hash_sha256": "9c167..."
}

Con esos eventos podemos medir deriva explicativa. Una forma sencilla es comparar la distribución de feature principal entre dos ventanas:

DTV(Pt,Pt1)=12fPt(f)Pt1(f)D_{TV}(P_t, P_{t-1}) = \frac{1}{2} \sum_f \left| P_t(f) - P_{t-1}(f) \right|
SímboloSignificado
Pt(f)P_t(f)Proporción de casos donde la feature ff fue la explicación principal en la ventana tt.
Pt1(f)P_{t-1}(f)La misma proporción en la ventana anterior.
DTVD_{TV}Distancia total variation entre distribuciones.

En la muestra actual, la distribución de feature principal queda así:

Feature principalProporción
deadline_hours0,50
student_wait_days0,30
missing_payment0,15
prior_cases0,05

Si en la siguiente release prior_cases pasa de 0,05 a 0,45, no basta con decir “el accuracy sigue bien”. Algo cambió en la razón operativa de las decisiones. Puede ser un cambio de datos, un cambio de política, un error de feature engineering o una señal nueva real. Hay que investigarlo.

LLMs: explicación textual, traza y mecanismo

En modelos de lenguaje hay una confusión habitual: pedir al modelo que explique su respuesta y tratar esa explicación como mecanismo interno. Son cosas distintas.

NivelQué esQué puede aportarQué no garantiza
Explicación textualUna justificación generada en lenguaje natural.Puede ayudar a revisar una respuesta.No prueba cómo se produjo la salida.
Traza operativaPrompt, mensajes, herramientas, documentos, scores, coste y eventos.Permite depurar una run.No abre las capas internas del modelo.
Evidencia externaChunks, citas, resultados de herramientas y validaciones.Permite comprobar afirmaciones.No demuestra causalidad interna.
Interpretabilidad mecanicistaAnálisis de activaciones, circuitos o intervenciones internas.Puede estudiar mecanismos concretos.Es costosa, parcial y no siempre trasladable a producto.

Para ingeniería aplicada, muchas veces la traza vale más que una explicación verbal. Si un agente consulta una herramienta, recupera un documento, cambia una respuesta y pide revisión por score bajo, eso se puede auditar. Si solo dice “he razonado cuidadosamente”, no tenemos suficiente.

Una regla práctica para alumnos:

En LLMs, no confundas explicación narrada con evidencia. Guarda trazas, contratos y resultados verificables.

Manos a la obra

Práctica: auditar una explicación que se pueda defender.

Kit ejecutable de este capítulo: kit descargable.

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/ai/interpretability_audit.py --write

El kit del capítulo está en:

kit/

Construye una auditoría pequeña pero completa para un modelo lineal de priorización de tickets. El script no necesita dependencias externas.

Estructura

kit/
  README.md
  data/ticket_features.csv
  policies/interpretability_policy.json
  ops/ai/interpretability_audit.py
  output/interpretability_report.json
  output/explanation_contract.json
  output/ci_explanation_gate.json
  output/model_card_interpretability.md
  output/interpretability_decision.md

Cómo lo ejecutas

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/ai/interpretability_audit.py --write
cat output/interpretability_decision.md

Qué hace el script

PiezaQué produce
PredicciónScore y clase para cada ticket.
Explicación localContribuciones por feature.
Prueba de borradoCaída de score al neutralizar la feature superior.
Importancia globalCaída de accuracy al permutar cada feature.
EstabilidadAcuerdo de top feature ante perturbaciones pequeñas.
ContrafactualesCambios accionables que podrían mover una decisión.
ComprehensivenessCaída media al eliminar las dos features explicadas.
SuficienciaDiferencia media usando solo las dos features explicadas.
Revisión de proxiesCorrelación máxima entre features para detectar señales redundantes.
Contrato de explicaciónCampos obligatorios, consumidores y usos excluidos.
Gate de CIResumen pass/fail para automatizar release.
Model cardFragmento documentando explicación y límites.
DecisiónMarkdown con checks y conclusión técnica.

Salida esperada

El kit genera, entre otras cosas:

{
  "accuracy": 0.75,
  "stability": {
    "top1_agreement": 0.9667
  },
  "audit_checks": [
    {
      "name": "deletion_top_feature_drop",
      "passes": true
    },
    {
      "name": "permutation_importance_drop",
      "passes": true
    },
    {
      "name": "stability_top1",
      "passes": true
    },
    {
      "name": "counterfactual_available",
      "passes": true
    },
    {
      "name": "comprehensiveness_top2",
      "passes": true
    },
    {
      "name": "sufficiency_top2",
      "passes": true
    },
    {
      "name": "feature_proxy_scan",
      "passes": true
    }
  ]
}

Y una decisión:

Estado: defendible.

La explicación local se acepta solo porque puede contrastarse con pruebas de borrado,
importancia global, estabilidad y contrafactuales accionables.

La práctica importante está en discutir límites:

ResultadoLectura
accuracy = 0.75El modelo no es perfecto; explicación no compensa evaluación pobre.
top1_agreement = 0.9667La feature principal es estable ante perturbaciones pequeñas.
deadline_hours cae 0,25 al permutarGlobalmente pesa mucho.
Hay contrafactual accionablePodemos convertir explicación en siguiente paso.
comprehensiveness_top2 = 0.210094Al quitar las dos señales principales, la salida pierde fuerza.
sufficiency_top2 = 0.063245Las dos señales principales casi bastan para reconstruir la salida.
Correlación máxima 0,8837prior_cases y student_wait_days deberían revisarse como posible redundancia.

El resultado no se queda en reporte. También deja dos artefactos útiles para ingeniería:

ArchivoPara qué sirve
output/explanation_contract.jsonDefine cómo puede consumirse una explicación y qué campos debe traer.
output/ci_explanation_gate.jsonPermite meter los checks de explicación en CI antes de publicar una versión.

Cómo encaja todo

flowchart TD
  subgraph anteriores["Facsímil 7 · Lo que ya construimos"]
    C1["C01<br/>Eval como decisión"]
    C2["C02<br/>Métricas y coste"]
    C3["C03<br/>RAG y groundedness"]
    C4["C04<br/>Evaluadores y trazas"]
    C5["C05<br/>Calibración e incertidumbre"]
  end

  subgraph capitulo["C06 · Interpretabilidad práctica"]
    Q["Pregunta de ingeniería"]
    Local["Explicación local"]
    Global["Explicación global"]
    CF["Contrafactual"]
    Faith["Fidelidad y estabilidad"]
    Contract["Contrato de explicación"]
    Gate["Gate de CI"]
    Drift["Deriva de explicaciones"]
    Card["Model card"]
    Decision["Decisión defendible"]
    Lab["Laboratorio final"]
  end

  subgraph despues["Lo que prepara"]
    F8["F8<br/>Datos, slices y linaje"]
    F9["F9<br/>Gobernanza y controles"]
    F11["F11<br/>Producto y experiencia"]
  end

  C1 -->|"define para qué explicar"| Q
  C2 -->|"aporta coste y errores"| Decision
  C3 -->|"exige evidencia recuperada"| Local
  C4 -->|"aporta trazas evaluables"| Faith
  C5 -->|"separa score y confianza"| Decision

  Q -->|"elige método"| Local
  Q -->|"elige método"| Global
  Q -->|"elige método"| CF
  Local -->|"se contrasta con"| Faith
  Global -->|"se contrasta con"| Faith
  CF -->|"debe ser accionable"| Decision
  Faith -->|"define mínimos para"| Gate
  Gate -->|"bloquea o permite"| Decision
  Faith -->|"documenta límites en"| Card
  Card -->|"alimenta"| Contract
  Contract -->|"fija consumidores y campos"| Decision
  Contract -->|"exige trazas para"| Drift
  Drift -->|"detecta cambios en"| Decision
  Decision -->|"se practica en"| Lab

  Lab -->|"pide datos trazables para"| F8
  Card -->|"apoya controles en"| F9
  Gate -->|"se integra con controles en"| F9
  Drift -->|"depende de linaje en"| F8
  CF -->|"afecta comunicación en"| F11

  classDef chapter fill:#ffffff,stroke:#111111,color:#111111,stroke-width:1.4px;
  classDef external fill:#f7f7f7,stroke:#777777,color:#111111,stroke-width:1.1px,stroke-dasharray: 5 4;
  class Q,Local,Global,CF,Faith,Contract,Gate,Drift,Card,Decision,Lab chapter;
  class C1,C2,C3,C4,C5,F8,F9,F11 external;

Vocabulario aprendido

TérminoDefinición breve
InterpretabilidadCapacidad de entender una decisión o comportamiento con una finalidad concreta.
Explicación localExplicación de una predicción concreta.
Explicación globalResumen del comportamiento general del modelo.
FidelidadCorrespondencia entre explicación y comportamiento real del modelo.
PlausibilidadFacilidad con la que una persona acepta una explicación como razonable.
AtribuciónReparto de una salida entre features, tokens, regiones o componentes.
LIMEAproximación local de un modelo complejo mediante un modelo interpretable.
SHAPMarco de atribución basado en valores de Shapley.
Integrated GradientsMétodo de atribución que integra gradientes desde una línea base hasta la entrada.
Grad-CAMMétodo visual que localiza regiones relevantes para una clase.
ContrafactualCambio mínimo de entrada que produciría otra decisión.
Importancia por permutaciónCaída de métrica al desordenar una feature.
Sanity checkPrueba para detectar explicaciones que no responden al modelo o datos reales.
TCAVTécnica que mide sensibilidad a conceptos definidos por personas.
Interpretabilidad mecanicistaEstudio de componentes internos y circuitos del modelo mediante análisis e intervenciones.
Contrato de explicaciónAcuerdo técnico que fija finalidad, consumidores, campos obligatorios, linaje y usos excluidos.
ComprehensivenessPrueba que elimina las features explicadas y comprueba cuánto cae el score.
SuficienciaPrueba que conserva solo las features explicadas y comprueba cuánto se parece el score al original.
Deriva de explicacionesCambio temporal o entre versiones en las razones principales del modelo.
ProxyFeature que puede representar indirectamente otra variable o mezclar señales que conviene separar.
RecourseCambio accionable que una persona o equipo puede realizar para mover una decisión o resolver un caso.

Dónde solía tropezar yo

TropiezoPor qué ocurreAntídoto
Aceptar explicaciones porque suenan bienLa explicación textual parece convincente.Separar plausibilidad de fidelidad.
Usar un método para todoLIME, SHAP o saliency parecen universales.Empezar por la pregunta de ingeniería.
Enseñar atribuciones sin pruebasLa tabla queda elegante.Añadir borrado, permutación y estabilidad.
Proponer contrafactuales imposiblesLa optimización encuentra cambios absurdos.Filtrar por accionabilidad y aceptabilidad.
Confundir atención con explicaciónUn peso de atención parece intuitivo.Verificar si cambia la salida y si el mecanismo lo sostiene.
Olvidar los slicesLa explicación global tapa segmentos malos.Auditar por slice y por caso frontera.
No declarar quién puede usar la explicaciónEl mismo artefacto se usa para diagnóstico, soporte y comunicación externa.Escribir contrato de explicación.
Dejar explicaciones fuera de CILa release pasa métricas, pero cambia la lógica explicativa.Añadir gate con borrado, suficiencia, estabilidad y proxies.
No vigilar deriva de razonesEl modelo sigue acertando, pero decide por señales distintas.Monitorizar distribución de top features.
Tratar proxy como causalidadUna correlación alta parece una explicación causal.Revisar pares de features y validar con datos o dominio.
Confundir explicación textual de LLM con mecanismoLa respuesta suena razonada.Pedir trazas, evidencias y contratos de salida.

Antes de pasar página

Antes de cerrar el facsímil, deberías poder responder:

  1. ¿Qué diferencia hay entre interpretabilidad, explicación y transparencia?
  2. ¿Por qué una explicación plausible puede ser infiel?
  3. ¿Cuándo preferirías un modelo interpretable frente a explicar una caja negra?
  4. ¿Qué pregunta responde LIME y qué no responde?
  5. ¿Qué aporta SHAP y qué supuestos conviene revisar?
  6. ¿Por qué Integrated Gradients depende de una línea base?
  7. ¿Qué comprobarías antes de confiar en un mapa visual?
  8. ¿Qué hace que un contrafactual sea accionable?
  9. ¿Cómo usarías pruebas de borrado y permutación?
  10. ¿Qué debería entrar en una model card sobre interpretabilidad?
  11. ¿Qué campos mínimos pondrías en un contrato de explicación?
  12. ¿Qué diferencia hay entre comprehensiveness y suficiencia?
  13. ¿Qué señal te daría una deriva de explicaciones?
  14. ¿Por qué una correlación alta entre features puede pedir revisión?
  15. ¿Qué produce el kit del capítulo?
  16. ¿Cómo conecta interpretabilidad con EvalOps y calibración?

En resumen

IdeaQué te llevas
Interpretar es responder una pregunta situada.No existe “explicación general” útil para todo.
Plausibilidad no basta.Una explicación debe probar fidelidad, estabilidad y utilidad.
Cada método tiene límites.LIME, SHAP, Grad-CAM, contrafactuales y TCAV responden cosas distintas.
Los contrafactuales necesitan criterio humano.Mínimo no significa accionable ni aceptable.
Las explicaciones se documentan.Model card, datos, thresholds, checks y decisión.
Las explicaciones también tienen contrato.Deben declarar finalidad, consumidores, linaje y campos obligatorios.
Las explicaciones se testean.CI puede revisar borrado, suficiencia, estabilidad, proxies y contrafactuales.
Las explicaciones pueden derivar.En producción conviene vigilar qué razones dominan cada versión.
El facsímil se cierra practicando.El laboratorio integra métricas, RAG, evaluadores, calibración e interpretación.

Laboratorio

Un laboratorio, dentro de este libro, es un espacio de práctica guiada. No es un examen para pillar a nadie. Es el lugar donde convertimos el facsímil en trabajo real: construir, medir, explicar y defender.

En este laboratorio cerramos el facsímil 7. Los retos movilizan todo lo visto:

TemaCapítulo
Decidir para qué sirve una evalC01
Medir errores y costeC02
Evaluar RAG por capasC03
Evaluar respuestas y trazas con rúbricasC04
Calibrar scores e incertidumbreC05
Auditar interpretabilidadC06

Los dos retos tienen resolución. El primero usa el kit de interpretabilidad. El segundo construye un expediente completo de evaluación para una release de IA.

Cómo trabajar este laboratorio

Este laboratorio no va de hacer una explicación bonita. Va de defender una release. La pregunta profesional no es “¿entiendo este score?”, sino:

¿Puedo usar esta evaluación para publicar, limitar o bloquear una versión de IA?

Trabaja con esta secuencia:

PasoQué hacesEvidencia esperada
1Ejecutas el kit de interpretabilidad.interpretability_report.json.
2Lees la decisión generada.interpretability_decision.md.
3Revisas el contrato de explicación.explanation_contract.json.
4Compruebas el gate automatizable.ci_explanation_gate.json.
5Preparas documentación de release.model_card_interpretability.md.
6Escribes una decisión.decision.md.

Un resultado fuerte tiene tres capas: números, interpretación y acción. Si solo hay números, falta criterio. Si solo hay interpretación, falta evidencia. Si no hay acción, no hay ingeniería.

Reto 1: defender una explicación local

Contexto

El equipo de soporte quiere saber por qué el ticket t001 se marca como urgente. La respuesta no puede ser “porque el score es alto”. Tienes que enseñar qué features empujan la decisión y qué pruebas sostienen esa explicación.

El caso tiene intención: t001 no se explica para convencer con una frase amable, sino para probar si la explicación sirve como herramienta interna. Una explicación interna debe permitir depurar, revisar y decidir si el comportamiento está dentro del contrato. Una explicación hacia usuario final exigiría otro lenguaje, otra validación y otros límites.

Objetivo

Ejecutar el kit, leer la explicación local de t001, contrastarla con borrado, importancia global y estabilidad, y escribir una decisión.

Enunciado

Ejecuta:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/ai/interpretability_audit.py --write
cat output/interpretability_decision.md
cat output/model_card_interpretability.md
python3 -m json.tool output/explanation_contract.json
python3 -m json.tool output/ci_explanation_gate.json

Después responde:

  1. ¿Qué tres features explican más t001?
  2. ¿Qué feature pesa más globalmente?
  3. ¿La explicación es estable?
  4. ¿Hay contrafactual accionable en algún caso?
  5. ¿Qué dice el gate de CI?
  6. ¿Qué uso permite el contrato de explicación?
  7. ¿La usarías para publicar el sistema?
  8. ¿Qué parte no mostrarías automáticamente a un usuario final?
  9. ¿Qué monitorizarías en producción para saber si la explicación cambia?
  10. ¿Qué check convertirías en obligatorio para la siguiente release?

Resolución paso a paso

Primero leemos el caso local. El reporte indica para t001:

FeatureContribución
student_wait_days1,56
missing_payment1,25
prior_cases0,84

El score de t001 es 0,8596 y la predicción es positiva. La explicación local dice que la prioridad sube por días de espera, pago pendiente y casos previos.

Después comprobamos si esa explicación aguanta pruebas. Si la feature principal no cambia el score al borrarla, la explicación sería sospechosa. Si la importancia global cuenta otra historia, el caso local quizá sea anecdótico. Si la explicación cambia demasiado con perturbaciones pequeñas, no sirve para un gate de release.

En este kit, las señales son:

PruebaResultado
Máxima caída en borrado0,444993
Mayor caída por permutación0,25
Estabilidad top-10,9667
Contrafactual accionableExiste al menos un caso donde cambia la decisión.
Comprehensiveness top-20,210094
Suficiencia top-20,063245
Proxy scanprior_cases y student_wait_days tienen correlación 0,8837.
Gate de CIpass, con recomendación de uso interno monitorizado.

La correlación entre prior_cases y student_wait_days no bloquea porque queda bajo el máximo configurado, pero no debe ignorarse. En una revisión real pediría mirar si ambas variables están midiendo casi la misma fricción operativa. Si empiezan a moverse juntas en producción, la explicación puede hacerse menos informativa.

El contrafactual útil también se interpreta con cuidado. Para t001, resolver el pago pendiente cambia el score de 0.859603 a 0.636915 y la predicción pasa de 1 a 0. Eso no significa culpar al pago. Significa que, bajo el modelo y sus pesos, esa variable es accionable y tiene fuerza suficiente para cambiar la salida.

Respuesta

Yo aceptaría la explicación como defendible para diagnóstico interno, no como explicación final al usuario. El contrato permite su uso por ingeniería, soporte N2 y producto; no permite usarla como decisión final sin revisión ni como comunicación automática.

Para usuario final la traduciría así:

El caso se prioriza porque acumula varios indicadores operativos: espera prolongada, posible pago pendiente y antecedentes relacionados. Antes de actuar, conviene revisar documentación y estado de pago.

Entrega profesional esperada

release-interpretability-review/
  interpretability_decision.md
  interpretability_report.json
  explanation_contract.json
  ci_explanation_gate.json
  model_card_interpretability.md
  reviewer_notes.md

reviewer_notes.md debe incluir:

  1. La explicación local de t001 en términos técnicos.
  2. La versión traducida para soporte o producto.
  3. Qué pruebas sostienen la explicación.
  4. Qué prueba sería suficiente para dejarla en revisión.
  5. Qué consumidor puede usarla y con qué límites.
  6. Qué señal monitorizarías en producción.

Por qué funciona

No aceptamos la explicación por estética. La aceptamos porque:

  1. Está conectada al cálculo del modelo.
  2. La feature principal tiene efecto medible.
  3. Las features globales relevantes coinciden con señales razonables.
  4. La explicación no se rompe con perturbaciones pequeñas.
  5. El contrato declara consumidores y campos obligatorios.
  6. El gate de CI deja una condición automatizable.
  7. El reporte deja trazas y model card.

Variaciones

  • Cambia el umbral a 0,75 y mira qué casos cambian.
  • Cambia el peso de docs_attached y explica el impacto.
  • Añade un slice y decide si la explicación sigue siendo estable.

Reto 2: preparar un expediente de evaluación completo

Contexto

Vas a publicar una nueva versión de un asistente RAG con agente de soporte. El equipo no quiere una opinión general; quiere un expediente técnico.

El reto simula una situación habitual: hay varias piezas que parecen razonables por separado, pero la release solo debería avanzar si el paquete completo aguanta. Un RAG con buena groundedness puede fallar por calibración. Un evaluador LLM puede puntuar bien y aun así tener demasiados pases indebidos. Una explicación puede ser plausible y no superar checks de fidelidad. El expediente fuerza a mirar la release por capas.

Objetivo

Diseñar el paquete mínimo de evaluación que permita publicar, limitar o bloquear una release.

Enunciado

Construye este expediente:

release-eval/
  eval_contract.json
  rag_eval_report.json
  evaluator_metaeval.json
  calibration_manifest.json
  interpretability_report.json
  explanation_contract.json
  ci_explanation_gate.json
  model_card_fragment.md
  release_eval_report.json
  source_evidence_matrix.csv
  ci_release_gate.json
  decision.md

Debe responder:

  1. ¿Qué decisión permite tomar la eval?
  2. ¿Qué métricas de error y coste se usan?
  3. ¿Cómo se evalúa RAG?
  4. ¿Cómo se valida el evaluador LLM?
  5. ¿Qué score está calibrado?
  6. ¿Qué explicación se puede defender?
  7. ¿Qué contrato permite consumir esa explicación?
  8. ¿Qué condición bloquea release?
  9. ¿Qué pieza se puede automatizar en CI?
  10. ¿Qué pieza requiere revisión humana o de producto?
  11. ¿Qué métrica vigilarías durante la primera semana?

El kit real está en:

kit/

Antes de construir el expediente final, genera las evidencias de calibración e interpretabilidad:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/ai/calibrate_policy.py --write

cd ../c06-interpretabilidad-laboratorio
python3 ops/ai/interpretability_audit.py --write

Después ejecuta el cierre:

cd ../laboratorio-cierre
python3 ops/build_release_eval_pack.py --write
python3 -m json.tool output/ci_release_gate.json
cat output/decision.md

La salida de referencia no dice “todo perfecto”. Dice publicar_con_condiciones: el paquete no tiene bloqueos, pero conserva puntos que un equipo serio no debería ignorar, como cobertura de cola larga en RAG e intervalo Wilson amplio en error automático.

También hay una variante para practicar el bloqueo:

python3 ops/build_release_eval_pack.py \
  --rag-report evidence/rag_eval_report_student.json \
  --evaluator-report evidence/evaluator_metaeval_student.json \
  --output-dir output/student \
  --write
python3 -m json.tool output/student/ci_release_gate.json
cat output/student/decision.md

En esa variante el resultado debe ser bloquear, porque RAG no sostiene suficientemente groundedness, citas y abstención, y el evaluador deja pasar demasiados casos que no debería.

Resolución paso a paso

Primero, el contrato. No empieza por métricas porque sí; empieza por decisión:

{
  "release_id": "support-rag-agent@2.1.0",
  "decision": "publicar, limitar o bloquear release",
  "must_pass": {
    "rag_groundedness": 0.9,
    "citation_acceptance": 0.88,
    "auto_error_rate": 0.18,
    "review_rate_max": 0.6,
    "evaluator_undue_pass_rate": 0.08,
    "interpretability_checks": "all_pass"
  }
}

Después, cada capítulo aporta una pieza. Lo importante es que ninguna pieza decide sola:

PiezaViene deQué exige
eval_contract.jsonC01Decisión, alcance y condición de bloqueo.
rag_eval_report.jsonC03Retrieval, groundedness, citas y abstención.
evaluator_metaeval.jsonC04Evaluador medido contra referencia.
calibration_manifest.jsonC05Score, calibrador, umbrales, slices y triggers.
interpretability_report.jsonC06Fidelidad, estabilidad, contrafactuales y model card.
explanation_contract.jsonC06Finalidad, consumidores, campos y linaje de explicación.
ci_explanation_gate.jsonC06Checks mínimos para automatizar una decisión de release.
source_evidence_matrix.csvC01-C06Matriz que conecta cada check con capítulo, métrica y fuente.
ci_release_gate.jsonC01-C06Salida mínima que podría usar un pipeline.

Ahora se interpreta el paquete:

CapaPreguntaSalida esperada
Eval general¿Qué decisión permite tomar?Contrato con must_pass.
RAG¿La respuesta se apoya en evidencia?Groundedness, citas y abstención.
Evaluador¿El evaluador coincide con referencia fiable?Metaevaluación y pases indebidos bajo límite.
Calibración¿El score significa riesgo operativo?Umbrales, zona gris y revisión.
Interpretabilidad¿La explicación se sostiene?Checks, contrato y model card.
Operación¿Qué pasa si algo cambia?Canary, monitorización y rollback.

La decisión Markdown podría decir:

# Decisión de release

Estado: publicar con condiciones.

Motivo:
- RAG pasa groundedness y citas.
- Evaluador automático pasa metaevaluación, pero queda cerca del límite en casos de soporte largo.
- Calibración permite automatizar fuera de zona gris.
- Interpretabilidad pasa checks técnicos y contrato de uso interno.

Condiciones:
- Canary al 10%.
- Revisión obligatoria para slice `matricula`.
- Recalibrar si cambia índice RAG o prompt.
- Vigilar distribución de razones principales durante la primera semana.

Respuesta

Publicaría solo con condiciones si todas las piezas pasan y ninguna depende de una muestra demasiado pequeña. Bloquearía si:

BloqueoMotivo
RAG no sostiene citasEl usuario recibe afirmaciones sin evidencia.
Evaluador aprueba fallos gravesEl gate deja pasar regresiones.
Calibrador caducadoEl score ya no significa lo que creemos.
Interpretabilidad no pasa sanity checksLa explicación puede crear confianza falsa.
Gate de explicación fallaLa explicación no resiste mínimos técnicos.
Contrato de explicación incompletoNo sabemos quién puede usarla ni con qué campos.
No hay ownerNadie responde por la política.

La respuesta fuerte no se queda en “publicar con condiciones”. Debe decir qué condiciones:

  1. Canary al 10% con trazas completas.
  2. Revisión obligatoria para zona gris de calibración.
  3. Bloqueo si groundedness o cita válida cae bajo umbral.
  4. Recalibración si cambia modelo, prompt, retriever o índice.
  5. Monitorización de distribución de top features y motivos de abstención.
  6. Revisión semanal de errores por slice durante la primera ventana.

Validar la entrega

Cuando el alumno haya construido su carpeta, puede pasar el checker:

# Descomprime el ZIP del capítulo y ejecuta estos comandos dentro de esa carpeta
python3 ops/check_student_submission.py --submission-dir solutions/reference --write

Para una entrega propia:

python3 ops/check_student_submission.py --submission-dir solutions/mi-equipo --write --fail-on-missing

La solución de referencia obtiene 70/70. Ese número no significa que la release sea perfecta; significa que la entrega contiene contrato, evidencias, gates, matriz de fuentes y decisión defendible.

Entrega profesional esperada

release-eval/
  eval_contract.json
  rag_eval_report.json
  evaluator_metaeval.json
  calibration_manifest.json
  interpretability_report.json
  explanation_contract.json
  ci_explanation_gate.json
  model_card_fragment.md
  release_eval_report.json
  source_evidence_matrix.csv
  ci_release_gate.json
  decision.md

decision.md debe explicar:

  1. Qué se publicaría o bloquearía.
  2. Qué evidencia sostiene RAG.
  3. Qué límites tiene el evaluador.
  4. Qué dice la calibración y dónde hay incertidumbre.
  5. Qué permite el contrato de explicación.
  6. Qué condición entra en CI.
  7. Qué tarea queda para la siguiente iteración.

Por qué funciona

El expediente evita el error de publicar por una sola métrica. Una release de IA necesita evidencia por capas: datos, recuperación, salida, evaluación, calibración, explicación y operación.

También fuerza una idea clave del facsímil 7: evaluar no es calcular métricas; es decidir qué evidencia cambia una decisión. Cada archivo del expediente existe porque alguien debe poder auditarlo después.

Cómo explicarlo a otra persona

“No publicamos porque el modelo parezca bueno. Publicamos si el expediente demuestra que responde con evidencia, se evalúa con criterios, calibra su incertidumbre, puede explicarse cuando falla y deja claro qué hacer si cambia el contexto.”

Variaciones

  • Cambia publicar con condiciones por bloquear y escribe qué evidencia faltaría.
  • Añade coste p95 por run al contrato.
  • Añade una muestra nueva de producción y decide si hay deriva.

Cierre del laboratorio

Si completas estos dos retos, el facsímil 7 deja de ser una lista de métricas. Se convierte en una práctica profesional: defines una decisión, mides errores, evalúas RAG, validas evaluadores, calibras scores, interpretas salidas y documentas límites.

La pregunta final es:

¿Puedo defender esta release con evidencia, o solo tengo una demo que me gusta?

Para saber más

Adebayo, J., Gilmer, J., Muelly, M., Goodfellow, I., Hardt, M. y Kim, B. (2018). Sanity Checks for Saliency Maps. Advances in Neural Information Processing Systems. https://papers.nips.cc/paper/2018/hash/294a8ed24b1ad22ec2e7efea049b8737-Abstract.html

Doshi-Velez, F. y Kim, B. (2017). Towards A Rigorous Science of Interpretable Machine Learning. arXiv. https://arxiv.org/abs/1702.08608

Jacovi, A. y Goldberg, Y. (2020). Towards Faithfully Interpretable NLP Systems: How Should We Define and Evaluate Faithfulness? Proceedings of ACL. https://aclanthology.org/2020.acl-main.386/

Kim, B., Wattenberg, M., Gilmer, J., Cai, C., Wexler, J., Viégas, F. y Sayres, R. (2018). Interpretability Beyond Feature Attribution: Quantitative Testing with Concept Activation Vectors. ICML. https://arxiv.org/abs/1711.11279

Lipton, Z. C. (2018). The Mythos of Model Interpretability. Communications of the ACM, 61(10), 36-43. https://doi.org/10.1145/3233231

Lundberg, S. M. y Lee, S.-I. (2017). A Unified Approach to Interpreting Model Predictions. Advances in Neural Information Processing Systems. https://papers.nips.cc/paper/7062-a-unified-approach-to-interpreting-model-predictions

Meng, K., Bau, D., Andonian, A. y Belinkov, Y. (2022). Locating and Editing Factual Associations in GPT. Advances in Neural Information Processing Systems. https://papers.nips.cc/paper_files/paper/2022/hash/6f1d43d5a82a37e89b0665b33bf3a182-Abstract-Conference.html

Mitchell, M., Wu, S., Zaldivar, A., Barnes, P., Vasserman, L., Hutchinson, B., Spitzer, E., Raji, I. D. y Gebru, T. (2019). Model Cards for Model Reporting. FAT, 220-229. https://doi.org/10.1145/3287560.3287596

Ribeiro, M. T., Singh, S. y Guestrin, C. (2016). Why Should I Trust You? Explaining the Predictions of Any Classifier. KDD, 1135-1144. https://doi.org/10.1145/2939672.2939778

Rudin, C. (2019). Stop Explaining Black Box Machine Learning Models for High Stakes Decisions and Use Interpretable Models Instead. Nature Machine Intelligence, 1, 206-215. https://doi.org/10.1038/s42256-019-0048-x

Selvaraju, R. R., Cogswell, M., Das, A., Vedantam, R., Parikh, D. y Batra, D. (2017). Grad-CAM: Visual Explanations from Deep Networks via Gradient-Based Localization. ICCV. https://doi.org/10.1109/ICCV.2017.74

Sundararajan, M., Taly, A. y Yan, Q. (2017). Axiomatic Attribution for Deep Networks. ICML, 3319-3328. https://proceedings.mlr.press/v70/sundararajan17a.html

Tabassi, E. (2023). Artificial Intelligence Risk Management Framework (AI RMF 1.0). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.100-1

Wachter, S., Mittelstadt, B. y Russell, C. (2017). Counterfactual Explanations without Opening the Black Box: Automated Decisions and the GDPR. Harvard Journal of Law and Technology, 31, 841-887. https://arxiv.org/abs/1711.00399

Notas

  1. Lipton, Z. C. (2018). The Mythos of Model Interpretability. Communications of the ACM, 61(10), 36-43. https://doi.org/10.1145/3233231

  2. Doshi-Velez, F. y Kim, B. (2017). Towards A Rigorous Science of Interpretable Machine Learning. arXiv. https://arxiv.org/abs/1702.08608

  3. Jacovi, A. y Goldberg, Y. (2020). Towards Faithfully Interpretable NLP Systems: How Should We Define and Evaluate Faithfulness? ACL. https://aclanthology.org/2020.acl-main.386/

  4. Ribeiro, M. T., Singh, S. y Guestrin, C. (2016). Why Should I Trust You? Explaining the Predictions of Any Classifier. KDD, 1135-1144. https://doi.org/10.1145/2939672.2939778

  5. Lundberg, S. M. y Lee, S.-I. (2017). A Unified Approach to Interpreting Model Predictions. NeurIPS. https://papers.nips.cc/paper/7062-a-unified-approach-to-interpreting-model-predictions

  6. Sundararajan, M., Taly, A. y Yan, Q. (2017). Axiomatic Attribution for Deep Networks. ICML, 3319-3328. https://proceedings.mlr.press/v70/sundararajan17a.html

  7. Selvaraju, R. R., Cogswell, M., Das, A., Vedantam, R., Parikh, D. y Batra, D. (2017). Grad-CAM: Visual Explanations from Deep Networks via Gradient-Based Localization. ICCV. https://doi.org/10.1109/ICCV.2017.74

  8. Adebayo, J., Gilmer, J., Muelly, M., Goodfellow, I., Hardt, M. y Kim, B. (2018). Sanity Checks for Saliency Maps. NeurIPS. https://papers.nips.cc/paper/2018/hash/294a8ed24b1ad22ec2e7efea049b8737-Abstract.html

  9. Wachter, S., Mittelstadt, B. y Russell, C. (2017). Counterfactual Explanations without Opening the Black Box: Automated Decisions and the GDPR. Harvard Journal of Law and Technology, 31, 841-887. https://arxiv.org/abs/1711.00399

  10. Rudin, C. (2019). Stop Explaining Black Box Machine Learning Models for High Stakes Decisions and Use Interpretable Models Instead. Nature Machine Intelligence, 1, 206-215. https://doi.org/10.1038/s42256-019-0048-x

  11. Kim, B., Wattenberg, M., Gilmer, J., Cai, C., Wexler, J., Viégas, F. y Sayres, R. (2018). Interpretability Beyond Feature Attribution: Quantitative Testing with Concept Activation Vectors. ICML. https://arxiv.org/abs/1711.11279

  12. Meng, K., Bau, D., Andonian, A. y Belinkov, Y. (2022). Locating and Editing Factual Associations in GPT. NeurIPS. https://papers.nips.cc/paper_files/paper/2022/hash/6f1d43d5a82a37e89b0665b33bf3a182-Abstract-Conference.html