← 返回学习中心 · Skills 与决策OiOi Academy
SKILLS 生态 · 04

用 Evals 系统化评测 Agent Skills

OpenAI 官方指南全文中文讲解:从“感觉变好了”到“拿出证据”——给 Codex Skill 建一条可复现的评测流水线。

来源 OpenAI Developers 官方博客 原文 developers.openai.com/blog/eval-skills 对象 Codex Skill
🤔

开篇:你怎么知道 Skill 真的变好了?

当你为 Codex 这类 Agent 迭代一个 Skill 时,很难判断自己究竟是真正改进了它,还是仅仅改变了它的行为:某个版本感觉更快,另一个版本似乎更可靠,然后回归问题就悄悄出现了——Skill 没有被触发、跳过了某个必需步骤,或者留下了多余的文件。

从本质上讲,Skill 是面向 LLM 的一组有组织的提示词和指令集合。改进 Skill 最可靠的方式,就是像评估 LLM 应用中的任何其他提示词一样去评估它。

高中生类比
Eval(评估)就像给 Skill 做体检:不靠“气色看起来不错”(感觉),而是抽血化验(跑测试用例)、看指标(打分),下次复查对比指标,就知道是真好了还是心理作用。
记住:一个 eval 的完整形态:一个 prompt → 一次被记录的运行(trace+产物)→ 一小组检查项 → 一个可随时间对比的分数。

Agent Skill 的 eval 很像轻量端到端测试:跑一遍 Agent,记录发生了什么,按一小组规则打分。

🎯

1. 先定义成功,再写 Skill

在写 Skill 之前,先写下“成功”是什么——用能实际测量的措辞。分成四类:

  • Outcome goals(结果目标):任务完成了吗?App 跑起来了吗?
  • Process goals(过程目标):Codex 有没有调用 Skill、按你预期的工具和步骤走?
  • Style goals(风格目标):输出符合你要求的约定吗?
  • Efficiency goals(效率目标):有没有瞎折腾(比如不必要的命令、烧太多 token)?

清单保持小而聚焦,只抓必须通过的项。目标不是一开始就编码所有偏好,而是抓住你最在乎的行为。

本文用一个 demo app 搭建 Skill 举例:有些检查很具体——跑没跑 npm install?package.json 生成没?再配一个结构化的风格 rubric 评约定和排版。这种组合是故意的:要的是能早发现具体回归的、快的、有针对性的信号,而不是最后给一个笼统的通过/不通过。

📝

2. 写出 Skill(含完整示例)

Codex Skill 就是一个目录,里面有一个 SKILL.md:YAML front matter(name、description)+ Markdown 指令,还可以带资源和脚本。

记住:name 和 description 比你想象的重要:它们是 Codex 决定“要不要调用这个 Skill、什么时候把 SKILL.md 注入上下文”的主要信号。写得含糊或贪多,Skill 就触发不可靠。

最快上手:用 Codex 内置的 skill creator(它自己本身也是一个 Skill,github.com/openai/skills/tree/main/skills/.system/skill-creator),在终端里跑:

$skill-creator

它会问你:Skill 做什么、何时触发、是纯指令还是带脚本(默认推荐纯指令)。

示例 Skill:setup-demo-app(故意写得很“有主见”)

本文用一个极简示例:可预测、可重复地搭一个 React demo app。没有明确约束,就没有可评测的东西——所以它故意立场鲜明:

  • 用 Vite 的 React + TypeScript 模板脚手架;
  • 用官方 Vite 插件方式配 Tailwind CSS;
  • 强制最小、一致的文件结构;
  • 明确的“完成定义”,让成功一目了然。

贴到 .codex/skills/setup-demo-app/SKILL.md(仓库级)或 ~/.codex/skills/setup-demo-app/SKILL.md(用户级)即可用:

---
name: setup-demo-app
description: Scaffold a Vite + React + Tailwind demo app with a small, consistent project structure.
---

## When to use this

Use when you need a fresh demo app for quick UI experiments or reproductions.

## What to build

Create a Vite React TypeScript app and configure Tailwind. Keep it minimal.

Project structure after setup:

- src/
  - main.tsx (entry)
  - App.tsx (root UI)
  - components/
    - Header.tsx
    - Card.tsx
  - index.css (Tailwind import)
- index.html
- package.json

Style requirements:

