Lección 6: Práctica: construye un circuito de evaluación para tu agente
Objetivos de aprendizaje:
- Unir conjuntos de evaluación, calificación estratificada y bucles de arnés en un
eval-runner.mjs ejecutable — una tarea de evaluación por bucle independiente
- Hacer que los reportes capturen no solo la tasa de aprobación, sino también la duración por tarea, el número de llamadas a herramientas, el consumo de tokens y los errores de herramienta, y usar esas columnas para diagnosticar problemas
- Usar este circuito para medir el impacto real de un cambio en el prompt de sistema, y atrapar un verificador demasiado estricto que rechaza salidas correctas
Requisitos: Leer las lecciones 1–5, tener a mano el bucle de arnés del curso 7 y poder ejecutarlo | Anterior: Lección 5 <<
Las primeras cinco lecciones fueron todas componentes: verifica el estado final y no paso por paso (lección 2), verificaciones deterministas primero y ojo con los verificadores demasiado estrictos (lección 3), el texto de forma libre solo admite jueces LLM (lección 4), los conjuntos de evaluación arrancan con unas veinte tareas reales (lección 5). Cada uno tiene sentido por su cuenta, pero después de cambiar tu prompt sigues sin tener algo que puedas ejecutar con un solo comando para que los números te digan «mejor o peor».
Esta lección suelda los componentes. Lo que obtienes es un archivo de trescientas líneas que corre en menos de dos segundos. La guía oficial sobre «cómo ejecutar evaluaciones» es directa: usa llamadas programáticas y directas a la API del LLM; usa bucles agénticos simples — bucles while que alternan llamadas al LLM y llamadas a herramientas — una tarea de evaluación por bucle1. Ese es exactamente el bucle guiado por stop_reason del curso 7 de esta serie. Puedes trasplantarlo tal cual.
Cómo se ve cuando corre
Guarda el eval-runner.mjs completo que aparece más adelante en esta lección y después node eval-runner.mjs:
Esto no es un ejemplo hecho a mano — está copiado textualmente de una ejecución real en un directorio temporal. Copia el código completo y córrelo una vez; todo excepto la columna «Duración» (tiempo real de reloj, que varía con la carga de la máquina) va a coincidir hasta el milisegundo. Los números son los mismos porque el cliente stub devuelve respuestas prefijadas.
Esta salida contiene todo lo que enseña esta lección: cinco tareas ejecutando cada una su propio bucle, dos modos de calificación mezclados en una sola tabla, tasa de aprobación más cuatro columnas de diagnóstico, y la diferencia entre dos versiones colapsada en una tabla comparativa. El resto de la lección lo desempaqueta.
Las cinco piezas de un circuito
- Sistema bajo prueba: definiciones de herramientas, implementaciones reales de las herramientas y los datos detrás de ellas. La evaluación ejecuta «el agente usa tus herramientas para trabajar» — las herramientas son parte de lo que estás probando.
- Cliente stub: un
messages.create falso que devuelve respuestas prefijadas en una cola fija, y que vuelve reproducible todo el circuito.
- Conjunto de evaluación: un arreglo
tasks, cada entrada es {id, prompt, verify}. Requisito oficial: cada prompt de evaluación debería estar emparejado con una respuesta o un resultado verificable1 — un prompt sin verificador no es una tarea de evaluación, es una demo.
- Calificación: lo que se puede calificar de forma determinista va a una función
verify; el texto de forma libre va al juez.
- Bucle y reporte: una tarea, un bucle
while; al terminar, agrega las métricas en una tabla.
Una cosa que conviene dejar clara desde ya: las tareas no comparten messages. El messages de cada tarea arranca solo con el prompt de usuario de esa tarea, corre su propio bucle y después se descarta1. Por qué esto importa tanto — el cuestionario del medio lo va a preguntar directamente.
Pieza uno: las herramientas y los datos detrás de ellas
El sistema bajo prueba es un asistente de pedidos, cuatro pedidos, dos herramientas: search_orders (busca por nombre de cliente o estado, devuelve una lista de IDs de pedido) y get_order (consulta el detalle de un solo pedido por su ID). Dos detalles son deliberados: search_orders solo devuelve IDs de pedido sin montos, lo que obliga al agente a llamar get_order de nuevo por cada pedido — la columna «Llamadas» del reporte va a exponer este defecto de diseño. El otro: lanza un error cuando ambas condiciones de filtro vienen vacías:
Este es el error de herramienta por «parámetro inválido». La guía oficial dice que cuando estos errores se agrupan, normalmente significa que las descripciones de herramientas deberían ser más claras o necesitan ejemplos1. Lo vamos a ver en el reporte en un momento. Los errores de herramienta no son caídas — el bloque de ejecución de herramientas atrapa la excepción, la envuelve en un tool_result con is_error: true, se lo devuelve al modelo e incrementa un contador. tool_use y tool_result se emparejan por tool_use_id — esa es la base que puso el curso 7, aquí solo agregamos dos contadores.
Pieza dos: el cliente stub y el interludio de verificación
Hace falta una pausa acá, si no, ninguno de los números de abajo se sostiene.
Claude de verdad no es determinista: el mismo prompt ejecutado dos veces puede tomar caminos completamente distintos2. Eso es bueno para producción y desastroso para lecciones de demostración — corres hoy y sacas 3/5, mañana 4/5, y no puedes decir si la diferencia viene del cambio de prompt o del humor del modelo. Así que las lecciones prácticas de los cursos 8 y 9 usan todas el mismo método: cambia el modelo por un stub que devuelve respuestas prefijadas en una cola fija, para volver el comportamiento probado una variable controlada. Esto verifica la lógica de control que escribiste, no el desempeño del modelo ese día.
Cuando la cola se agota lanza un error, sin respuesta de reserva — si el bucle da una vuelta de más lo ves de inmediato: Error: [stub] cola de respuestas de v1/t2-pending agotada (1 solicitudes emitidas) (ese es el texto de error real después de que borré la última respuesta de la cola de t2), y no un end_turn falso colándose. Cada respuesta carga su propio latency_ms; el stub duerme realmente ese tiempo, así que la columna «Duración» mide cuántos turnos tomó el bucle. Cada tarea recibe un cliente fresco con su propio script; los cursores no se cruzan entre tareas.
La diferencia entre las dos versiones del prompt está fijada en las dos colas de respuestas del stub. En un escenario real cambias el prompt de sistema y el comportamiento del modelo lo sigue; acá no tengo modelo, así que preescribí SCRIPT_V1 y SCRIPT_V2, dejando que v2 devuelva respuestas distintas en dos tareas — «supongamos que el prompt v2 hace efecto y el modelo responde así» queda codificado como datos:
Usa la sintaxis de propagación para heredar de v1 y lista solo las entradas que cambian — quien lea el código ve de un vistazo el alcance de la diferencia. Este circuito se verifica a sí mismo: si los verificadores califican bien, si las métricas se registran con exactitud, si los reportes calculan correcto, si dos ejecuciones se pueden comparar. Cuando cambies a un cliente real, el circuito no cambia — solo los números empiezan a saltar.
Pieza tres: el conjunto de evaluación — cuatro normales más un caso límite
La lección 5 dijo que los conjuntos de evaluación deberían corresponder a la distribución real y cubrir casos límite3; lo oficial también advirtió contra los entornos sandbox demasiado simplistas que no ponen las herramientas bajo suficiente complejidad1. Acá solo entran cinco tareas por espacio, pero la estructura sigue la de los conjuntos de evaluación reales:
t3-no-orderid merece mención especial. El prompt es «Ayúdame a revisar el estado de ese pedido» — ¿cuál? No lo especifica. El comportamiento ideal es pedir el ID de pedido en vez de adivinar uno para consultarlo. La documentación oficial es cuidadosa con este comportamiento: si el prompt del usuario no provee suficiente información para llenar todos los parámetros requeridos, Claude Opus tiene muchas más probabilidades de reconocer el parámetro faltante y pedirlo, pero ese comportamiento no está garantizado, especialmente para prompts más ambiguos y modelos menos capaces4. Los comportamientos «no garantizados» son exactamente los que un conjunto de evaluación debería cubrir — las cosas garantizadas no necesitan prueba.
El r que recibe verify contiene no solo answer sino también toolCalls, toolErrors y tokens, así que el verificador puede revisar «estado final más métricas clave» y no solo texto: t3 de hecho revisa «llamó cero herramientas», t5 revisa «reportó exactamente un error y dijo con honestidad que no lo encontró» — el «estado final primero» de la lección 2 se materializa a través de estos campos. note es para humanos; cuando una tarea falla, el reporte imprime el criterio junto a la respuesta real del agente.
Si tu tarea de la lección 5 usó el conjunto de campos {id, prompt, expected, verifier, rubricRef, tags, split}, mapéalo ahora para evitar confusiones: el verifier de la lección 5 acá se llama grader y es solo para mostrar — el tipo de calificación real lo determina si esta tarea tiene una función verify o judge: true. Las afirmaciones declarativas de expected acá se escriben directamente dentro del cuerpo de la función verify (las afirmaciones de cada tarea se ven distintas; escribirlas como funciones es más simple que diseñar un formato universal de afirmaciones). rubricRef queda incorporado como JUDGE_PROMPT, ya que la suite entera tiene un solo caso de juez. tags y split se omiten por brevedad; la disciplina del conjunto reservado se repite como siempre en la sección «Alcance». Tu JSON de la lección 5 no quedó obsoleto — es la versión declarativa de este arreglo TASKS. Avanzar significa traducir cada afirmación a una función.
Pieza cuatro: calificación estratificada, primero lo determinista
Los métodos de calificación tienen un orden: la calificación basada en código es la más rápida y la más confiable, escala extremadamente bien pero le falta matiz para juicios complejos; la calificación basada en LLM es rápida y flexible, puede manejar juicios complejos, pero primero prueba que es confiable y después escala; la calificación humana es la más flexible y de mayor calidad pero lenta y cara, evítala si es posible3.
Así que la regla es: lo que se puede calificar por código nunca va a un juez. Cuatro de las cinco tareas de acá usan verify; solo t4-refund-note, ese pedazo de texto de forma libre, va al juez — «¿este párrafo se le puede mandar a un cliente?» no se responde con coincidencia de cadenas. La forma del juez sigue a la lección 4: rúbrica fijada en tres ítems, formato de salida fijado en JSON, primero razonar y después puntuar:
Cada punto tiene su fuente: haz que el juez primero razone y después puntúe, y luego descarta el razonamiento — mejora la calidad de la calificación, en especial para tareas que requieren juicios complejos3; la salida debería ser empírica o específica, no una evaluación puramente cualitativa3; y «una sola llamada al LLM, un solo prompt, salida de 0.0 a 1.0 más un aprobado/fallido» es la combinación que lo oficial encontró más consistente y más alineada con los juicios humanos después de probar varios esquemas de juez en su sistema multiagente de investigación2.
El juez acá también es un stub: la respuesta de v1 no tiene el tiempo de acreditación, dos de tres ítems dan 0.67 y se califica fallido; v2 lo agregó, los tres aciertan y dan 1.00, calificado aprobado. La puntuación es autoconsistente con la rúbrica — tres ítems binarios promediados solo pueden caer en 0, 0.33, 0.67 o 1.00; una puntuación de 0.85 significaría que el juez no siguió la aritmética de la rúbrica. El juez mismo quema tokens; su consumo se suma a los tokens de esa tarea, y por eso t4 llama una sola herramienta pero sus tokens no son bajos.
Una disciplina más de la lección 4: el modelo que trabajó no debería calificarse a sí mismo. Lo oficial dice que hagas que una instancia fresca del modelo intente refutar el resultado — el que hace el trabajo no es el que lo califica5. En código: el juez usa su propio cliente, su propio prompt de sistema, su propio arreglo de messages, solo ve el prompt de la tarea y la respuesta a calificar, y no ve la transcripción de llamadas a herramientas del agente.
Pieza cinco: bucle y reporte
El bucle es el bucle del curso 7 textual, con el esqueleto sin cambios — solo se agregaron el model y el max_tokens que la API real exige (el stub los ignora), y después se envolvió con contadores:
messages es una variable local dentro de runTask; la función retorna y desaparece. Esa es la implementación entera de «las tareas no comparten contexto» — no hace falta ningún mecanismo extra, basta con no sacarla de ahí.
Para las métricas, la lista de verificación oficial es: más allá de la exactitud de alto nivel, recolecta también el tiempo total de ejecución de cada llamada a herramienta y de cada tarea, el número total de llamadas a herramientas, el consumo total de tokens y los errores de herramienta1. Las columnas de la tabla del reporte siguen exactamente esa lista. La tasa de aprobación solo te dice «si aprobó», estas columnas te dicen «cómo aprobó» — una tarea que aprueba llamando doce herramientas y una que aprueba con dos llamadas son dos niveles de calidad distintos. Estas columnas también se autodocumentan: muchas llamadas a herramientas redundantes normalmente sugieren que los parámetros de paginación o de límite de tokens necesitan ajuste; muchos errores de herramienta por parámetros inválidos normalmente sugieren que las descripciones de herramientas podrían ser más claras o necesitan mejores ejemplos1. Los ejercicios van a usar esto directamente.
Que el reporte sea legible para humanos tiene valor intrínseco. La sugerencia oficial es: haz que Claude muestre evidencia en vez de afirmaciones de éxito — la salida de las pruebas, el comando que ejecutó y lo que devolvió, o una captura de pantalla del resultado; revisar evidencia es más rápido que volver a ejecutar la verificación tú mismo, y funciona para sesiones que no estuviste mirando5. Esta tabla del reporte es esa evidencia — pégala en la descripción de un PR o mándasela a un colega, y puede juzgar sin volver a ejecutar nada. (El único detalle al imprimir es que los caracteres CJK de ancho completo cuentan como ancho 2 y un padEnd crudo desalinea — el código tiene un pad consciente del ancho.)
El eval-runner.mjs completo
Cópialo y guárdalo como eval-runner.mjs; node eval-runner.mjs lo ejecuta directo. Sin dependencias, sin package.json, Node 18+ (usa await de nivel superior, así que la extensión debe ser .mjs).
Recuperando la trampa de la lección 3: verificadores demasiado estrictos
La lección 3 cubrió una trampa, en las palabras exactas de lo oficial: evita los verificadores demasiado estrictos que rechazan respuestas correctas por diferencias espurias como el formato, la puntuación o formulaciones alternativas válidas1. Suena de sentido común pero es casi inevitable en código, porque los verificadores demasiado estrictos son los más fáciles de escribir.
El circuito tiene uno incrustado. t1-total tiene dos versiones de verificador; la antigua es pass: r.answer.includes("1280.00") — parece a prueba de balas: la respuesta correcta es 1280.00, así que revisa si la respuesta contiene esa cadena. Ejecuta node eval-runner.mjs --strict-verify (abajo se pega solo el reporte de v1; el reporte de v2 y la tabla de diferencias se imprimen como siempre):
Esto también sale de una ejecución real. Mira el detalle de t1-total: el agente respondió «por un total de ¥1,280.00» — monto correcto, pedidos correctos, redacción normal. Su único crimen es poner una coma como separador de miles entre el 1 y el 280, así que includes("1280.00") devuelve false y una respuesta completamente correcta se califica fallida.
En este punto arregla el verificador, no el agente. Los reportes solo te dicen «t1 fallido», no te dicen de quién es la culpa; la forma de saberlo es leer las palabras reales del agente en el detalle — y para eso exactamente es que los reportes imprimen la respuesta cruda. El arreglo es la normalización. La descripción oficial de la coincidencia exacta ya incluye este paso: las evaluaciones por coincidencia exacta miden si la salida del modelo coincide con una respuesta correcta predefinida, típicamente después de normalizar los espacios en blanco y las mayúsculas3. Los escenarios con montos necesitan más lavado — símbolos de moneda, separadores de miles, unidades — así que el verificador corregido lava primero el ruido, extrae los números y después compara numéricamente:
Quita --strict-verify y ejecuta otra vez; t1-total pasa de 0.00 a 1.00, y la línea base de v1 sube de 2/5 de vuelta a 3/5 — y entre medio, el agente no cambió ni un carácter, y la cola de respuestas del stub tampoco cambió ni un carácter. La puntuación cambió pero el sistema bajo prueba no — esa es la prueba de fuego del «problema del verificador».
Un comentario al margen sobre el alcance: normalizar no es «cuanto más flojo mejor». Afloja hasta «si parece contener 1280, aprueba», y el agente que responde «total de 1280 pedidos, monto desconocido» también aprueba. Los verificadores deberían pararse en «deja pasar las diferencias irrelevantes, bloquea los errores sustantivos» — y el único método para encontrar esa posición es probar con respuestas reales.
Cambia un solo lugar del prompt y mira moverse la puntuación
Circuito calibrado, listo para trabajo real. Cambié un solo lugar — el prompt de sistema, agregándole dos reglas después de v1:
Esas dos no son inventadas; salen de leer los «Casos fallidos» del reporte de v1: t3 falla porque adivinó un ID de pedido con parámetros incompletos, t4 recibió descuento por faltarle el tiempo de acreditación. El reporte dice qué, tú cambias eso — esa es la diferencia más concreta entre tener circuito y no tenerlo. Sin circuito, después de cambiar el prompt puedes echarle un ojo a la salida y sentir que «parece mejor»; con circuito, «cuál mejoró, cuál se quedó igual, algo retrocedió» son tres líneas de números.
Vuelve a ejecutar: la tabla de diferencias es el último segmento de la salida del comienzo — tasa de aprobación de 60% a 100%, dos tareas pasan de fallido a aprobado, las otras tres no se mueven. Esa última media oración importa tanto como la primera: dice que este cambio no rompió lo que ya funcionaba. Sin circuito, después de cambiar el prompt solo miras la salida una vez y piensas «se ve mejor»; con circuito, «cuál mejoró / cuál igual / algo retrocedió» son tres filas de números.
La formulación oficial para esto es: con evaluaciones puedes medir con mucha más confianza el impacto de tu ingeniería de prompts; incluso refinamientos pequeños a las descripciones de herramientas pueden rendir mejoras dramáticas1. Acá también hay una ganga para agarrar: en el desarrollo temprano de agentes los cambios tienden a tener un impacto dramático porque todavía abunda la fruta al alcance de la mano — un retoque del prompt podría subir la tasa de éxito de 30% a 80%; con tamaños de efecto así de grandes puedes detectar los cambios con apenas unos pocos casos de prueba2. Ahora tienes solo cinco tareas — eso no es un déficit, es el punto de partida.
Mira otra vez las columnas de métricas: las llamadas a herramientas de v2 bajaron de 8 a 6, los errores de herramienta de 2 a 1, y los tokens bajaron casi mil, porque t3 ya no adivina a ciegas para llamar herramientas. El mismo cambio mejoró simultáneamente la exactitud y el costo — este tipo de cosa solo se vuelve visible cuando registras estas columnas juntas.
Alcance: qué administra este circuito y qué no
Qué administra: un agente, un lote de tareas, una ejecución en tu máquina, un reporte legible para humanos.
Cambiar a un modelo real — la estructura del circuito no cambia. Reemplaza stubClient(...) por el cliente real de @anthropic-ai/sdk; el bucle while de runTask no cambia ni una línea — ya está escrito con la forma de stop_reason / tool_use / tool_result de la API real; los parámetros requeridos model y max_tokens ya están ahí (el stub los ignora, el cliente real los usa). Después del cambio dos cosas se mueven: las puntuaciones van a temblar porque los agentes no son deterministas entre ejecuciones ni siquiera con prompts idénticos2, así que no leas de más una sola ejecución; y correr una ronda cuesta dinero y tiempo, con cinco tareas da igual, pero con doscientas ya conviene pensar en concurrencia y costo.
Qué no administra: enganchar las evaluaciones a CI, ejecutarlas en cada commit, compararlas contra versiones históricas, bloquear merges cuando las puntuaciones bajan de cierto umbral — todas son prácticas de ingeniería comunes y sí funcionan bien, pero esta lección no las desarrolla. El Nivel 2 de los ejercicios te va a llevar por «comparar dos reportes», y el resto de la orquestación es trabajo de tu CI.
Una disciplina más de la lección 5 para repetir: no ajustes contra el conjunto reservado. Sigues los reportes para cambiar prompts; después de varias rondas las puntuaciones definitivamente van a subir, pero la subida podría ser solo «puntuaciones en estas cinco tareas». La práctica oficial es apoyarse en conjuntos de prueba reservados para asegurar que no hay sobreajuste a las evaluaciones «de entrenamiento»1. Así que en un montaje real las tareas deberían dividirse en dos pilas: una corre a diario para orientarte, la otra queda bajo llave y solo se abre cuando piensas «esta versión debería funcionar» — las puntuaciones de la primera pila son navegación, las de la segunda son veredicto.
Último recordatorio viejo: las evaluaciones automáticas se van a perder cosas. Los evaluadores humanos siempre dan con casos límite que las evaluaciones no ven — alucinaciones ante consultas inusuales, fallos sistémicos, sesgos sutiles de selección de fuentes2. Que el circuito corra sin problemas no significa que dejes de usarlo tú mismo.
💻 Ejercicios
Resumen
- La forma estándar de ejecutar evaluaciones son llamadas programáticas y directas a la API más bucles agénticos simples — una tarea de evaluación por bucle; las tareas no comparten
messages, o el contexto de la tarea anterior contamina a la siguiente y los resultados dejan de ser comparables1.
- Cada prompt de evaluación debería estar emparejado con un resultado verificable; los verificadores forman un espectro que va de la comparación exacta de cadenas a pedirle al modelo que juzgue — lo que se puede calificar por código nunca va a un juez, porque la calificación basada en código es la más rápida, la más confiable y escala extremadamente bien1 3.
- El texto de forma libre va al juez; la forma es una sola llamada, un solo prompt, salida de 0.0 a 1.0 más aprobado/fallido; la rúbrica debe razonar primero y puntuar después, con el formato de salida fijado2 3.
- Más allá de la tasa de aprobación, los reportes deben registrar la duración de la tarea, el número de llamadas a herramientas, el consumo de tokens y los errores de herramienta; estas columnas se autodocumentan — las llamadas redundantes apuntan a parámetros de paginación/volumen de retorno que necesitan ajuste, y los errores de parámetro inválido apuntan a descripciones de herramientas que necesitan claridad1.
- Los verificadores demasiado estrictos rechazan respuestas correctas: el formato, la puntuación y formulaciones distintas pero razonables pueden tumbar una comparación literal; normaliza antes de la coincidencia exacta1 3. La puntuación cambió pero el sistema bajo prueba no — es culpa del verificador.
- Con un circuito, el impacto de un cambio de prompt se vuelve medible; incluso refinamientos pequeños pueden rendir mejoras dramáticas; los tamaños de efecto tempranos son grandes y unos pocos casos bastan para ver diferencias1 2. El reporte mismo es evidencia que otros pueden revisar, más rápido que volver a ejecutar la verificación tú mismo, y sirve para sesiones que no estuviste mirando5.
- Sigue los reportes para cambiar prompts y las puntuaciones van a subir, pero la subida podría ser solo sobre este lote de tareas; guarda bajo llave el conjunto reservado para evitar el sobreajuste1. Las evaluaciones automáticas tienen puntos ciegos; los evaluadores humanos siguen atrapando casos límite que las evaluaciones no ven2.
Después de terminar este curso
Mirando hacia atrás, el hilo principal es en realidad corto. La lección 1 separó «parece terminado» de «está terminado» — sin verificaciones ejecutables, «parece terminado» es la única señal disponible y tú te conviertes en el paso de verificación5. La lección 2 fijó qué verificar: los agentes podrían recorrer caminos razonables completamente distintos hacia el mismo objetivo, así que evalúa el estado final, no revises la trayectoria paso por paso2. La lección 3 convirtió las «verificaciones» en verificadores deterministas ejecutables que emiten aprobado/fallido, y también advirtió que los verificadores demasiado estrictos rechazan respuestas correctas1. La lección 4 se ocupó del texto de forma libre — rúbricas, formato de salida, y que el modelo que trabajó no debería calificarse a sí mismo2 5. La lección 5 resolvió «con cuántos casos verificar»: unas veinte tareas reales alcanzan para arrancar, no esperes a acumular cientos para empezar2. Esta lección soldó las primeras cinco en un archivo de trescientas líneas.
Ese archivo no es complejo, corre en menos de dos segundos, pero lo que cambia es concreto: desde hoy, cuando cambies una versión del prompt, no dependes de «leer unos párrafos de salida y sentir que está mejor» para juzgar — ejecutas un comando y la tabla de diferencias de v1 a v2 habla por ti, igual que esta vez, cuando t3 y t4 se pusieron en verde mientras las otras tres se quedaron sin cambio. La próxima vez que tu agente diga «listo», tienes dos comandos y un código de salida para verificar esa afirmación.
La próxima vez que tu agente diga «listo», tienes un circuito ejecutable para verificarlo.