OpenAI 开发者博客讲解如何用 Evals 系统化测试 Codex Agent 技能
Testing Agent Skills Systematically with Evals
OpenAI 开发者博客发布教程,讲解如何像测试 LLM 应用一样,用 evals 系统化评估 Codex 的 Agent 技能。核心流程为定义可度量的成功标准、用 $skill-creator 创建技能、以 codex exec -- 捕获 JSONL 轨迹做确定性检查,再用 --output-schema 输出符合评分规则的 JSON 做风格等定性评估。
OpenAI 官方给出用 evals 系统化测试 Codex 技能的完整模式,从定义成功标准到确定性检查与评分规则均可迁移复用。
当你为 Codex 这类智能体迭代一项技能时,很难判断自己究竟是在真正改进它,还是仅仅改变了它的行为。某个版本感觉更快,另一个版本似乎更可靠,然后一个回归问题悄悄出现:技能没有被触发、跳过了某个必需步骤,或者留下了多余的文件。
从本质上讲,一项技能是给 LLM 的一组有组织的提示词和指令集合。随着时间推移改进技能最可靠的方式,就是像评估任何其他用于 LLM 应用的提示词那样去评估它。
Evals(evaluations 的缩写)用于检查模型的输出以及它产生输出所采取的步骤是否符合你的预期。与其问“这样感觉更好吗?”(或者依赖直觉),evals 让你能够提出具体的问题,例如:
- 智能体是否调用了该技能?
- 它是否运行了预期的命令?
- 它产生的输出是否遵循了你所关心的约定?
具体来说,一个 eval 是:一个提示词 → 一次被捕获的运行(trace + artifacts)→ 一小组检查 → 一个可以随时间比较的分数。
在实践中,针对智能体技能的 evals 看起来很像轻量级的端到端测试:你运行智能体,记录发生了什么,然后根据一小组规则对结果打分。
本文介绍了一种用 Codex 实现这一点的清晰模式,从定义成功开始,然后加入确定性检查和基于评分标准的打分,让改进(和回归)变得一目了然。
1. 在编写技能之前先定义成功
在编写技能本身之前,先写下“成功”意味着什么,并用你实际可以衡量的方式来表达。一个有用的思路是把检查分成几个类别:
- 结果目标:任务完成了吗?应用能运行吗?
- 过程目标:Codex 是否调用了该技能,并遵循了你预期的工具和步骤?
- 风格目标:输出是否遵循了你要求的约定?
- 效率目标:它是否在没有反复折腾(例如不必要的命令或过多的 token 消耗)的情况下达成目标?
让这份清单保持精简,并聚焦于必须通过的检查。目标不是预先编码每一条偏好,而是捕捉你最关心的行为。
例如,在本文中,指南评估的是一项用于搭建演示应用的技能。有些检查是具体的。它是否运行了 npm install?它是否创建了 package.json?指南将这些检查与一个结构化的风格评分标准配对,以评估约定和布局。
这种组合是有意为之的。你需要快速、有针对性的信号,以便尽早发现具体的回归问题,而不是在最后给出一个单一的通过/失败判定。
2. 创建技能
一个 Codex 技能是一个目录,其中包含一个 SKILL.md 文件,该文件包含 YAML front matter(name、description),随后是定义技能行为的 Markdown 指令,以及可选的资源和脚本。名称和描述的重要性可能超出你的想象。它们是 Codex 用来决定是否调用该技能,以及何时将 SKILL.md 的其余部分注入智能体上下文的主要信号。如果这些内容含糊不清或承载过多,技能就无法可靠地触发。
最快的入门方式是使用 Codex 内置的技能创建器(它本身也是一个技能)。它会引导你完成:
$skill-creator创建者会问你该技能做什么、何时应触发,以及它是仅指令型还是脚本支持型(默认推荐仅指令型)。要了解更多关于创建技能的信息,请查看文档。
一个示例技能
本文使用一个刻意简化的示例:一个以可预测、可重复的方式搭建小型 React 演示应用的技能。
该技能将:
- 使用 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这个示例技能有意采取有主见的立场。没有明确的约束,就没有具体的东西可供评估。
由于技能调用在很大程度上取决于 SKILL.md 中的名称和描述,首先要检查的是 setup-demo-app 技能是否在你预期时触发。
在早期,明确激活该技能,可以通过 /skills 斜杠命令,或使用 $ 前缀引用它,在一个真实仓库或临时目录中运行,并观察它在何处出错。这正是你发现遗漏之处的地方:技能完全不触发、触发过于积极,或运行但偏离预期步骤的情况。
在这个阶段,你不是在优化速度或打磨程度。你是在寻找技能所做的隐藏假设,例如:
-
触发假设:像“set up a quick React demo”这样应该调用
setup-demo-app的提示却没有调用,或者更通用的提示(“add Tailwind styling”)意外触发了它。 -
环境假设:技能假设它在空目录中运行,或者假设
npm可用且优先于其他包管理器。 -
执行假设:代理跳过
npm install,因为它假设依赖已安装,或者在 Vite 项目存在之前就配置 Tailwind。
一旦你准备好让这些运行可重复,就切换到 codex exec。它是为自动化和 CI 设计的:它将进度流式输出到 stderr,只将最终结果写入 stdout,这使得运行更容易脚本化、捕获和检查。
默认情况下,codex exec 在受限沙箱中运行。如果你的任务需要写入文件,请使用 --full-auto 运行它。作为一般规则,尤其是在自动化时,使用完成任务所需的最小权限。
一次基本的手动运行可能如下所示:
codex exec --full-auto \
'Use the $setup-demo-app skill to create the project in this directory.'这第一次动手实践与其说是验证正确性,不如说是发现边缘情况。你在这里所做的每一次手动修复,例如添加缺失的 npm install、修正 Tailwind 设置,或收紧触发描述,都是未来评估的候选对象,这样你就可以在大规模评估之前锁定预期行为。
4. 使用小型、有针对性的提示集尽早发现回归
你不需要大型基准测试就能从评估中获得价值。对于单个技能,10–20 个提示的小集合就足以发现回归并尽早确认改进。
从一个小型 CSV 开始,并随着你在开发或使用过程中遇到真实失败而逐步扩展。每一行都应代表一种你关心 setup-demo-app 技能是否激活的情况,以及激活时成功是什么样子。
例如,一个初始的 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"这些案例中的每一个都在测试略微不同的东西:
-
显式调用(
test-01)
此提示直接点名该技能。它确保 Codex 在被要求时能够调用setup-demo-app,并且技能的名称、描述或指令的更改不会破坏直接使用。 -
隐式调用(
test-02)
此提示恰好描述了该技能所针对的场景,设置一个最小的 React + Tailwind 演示,而不提及技能名称。它测试SKILL.md中的名称和描述是否足够强大,让 Codex 能够自行选择该技能。 -
上下文调用(
test-03)
此提示添加了领域上下文(Responses API),但仍然需要相同的基础设置。它检查该技能是否能在真实、略带噪声的提示中触发,以及生成的应用程序是否仍然符合预期的结构和约定。 -
负向对照(
test-04)
此提示不应调用setup-demo-app。这是一个常见的相邻请求(“向现有应用添加 Tailwind”),可能会无意中匹配该技能的描述(“React + Tailwind 演示”)。至少包含一个should_trigger=false用例有助于捕获误报,即 Codex 过于急切地选择该技能,在用户想要对现有项目进行增量更改时却搭建了一个新项目。
这种混合是有意为之的。一些评估应确认该技能在被显式调用时行为正确;另一些则应检查它是否能在用户从未提及该技能的真实世界提示中激活。
当你发现遗漏、未能触发技能的提示,或输出偏离预期的情况时,将它们添加为新行。随着时间的推移,这个小 CSV 会成为该 setup-demo-app 技能必须持续做对的场景的活记录。
随着时间的推移,这个小数据集会成为该技能必须持续做对的事情的活记录。
5. 从轻量级确定性评分器开始
这是评估步骤的核心:使用 codex exec --json,让你的评估框架能够对实际发生的事情进行评分,而不仅仅是最终输出看起来是否正确。
当你启用 --json 时,stdout 会变成结构化事件的 JSONL 流。这使得编写与你关心的行为直接绑定的确定性检查变得简单,例如:
- 它是否运行了
npm install? - 它是否创建了
package.json? - 它是否按预期顺序调用了预期的命令?
这些检查有意保持轻量。它们在你添加任何基于模型的评分之前,为你提供快速、可解释的信号。
一个最小的 Node.js 运行器
一个“足够好”的方法如下:
- 对每个提示,运行
codex exec --json --full-auto "<prompt>" - 将 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. 使用 Codex 和基于评分标准的评分进行定性检查
确定性检查回答了“它是否完成了基本工作?”,但它们没有回答“它是否按照你想要的方式完成了?”
对于像 setup-demo-app 这样的技能,许多要求是定性的:组件结构、样式约定,或者 Tailwind 是否遵循预期的配置。这些仅靠基本的文件存在检查或命令计数很难捕捉。
一个务实的解决方案是在你的评估流水线中加入第二个由模型辅助的步骤:
- 运行 setup 技能(这会将代码写入磁盘)
- 对生成的仓库运行只读风格检查
- 要求返回结构化响应,以便你的测试框架能够一致地评分
Codex 通过 --output-schema 直接支持这一点,它会将最终响应约束为你定义的 JSON Schema。
一个小型评分标准 schema
首先定义一个小的 schema,用来捕获你关心的检查项。例如,创建 evals/style-rubric.schema.json:
{
"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
}这个 schema 为你提供了稳定的字段(overall_pass、score、每项检查的结果),你可以将它们组合、对比并随时间跟踪。
风格检查提示词
接下来,运行第二个 codex exec,它只检查仓库并输出符合评分标准的 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. 随着技能成熟扩展你的评估
一旦核心循环就位,你就可以朝着对你的技能最重要的方向扩展评估。从小处着手,然后只在能带来真正信心的地方叠加更深入的检查。
一些示例包括:
-
命令数量和反复折腾:统计 JSONL 轨迹中的
command_execution项,以捕捉智能体开始循环或重复运行命令的回归。Token 使用情况也可在turn.completed事件中获取。 -
Token 预算:跟踪
usage.input_tokens和usage.output_tokens,以发现意外的提示词膨胀,并比较各版本之间的效率。 -
构建检查:在技能完成后运行
npm run build。这作为更强的端到端信号,可捕捉损坏的导入或配置错误的工具。 -
运行时冒烟检查:启动
npm run dev并用curl访问开发服务器,或者如果你已经有轻量级的 Playwright 检查就运行它。有选择地使用它。它能增加信心,但会耗费时间。 -
仓库整洁度:确保运行不会生成多余文件,并且
git status --porcelain为空(或匹配显式的允许列表)。 -
沙箱和权限回归:验证技能在不超过你预期权限的情况下仍能正常工作。一旦实现自动化,最小权限默认值就最为重要。
模式是一致的:从能解释行为的快速检查开始,然后只在能降低风险时添加更慢、更重的检查。
8. 关键要点
这个小小的 setup-demo-app 示例展示了从“感觉更好”到“有证据”的转变:运行智能体,记录发生了什么,并用一小组检查来评分。一旦这个循环存在,每次调整都更容易确认,每次回归都变得清晰。以下是关键要点:
- 衡量重要的事情。好的评估让回归清晰、失败可解释。
- 从可检查的完成定义开始。使用
$skill-creator来引导,然后收紧指令,直到成功变得明确无误。 - 让评估扎根于行为。用
codex exec --json捕获 JSONL,并针对command_execution事件编写确定性检查。 - 在规则不足的地方使用 Codex。用
--output-schema添加一个结构化的、基于评分标准的检查环节,以可靠地评估风格和约定。 - 让真实失败驱动覆盖。每一次手动修复都是一个信号。把它变成测试,让技能持续做对。
来源:OpenAI Developers:Blog(网页) · developers.openai.com