- TypeScript components
- Functional components only
- Tailwind classes for styling (no CSS modules)
- No extra UI libraries

## Steps

1. Scaffold with Vite using the React TS template:
   npm create vite@latest demo-app -- --template react-ts

2. Install dependencies:
   cd demo-app
   npm install

3. Install and configure Tailwind using the Vite plugin.
   - npm install tailwindcss @tailwindcss/vite
   - Add the tailwind plugin to vite.config.ts
   - In src/index.css, replace contents with:
     @import "tailwindcss";

4. Implement the minimal UI:
   - Header: app title and short subtitle
   - Card: reusable card container
   - App: render Header + 2 Cards with placeholder text

## Definition of done

- npm run dev starts successfully
- package.json exists
- src/components/Header.tsx and src/components/Card.tsx exist
🖱

3. 手动触发:把隐藏假设揪出来

因为触发极度依赖 SKILL.md 里的 name 和 description,第一件事就是验证:它在你期望的时候触发吗?

早期先显式激活(用 /skills 斜杠命令,或 $ 前缀引用),在真实仓库或临时目录里跑,看它在哪里翻车。这一步揪的是 miss:该触发没触发、不该触发乱触发、触发了但跑偏。

这时不求快、不求漂亮,只找 Skill 藏着的假设:

  • 触发假设:“set up a quick React demo”这种该触发的没触发,或“add Tailwind styling”这种泛泛的被误触发;
  • 环境假设:Skill 假设跑在空目录、假设有 npm 且偏好 npm 而非其他包管理器;
  • 执行假设:Agent 跳过 npm install(以为依赖已装好),或 Vite 项目还没建就先配 Tailwind。

要可重复跑,就换 codex exec:专为自动化和 CI 设计,进度走 stderr,最终结果只写 stdout,好脚本化、好捕获、好检查。默认跑在受限沙箱;任务要写文件就加 --full-auto。自动化时坚持最小权限原则。

一次手动跑长这样:

codex exec --full-auto \
  'Use the $setup-demo-app skill to create the project in this directory.'
高中生类比
这一步像新车磨合期:不飙速度,专门听异响。每次手动修好的问题(补上漏掉的 npm install、修正 Tailwind 配置、收紧触发描述),都是未来 eval 的候选——先把“应该是什么样”锁死,再规模化评测。
🧾

4. 小批量 prompt 集:早发现回归

不需要大 benchmark。单个 Skill,10–20 个 prompt 就够早期发现回归、确认改进。

从一个小 CSV 起步,开发/使用中遇到真实失败就加行。每行代表一个你在乎的场景:Skill 该不该激活,激活后成功长什么样。

示例 evals/setup-demo-app.prompts.csv:

id,should_trigger,prompt
test-01,true,"Create a demo app named `devday-demo` using the $setup-demo-app skill"
test-02,true,"Set up a minimal React demo app with Tailwind for quick UI experiments"
test-03,true,"Create a small demo app to showcase the Responses API"
test-04,false,"Add Tailwind styling to my existing React app"

四种调用类型,各测不同的东西(点展开):

prompt 直接点名 Skill。确保 Codex 被要求时能调用,且 Skill 改名、改描述、改指令不会搞坏直接用法。

prompt 精确描述 Skill 的目标场景(最小 React+Tailwind demo),但不提 Skill 名。测 SKILL.md 的 name+description 是否够强,能让 Codex 自己选中它。

加了领域上下文(Responses API),但底层还是同一个搭建任务。测 Skill 在真实、带噪音的 prompt 里能否触发,且产物仍符合预期结构和约定。

这个 prompt 不该触发 setup-demo-app。它是常见的相邻请求(“给现有 App 加 Tailwind”),容易误命中 Skill 描述里的“React+Tailwind demo”。至少放一个 should_trigger=false 的用例,专门抓误报——Codex 选得太积极、用户只想增量改却被新建了个项目。

记住:这个组合是故意的:有的 eval 确认显式调用时行为正确,有的确认用户根本不提 Skill的真实 prompt 里也能激活。以后每发现一个 miss(没触发、输出跑偏),就加一行——这个小 CSV 会长成“Skill 必须一直做对的事”的活档案。
⚙

5. 轻量确定性检查:评测的核心

这是评测步骤的核心:用 codex exec --json,让评测框架给实际发生的事打分,而不是只看最终输出好不好看。

