====== 21. Evaluación de skills ======
Una //skill// se prueba como el código: primero que **está bien hecha** (los checks) y después que **funciona** (los //evals//), repitiendo cada prueba varias veces porque el modelo no responde siempre igual. Cómo se construye la //skill// está en [[cursos:sdd:19-creando-skills|Estructura de un skill]].
Más información:
* [[https://github.com/anthropics/skills/tree/main/skills/skill-creator|skill-creator]]: la //skill// de Anthropic para crear y evaluar //skills//.
* [[https://www.anthropic.com/research/statistical-approach-to-model-evals|A statistical approach to model evaluations]]: por qué un resultado de //eval// sin su intervalo de confianza no dice nada.
* [[https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval|Wilson score interval]]: el intervalo que se usa aquí para la tasa de aprobado.
----
===== 1. Los checks: la forma =====
Antes de probar si una //skill// funciona, se comprueba que **está bien hecha**: una lista de comprobaciones que tiene que pasar entera antes de darla por terminada. Casi todo lo del [[cursos:sdd:19-creando-skills|tema 20]] se puede comprobar con un script, sin gastar tokens:
* La carpeta y el H1 se llaman igual que el ''name'', y hay un solo ''SKILL.md''.
* La ''description'' existe y no pasa de 1.024 caracteres.
* La tabla índice está antes de cualquier otra sección y no hay huérfanos ni rutas a ficheros que no existen.
* Nombres en //kebab-case//, sin repetir la carpeta y sin carpetas fuera de la tabla.
* Todo ''.md'' de más de 100 líneas tiene índice; ninguno pasa de 2.500 palabras.
* Las de procedimiento tienen ''User Input'', ''Outline'', ''STOP conditions'' y ''Quick Guidelines'' con 10 puntos como máximo.
Lo demás (que la ''description'' diga el cuándo, que no haya teoría que el modelo ya sabe, que cada ❌ lleve su porqué) lo revisa un modelo o una persona.
Esa lista y su script se guardan en una //skill// de conocimiento, ''k-skill'', con todas las reglas del tema 20. Así el agente las aplica al crear o revisar cualquier //skill// del proyecto.
===== 2. evals/: el comportamiento =====
Una batería de casos que se repite **al editar la //skill// y al cambiar de modelo**. Cada caso responde a dos preguntas:
* **¿Se activa?** Que se cargue solo dice que el modelo la encontró, no que la siguiera.
* **¿Aporta?** El caso se ejecuta **con la //skill// y sin ella**, y la diferencia de nota es lo que aporta. Si sin ella también aprueba, sobra para ese caso.
.claude/skills/k-servicios/evals/
├── update-sin-filtrar/ <- un caso, con el nombre de lo que prueba
│ ├── prompt.md <- lo que escribiría el usuario
│ ├── case.yaml <- opcional: dice qué script prepara los ficheros
│ ├── fixture.sh <- opcional: crea los ficheros de partida
│ └── graders/
│ ├── filtra-campos.md <- un evaluador por fichero
│ └── skill-activada.md
└── results/ <- las notas de cada evaluación; va en .gitignore
==== El caso ====
* **''prompt.md''**: lo que pediría un usuario, **sin nombrar la //skill//**: si la nombra, se prueba que obedece, no que se activa. En el frontmatter, cuántas vueltas puede dar el agente (''max_turns'') y cuántas veces se ejecuta el caso (''runs'').
---
max_turns: 30
runs: 10
---
Añade a PedidoServiceImpl un método update que permita cambiar la dirección y el teléfono del pedido.
* **''fixture.sh''**: deja los ficheros mínimos de partida, aquí un ''PedidoServiceImpl.java'' sin ''update''. Lo enlaza ''case.yaml'':
schema_version: "1.1"
name: update-sin-filtrar
context:
scaffold_script: fixture.sh
==== Los evaluadores ====
En ''graders/'', un evaluador por fichero, con su ''type'' en el frontmatter. Sobre el resultado, con un modelo que hace de juez:
---
type: llm
focus: { source: file, path: PedidoServiceImpl.java }
---
PASS si el update solo deja cambiar la dirección y el teléfono.
FAIL si copia sobre la entidad todo lo que recibe, o si el cliente puede cambiar el estado o el importe.
Y sobre el proceso, con un script:
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"k-servicios"'
---
Los tipos de evaluador:
^ Tipo ^ Aprueba cuando ^ Lo aplica ^
| ''regex'' | Un patrón aparece (o no) en la respuesta o en un fichero | Un script |
| ''tool_used'' | Se llamó a una herramienta, con ''min'' y ''max'' veces | Un script |
| ''tool_order'' | Una herramienta se llamó antes que otra | Un script |
| ''file_exists'' | Se creó un fichero que cumple el patrón | Un script |
| ''llm'' | Un juez vota PASS en dos de tres votos | Un modelo juez |
Cómo se hacen bien:
* **Al menos 3 casos**, sacados de **fallos reales** del modelo sin la //skill//, y **uno negativo**: un prompt parecido en el que la //skill// no debe activarse.
* **Al menos 2 evaluadores por caso**: uno sobre el resultado y otro sobre el proceso.
* **Deterministas primero**: ''regex'' sobre ficheros largos; ''llm'' solo para salidas cortas y con un PASS y un FAIL concretos.
* **El juez, de otro modelo** que el evaluado: un modelo tiende a aprobar lo suyo.
**''skill-activada'' no cuenta en la nota.** Sin la //skill// siempre suspende, así que inflaría la diferencia. Claude Code lo sabe: un ''tool_used'' sobre ''Skill'' solo dice si se activó.
**El modelo evaluado no puede leer ''evals/''.** Si ve los criterios, aprueba por imitación. Ni el prompt ni ''fixture.sh'' apuntan a nada de ''evals/''.
==== Ejecutarlos ====
En Claude Code, con ''claude plugin eval'' y la carpeta de la //skill//. Por defecto ejecuta cada caso con la //skill// y sin ella, y da la diferencia:
claude plugin eval .claude/skills/k-servicios --scaffold --runs 10 --judge-model haiku
* ''--scaffold'': ejecuta el ''fixture.sh'' de cada caso. Sin él, el caso empieza sin ficheros.
* ''--runs 10'': veces que se ejecuta cada caso (por defecto, 3; ver la sección siguiente).
* ''--judge-model haiku'': el modelo de los evaluadores ''llm''.
* ''--case update-sin-filtrar'': solo ese caso.
Al terminar saca una tabla por caso con su nota y su ''PASS%'', y deja el detalle en ''evals/results//''.
----
===== 3. Repetir la prueba: intervalos de confianza =====
El modelo no responde siempre igual, así que cada caso se ejecuta $n$ veces y lo que se mide es la **tasa de aprobado**: $k$ aprobados de $n$. Esa tasa es una estimación de la de verdad, y con pocas ejecuciones, muy imprecisa. Lo que dice cuánto te puedes fiar es su **intervalo de confianza al 95 %**: el rango en el que está la tasa real.
**3 de 3 no es un 100 %.** Con 3 ejecuciones, todo aprobado solo dice que la tasa real está entre el 44 % y el 100 %: la //skill// podría fallar más de la mitad de las veces. Y 3 es lo que hace ''claude plugin eval'' por defecto.
==== El intervalo de Wilson ====
Con $\hat p = k/n$ y $z = 1{,}96$ para el 95 %:
$$\frac{\hat p + \frac{z^2}{2n} \pm z\sqrt{\frac{\hat p(1-\hat p)}{n} + \frac{z^2}{4n^2}}}{1 + \frac{z^2}{n}}$$
No uses la fórmula de clase de estadística, $\hat p \pm z\sqrt{\hat p(1-\hat p)/n}$: con todo aprobado o todo suspendido da un intervalo de anchura cero, justo en los casos más frecuentes en un //eval//.
Con este script se calcula a partir de lo que da ''claude plugin eval'':
import sys
from math import sqrt
def wilson(k, n, z=1.96):
p = k / n
centro = (p + z*z/(2*n)) / (1 + z*z/n)
margen = z * sqrt(p*(1-p)/n + z*z/(4*n*n)) / (1 + z*z/n)
return centro - margen, centro + margen
k, n = int(sys.argv[1]), int(sys.argv[2])
bajo, alto = wilson(k, n)
print(f"{k}/{n}: {bajo:.0%} – {alto:.0%}")
python3 intervalo.py 9 10
9/10: 60% – 98%
Lo que da según el número de ejecuciones:
^ Resultado ^ Intervalo al 95 % ^
| 3/3 | 44 % – 100 % |
| 5/5 | 57 % – 100 % |
| 10/10 | 72 % – 100 % |
| 20/20 | 84 % – 100 % |
| 9/10 | 60 % – 98 % |
| 5/10 | 24 % – 76 % |
==== ¿Aporta la skill? Que no se solapen ====
La //skill// aporta en un caso cuando el intervalo **con** ella y el intervalo **sin** ella **no se solapan**. Si se solapan, la diferencia puede ser suerte:
^ Ejecuciones ^ Con la //skill// ^ Sin la //skill// ^ ¿Aporta? ^
| 10 | 9/10 (60 % – 98 %) | 5/10 (24 % – 76 %) | No se sabe: se solapan |
| 30 | 27/30 (74 % – 97 %) | 15/30 (33 % – 67 %) | Sí |
Cuántas veces ejecutar:
* **10 por caso** para empezar: ''--runs 10'' o ''runs: 10'' en el ''prompt.md''.
* **30 en los casos que se solapan**, solo esos, con ''--case'':
claude plugin eval .claude/skills/k-servicios --scaffold --runs 30 --case update-sin-filtrar
Si con 30 ejecuciones los intervalos siguen solapándose, la //skill// no aporta en ese caso, o aporta tan poco que no compensa los tokens que añade.
----
===== Ejercicios =====
==== Ejercicio 1 ====
Crea la carpeta ''evals/'' de la //skill// ''do-crear-test'' del [[cursos:sdd:19-creando-skills#ejercicio_3|ejercicio 3 del tema 20]] con tres casos: dos que salgan de fallos que hayas visto al pedir tests sin la //skill// y uno negativo. Rellena la tabla:
^ Caso ^ Prompt ^ Evaluador del resultado ^ Evaluador del proceso ^
| | | | |
| | | | |
| | | | |
==== Ejercicio 2 ====
Calcula el intervalo de confianza al 95 % de cada resultado y di si la //skill// aporta en cada caso:
^ Caso ^ Con la //skill// ^ Intervalo ^ Sin la //skill// ^ Intervalo ^ ¿Aporta? ^
| ''test-con-id'' | 10/10 | | 6/10 | | |
| ''test-sin-mock'' | 8/10 | | 7/10 | | |
| ''test-negativo'' | 3/3 | | 3/3 | | |
==== Ejercicio 3 ====
Ejecuta los casos del ejercicio 1 con ''--runs 10''. Rellena la tabla y repite con ''--runs 30'' los casos en los que los intervalos se solapen:
^ Caso ^ Ejecuciones ^ Con la //skill// ^ Intervalo ^ Sin la //skill// ^ Intervalo ^ ¿Aporta? ^
| | | | | | | |
| | | | | | | |
| | | | | | | |