540

Promptfoo para agent skills Medir si una skill se activa, qué hace y qué devuelve.

Una agent skill es un paquete de instrucciones que un agente como Claude Code carga cuando la petición encaja con su descripción. Este documento recoge qué ofrece promptfoo cuando lo que quieres medir es una skill: si se activa, qué hace por dentro y si su salida se mantiene.

Actualizado · Septiembre 2026
01

Qué es

Promptfoo es un ejecutor de tests para prompts y agentes. Declaras los casos en un fichero YAML, lanzas promptfoo eval y obtienes una tabla con el resultado de cada caso.

Corre en local. No hay cuenta ni servidor. Las claves de API son las del proveedor del modelo. La superficie mínima es Node, un YAML y un comando.

Tiene una guía dedicada a testear agent skills, con un tipo de assert propio para comprobar la activación. Eso es lo que recoge este documento.

02

Qué ofrece para skills

El provider del Agent SDK

Ejecuta el agente de verdad, con la skill cargada. Es la pieza que conecta promptfoo con tu directorio de skills.

  • working_dir: el directorio donde corre el agente.
  • setting_sources: ['project']: descubre las skills de .claude/skills/ por su cuenta.
  • skills: ['nombre']: limita la ejecución a una skill concreta.
  • output_format: fuerza un esquema JSON de salida.

Nota. Es el Agent SDK, no tu sesión de Claude Code. Lee las skills y los settings del proyecto, así que la medida se acerca mucho. Tus hooks, plugins y servidores MCP de usuario quedan fuera.

Activación: skill-used

Comprueba que la skill se disparó. Su pareja, not-skill-used, comprueba que no se disparó cuando no tocaba.

Aquí está el valor más directo cuando varias skills compiten por la misma petición. La documentación de promptfoo llama a estos casos boundary prompts: peticiones que deben ir a la skill hermana y no a la propia.

Nota. Para medir el enrutado no uses el filtro skills. Si limitas la ejecución a una sola skill, el caso de frontera no puede fallar y el test no mide nada.

Traza: la familia trajectory

Asevera sobre lo que el agente hizo, no solo sobre lo que devolvió.

  • trajectory:tool-used: llamó a esta herramienta.
  • trajectory:tool-args-match: la llamó con estos argumentos.
  • trajectory:tool-sequence: las llamó en este orden.
  • trajectory:step-count: dio como mucho N pasos de este tipo.
  • trajectory:goal-success: un juez valora si la traza cumple el objetivo.

Promptfoo trae su propio receptor y visor de trazas.

Asserts deterministas

Los baratos, sin modelo por medio: contains, not-contains, equals, is-json, regex, cost, latency. Y los abiertos, javascript y python, con acceso al output y al contexto del caso.

Para contenido esto cubre más de lo que parece. En 540 tenemos las reglas de voz de nuestros textos en un fichero (el Voice DNA), y buena parte de esas reglas son prohibiciones comprobables con una expresión regular.

Nota. Estos asserts corren gratis y no tienen varianza. Todo lo que se pueda comprobar aquí, no debe subir al juez.

Juez LLM

Para lo que no cabe en una regla. El general es llm-rubric: le das un criterio en texto y puntúa. La familia incluye select-best (compara variantes y elige), factuality, answer-relevance, g-eval y max-score.

El juez se fija para toda la suite o por assert, y threshold endurece el aprobado.

Nota. El juez tiene varianza. El mismo output puede sacar 0.7 y 0.85 en dos ejecuciones, así que sirve para detectar caídas grandes, no décimas. Y conviene que el juez no sea el mismo modelo que escribió el texto: un modelo reconoce sus propios patrones como correctos.

La no-determinación: --repeat N

Una ejecución de agente no repite resultado. --repeat N corre cada caso N veces y te da una tasa en vez de un pass o fail. Cada repetición es una sesión completa de agente, así que la tasa fiable se paga en tokens y en minutos.

03

Dos ejes: la skill y el modelo

Promptfoo cruza cada caso de test con cada provider declarado. Los casos se escriben una vez y salen tantas columnas comparables como providers pongas. De ahí vienen dos preguntas distintas con la misma suite.

Eje A: cambia la skill

Dos providers con el mismo modelo y distinto working_dir, así que necesitas las dos versiones de la skill en disco a la vez. La versión vieja contra la nueva, con todo lo demás fijo: la diferencia que veas es tuya.

