用 Evals 系统化评测 Agent Skills
OpenAI 官方指南全文中文讲解:从“感觉变好了”到“拿出证据”——给 Codex Skill 建一条可复现的评测流水线。
开篇:你怎么知道 Skill 真的变好了?
当你为 Codex 这类 Agent 迭代一个 Skill 时,很难判断自己究竟是真正改进了它,还是仅仅改变了它的行为:某个版本感觉更快,另一个版本似乎更可靠,然后回归问题就悄悄出现了——Skill 没有被触发、跳过了某个必需步骤,或者留下了多余的文件。
从本质上讲,Skill 是面向 LLM 的一组有组织的提示词和指令集合。改进 Skill 最可靠的方式,就是像评估 LLM 应用中的任何其他提示词一样去评估它。
Eval(评估)就像给 Skill 做体检:不靠“气色看起来不错”(感觉),而是抽血化验(跑测试用例)、看指标(打分),下次复查对比指标,就知道是真好了还是心理作用。
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 指令,还可以带资源和脚本。
最快上手:用 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 选得太积极、用户只想增量改却被新建了个项目。
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 配置是否符合预期——光靠文件存在性、命令计数抓不住。
务实的解法是加第二层、模型辅助的评测:
- 跑 setup skill(代码写到磁盘);
- 对产物仓库跑一次只读的风格检查;
- 要求结构化返回,让框架能稳定打分。
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 才会一直做对。
术语翻卡:点卡片看解释
自测一下:点问题展开参考答案
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)、沙箱与权限回归。原则:先加快的、能解释行为的检查。