开了 --json,stdout 变成 JSONL 结构化事件流。写确定性检查就顺理成章了:

  • 跑没跑 npm install?
  • package.json 生成没?
  • 命令是不是按预期的顺序调的?

这些检查故意做得很轻:快、可解释,在上模型裁判之前先给信号。

一个最小 Node.js runner(原文完整代码)

思路:① 每个 prompt 跑 codex exec --json --full-auto;② JSONL 存盘;③ 解析事件流跑确定性检查。

// evals/run-setup-demo-app-evals.mjs
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import path from "node:path";

function runCodex(prompt, outJsonlPath) {
  const res = spawnSync(
    "codex",
    [
      "exec",
      "--json", // REQUIRED: emit structured events
      "--full-auto", // Allow file system changes
      prompt,
    ],
    { encoding: "utf8" }
  );

  mkdirSync(path.dirname(outJsonlPath), { recursive: true });

  // stdout is JSONL when --json is enabled
  writeFileSync(outJsonlPath, res.stdout, "utf8");

  return { exitCode: res.status ?? 1, stderr: res.stderr };
}

function parseJsonl(jsonlText) {
  return jsonlText
    .split("\n")
    .filter(Boolean)
    .map((line) => JSON.parse(line));
}

// deterministic check: did the agent run `npm install`?
function checkRanNpmInstall(events) {
  return events.some(
    (e) =>
      (e.type === "item.started" || e.type === "item.completed") &&
      e.item?.type === "command_execution" &&
      typeof e.item?.command === "string" &&
      e.item.command.includes("npm install")
  );
}

// deterministic check: did `package.json` get created?
function checkPackageJsonExists(projectDir) {
  return existsSync(path.join(projectDir, "package.json"));
}

// Example single-case run
const projectDir = process.cwd();
const tracePath = path.join(projectDir, "evals", "artifacts", "test-01.jsonl");

const prompt =
  "Create a demo app named demo-app using the $setup-demo-app skill";

runCodex(prompt, tracePath);

const events = parseJsonl(readFileSync(tracePath, "utf8"));

console.log({
  ranNpmInstall: checkRanNpmInstall(events),
  hasPackageJson: checkPackageJsonExists(path.join(projectDir, "demo-app")),
});

价值在于一切都是确定、可调试的:检查挂了,打开 JSONL 文件逐行看发生了什么;每个命令执行都是有序的 item.* 事件。回归的原因好解释、好修——这正是这个阶段想要的。

📊

6. Rubric 评分:规则答不上来的,问模型

确定性检查回答“基本功做没做”,但回答不了“是不是按我想要的方式做的”。

对 setup-demo-app 这类 Skill,很多要求是定性的:组件结构、样式约定、Tailwind 配置是否符合预期——光靠文件存在性、命令计数抓不住。

务实的解法是加第二层、模型辅助的评测:

  1. 跑 setup skill(代码写到磁盘);
  2. 对产物仓库跑一次只读的风格检查;
  3. 要求结构化返回,让框架能稳定打分。

Codex 的 --output-schema 直接支持这个:把最终回答约束成你定义的 JSON Schema。

Rubric schema(原文)

建 evals/style-rubric.schema.json,字段稳定(overall_pass、score、逐项结果),方便合并、对比、长期跟踪:

{
  "type": "object",
  "properties": {
    "overall_pass": { "type": "boolean" },
    "score": { "type": "integer", "minimum": 0, "maximum": 100 },
    "checks": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "pass": { "type": "boolean" },
          "notes": { "type": "string" }
        },
        "required": ["id", "pass", "notes"],
        "additionalProperties": false
      }
    }
  },
  "required": ["overall_pass", "score", "checks"],
  "additionalProperties": false
}

风格检查 prompt(只读仓库,打 rubric 分)

再跑一次 codex exec,只检查仓库,输出符合 rubric 的 JSON:

codex exec \
  "Evaluate the demo-app repository against these requirements:
   - Vite + React + TypeScript project exists
   - Tailwind is configured via @tailwindcss/vite and CSS imports tailwindcss
   - src/components contains Header.tsx and Card.tsx
   - Components are functional and styled with Tailwind utility classes (no CSS modules)
   Return a rubric result as JSON with check ids: vite, tailwind, structure, style." \
  --output-schema ./evals/style-rubric.schema.json \
  -o ./evals/artifacts/test-01.style.json

