Evaluaciones autoalojadas para agentes de IA
Crea un ciclo de evaluación propio con casos reales, comprobaciones deterministas, juez LLM y una tasa de aprobados registrada en cada commit.
Qué son las evaluaciones autoalojadas para agentes de IA
Las evaluaciones autoalojadas para agentes de IA son cuatro elementos que mantiene en su propio repositorio: un archivo de casos guardados, un script que ejecuta el agente con esos casos, un conjunto de comprobaciones que puntúa cada respuesta y una tabla de resultados que puede consultar. Ninguno de estos elementos necesita un proveedor. Todo el ciclo requiere unos cientos de líneas de Python y un solo archivo de SQLite.
El agente funcionó en la demostración porque usted eligió personalmente 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 relevantes en casos, evaluar cada caso con cada cambio y guardar la tasa de aprobados junto al commit que la produjo. El mismo ciclo funciona independientemente de aquello sobre lo que ejecute el agente, y los frameworks de agentes autoalojados que merece la pena ejecutar se diferencian principalmente por la cantidad de información de la traza que proporcionan de forma predeterminada.
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. Los cuatro elementos pueden cambiar sin modificar el código de la aplicación, por lo que una revisión de código normal no detecta ningún problema.
La causa más habitual es editar el prompt. Añade una frase para evitar una respuesta grosera. 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 en su lugar la respuesta es una disculpa cortés. 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 recuerdes de memoria, porque una tasa de aprobaciones que disminuye el día que cambiaste de modelo sólo se puede diagnosticar si el modelo aparece en la fila.
La tercera causa son las herramientas. Cambiar la redacción de una descripción de herramienta modifica el momento en que el modelo decide llamarla. Si tus herramientas llegan mediante servidores MCP que se ejecutan 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.
Cree el conjunto base a partir de las trazas que ya recopila
No invente casos de evaluación. Extráigalos del tráfico. Si ya ejecuta trazas autogestionadas de Langfuse para su agente, cada solicitud se almacena con su entrada, sus llamadas a herramientas y su salida. Ese es exactamente el material sin procesar que necesita un caso.
Exporte una ventana de observaciones raíz mediante la API pública. Usa autenticación básica, con su clave pública como nombre de usuario y su 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 análisis. 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 vea realmente y 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."}Estas cinco reglas mantienen el valor del conjunto:
- Entre 40 y 80 casos es suficiente para empezar. Con menos de 20, un caso inestable cambia la tasa de aprobación en 5 puntos y se termina ignorando un número que cambia 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 correcta.
- Un comportamiento por caso. Un caso que comprueba a la vez el importe del reembolso y el tono no aporta información cuando falla.
idnunca cambia, porque el identificador permite comparar la ejecución de hoy con la del mes pasado.- Redacte los datos antes de confirmar los cambios. Este archivo se incluirá en git, así que elimine los nombres de clientes y cualquier número de pedido que no le pertenezca.
Priorice primero las comprobaciones deterministas, porque no tienen coste
Todo lo que tenga una respuesta correcta se comprueba con una aserción simple. No se usa ningún modelo, no hay coste ni ambigüedad. Las comprobaciones deterministas detectan las regresiones estructurales, que son las que rompen los sistemas que rodean a su 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 a su agente. Todo lo demás del arnés 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 con 3 llamadas y mañana necesita 11 ha sufrido una regresión aunque la respuesta final sea correcta, porque cada llamada tiene un coste.
LLM como juez y las cuatro formas en que falla
Lo que supera las aserciones necesita un evaluador que lea. Un juez 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 contesta lo que pidió el usuario».
Cuatro reglas hacen que un juez sea utilizable:
- Veredicto binario, nunca una puntuación de 1 a 10. Una escala devuelve 7 y 8 para casi todo, así que 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 juez la respuesta esperada siempre que el caso tenga una. Evaluar comparando con una referencia es mucho más sencillo que evaluar en abstracto.
- Obligue a usar un 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. Hacerlas es importante, porque un juez 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. Póngalo a prueba: tome diez respuestas que el juez haya rechazado, añada a cada una dos párrafos de texto de relleno convincente que no incorporen ningún dato nuevo y vuelva a evaluarlas. Si algún veredicto cambia a aprobado, existe sesgo por longitud y debe corregir la rúbrica.
Preferencia por el propio modelo. Un juez suele evaluar con más benevolencia las respuestas de su propia familia de modelos que las respuestas de otra familia. Póngalo a prueba: evalúe las mismas 30 respuestas con jueces de dos familias distintas y compare los veredictos caso por caso. Cuando no coincidan, lea usted mismo el caso.
Sesgo de posición. Si usa el juez para comparar dos respuestas, A y B, cambie el orden y ejecute la evaluación de nuevo. Si el veredicto cambia al invertirlas, la comparación por pares todavía no es segura para esa rúbrica.
Deriva de la rúbrica. Los criterios imprecisos producen jueces complacientes. «¿La respuesta es útil?» da por buena casi cualquier respuesta. «¿La respuesta indica el importe del reembolso en dólares?» sólo da por buenas las respuestas que usted pretendía aceptar. Reescriba cada criterio hasta que indique el dato que se comprueba.
Una medida de control cubre los cuatro casos. Conserve 30 casos que haya etiquetado manualmente y compare el juez con sus etiquetas cada vez que cambie el modelo del juez o su prompt. 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 juez es código, por lo que debe versionarse y revisarse como el código.
Gradúa con un modelo económico y escala a un modelo frontier
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. Ordena los evaluadores por precio y detente 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 a la semana, supone 2,400 llamadas al evaluador por semana antes de ejecutar el trabajo nocturno.
Dos descuentos se aplican directamente al trabajo de evaluación y son acumulables. 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 tanto, la caché se amortiza después de un solo uso posterior. Estos son los precios de lista de Anthropic en 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 escalera, en orden:
- Comprobaciones deterministas en cada caso. No tienen ningún coste de API.
- Un modelo pequeño como evaluador para los casos que superen esas comprobaciones.
- Un evaluador frontier sólo cuando el evaluador pequeño indique que falla o indique que pasa 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 menor coste, así que mide ese intercambio en lugar de darlo por supuesto. Una vez al mes, evalúa también todo el conjunto con el evaluador estricto y compara las dos columnas. Si discrepan en más de un puñado de casos, la rúbrica es demasiado poco precisa para el modelo pequeño, y eso es lo que debes corregir. Controlar lo que gasta el propio agente es un trabajo distinto, explicado en control de costes de un agente de IA en un VPS.
Registrar la tasa de aprobación a lo largo del tiempo en un sistema propio
Una tasa de aprobación que no puede asociarse a un commit es sólo una impresión. Almacene una fila por caso y ejecución, con el commit y el modelo en la propia 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 son unas 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 hacerlo sobre cada commit del repositorio. Un hook de 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 y no 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ó, más diez casos aprobados seleccionados al azar. Los casos aprobados aleatorios son la mitad importante, porque un juez que ha empezado a aprobar silenciosamente respuestas incorrectas parece perfecto en cualquier panel basado en sus propios veredictos.
Quince casos a tres minutos cada uno son 45 minutos por semana. Esta revisión devuelve correcciones a 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, para que el acuerdo entre el juez y la persona sea una consulta y no un recuerdo.
Qué falla en el propio harness de evaluación
anthropic.RateLimitError en la primera ejecución completa. Sesenta casos ejecutados a la vez superan el límite de solicitudes o tokens de su nivel. Limite la concurrencia a cuatro workers y traslade la ejecución nocturna a la Batch API.
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) del juez. El modelo respondió con prosa o incluyó su JSON en un bloque de código. Reintente una vez y registre el caso como error. Nunca permita que un fallo de análisis cuente como aprobado, porque una suite que convierte los errores en aprobados 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 muestrea su salida. Ejecute el caso inestable tres veces y registre 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á.
Degradación del conjunto de referencia. Alguien edita una respuesta esperada para que la suite aparezca en verde. Revise las diferencias de evals/cases.jsonl con el mismo cuidado que las diferencias del agente, porque ese archivo es su definición escrita de lo correcto.
Una suite que nunca falla. Una tasa de aprobados mantenida en el 100% durante un mes significa que el conjunto ha dejado de seguir al producto. Extraiga diez trazas recientes, encuentre aquellas que el agente gestionó mal y añádalas. Después, provoque un fallo intencionadamente y confirme que la ejecución aparece en rojo. Esta comprobación es la aplicación de mutation testing a una suite de pruebas y la única forma de saber que su conjunto todavía detecta fallos.
FAQ
¿Cuántos casos necesita un conjunto de evaluación de un agente de IA?
Empiece con 40 a 80 casos y amplíe el conjunto a partir de fallos reales. Por debajo de unos 20 casos, un resultado inestable cambia la tasa de aprobación en 5 puntos, por lo que la cifra deja de aportar información. Por encima de unos cientos, cada ejecución cuesta dinero y tiempo reales, mientras que cada caso adicional aporta poca cobertura. La medida importante no es el número de casos, sino la proporción de tipos de fallo 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 suelen aprobarse más veces. También muestran preferencia por sí mismos: evalúan con más benevolencia las respuestas generadas por modelos de su propia familia. 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 imprecisa para utilizarla.
¿Qué modelo debe evaluar las pruebas?
Empiece por los modelos baratos 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 casos aprobados con claridad. Sólo los fallos y los veredictos con poca confianza pasan a un modelo frontier. 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. Además, como las ejecuciones de evaluación son asíncronas, Batch API reduce a la mitad cualquiera de esas cifras.
¿Las evaluaciones sustituyen a la monitorizació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 monitorización indican qué situaciones reales están encontrando los usuarios en ese momento, incluidos los inputs que ningún caso cubre. Ambos se retroalimentan: los traces proporcionan casos nuevos y el conjunto de evaluación determina si la corrección funcionó realmente.