Eje B: cambia el modelo

Dos providers con la misma skill y distinto model. Responde si la skill sobrevive a un cambio de modelo, y si baja de gama sin romperse.

Las cuatro columnas

providers:
  - id: anthropic:claude-agent-sdk
    label: v1 · opus5
    config:
      model: claude-opus-5
      working_dir: ./fixtures/skill-v1
      setting_sources: ['project']

  - id: anthropic:claude-agent-sdk
    label: v2 · opus5
    config:
      model: claude-opus-5
      working_dir: ./fixtures/skill-v2
      setting_sources: ['project']

  - id: anthropic:claude-agent-sdk
    label: v1 · opus4.8
    config:
      model: claude-opus-4-8
      working_dir: ./fixtures/skill-v1
      setting_sources: ['project']

  - id: anthropic:claude-agent-sdk
    label: v2 · opus4.8
    config:
      model: claude-opus-4-8
      working_dir: ./fixtures/skill-v2
      setting_sources: ['project']

# los tests van una sola vez, debajo

Qué te dice el 2×2

Los dos ejes por separado ya sirven. Juntos separan dos cosas que a mano se confunden.

La v2 gana en los dos modelos. La mejora está en la skill. Es el resultado que buscas.

La v2 solo gana en el modelo nuevo. Has escrito la skill contra un modelo concreto. Funciona, pero no es portable.

Las dos versiones caen al bajar de modelo. El problema no es tu redacción. Esa skill necesita el modelo de arriba, y eso es un dato de coste.

Ninguna columna se mueve. Los casos no discriminan. El eval no mide lo que crees que mide, y toca reescribir los casos.

Por qué importa. Cuando sale un modelo nuevo, no sabes si tu catálogo de skills sigue comportándose. Con más de una decena de skills, a mano no se comprueba. El eje B convierte esa pregunta en una ejecución.

El coste multiplica. Columnas por casos por --repeat. Cuatro columnas, cinco casos y tres repeticiones son sesenta sesiones de agente. Empieza con dos columnas y un solo eje.

04

Dónde queda skill-creator

En 540 escribimos las skills con skill-creator, la skill oficial de Anthropic para crear y evaluar skills, y también trae evals. Las dos herramientas atacan momentos distintos del ciclo de vida de una skill.

FASE 1 · DISEÑAR skill-creator escribir ejecutar humano itera hasta que convence visor lado a lado · benchmark · tasa de disparo la skill ya vale, y ahora hay que no romperla FASE 2 · VIGILAR promptfoo suite en YAML · corre sola · sin humano en el bucle falla cuando tocas la skill y algo se mueve JUZGA una persona, en un visor sirve para lo subjetivo JUZGA asserts en un fichero sirve para lo repetible
Las dos fases no compiten. Una acompaña a la persona que escribe la skill. La otra vigila la skill escrita.
skill-creator promptfoo
MomentoDiseñar y mejorar la skillVigilar que no se rompa
Quién juzgaUn subagente grader y una persona en un visor HTML, con feedback por casoAsserts en YAML: deterministas, traza y juez
Métricaspass_rate, tokens y duración, con media, desviación y delta entre iteracionespass y score, más cost y latency
Reproducible en CINo, es una sesión interactivaSí, es su razón de ser
EntornoTu Claude Code real, con tus hooks y settingsEl Agent SDK, con las skills y settings del proyecto
05

Qué validaríamos, con deslop

Tomo deslop, una skill de 540 que quita de un texto lo que suena a IA, como ejemplo porque sirve para las tres clases de assert. Su description ya trae bloques de usar cuando y no usar para, su flujo interno está fijado por pasos, y su catálogo de patrones de slop está enumerado.

Un solo fichero, cuatro preguntas y tres columnas. Los casos se escriben una vez y corren contra las tres.

1 · Las columnas contra las que corre todo

# la traza hace falta para los asserts de trajectory del bloque 3
tracing:
  enabled: true
  otlp:
    http:
      enabled: true

