Evaluar la calidad de un skill con evals
Evaluar la calidad de un skill con evals
Escribiste un skill, lo probaste con un prompt y pareció funcionar. Ese es el punto de partida de la guía oficial de evaluación, y también el problema: “pareció funcionar” no responde ninguna de las preguntas que importan. ¿Funciona de forma fiable? ¿Con prompts variados? ¿En casos límite? ¿Mejor que no tener skill alguno?
Antes de entrar, una aclaración de autoridad que arrastramos desde
la especificación al detalle:
nada de lo que sigue es un requisito del formato. La especificación no define
evals, ni el directorio evals/, ni el archivo evals.json. Son guías de autoría
publicadas junto al estándar. Los únicos límites duros siguen siendo los de name
(64 caracteres), description (1024) y compatibility (500).
Por qué el ojímetro no basta
Un skill es texto que modifica el comportamiento de un modelo no determinista. Eso tiene tres consecuencias incómodas:
- Una sola corrida no es evidencia. El mismo prompt puede producir una salida excelente una vez y una mediocre a continuación.
- No sabes si el mérito es del skill. Sin línea base, estás atribuyendo al skill un resultado que el agente ya conseguía solo. Es la advertencia de las buenas prácticas: si el agente ya hace toda la tarea bien sin el skill, el skill quizá no aporta valor.
- El costo es invisible sin medirlo. Un skill que mejora la calidad pero triplica el consumo de tokens es un trade-off distinto de uno que mejora y además es más barato.
La iteración guiada por evals ataca las tres: repite, compara contra baseline y registra tiempo y tokens.
Dos preguntas distintas, dos baterías distintas
Un skill puede fallar de dos maneras independientes, y cada una se mide con un instrumento distinto. Confundirlas es el error más común al empezar.
| Pregunta | Qué mide | Dónde se ataca | Capítulo |
|---|---|---|---|
| ¿Se dispara cuando debe? | Que el agente cargue el SKILL.md ante la tarea correcta y no ante una parecida | La description del frontmatter | Optimizar la description |
| ¿Produce la salida correcta? | Que, una vez cargado, el skill haga que la tarea termine bien | El cuerpo Markdown, los scripts y los recursos | Este capítulo |
Son dos bucles de optimización separados, con artefactos distintos:
flowchart TD
A["Skill en desarrollo"] --> B["Bucle 1 · disparo<br/>eval_queries.json con should_trigger"]
A --> C["Bucle 2 · calidad de salida<br/>evals/evals.json con assertions"]
B --> D["Se itera SOLO la description<br/>métrica: trigger rate"]
C --> E["Se itera cuerpo, scripts y referencias<br/>métrica: pass rate contra baseline"]
D --> F["Skill listo"]
E --> F
Recordatorio del bucle 1 para no repetir el capítulo 6: unas 20 queries, 8-10 positivas y 8-10 negativas, cada una corrida varias veces (3 es un punto de partida razonable), umbral de trigger rate en 0.5, split train (~60%) / validation (~40%) fijo entre iteraciones, y selección de la mejor iteración por su validation pass rate, no por ser la última. El resto del capítulo es el bucle 2.
Diseñar los casos de prueba
Un caso de prueba tiene tres partes:
- Prompt: un mensaje de usuario realista, del tipo que alguien escribiría de verdad.
- Expected output: una descripción legible por humanos de cómo se ve el éxito.
- Input files (opcional): archivos con los que el skill tiene que trabajar.
Los casos de prueba viven en evals/evals.json dentro del directorio del skill:
{
"skill_name": "csv-analyzer",
"evals": [
{
"id": 1,
"prompt": "I have a CSV of monthly sales data in data/sales_2025.csv. Can you find the top 3 months by revenue and make a bar chart?",
"expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",
"files": ["evals/files/sales_2025.csv"]
},
{
"id": 2,
"prompt": "there's a csv in my downloads called customers.csv, some rows have missing emails — can you clean it up and tell me how many were missing?",
"expected_output": "A cleaned CSV with missing emails handled, plus a count of how many were missing.",
"files": ["evals/files/customers.csv"]
}
]
}
Cuatro consejos de la guía para escribir buenos prompts de prueba:
- Empieza con 2 o 3 casos. No sobreinviertas antes de ver la primera ronda de resultados. El set se amplía después.
- Varía los prompts. Distintas formulaciones, niveles de detalle y formalidad: unos casuales (“hey can you clean up this csv”), otros precisos (“Parse the CSV at data/input.csv, drop rows where column B is null, and write the result to data/output.csv”).
- Cubre casos límite. Al menos un prompt que pruebe una condición de frontera: una entrada malformada, una petición inusual, o un caso donde las instrucciones del skill puedan resultar ambiguas.
- Usa contexto realista. Los usuarios reales mencionan rutas de archivo, nombres de columna y contexto personal. Prompts como “process this data” son demasiado vagos para probar nada útil.
Y una regla de orden: todavía no definas checks de pass/fail. Las assertions se escriben después de ver qué produce la primera corrida.
Ejecutar las evals: con skill y sin skill
El patrón central es correr cada caso dos veces: una con el skill y otra sin él (o con una versión anterior). La corrida sin skill es la línea base contra la que se compara todo.
Estructura del workspace
Los resultados van a un directorio de workspace hermano del directorio del skill. Cada
pasada completa por el bucle tiene su propio iteration-N/, y dentro cada caso de
prueba tiene un directorio con subdirectorios with_skill/ y without_skill/:
csv-analyzer/
- SKILL.md
- evals/
- evals.json
csv-analyzer-workspace/
- iteration-1/
- eval-top-months-chart/
- with_skill/
- outputs/ # Files produced by the run
- timing.json # Tokens and duration
- grading.json # Assertion results
- without_skill/
- outputs/
- timing.json
- grading.json
- eval-clean-missing-emails/
- ... # Misma estructura que el anterior
- benchmark.json # Aggregated statistics
El único archivo que escribes a mano es evals/evals.json. Los demás
(grading.json, timing.json, benchmark.json) se producen durante el proceso: por
el agente, por scripts o por ti. Y fíjate en que el workspace vive fuera del
directorio del skill: los resultados no se empaquetan con el skill, que solo lleva
evals/ con el JSON de casos y sus archivos de entrada.
Aislamiento del contexto
La guía pide que cada corrida de eval empiece con un contexto limpio — es una
recomendación de autoría, no un requisito del formato — sin estado residual de
corridas anteriores ni del propio proceso de
desarrollo del skill: así el agente sigue únicamente lo que el SKILL.md le dice, y no
lo que le explicaste en la conversación donde lo escribiste. En entornos con subagentes
el aislamiento es natural, porque cada tarea hija arranca limpia; sin subagentes, se usa
una sesión separada por corrida.
Para cada corrida hay que proveer cuatro cosas: la ruta del skill (o ninguna, para la baseline), el prompt de prueba, los archivos de entrada y el directorio de salida. Así se ve la instrucción para una corrida con skill:
Execute this task:
- Skill path: /path/to/csv-analyzer
- Task: I have a CSV of monthly sales data in data/sales_2025.csv.
Can you find the top 3 months by revenue and make a bar chart?
- Input files: evals/files/sales_2025.csv
- Save outputs to: csv-analyzer-workspace/iteration-1/eval-top-months-chart/with_skill/outputs/
Para la baseline, el mismo prompt sin la ruta del skill, guardando en
without_skill/outputs/.
Cuando ya existe una versión anterior
Al mejorar un skill que ya está en uso, la baseline no es “sin skill”: es la versión
anterior. Se toma un snapshot antes de editar (cp -r <skill-path> <workspace>/skill-snapshot/), se apunta la corrida base al snapshot y se guarda en
old_skill/outputs/ en lugar de without_skill/.
Registrar tiempo y tokens
Los datos de tiempo permiten comparar cuánto cuesta el skill respecto a la baseline. Al terminar cada corrida se registra el conteo de tokens y la duración:
{ "total_tokens": 84852, "duration_ms": 23332 }
De dónde salen esos números depende del cliente. En clientes con subagentes, la
notificación de finalización de tarea suele incluir total_tokens y duration_ms;
conviene guardarlos de inmediato, porque muchas veces no quedan persistidos en ningún
otro sitio.
Escribir assertions
Las assertions son afirmaciones verificables sobre lo que la salida debe contener o lograr. Se añaden después de ver la primera ronda de salidas.
| Assertion | Veredicto | Por qué |
|---|---|---|
The output file is valid JSON | Buena | Verificable programáticamente |
The bar chart has labeled axes | Buena | Específica y observable |
The report includes at least 3 recommendations | Buena | Contable |
The output is good | Débil | Demasiado vaga para calificar |
The output uses exactly the phrase 'Total Revenue: $X' | Débil | Demasiado frágil: una salida correcta con otra redacción fallaría |
No todo necesita una assertion. Algunas cualidades — el estilo de escritura, el diseño visual, si la salida “se siente bien” — son difíciles de descomponer en checks de pass/fail. Esas se cazan en la revisión humana.
Las assertions se agregan como una clave más de cada caso en evals/evals.json, junto
a prompt, expected_output y files:
{
"id": 1,
"prompt": "I have a CSV of monthly sales data in data/sales_2025.csv. Can you find the top 3 months by revenue and make a bar chart?",
"expected_output": "A bar chart image showing the top 3 months by revenue, with labeled axes and values.",
"files": ["evals/files/sales_2025.csv"],
"assertions": [
"The output includes a bar chart image file",
"The chart shows exactly 3 months",
"Both axes are labeled",
"The chart title or caption mentions revenue"
]
}
Calificar las salidas
Calificar (grading) significa evaluar cada assertion contra las salidas reales y registrar PASS o FAIL con evidencia específica. La evidencia debe citar o referenciar la salida, no limitarse a expresar una opinión.
El enfoque más simple es entregar salidas y assertions a un LLM y pedirle que evalúe cada una. Para las que se pueden comprobar con código (JSON válido, número de filas correcto, archivo con las dimensiones esperadas) conviene un script de verificación: los scripts son más fiables que el juicio de un LLM para checks mecánicos, y reutilizables entre iteraciones.
{
"assertion_results": [
{
"text": "The output includes a bar chart image file",
"passed": true,
"evidence": "Found chart.png (45KB) in outputs directory"
},
{
"text": "Both axes are labeled",
"passed": false,
"evidence": "Y-axis is labeled 'Revenue ($)' but X-axis has no label"
},
{
"text": "The chart title or caption mentions revenue",
"passed": true,
"evidence": "Chart title reads 'Top 3 Months by Revenue'"
}
],
"summary": { "passed": 2, "failed": 1, "total": 3, "pass_rate": 0.67 }
}
Dos principios de calificación
- Exige evidencia concreta para un PASS. No des el beneficio de la duda. Si una assertion dice “incluye un resumen” y la salida tiene una sección titulada “Summary” con una sola frase vaga, eso es FAIL: la etiqueta está, la sustancia no.
- Revisa las assertions, no solo los resultados. Mientras calificas, detecta las demasiado fáciles (pasan siempre, dé igual la calidad del skill), las demasiado difíciles (fallan siempre aunque la salida sea buena) y las no verificables desde la salida. Corrígelas para la siguiente iteración.
Comparación ciega entre versiones
Para comparar dos versiones de un skill existe una técnica complementaria: la comparación ciega. Se presentan ambas salidas a un LLM juez sin revelar cuál viene de qué versión, y el juez puntúa cualidades holísticas — organización, formato, usabilidad, pulido — con su propia rúbrica, libre de sesgo sobre cuál “debería” ser mejor. Complementa al grading: dos salidas pueden pasar todas las assertions y aun así diferir mucho en calidad global.
Agregar los resultados
Una vez calificadas todas las corridas de la iteración, se calculan estadísticas por
configuración y se guardan en benchmark.json, junto a los directorios de eval:
{
"run_summary": {
"with_skill": {
"pass_rate": { "mean": 0.83, "stddev": 0.06 },
"time_seconds": { "mean": 45.0, "stddev": 12.0 },
"tokens": { "mean": 3800, "stddev": 400 }
},
"without_skill": {
"pass_rate": { "mean": 0.33, "stddev": 0.10 },
"time_seconds": { "mean": 32.0, "stddev": 8.0 },
"tokens": { "mean": 2100, "stddev": 300 }
},
"delta": { "pass_rate": 0.50, "time_seconds": 13.0, "tokens": 1700 }
}
}
El bloque delta es la lectura clave: dice qué cuesta el skill (más tiempo, más
tokens) y qué compra (mayor pass rate). Un skill que suma 13 segundos y mejora el
pass rate en 50 puntos porcentuales probablemente vale la pena. Uno que duplica el
consumo de tokens para una mejora de 2 puntos, probablemente no.
Advertencia estadística: stddev solo es significativa con varias corridas por eval. En
las primeras iteraciones, con 2 o 3 casos y una corrida cada uno, mira los conteos
crudos de PASS y el delta.
Analizar patrones
Las estadísticas agregadas pueden esconder patrones importantes. Después de calcular el benchmark, cinco acciones concretas:
| Patrón observado | Qué significa | Acción |
|---|---|---|
| La assertion siempre pasa en ambas configuraciones | El modelo la resuelve sin el skill | Eliminar o reemplazar: infla el pass rate con skill sin reflejar valor real |
| La assertion siempre falla en ambas | O está rota, o el caso es demasiado difícil, o comprueba lo equivocado | Investigar y arreglar antes de la siguiente iteración |
| Pasa con skill, falla sin él | Aquí está el valor del skill | Entender por qué: qué instrucción o script marcó la diferencia |
| Resultados inconsistentes entre corridas (stddev alto) | Eval flaky, o instrucciones ambiguas que el modelo interpreta distinto cada vez | Endurecer las instrucciones: añadir ejemplos o guía más específica |
| Outliers de tiempo o tokens | Algo se atascó en esa corrida | Leer la transcripción de ejecución para encontrar el cuello de botella |
La cuarta fila responde a la pregunta de estabilidad. Un skill que pasa el 100% de las assertions en una corrida y el 40% en la siguiente no es un skill del 100%: es un skill inestable, y la causa casi nunca es el azar puro sino una instrucción que admite dos lecturas. La solución no es repetir hasta que salga bien, es desambiguar el texto.
Revisar con un humano
El grading por assertions solo comprueba aquello para lo que se te ocurrió escribir una
assertion. Un revisor humano aporta perspectiva fresca: detecta problemas que no
anticipaste, nota cuándo la salida es técnicamente correcta pero no da en el clavo, y ve
cosas difíciles de expresar como pass/fail. El feedback se registra por caso, en un
feedback.json del workspace:
{
"eval-top-months-chart": "The chart is missing axis labels and the months are in alphabetical order instead of chronological.",
"eval-clean-missing-emails": ""
}
“The chart is missing axis labels” es accionable; “looks bad” no lo es. Un feedback vacío significa que la salida se veía bien. En el paso de iteración, el esfuerzo se concentra en los casos donde hubo quejas concretas.
Iterar sobre las instrucciones
Después de calificar y revisar tienes tres fuentes de señal, y cada una apunta a un tipo distinto de problema:
flowchart TD
A["Assertions fallidas"] --> D["Propuesta de cambios al SKILL.md"]
B["Feedback humano"] --> D
C["Transcripciones de ejecución"] --> D
A -.-> A2["Huecos específicos:<br/>paso ausente · instrucción poco clara<br/>caso no cubierto"]
B -.-> B2["Problemas amplios de calidad:<br/>enfoque erróneo · mala estructura<br/>correcto pero inútil"]
C -.-> C2["El porqué:<br/>instrucción ignorada = ambigua<br/>tiempo perdido = simplificar o quitar"]
La forma más efectiva de convertirlas en mejoras es dárselas las tres — junto con el
SKILL.md actual — a un LLM y pedirle que proponga cambios: sintetiza patrones entre
assertions fallidas, quejas del revisor y comportamiento en las transcripciones que
sería tedioso conectar a mano. La guía nombra cuatro criterios para ese prompt:
- Generalizar desde el feedback. El skill se usará con muchos prompts distintos, no solo con tus casos. Las correcciones deben atacar la causa de fondo en general, no añadir parches estrechos para ejemplos concretos.
- Mantener el skill delgado. Si las transcripciones muestran trabajo desperdiciado (validaciones innecesarias, salidas intermedias que nadie usa), esas instrucciones se quitan. Y si el pass rate se estanca a pesar de añadir más reglas, es probable que el skill esté sobre-restringido: prueba a quitar instrucciones y mira si los resultados se sostienen o mejoran.
- Explicar el porqué. Las instrucciones razonadas (“Do X because Y tends to cause Z”) funcionan mejor que las directivas rígidas (“ALWAYS do X, NEVER do Y”): los modelos las siguen de forma más fiable cuando entienden el propósito. Es el principio de calibración de Escribir buenas instrucciones.
- Empaquetar el trabajo repetido. Si en cada corrida el agente escribió por su
cuenta un helper parecido, esa es la señal para empaquetarlo en
scripts/, como vimos en Scripts y comandos dentro de un skill.
El bucle
flowchart LR
A["1 · Proponer mejoras<br/>señales + SKILL.md → LLM"] --> B["2 · Revisar y aplicar<br/>los cambios"]
B --> C["3 · Re-ejecutar todos los casos<br/>en iteration-N+1/"]
C --> D["4 · Calificar y agregar<br/>grading.json → benchmark.json"]
D --> E["5 · Revisar con un humano<br/>feedback.json"]
E --> A
Una batería concreta, de principio a fin
Supongamos un skill release-notes que redacta notas de versión a partir del log de
git. Su release-notes/evals/evals.json en la primera iteración, con solo tres casos y
sin assertions todavía:
{
"skill_name": "release-notes",
"evals": [
{
"id": 1,
"prompt": "Necesito las notas de la v2.4.0. El rango es v2.3.0..HEAD en el repo que tienes abierto, y el equipo de soporte las lee sin contexto técnico.",
"expected_output": "Un Markdown con secciones por tipo de cambio, cada entrada en lenguaje de usuario y con el número de PR enlazado.",
"files": ["evals/files/git-log-v2.4.0.txt"]
},
{
"id": 2,
"prompt": "oye puedes sacarme el changelog de esta semana? hay como 4 commits nada más",
"expected_output": "Un Markdown breve, sin secciones vacías, que no invente cambios que no estén en el log.",
"files": ["evals/files/git-log-semana.txt"]
},
{
"id": 3,
"prompt": "Genera las notas de la v3.0.0. Ojo que hay un breaking change en la API de autenticación y dos commits con mensaje fix stuff.",
"expected_output": "Un Markdown que destaque el breaking change al inicio y que marque los commits sin mensaje útil como pendientes de aclarar, en vez de inventarles una descripción.",
"files": ["evals/files/git-log-v3.0.0.txt"]
}
]
}
El caso 3 es el caso límite: entrada ambigua a propósito, para ver si el skill inventa o si declara la ambigüedad.
Tras la primera ronda de corridas se leen las salidas y recién ahí se escriben las assertions del caso 3:
{
"id": 3,
"assertions": [
"La salida es Markdown válido con al menos un encabezado de nivel 2",
"El breaking change de autenticación aparece antes que cualquier otra sección",
"Los dos commits con mensaje 'fix stuff' aparecen marcados como pendientes de aclarar",
"Ninguna entrada describe un cambio que no exista en el log de entrada",
"Cada entrada referencia el hash corto o el número de PR de su commit"
]
}
La ejecución de la iteración 1 prepara los directorios y lanza las seis corridas — tres casos por dos configuraciones — cada una en contexto limpio:
mkdir -p release-notes-workspace/iteration-1/eval-v3-breaking/{with_skill,without_skill}/outputs
# Corrida con skill: skill path ./release-notes, salidas en with_skill/outputs/
# Corrida baseline: sin skill path, salidas en without_skill/outputs/
Resultado agregado de esa primera iteración, en el run_summary del benchmark.json:
| Configuración | pass_rate (mean/stddev) | time_seconds | tokens |
|---|---|---|---|
with_skill | 0.73 / 0.21 | 51.0 | 5200 |
without_skill | 0.40 / 0.12 | 28.0 | 2400 |
delta | +0.33 | +23.0 | +2800 |
Lectura del benchmark, aplicando el análisis de patrones:
deltade pass rate +0.33: el skill aporta, pero cuesta 2800 tokens y 23 segundos extra por corrida. Aceptable para algo que se hace una vez por release; discutible para algo que se hace cien veces al día.stddevde 0.21 con skill: hay inestabilidad. La assertion del breaking change pasa unas veces y otras no. Diagnóstico: elSKILL.mddecía “destaca los breaking changes” sin decir dónde. Corrección: “Coloca los breaking changes en una sección## Breaking changesal inicio del documento, porque el lector decide si actualizar en los primeros diez segundos”.- “La salida es Markdown válido con al menos un encabezado de nivel 2” pasó en las seis corridas, con y sin skill. Se elimina: no discrimina y solo infla el pass rate.
- La assertion de los hashes falló en ambas configuraciones las tres veces. El log de ejemplo no incluía hashes: la assertion estaba rota, no el skill.
- En dos transcripciones el agente escribió por su cuenta un parser de
git logcasi idéntico. Señal de empaquetado: ese parser pasa ascripts/parse-log.py.
La iteración 2 aplica esos cinco cambios y vuelve a correr los tres casos en
release-notes-workspace/iteration-2/.
Cuándo un skill está listo
La guía da un criterio de parada con tres condiciones, y basta con una: estás satisfecho con los resultados, el feedback humano viene consistentemente vacío, o ya no ves mejora significativa entre iteraciones. En la práctica conviene traducirlo a una lista comprobable antes de distribuir el skill:
- El bucle de disparo pasa sobre el validation set y sobre queries frescas.
- El
deltade pass rate frente a la baseline es claramente positivo. Si es cero, el agente ya hacía la tarea solo y el skill no aporta. - El costo del
deltaen tiempo y tokens es aceptable para la frecuencia real de uso de esa tarea. - El
stddevdel pass rate es bajo: el skill se comporta igual entre corridas. - No quedan assertions que pasen siempre ni que fallen siempre en ambas configuraciones.
- El caso límite pasa, o falla de forma explícita y declarada en vez de inventar.
- Las últimas dos iteraciones no movieron la aguja.
-
skills-ref validate ./release-notespasa sin errores.
Un matiz sobre la última: skills-ref validate comprueba que el frontmatter cumple la
especificación; las evals comprueban que el skill sirve. Un skill puede pasar la
validación y ser inútil, y puede ser excelente y no pasarla por un guion de más en el
name. Hacen falta las dos.
Cuando el skill supera esta lista, está listo para distribuirse. Si vas a empaquetarlo junto a servidores MCP y otros componentes, el curso hermano Agent Plugins Spec cubre ese formato; y para estudiar baterías de skills ya maduras, el catálogo del curso Google Skills tiene más de cien ejemplos reales.
Resumen
- El estándar no define evals:
evals/evals.json, el workspace y elbenchmark.jsonson guía de autoría publicada junto a la especificación, no requisitos del formato. - Hay dos bucles independientes: el de disparo (se itera la
description, métrica = trigger rate) y el de calidad de salida (se itera el cuerpo, los scripts y las referencias, métrica = pass rate contra baseline). - Un caso de prueba tiene prompt realista, expected output legible y, opcionalmente, archivos de entrada. Empieza con 2 o 3, varía formulación y detalle, incluye un caso límite.
- El patrón central es correr cada caso dos veces: con skill y sin skill (o contra la versión anterior), cada corrida en contexto limpio. Sin baseline no sabes si el mérito es del skill.
- Las assertions se escriben después de la primera ronda, son verificables y específicas, y no todo necesita una: el estilo y la sensación se dejan a la revisión humana. El grading exige PASS con evidencia concreta que cite la salida.
- El
deltadelbenchmark.jsondice qué cuesta el skill (tiempo, tokens) y qué compra (pass rate).stddevalto = inestabilidad = instrucciones ambiguas. - Al iterar: generaliza, mantén el skill delgado, explica el porqué y empaqueta el
trabajo repetido en
scripts/. Se para cuando estás satisfecho, el feedback viene vacío o las iteraciones dejan de mover la aguja.
Siguiente: Distribución: dónde viven los skills y cómo se instalan