Evaluaciones autoalojadas para agentes de IA
Crea un ciclo de evaluación propio con trazas reales, comprobaciones deterministas y un juez LLM. Controla la tasa de aprobados por commit, como 51 de 60.
Qué son las evaluaciones autoalojadas para agentes de IA
Las evaluaciones autoalojadas para agentes de IA son cuatro elementos que se mantienen en el propio repositorio: un archivo con casos guardados, un script que ejecuta el agente sobre ellos, un conjunto de comprobaciones que puntúa cada respuesta y una tabla de resultados que se puede consultar. Ninguno de estos elementos necesita un proveedor. Todo el ciclo requiere unos cientos de líneas de Python y un archivo SQLite.
El agente funcionó en la demostración porque seleccionó usted mismo las cinco entradas. Falló en la segunda semana porque cambió una línea del prompt, cambió el modelo o cambió la descripción de una herramienta, y ninguna medición cubría esos cambios. Un ciclo de evaluación convierte «ahora parece peor» en «la tasa de aprobados pasó de 58 de 60 a 51 de 60 en el commit 4f1c9ab».
El ciclo tiene cuatro pasos, y esta guía dedica una sección a cada uno: recopilar trazas reales, convertir las más interesantes en casos, evaluar cada caso con cada cambio y almacenar la tasa de aprobados junto al commit que la produjo. El mismo ciclo funciona con independencia de aquello sobre lo que ejecute el agente, y los frameworks de agentes autoalojados que merece la pena ejecutar se diferencian sobre todo por la cantidad de información de la traza que proporcionan directamente.
Por qué el agente falla en la segunda semana
Un agente está compuesto por un prompt, un modelo, un conjunto de definiciones de herramientas y el contexto que se recupera en tiempo de ejecución. Cualquiera de estos cuatro elementos puede cambiar sin modificar el código de la aplicación, por lo que una revisión de código normal no encuentra nada que objetar.
La causa más habitual es editar el prompt. Añade una frase para evitar una respuesta descortés. Esa frase cambia el comportamiento con entradas que nadie volvió a probar, y las trazas lo muestran claramente: la traza de la semana pasada para la misma pregunta contiene una llamada a la herramienta create_refund; la de esta semana no contiene ninguna, y la respuesta es una disculpa cortés en su lugar. No se produjo ningún error, por lo que no se activó ninguna alerta.
La segunda causa es el modelo. Registra la cadena exacta del modelo que enviaste en cada ejecución, claude-haiku-4-5-20251001, en lugar de usar una abreviatura que sólo recuerdas de memoria, porque una tasa de éxito que cae el día que cambiaste de modelo sólo se puede diagnosticar si el modelo aparece en el registro.
La tercera causa son las herramientas. Cambiar la redacción de la descripción de una herramienta modifica el momento en que el modelo decide llamarla. Si las herramientas llegan mediante servidores MCP ejecutados en un VPS, el esquema reside en otro proceso, por lo que puede cambiar sin que exista ninguna diferencia en tu repositorio. La cuarta causa es la recuperación: la misma pregunta consulta un índice que se reconstruyó durante la noche, y la respuesta utiliza el documento nuevo.
Construya el conjunto de referencia a partir de las trazas que ya recopila
No invente casos de evaluación. Extráigalos del tráfico. Si ya ejecuta trazas de Langfuse autohospedado para su agente, cada solicitud se almacena con su entrada, sus llamadas a herramientas y su salida. Ese es exactamente el material bruto que necesita un caso.
Exporte una ventana de observaciones raíz mediante la API pública. Usa autenticación básica, con la clave pública como nombre de usuario y la clave secreta como contraseña.
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'Lea un registro antes de escribir cualquier analizador. Las filas se devuelven en data, pero los nombres de los campos que contienen la pregunta y la respuesta dependen de cómo instrumente el agente sus spans. Por tanto, asigne los campos según lo que realmente vea, no según lo que esperaba. Después, escriba los casos manualmente, con un objeto JSON por línea, en evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}Cinco reglas mantienen el valor del conjunto:
- Entre 40 y 80 casos es suficiente para empezar. Por debajo de 20, un caso inestable cambia la tasa de aprobados en 5 puntos y se termina ignorando una cifra que salta sin motivo.
- Cada error de producción que corrija se convierte en un caso el mismo día de la corrección. Este hábito hace que el conjunto crezca en la dirección adecuada.
- Un comportamiento por caso. Un caso que comprueba a la vez el importe del reembolso y el tono no le indica nada cuando falla.
idnunca cambia, porque el identificador permite comparar la ejecución de hoy con la del mes pasado.- Elimine los datos confidenciales antes de hacer el commit. Este archivo se incluirá en git, así que quite los nombres de clientes y los números de pedido que no le pertenezcan.
Aplique primero las comprobaciones deterministas, porque no tienen coste
Todo lo que tiene una respuesta correcta debe validarse con una aserción simple. No requiere una llamada al modelo, no tiene coste ni deja margen a la ambigüedad. Las comprobaciones deterministas detectan las regresiones estructurales, que son las que rompen los sistemas que rodean al agente: el JSON no se puede analizar, la herramienta nunca se llamó, reaparece la frase prohibida o la respuesta no cita ninguna fuente.
Sólo una función conoce los detalles del agente. Todo lo demás del harness es genérico.
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failuresMantenga el presupuesto de herramientas en esa lista. Un agente que hoy resuelve un caso en 3 llamadas y mañana necesita 11 ha sufrido una regresión aunque la respuesta final sea correcta, porque cada llamada tiene un coste.
Un LLM como evaluador y las cuatro formas en que falla
Todo lo que supera las aserciones necesita un evaluador que lea. Un evaluador basado en un LLM es una segunda llamada al modelo: recibe la pregunta, la respuesta del agente y un criterio, y después devuelve un veredicto. Es la única forma práctica de evaluar si «la respuesta responde a lo que preguntó el usuario».
Cuatro reglas hacen que un evaluador sea útil:
- Veredicto binario, nunca una puntuación de 1 a 10. Una escala devuelve 7 y 8 para casi todo. Por tanto, el número nunca cambia y no se aprende nada de él.
- Un criterio por llamada. Pregunte por el importe del reembolso o por el tono, pero no por ambos a la vez.
- Proporcione al evaluador la respuesta esperada cuando el caso tenga una. Evaluar comparando con una referencia es mucho más fácil que evaluar de forma abstracta.
- Fuerce el formato de salida y analícelo de forma estricta.
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)Ahora, los modos de fallo. Cada uno tiene una prueba que puede ejecutar esta tarde. Hacer estas pruebas es importante porque un evaluador no verificado produce números que parecen precisos, pero no significan nada.
Sesgo por longitud. Las respuestas más largas superan la evaluación con mayor frecuencia. Pruébelo: tome diez respuestas que el evaluador haya rechazado, añada a cada una dos párrafos de texto seguro de sí mismo que no aporten ningún dato nuevo y vuelva a evaluarlas. Si algún veredicto cambia a aprobado, se trata de un sesgo por longitud. En ese caso, debe corregir la rúbrica.
Preferencia por el propio modelo. A menudo, un evaluador califica con más benevolencia las respuestas de su propia familia de modelos que las respuestas de otra familia. Pruébelo: evalúe las mismas 30 respuestas con evaluadores de dos familias diferentes y compare los veredictos caso por caso. Cuando no coincidan, lea el caso personalmente.
Sesgo de posición. Si usa el evaluador para comparar dos respuestas, A y B, intercambie el orden y vuelva a ejecutar la evaluación. Si el veredicto cambia al intercambiar el orden, la comparación por pares todavía no es segura para esa rúbrica.
Deriva de la rúbrica. Los criterios imprecisos producen evaluadores demasiado permisivos. «¿La respuesta es útil?» aprueba casi cualquier cosa. «¿La respuesta indica el importe del reembolso en dólares?» sólo aprueba lo que se pretendía comprobar. Reescriba cada criterio hasta que nombre el dato que se debe comprobar.
Una medida de control cubre los cuatro casos. Conserve 30 casos que haya etiquetado manualmente y compare el evaluador con esas etiquetas cada vez que cambie el modelo o el prompt del evaluador. Si no coincide con usted en más de un caso de cada diez, corrija la rúbrica antes de confiar en cualquier tasa de aprobados que produzca. El evaluador es código, así que debe versionarse y revisarse como el código.
Graduación de bajo coste y escalado a un modelo de frontera
Evaluar cada caso con el modelo más caro en cada commit hace que la factura de evaluación crezca más que el agente que se está probando. Ordene los evaluadores por precio y deténgase en cuanto la respuesta sea clara.
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]Estas cifras suponen unos 1,200 tokens de entrada y 120 tokens de salida por llamada al evaluador, un tamaño realista para una pregunta, una respuesta y un criterio. Evaluar 1,000 casos cuesta 1.80 dólares estadounidenses con Claude Haiku 4.5 y 9.00 con Claude Opus 5. La diferencia parece insignificante hasta que se multiplica. Un conjunto de 60 casos, evaluado en cada commit con 40 commits semanales, supone 2,400 llamadas al evaluador por semana antes de ejecutar el trabajo nocturno.
Hay dos descuentos que se aplican directamente al trabajo de evaluación y que además se acumulan. Las ejecuciones de evaluación no son interactivas, por lo que la Batch API reduce a la mitad los precios de entrada y salida a cambio de una entrega asíncrona; esa es la primera fila del gráfico. La rúbrica y las instrucciones son idénticas byte por byte en cada llamada, por lo que el almacenamiento en caché de prompts resulta adecuado: una lectura de caché cuesta una décima parte del precio base de entrada, y una escritura de caché de cinco minutos cuesta 1.25 veces el precio base de entrada, por lo que la caché se amortiza después de un solo uso. Estos son los precios de lista de Anthropic a fecha de agosto de 2026, y Sonnet 5 tiene precios promocionales hasta el 31 de agosto de 2026, por lo que la tercera barra sube después de esa fecha.
La escala, en orden:
- Comprobaciones deterministas en todos los casos. No hay ningún coste de API.
- Un modelo pequeño como evaluador para los casos que superen esas comprobaciones.
- Un evaluador de frontera sólo cuando el evaluador pequeño indique un fallo o indique un aprobado con poca confianza.
- Revisión humana de una muestra pequeña, una vez por semana.
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]Esto intercambia parte de la precisión de la evaluación por un coste menor, así que mida esa diferencia en lugar de darla por supuesta. Una vez al mes, evalúe también todo el conjunto con el evaluador estricto y compare las dos columnas. Si no coinciden en más de unos pocos casos, la rúbrica es demasiado laxa para el modelo pequeño, y eso es lo que debe corregir. Controlar lo que gasta el propio agente es un trabajo independiente, descrito en control de costes de un agente de IA en un VPS.
Seguir la tasa de aprobaciones a lo largo del tiempo en un sistema propio
Una tasa de aprobaciones que no se puede asociar a un commit es sólo una impresión. Almacene una fila por caso y ejecución, con el commit y el modelo dentro de la fila.
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;Cargue el esquema con sqlite3 evals/results.db < evals/schema.sql y lea la tendencia con sqlite3 -box evals/results.db < evals/passrate.sql. Un año de ejecuciones diarias sobre 60 casos equivale aproximadamente a 22,000 filas, por lo que el almacén nunca se convierte en un proyecto independiente. Ejecutar SQLite en producción en un VPS explica los ajustes que empiezan a ser importantes si este archivo se comparte entre varias máquinas.
El runner muestra la misma información para una persona:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amountEjecute la suite sobre los cambios que pueden romper un agente, es decir, cambios en prompts, modelos y herramientas, en lugar de ejecutarla con cada commit del repositorio. Un hook pre-push cubre el subconjunto rápido:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushLas ejecuciones completas son más lentas y deben programarse. Un servicio y temporizador de systemd en el VPS ejecutado cada noche procesa todo el conjunto contra el prompt desplegado. Así se detectan los cambios que llegan desde fuera del repositorio, como una herramienta alojada cuyo comportamiento haya cambiado.
Revisión humana, mediante muestreo en lugar de una revisión exhaustiva
El juez se calibra con etiquetas humanas, por lo que alguien debe generarlas. Revise una muestra cada semana: todos los casos en los que el juez falló, además de diez casos aprobados seleccionados al azar. Los casos aprobados al azar son la mitad importante, porque un juez que ha empezado a aprobar respuestas incorrectas puede parecer perfecto en cualquier panel basado en sus propios veredictos.
Quince casos a tres minutos cada uno representan 45 minutos a la semana. Esta revisión devuelve correcciones para la rúbrica cuando usted y el juez discrepan, además de casos nuevos para tipos de fallo que nadie había previsto. Escriba el veredicto humano en la misma tabla con graded_by establecido en human, de modo que el acuerdo entre el juez y la persona pueda consultarse en lugar de depender de la memoria.
Qué falla en el propio sistema de evaluación
anthropic.RateLimitError en la primera ejecución completa. Sesenta casos ejecutados simultáneamente superan el límite de solicitudes o tokens de tu nivel. Limita la concurrencia a cuatro workers y programa la ejecución nocturna con Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) del evaluador. El modelo respondió con prosa o incluyó su JSON en un bloque de código. Reintenta una vez y, después, registra el caso como error. No permitas que un fallo de análisis cuente como aprobado. Una suite que convierte los errores en aprobaciones se acerca al 100 % mientras el agente empeora.
Casos inestables. La misma entrada se aprueba en una ejecución y falla en la siguiente porque el agente genera su salida mediante muestreo. Ejecuta el caso inestable tres veces y registra la fracción en lugar de eliminarlo. Un caso que se aprueba en dos de tres ejecuciones revela un problema real de robustez, y un cliente lo encontrará.
Desactualización del conjunto de referencia. Alguien edita una respuesta esperada para que la suite aparezca en verde. Revisa las diferencias de evals/cases.jsonl con el mismo cuidado que las diferencias del agente, porque ese archivo es tu definición escrita de lo correcto.
Una suite que nunca falla. Una tasa de aprobaciones del 100 % durante un mes indica que el conjunto ha dejado de reflejar el producto. Extrae diez trazas recientes, busca aquellas que el agente gestionó mal y añádelas.
FAQ
¿Cuántos casos necesita un conjunto de evaluación de un agente de IA?
Empiece con 40 a 80 y amplíe el conjunto a partir de fallos reales. Por debajo de unos 20 casos, un resultado inestable puede cambiar la tasa de aprobados en 5 puntos, por lo que la cifra deja de ser informativa. Después de unos cientos de casos, cada ejecución consume dinero y tiempo reales, mientras que cada caso adicional aporta poca cobertura. La medida importante no es el recuento, sino la proporción de tipos de fallos conocidos en producción que aparecen al menos una vez en el conjunto.
¿Puedo confiar en un juez LLM para evaluar mi agente?
Sólo después de medirlo con sus propias etiquetas. Conserve 30 casos evaluados manualmente y compare el juez con ellos cada vez que cambie el modelo o el prompt del juez. Los jueces muestran sesgo hacia las respuestas largas: las respuestas con contenido añadido se aprueban con más frecuencia. También muestran preferencia por sí mismos: suelen evaluar de forma más favorable las respuestas generadas por su propia familia de modelos. Ambos sesgos se pueden comprobar: añada contenido a una respuesta fallida y vuelva a evaluarla, o evalúe las mismas respuestas con un juez de otra familia. Si el juez no coincide con sus etiquetas en más de un caso de cada diez, la rúbrica es demasiado ambigua para utilizarla.
¿Qué modelo debería evaluar las pruebas?
Empiece por lo barato y escale cuando sea necesario. Las aserciones deterministas no tienen coste, por lo que se ejecutan primero en todos los casos. Un modelo pequeño gestiona los aprobados claros. Sólo los fallos y los veredictos con baja confianza pasan a un modelo de frontera. Con las tarifas de lista de agosto de 2026, evaluar 1,000 casos cuesta aproximadamente 1.80 dólares estadounidenses con Claude Haiku 4.5 y aproximadamente 9.00 con Claude Opus 5. Como las ejecuciones de evaluación son asíncronas, la Batch API reduce a la mitad cualquiera de esas cifras.
¿Las evaluaciones sustituyen a la supervisión en producción?
No, porque responden a preguntas distintas. Un conjunto de evaluación indica si un cambio que está a punto de publicar mejora o empeora un conjunto fijo de casos. El tracing y la supervisión indican qué están ejecutando los usuarios reales en ese momento, incluidos los datos de entrada que ningún caso cubre. Ambos procesos se retroalimentan: los traces proporcionan casos nuevos y el conjunto de evaluación determina si la corrección funcionó realmente.