# sin el filtro `skills`, para que el enrutado pueda fallar de verdad
providers:
  - id: anthropic:claude-agent-sdk
    label: deslop v2 · opus5
    config:
      model: claude-opus-5
      working_dir: ./fixtures/toolkit-v2
      setting_sources: ['project']

  - id: anthropic:claude-agent-sdk
    label: deslop v1 · opus5       # eje A
    config:
      model: claude-opus-5
      working_dir: ./fixtures/toolkit-v1
      setting_sources: ['project']

  - id: anthropic:claude-agent-sdk
    label: deslop v2 · sonnet5     # eje B
    config:
      model: claude-sonnet-5
      working_dir: ./fixtures/toolkit-v2
      setting_sources: ['project']

2 · Se activa cuando toca

tests:
  - vars:
      task: "este README suena a IA, quítale el plástico"
    assert:
      - type: skill-used
        value: deslop

  # frontera: pide un borrador nuevo, y deslop dice que eso no es lo suyo
  - vars:
      task: "escríbeme un README para el repo de tokens"
    assert:
      - type: not-skill-used
        value: deslop

  # frontera difícil: roza el terreno de deslop y aun así no es lo suyo
  - vars:
      task: "acorta el cierre de este post y arregla el gancho"
    assert:
      - type: not-skill-used
        value: deslop

Los negativos difíciles son los que valen. Una petición obviamente ajena no prueba nada. La que roza el terreno de la skill y aun así no le toca es la que descubre que dos description se pisan. Y el caso de frontera solo asevera que deslop no se activó, no qué skill se activó en su lugar: un test, una razón para fallar.

3 · Hizo por dentro lo que dice hacer

tests:
  - vars:
      target: fixtures/borrador-con-slop.md
      task: "aplica deslop a fixtures/borrador-con-slop.md
        --voz .claude/rules/540-voice-dna.md"
    assert:
      # leyó la entrada antes de editarla, no reescribió a ciegas
      - type: trajectory:tool-sequence
        value:
          steps:
            - Read
            - Edit

      # el flag --voz no es decorativo: tiene que leer la muestra
      - type: trajectory:tool-args-match
        value:
          name: Read
          args:
            file_path: '*540-voice-dna.md'

      # edita in-place: no deja un hermano -deslopped.md por ahí
      - type: javascript
        value: |
          const fs = require('fs')
          return !fs.existsSync('fixtures/borrador-con-slop-deslopped.md')

4 · El resultado no trae lo que la skill promete quitar

# deslop edita el fichero, así que el assert lee el fichero, no el stdout
tests:
  - vars:
      target: fixtures/borrador-con-slop.md
      task: "aplica deslop a fixtures/borrador-con-slop.md"
    assert:
      - type: javascript
        value: |
          const t = require('fs').readFileSync(context.vars.target, 'utf8')
          const slop = [
            /no es solo .{2,40}, es /i,
            /(dicho esto|en este sentido|cabe destacar)/i,
            /\b(crucial|robusto|potente|verdaderamente)\b/i,
            /(en resumen|en definitiva|como hemos visto)/i,
          ]
          const quedan = slop.filter(r => r.test(t))
          return { pass: quedan.length === 0, score: 1 - quedan.length / slop.length }

5 · Lo que la regla no alcanza

# el juez lee el criterio de voz de 540, sin duplicarlo en el YAML
defaultTest:
  options:
    provider:
      id: anthropic:claude-opus-5
      config:
        temperature: 0

tests:
  - vars:
      target: fixtures/borrador-con-slop.md
      task: "aplica deslop a fixtures/borrador-con-slop.md"
    assert:
      - type: llm-rubric
        value: file://.claude/rules/540-voice-dna.md
        threshold: 0.8

      # edición quirúrgica: que no haya reescrito lo que ya funcionaba
      - type: llm-rubric
        value: "El texto conserva la estructura y los datos del original.
          Solo cambian las frases que sonaban a texto generado."
        threshold: 0.8
06

Cómo se prueba

Si ya tienes Node, el resto son dos comandos y un fichero.

npx promptfoo@latest init
npm i @anthropic-ai/claude-agent-sdk   # dependencia opcional del provider
npx promptfoo@latest eval
npx promptfoo@latest view              # tabla de resultados en local

El paquete del Agent SDK obliga a un package.json. Metiéndolo en un directorio propio, por ejemplo evals/, no toca el resto del repo.

Para integrarlo después: promptfoo eval -o resultado.json --no-cache devuelve código de salida, y hay acción de GitHub.

07

Documentación