跳到正文

使用评估系统化地测试智能体技能

本文目录文章开头

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

从根本上说,技能是面向 LLM 的提示词与指令的有组织集合。随时间改进技能最可靠的方式,是像为 LLM 应用评估其他提示词那样去评估它。

Evals(evaluations 的缩写)会检查模型的输出以及它产生该输出所经历的步骤,是否与你的意图相符。evals 不是问“这感觉更好了吗?”(或依赖直觉),而是让你提出具体的问题,例如:

  • 智能体是否调用了该技能?
  • 它是否运行了预期的命令?
  • 它产生的输出是否遵循了你所关注的约定?

具体来说,一个 eval 是:一个提示词 → 一次捕获的运行(trace + 产物)→ 一组小型检查 → 一个可以随时间比较的分数。

在实践中,智能体技能的 eval 很像轻量级的端到端测试:你运行智能体,记录发生的事情,并根据一组小型规则对结果打分。

本文介绍一个清晰的模式,用 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 斜杠命令或使用 $ 前缀引用,在真实仓库或临时目录中显式激活该技能,并观察它在哪里出问题。这正是你发现遗漏之处的时候:技能完全不触发、触发过于频繁,或运行了但偏离预期步骤的情况。

在这个阶段,你不是在优化速度或完善度。你是在寻找技能所做的隐藏假设,例如:

  • 触发假设:像“快速搭建一个 React 演示”这样的提示应该调用 setup-demo-app 却没有,或者更通用的提示(“添加 Tailwind 样式”)无意中触发了它。

  • 环境假设:技能假设它运行在空目录中,或者假设 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 运行器

一个“足够好”的方法看起来是这样的:

  1. 对于每个提示词,运行 codex exec --json --full-auto "<prompt>"
  2. 将 JSONL 跟踪保存到磁盘
  3. 解析跟踪并对事件运行确定性检查
// 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 是否遵循预期的配置。这些很难仅通过基本的文件存在性检查或命令计数来捕捉。

一个务实的方案是在你的评估流程中加入第二个、由模型辅助的步骤:

  1. 运行设置技能(这会将代码写入磁盘)
  2. 对生成的仓库运行一次只读风格检查
  3. 要求一个结构化响应,以便你的测试框架能够一致地评分

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 项,以捕获代理开始循环或重复运行命令的回归。令牌使用量也可在 turn.completed 事件中获取。

  • 令牌预算:跟踪 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 可靠地对风格和规范进行评分。
  • 让真实的失败驱动覆盖率提升。每一次手动修复都是一个信号。将其转化为测试,让该技能持续正确执行。

阅读显示设置

调整正文排版

当前浏览器记住选择,应用于文章正文。手机上正文宽度以屏幕可用空间为限。

图表完整性提示

普通文章遇到图表保存问题时显示提示。