volver al blog

El LLM como juez: cuándo su nota vale algo

evaluaciónllmtesting

Cien respuestas y nadie para leerlas

Cambias el prompt del sistema, vuelves a lanzar el conjunto de pruebas y te quedan 120 respuestas delante. Leerlas a mano cuesta dos horas. Y tienes que hacerlo otra vez mañana, cuando toques el reranker.

Ahí es donde casi todos los equipos llegan a la misma idea: que las puntúe otro modelo. Es una buena idea. El problema es que la primera versión siempre se monta igual —“valora esta respuesta de 1 a 10”— y ese número no mide lo que crees que mide.

Un juez automático es un componente más de tu sistema. Tiene sesgos, tiene varianza y se degrada cuando cambias de modelo por debajo. Si no lo tratas como algo que hay que validar, has cambiado dos horas de lectura por una cifra que te da la razón.

Lo que un juez mide cuando no le dices qué mirar

Los sesgos de los jueces basados en LLM están medidos y son consistentes entre familias de modelos. Conviene conocerlos antes de fiarse de una media.

  • Posición. En una comparación entre dos respuestas, el modelo prefiere la primera con una frecuencia claramente superior al azar. No es ruido: es direccional y se puede anular.
  • Longitud. A igualdad de contenido, la respuesta más larga y estructurada gana. Un juez sin rúbrica premia las viñetas.
  • Autopreferencia. Un modelo puntúa mejor el texto generado por él mismo o por su propia familia. Si evalúas GPT con GPT, estás midiendo estilo compartido.
  • Tono seguro. Una respuesta rotunda y falsa puntúa por encima de una correcta que matiza. El juez no verifica hechos si no le das con qué.

Ninguno de los cuatro se arregla cambiando a un modelo más grande. Se arreglan con diseño: rúbrica explícita, comparación por parejas con intercambio de orden y una referencia contra la que contrastar.

Nota absoluta o comparación por parejas

Son dos formatos distintos con usos distintos, y elegir mal es el primer error de diseño.

Nota absoluta (1–5)Comparación por parejas
Qué te daUn valor por respuestaUn ganador entre dos
FiabilidadBaja sin rúbrica; se concentra en el 4Alta; el modelo compara mejor que estima
CosteUna llamada por respuestaDos llamadas por par (orden e inverso)
Sirve paraCuadro de mando, umbrales en CIDecidir entre dos configuraciones
Se rompe cuandoNo hay respuesta de referenciaQuieres una serie temporal comparable

La regla práctica: compara por parejas para decidir, puntúa en absoluto para vigilar. Cuando tienes que elegir entre el prompt A y el prompt B, la comparación directa es más fiable y más barata de interpretar. Cuando quieres un número que suba y baje en el panel a lo largo de los meses, necesitas la escala absoluta, con todo lo que implica sostenerla.

Un aviso sobre la escala: un juez sin rúbrica pone un 4 a casi todo. Si tus notas tienen una desviación típica de 0,3, el juez no está discriminando, está rellenando. Eso se ve en cinco minutos y ahorra semanas de decisiones fundadas en nada.

La rúbrica es el producto

El prompt del juez es el sitio donde se decide si la evaluación vale. Y lo que lo cambia todo no es pedir amabilidad, es descomponer la nota en criterios binarios verificables contra una referencia.

import json, statistics

RUBRICA = """Evalúa la RESPUESTA frente a la PREGUNTA y el CONTEXTO.
Responde solo con JSON y nada más.

Criterios, cada uno true o false:
- fundamentada: toda afirmación factual de la respuesta se apoya en el CONTEXTO.
- completa: cubre lo que pide la pregunta, sin dejar partes sin contestar.
- sin_invenciones: no añade datos, cifras ni nombres ausentes del CONTEXTO.
- se_abstiene_bien: si el CONTEXTO no contiene la respuesta, lo dice
  explícitamente en vez de improvisar. Si el contexto sí la contiene, true.

Formato: {"fundamentada": bool, "completa": bool, "sin_invenciones": bool,
"se_abstiene_bien": bool, "cita_problematica": "fragmento o null"}"""

def juzgar(cliente, pregunta: str, contexto: str, respuesta: str) -> dict:
    # temperature=0 no elimina la varianza, pero la reduce a algo manejable.
    salida = cliente.completar(
        sistema=RUBRICA,
        usuario=f"PREGUNTA:\n{pregunta}\n\nCONTEXTO:\n{contexto}\n\nRESPUESTA:\n{respuesta}",
        temperature=0,
        response_format="json",
    )
    return json.loads(salida)