--output-schema 的好处:拿到的不是难解析难对比的自由文本,而是可预测的 JSON,评测框架可以跨多次运行打分。以后把这套搬进 CI,Codex GitHub Action 支持用 codex-args 透传 --output-schema,自动化里强制同样的结构化输出。

🚀

7. Skill 成熟后:往深处扩展

核心循环跑起来后,按 Skill 真正需要信心的方向扩展。先小后大:先加快的、能解释行为的检查,重而慢的只在能降风险时加。

  • 命令计数与瞎折腾:数 JSONL 里 command_execution 的条数,抓 Agent 开始循环或重复跑命令的回归;turn.completed 事件里也有 token 用量;
  • Token 预算:跟踪 usage.input_tokens / usage.output_tokens,发现 prompt 意外膨胀,对比版本间效率;
  • 构建检查:Skill 跑完跑 npm run build——更强的端到端信号,能抓坏掉的 import 和配错的工具链;
  • 运行时冒烟:起 npm run dev 后 curl 一下,或跑个轻量 Playwright(有现成的才用)。有选择地用:加信心但花时间;
  • 仓库干净度:确保没生成多余文件,git status --porcelain 为空(或符合显式白名单);
  • 沙箱与权限回归:验证 Skill 不提权也能跑。自动化之后,最小权限默认最重要。
✅

8. 要点回顾

这个小小的 setup-demo-app 示例,演示了从“感觉变好了”到“拿出证据”的转变:跑一遍 Agent,记录发生了什么,用一小组检查项打分。循环建起来之后,每次微调都更容易确认,每次回归都清清楚楚。

  • Measure what matters(测真正重要的):好的 eval 让回归清晰、失败可解释;
  • 从可检查的完成定义起步:用 $skill-creator 起手,把指令收紧到成功无歧义;
  • 评测扎根在行为上:codex exec --json 抓 JSONL,对 command_execution 事件写确定性检查;
  • 规则够不着的地方用 Codex:--output-schema 加结构化 rubric,稳定地评风格和约定;
  • 让真实失败驱动覆盖率:每次手动修都是信号,把它变成测试,Skill 才会一直做对。
词

术语翻卡:点卡片看解释

Eval
evaluation
评测:prompt → 被记录的运行(trace+产物)→ 一小组检查 → 可对比的分数。
确定性检查
deterministic check
直接读 JSONL 事件流的是非判断(如 npm install 跑没跑),快、可解释、能进 CI。
Rubric grading
rubric grading
按评分表让模型给质量打分;用 --output-schema 约束成稳定 JSON,只做第二层补充。
JSONL trace
JSONL trace
codex exec --json 输出的结构化事件流,每个命令执行都是有序的 item.* 事件。
负向控制
negative control
should_trigger=false 的用例,专门抓误触发(如“加 Tailwind”不该新建整个项目)。
隐式触发
implicit invocation
不提 Skill 名、靠 description 语义命中的触发,考验 name+description 是否够强。
skill-creator
skill-creator
Codex 内置的元 Skill:访谈式生成 SKILL.md,$skill-creator 启动。
最小权限
least privilege
自动化时只给完成任务必需的权限;codex exec 默认受限沙箱,写文件才加 --full-auto。
测

自测一下:点问题展开参考答案

prompt → 被记录的运行(trace+产物)→ 一小组检查项 → 可随时间对比的分数。区别在于:感觉靠直觉、不可复现;eval 把每次运行记录下来打分,改进和回归都清晰可见。

test-01 显式调用(点名用 Skill,防改名改描述搞坏);test-02 隐式调用(不提名,测 description 语义命中);test-03 上下文调用(带真实噪音,测鲁棒性);test-04 负向控制(不该触发,抓误触发)。

确定性检查读 JSONL 事件流,回答“基本功做没做”(如 npm install 跑没跑),快、可解释、能进 CI;rubric 评分处理定性要求(组件结构、样式约定),用 --output-schema 拿到稳定 JSON 做第二层补充,不替代规则。

触发假设(该触发的没触发/不该的误触发)、环境假设(空目录?有 npm?)、执行假设(跳过 npm install?先配 Tailwind 后建项目?)。每个修好的问题都是未来 eval 的候选。

命令计数与瞎折腾、token 预算、npm run build 构建检查、运行时冒烟(dev+curl/Playwright)、仓库干净度(git status)、沙箱与权限回归。原则:先加快的、能解释行为的检查。