El equipo termina el RAG. Alguien arma un eval: cuarenta preguntas, un modelo grande que puntúa cada respuesta del 1 al 5 con el prompt "evaluá la calidad de esta respuesta", un promedio. Da 4,3. Se despliega.
Ese 4,3 no autoriza nada. Es una encuesta de satisfacción que se contesta a sí misma: no tiene unidad, su rango efectivo son dos valores y arrastra sesgos que están medidos y publicados. Una eval es otra cosa — un test de regresión con criterio de falla explícito — y casi nada de lo que se llama eval en la industria lo es.
Lo que hace todo el mundo
El patrón es siempre el mismo. Se escribe un set de prompts a mano, generalmente el mismo día que se termina el sistema y generalmente por la persona que lo construyó. Se le pide a un modelo que actúe de juez con una rúbrica del 1 al 5. Se promedia. Se arma un dashboard con faithfulness: 0.87, answer_relevancy: 0.91, context_precision: 0.84. Y se toma la decisión de deployar mirando esos tres números.
Esto se siente riguroso. Tiene decimales, tiene nombres en inglés, tiene un gráfico. Y no mide nada que sirva para decidir.
Por qué está mal
Primero: un promedio de una escala Likert generada por un modelo no tiene unidad. 4,3 contra 4,1 no significa nada, porque no sabés si esa diferencia viene del sistema o del ruido del juez. Aun con temperature=0 ningún proveedor te garantiza determinismo bit a bit entre corridas — el batching del lado del servidor cambia resultados. El mecanismo es aburrido y es real: la suma en punto flotante no es asociativa, (a+b)+c no da exactamente lo mismo que a+(b+c) en el último bit, y el orden en el que el kernel acumula depende de cómo particiona el trabajo, que depende del tamaño del batch, que depende de cuántos usuarios le están pegando al modelo en ese instante. Estás comparando dos números que se mueven solos.
Segundo: las rúbricas 1-5 colapsan el rango. Corré la misma rúbrica sobre cien respuestas y mirá el histograma. Casi todo cae en 4 y 5. Es un experimento de veinte minutos y lo podés reproducir hoy. Pero no hace falta correrlo para saber el resultado, y esa es la parte interesante: el juez no tiene un módulo de juicio. Tiene una distribución de probabilidad sobre los tokens que puede emitir después de "Puntaje:", y en el corpus con el que se entrenó 8/10 y 4/5 aparecen órdenes de magnitud más seguido que 3/10 o 2/5. Estás midiendo la frecuencia del token, no la calidad de la respuesta. Una métrica cuyo rango efectivo son dos valores no puede detectar una regresión de nada.
Tercero: el juez tiene sesgos medidos y publicados. El paper de referencia acá es Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena (Zheng et al., NeurIPS 2023, arXiv:2306.05685). La buena noticia: un juez fuerte alcanza más del 80% de acuerdo con evaluadores humanos, que es el mismo nivel de acuerdo que hay entre dos humanos. La mala: el mismo paper documenta position bias, verbosity bias y self-enhancement bias, y mide una columna que casi nadie replica — consistency, el porcentaje de veces que el juez da el mismo veredicto cuando invertís el orden de las dos respuestas. Con el prompt por defecto, GPT-4 mantiene su veredicto en torno al 65% de los casos. Uno de cada tres juicios se da vuelta por el orden en que le pasaste las respuestas. Large Language Models are not Fair Evaluators (arXiv:2305.17926) lo confirma de forma independiente.
Cuarto, y el que de verdad importa: el dataset está hecho de casos que ya sabés que funcionan. Las cuarenta preguntas las escribió la misma persona que escribió el prompt. Están sesgadas por disponibilidad hacia lo que el sistema hace bien.
Un eval que nunca falló no es un eval. Es una captura de pantalla.
El problema real
El problema no es de medición. Es de decisión binaria bajo incertidumbre: este cambio sale a producción o no sale. Esa pregunta tiene dos respuestas posibles y ninguna de las dos es 4,3.
Un eval es un test de regresión: un dataset versionado, un criterio de falla explícito, corriendo en CI, con suficiente potencia estadística para detectar el tamaño de regresión que te importa. Nada más que eso.
Todo lo que no pueda bloquear un merge es telemetría, no es un eval. La telemetría está bien — pero no te autoriza a deployar.
El dataset arranca en los fallos
Un dataset de evaluación no se escribe. Se cosecha. Salen de logs de producción, tickets de soporte y de los casos que rompieron. Veinte casos que ya te explotaron valen más que doscientos sintéticos, y los sintéticos generados por el mismo modelo que estás evaluando son peores todavía: heredan su distribución y evalúan lo que el modelo ya sabe hacer.
Cada caso lleva input, categoría y criterio de aceptación explícito. En YAML, versionado en el repo, al lado del código:
- id: transf-monto-con-puntos
categoria: extraccion_monto
fuente: ticket-4412
input: "mandale 1.250,50 a juan por el alquiler"
espera:
monto_centavos: 125050
moneda: ARS
prohibido: ["1250.50", "125050,00"]
- id: transf-monto-punto-miles
categoria: extraccion_monto
fuente: ticket-4551
input: "transferile 1.500 a mi vieja"
espera:
monto_centavos: 150000
moneda: ARS
- id: transf-monto-ambiguo
categoria: rechazo
fuente: prod-log-2026-05-14
input: "pasale unos mangos a mi hermana"
espera:
accion: pedir_aclaracion
tool_calls_prohibidas: ["ejecutar_transferencia"]
El segundo caso parece trivial y es el más caro de todos. En Argentina 1.500 es mil quinientos: el punto es separador de miles y la coma es el decimal. Un modelo entrenado mayoritariamente con la convención anglosajona lee 1.500 como uno coma cinco. La diferencia entre esas dos lecturas es un factor de mil en una transferencia. Y no hay rúbrica del 1 al 5 que lo detecte: la respuesta suena impecable, está redactada perfecto, y está equivocada por tres órdenes de magnitud.
El tercer caso es el otro que importa. Un sistema que interpreta "unos mangos" como un monto y ejecuta una transferencia es un incidente, no un 3 sobre 5.
Assertions primero, juez al final
La regla es simple y casi nadie la sigue: si lo podés verificar con código, no uses un juez. El LLM-as-judge es el último recurso, no el primero.
En un sistema de pagos, la mayoría de lo que te importa es verificable de forma determinística: el JSON valida contra el schema, el monto en centavos es exacto, la tool que se llamó es la correcta, la respuesta no contiene un CBU que no estaba en el contexto.
# evals/test_suite.py
from collections import defaultdict
import pytest, yaml
from app.agent import responder
CASOS = yaml.safe_load(open("evals/dataset.yaml"))
RESULTADOS = defaultdict(lambda: {"ok": 0, "total": 0})
@pytest.mark.parametrize("caso", CASOS, ids=lambda c: c["id"])
def test_caso(caso):
cat = caso["categoria"]
RESULTADOS[cat]["total"] += 1 # se cuenta ANTES de poder fallar
salida = responder(caso["input"])
esperado = caso["espera"]
if "monto_centavos" in esperado:
assert salida.monto_centavos == esperado["monto_centavos"]
for prohibida in esperado.get("tool_calls_prohibidas", []):
assert prohibida not in [t.name for t in salida.tool_calls]
for txt in esperado.get("prohibido", []):
assert txt not in salida.texto
RESULTADOS[cat]["ok"] += 1 # solo si pasó todo
El orden de esas dos líneas es todo el truco. Si contás el caso después de los asserts, un caso que falla nunca entra al registro y el denominador se achica solo: la categoría que más rompe es la que mejor pass rate reporta. Es un bug silencioso que te da números lindos, que es la peor clase de bug.
Y separá retrieval de generación, porque se arreglan distinto. El retriever se mide con un dataset de (query, doc_id_correcto) y recall@k: es determinístico, corre en segundos y no cuesta un centavo. Si tu recall@5 es 0,62, hay un 38% de los casos donde el documento correcto nunca llegó al context. Ningún prompt arregla eso. Vas a pasar dos semanas puliendo instrucciones para mover una métrica que está limitada por el chunking.
Al juez hay que evaluarlo
Para lo que queda —tono, si la respuesta contradice el contexto, si inventó una condición comercial— sí necesitás un juez. Con tres condiciones no negociables.
Uno: veredicto binario, y la evidencia antes que el veredicto. Una pregunta específica, con criterio explícito, que devuelva JSON con las claves en un orden que obligue al modelo a citar primero y concluir después.
JUEZ = """Vas a leer un CONTEXTO y una RESPUESTA.
Pregunta: ¿toda afirmación factual de la RESPUESTA está soportada por el CONTEXTO?
Devolvé JSON con estas claves EN ESTE ORDEN exacto:
{"afirmacion_no_soportada": "<cita textual de la RESPUESTA, o null>",
"por_que": "<una oración>",
"veredicto": "si" | "no"}
Una sola afirmación no soportada implica veredicto "no"."""
El orden no es cosmético. El modelo genera de izquierda a derecha: si el veredicto sale primero, todo lo que viene después es una racionalización de algo que ya dijo. Si la cita sale primero, el veredicto se condiciona en la evidencia. Y la cita es verificable por código: si esa string no está literalmente en la respuesta, el veredicto se descarta.
Dos: el juez también es una muestra, tratalo como tal. Una llamada al juez es un sorteo, no una medición. Corré cada juicio tres veces y quedate con la mayoría — y mandá a revisión humana los casos donde los tres votos no coinciden, porque esos son exactamente los ambiguos, y los ambiguos son los que te dicen qué línea de la rúbrica está mal escrita.
from collections import Counter
def juzgar(contexto, respuesta, n=3):
votos = [_una_llamada(contexto, respuesta) for _ in range(n)]
conteo = Counter(v["veredicto"] for v in votos)
veredicto, apariciones = conteo.most_common(1)[0]
return {
"veredicto": veredicto,
"unanime": apariciones == n,
"a_revision_humana": apariciones < n,
"votos": votos,
}
Sumale el chequeo de posición: corré cada comparación en los dos órdenes y descartá los casos donde el veredicto se da vuelta. Con el 65% de consistency del paper de MT-Bench, esto no es paranoia — es la única forma de saber si estás midiendo calidad o midiendo qué respuesta va primero.
Tres: medí al juez contra vos. Etiquetá cien casos a mano y calculá el acuerdo:
from sklearn.metrics import cohen_kappa_score, accuracy_score
print(accuracy_score(humano, juez), cohen_kappa_score(humano, juez))
Si kappa está por debajo de 0,6, la métrica que estás reportando es la opinión del juez, no tu criterio de negocio.
Cuántos casos: la parte que nadie hace
Esta es la cuenta que convierte un eval en evidencia. Tu suite tiene 30 casos y pasan 27: 90%. El intervalo de confianza de Wilson al 95% para eso va de 74,4% a 96,5%. Con 200 casos y el mismo 90%, va de 85,1% a 93,4%.
Y para detectar una caída real de 90% a 85% con 80% de poder estadístico necesitás alrededor de 690 casos por brazo. Para detectar una caída de 90% a 80%, unos 200.
La conclusión no es "armá 700 casos". Es que con 40 casos solo podés detectar catástrofes — lo cual está perfecto, siempre que lo digas. El pecado es escribir en el PR "subimos de 87% a 91%" con n=45. Esa diferencia es ruido y la estás vendiendo como mejora.
Si no calculaste el intervalo, no tenés una métrica. Tenés una anécdota con decimales.
Y nunca promedies categorías. El gate va por categoría, con umbrales distintos: 100% donde un fallo es plata, 85% donde es una molestia.
# evals/conftest.py
from evals.test_suite import RESULTADOS
UMBRALES = {"extraccion_monto": 1.00, "rechazo": 1.00, "tono": 0.85}
def pytest_sessionfinish(session, exitstatus):
bloqueado = []
ok_total = sum(r["ok"] for r in RESULTADOS.values())
n_total = sum(r["total"] for r in RESULTADOS.values())
for cat, r in sorted(RESULTADOS.items()):
tasa = r["ok"] / r["total"]
umbral = UMBRALES.get(cat, 0.85)
if tasa < umbral:
bloqueado.append(cat)
print(f"{'GATE' if tasa < umbral else 'ok '} {cat:20} "
f"{r['ok']}/{r['total']} = {tasa:.0%} (umbral {umbral:.0%})")
print(f" {'global':20} {ok_total}/{n_total} = {ok_total/n_total:.0%}")
session.exitstatus = 1 if bloqueado else exitstatus
Y esta es la escena que justifica todo el artículo:
ok rechazo 12/12 = 100% (umbral 100%)
ok tono 38/40 = 95% (umbral 85%)
GATE extraccion_monto 17/20 = 85% (umbral 100%)
global 67/72 = 93%
exit 1
93% global. Verde en el dashboard, verde en el reporte semanal, y el merge bloqueado igual — porque tres de veinte extracciones de monto están mal y cada una de esas tres es una transferencia por el número equivocado. Un promedio de 93% te habría dejado deployar eso.
El eval que se vuelve el objetivo
Acá está el agujero de esta tesis, y hay que decirlo en voz alta: si el eval bloquea el release, el eval se convierte en el objetivo. En tres o cuatro iteraciones alguien va a ajustar el prompt hasta que la suite dé 96%, y la tasa de quejas de soporte no se va a mover un milímetro. Es Goodhart, y llega solo.
La defensa es barata: reservá un holdout. Entre el 20% y el 30% de los casos no se miran al iterar, no se debuggean, no se usan para explicar por qué falló nada. Se corren una vez por release y se comparan contra el resto de la suite. Si el gate da 96% y el holdout da 78%, no mejoraste el sistema — memorizaste el dataset. Rotá el holdout cada tantos releases, y cuando un caso del holdout se contamina porque alguien lo miró para arreglar un bug, se pasa al set de desarrollo y no vuelve.
Lo que cuesta y lo que perdés
Números concretos, con precios de lista de la API de Anthropic al 9 de agosto de 2026 — verificá el vigente antes de hacer la cuenta con tus propios números, porque esto envejece. Claude Haiku 4.5 está a USD 1 por millón de tokens de input y USD 5 por millón de output.
Una suite de 300 casos, con ~1.500 tokens de input y ~200 de output por juicio, corrida en los dos órdenes para el chequeo de posición: 0,9 MTok de input y 0,12 MTok de output. USD 1,50 por corrida completa. Sumale el muestreo triple del juez y son 4,50. Cuarenta PRs en el mes: USD 180. Es menos de lo que cuesta una hora del equipo discutiendo si el cambio de prompt mejoró algo.
El trade-off real no es la plata en tokens. Son tres cosas.
- Las horas de etiquetado. Es el costo que nadie presupuesta y el único que no baja con el precio de los modelos. Etiquetar un caso a mano —leer el input, leer el contexto, decidir el criterio de aceptación— toma cerca de dos minutos si ya sabés cuál era la respuesta correcta. Cien casos son 3,3 horas: una tarde. Cuatrocientos casos son 13 horas de alguien que conoce el dominio, no de un becario. Y los 690 casos por brazo del cálculo de potencia son casi tres días completos de una persona senior.
- El determinismo. El Batch API te da 50% de descuento pero es asíncrono, así que si querés el eval bloqueando el merge lo pagás a precio full. Y un test que depende de un juez es flaky por construcción. Un test flaky termina desactivado en dos sprints. Por eso el juez tiene que cubrir la minoría de los casos y el gate tiene que tener margen sobre el umbral, no estar clavado al límite.
- El tiempo de CI. Cada minuto que le sumás al pipeline lo paga el equipo entero, todo el día.
Cuándo no hacer nada de esto
Una concesión, porque el artículo es absolutista de punta a punta y hay un caso donde está mal. Preguntate qué hacés distinto si el eval da 82% en vez de 91%. Si la respuesta honesta es "nada, igual lo lanzamos" —porque es un prototipo, porque el costo de un error es cero, porque el usuario es el equipo— entonces no armes la suite. Escribí un guardrail en runtime que rechace la salida inválida cuando pasa, y seguí. Un eval que nunca va a bloquear nada es la misma captura de pantalla del principio, solo que ahora te costó tres días de etiquetado.
Medir antes de creer no significa tener un número. Significa tener un criterio que pueda decir que no, con suficiente evidencia atrás como para bancarlo cuando alguien te pregunte por qué no sale hoy.
Para seguir
Lecturas
- Your AI Product Needs Evals — Hamel Husain. El caso de estudio completo: error analysis primero, después evals unitarias, después el juez. Si leés uno solo, es este.
- Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — Zheng et al. La fuente primaria de todo lo que dije sobre position bias, verbosity bias y consistency.
- Who Validates the Validators? — Shankar et al. Responde la pregunta incómoda de quién evalúa al evaluador, y muestra que alinear al juez con etiquetas humanas es iterativo, no una calibración de una sola vez.
- Demystifying evals for AI agents — Anthropic. Por qué un agente es más difícil de evaluar que un prompt suelto, y cómo diseñar los graders para eso.
- Evaluation best practices — OpenAI. La contraparte: graders determinísticos contra model-graded, y cómo evitar que la eval termine midiendo otra cosa.
Videos
- Why AI evals are the hottest new skill for product builders — Husain y Shankar en Lenny's Podcast. Casi dos horas, y baja a tierra el flujo real: error analysis primero, binario antes que escala 1-5.
- Stanford CME295, Lecture 8 — LLM Evaluation — la clase entera, con contaminación de datos y meta-evaluación incluidas. El marco conceptual completo, no recetas.
- From LLM as a Judge to Agent as a Judge — Aparna Dhinakaran, seis minutos. Qué se rompe cuando lo que evaluás es una trayectoria de agente y no un texto suelto.