def comparar(cliente, pregunta, contexto, a: str, b: str) -> str:
    """Parejas con intercambio de orden: anula el sesgo de posición."""
    primero = _preferencia(cliente, pregunta, contexto, a, b)   # A en posición 1
    segundo = _preferencia(cliente, pregunta, contexto, b, a)   # A en posición 2
    if primero == "1" and segundo == "2":
        return "a"
    if primero == "2" and segundo == "1":
        return "b"
    return "empate"  # El juez se contradice: no hay señal, y eso es un resultado.

def agregar(resultados: list[dict]) -> dict:
    """Nunca una media global: cada criterio por separado."""
    claves = ["fundamentada", "completa", "sin_invenciones", "se_abstiene_bien"]
    return {k: statistics.mean(r[k] for r in resultados) for k in claves}

Tres decisiones dentro de esas líneas merecen defenderse.

La primera: criterios binarios, no una escala. “¿Está fundamentada?” tiene respuesta; “¿qué calidad tiene, de 1 a 10?” no la tiene. Cuatro booleanos te dan cuatro series que puedes leer, y cuando una baja sabes qué se ha roto.

La segunda: el empate es un resultado válido. Si al invertir el orden el juez cambia de opinión, las dos respuestas son indistinguibles para él. Forzar un ganador ahí es fabricar señal. Además, la tasa de contradicción es la señal más directa de la fiabilidad del juez: por encima del 25 % en pares que tú consideras claramente distintos, la rúbrica no está funcionando.

La tercera: cita_problematica no es decorativa. Pedir el fragmento exacto que sostiene el veredicto obliga al modelo a mirar el texto y te deja algo que revisar a mano cuando dudes. Un juez que no puede señalar dónde está el problema casi siempre lo ha inventado.

Calibrar contra humanos, una vez y con cabeza

Un juez sin calibrar no es una medida, es una opinión automatizada. La calibración es aburrida y se hace una tarde.

Coge 60 respuestas que cubran el rango real, del acierto claro al fallo claro. Etiquétalas tú con la misma rúbrica que le das al modelo, criterio por criterio. Luego mide el acuerdo por criterio, no en global.

Lo que buscas es acuerdo por encima del 85 % en los criterios que van a mover decisiones. Si fundamentada concuerda el 92 % de las veces y completa el 61 %, el resultado es claro: usas el primero para tus umbrales y el segundo lo reescribes o lo dejas fuera. Un criterio mal definido no se salva con un modelo mejor.

Dos cosas que casi siempre salen de este ejercicio. Una: descubres que tu propia rúbrica era ambigua, y esa ambigüedad estaba también en el prompt de producción. Dos: aparece un puñado de casos donde el juez tiene razón y tú te habías equivocado al etiquetar. Ambas cosas son ganancias.

Congela ese conjunto de calibración en el repositorio. El día que cambies el modelo juez —y lo vas a cambiar, porque se depreca o abarata— vuelves a pasarlo y sabes en minutos si tus series históricas siguen siendo comparables. Sin eso, un cambio de versión del juez te mueve el panel y lo interpretas como una regresión del producto.

Dónde no usarlo

Hay cosas que un juez LLM no debe decidir, y reconocerlas te evita disgustos.

No lo uses donde haya una comprobación determinista disponible: si la respuesta debe ser un JSON con tres campos, eso lo valida un esquema, no un modelo. No lo uses como única puerta antes de producción en dominios con consecuencias —sanitario, legal, financiero—; ahí el juez prioriza qué revisa una persona, no la sustituye. Y no lo uses para medir preferencia de usuario: mide adherencia a tu rúbrica, que es otra cosa y correlaciona peor de lo que parece.

Qué hacer el lunes

Coge tus 30 últimas respuestas de producción, escribe una rúbrica de tres criterios binarios y pásalas por el juez. Luego etiqueta esas mismas 30 a mano y mira el acuerdo por criterio. Si no supera el 85 %, el problema está en la rúbrica, no en el modelo, y arreglarla cuesta una iteración de media hora.

Cuando el acuerdo aguante, mete la evaluación en CI con un umbral por criterio y guarda el conjunto de calibración junto al código. El valor no está en la nota de hoy: está en que el día que baje, sepas exactamente qué criterio se rompió.