用 Evals 系統化測試 Agent Skills
本文是 Testing Agent Skills Systematically with Evals 的繁體中文翻譯,原文由 Dominik Kundel、Gabriel Chua 撰寫。
當你在迭代像 Codex 這樣的 agent skill 時,很難判斷自己到底是在改進它,還是只是改變了它的行為。一個版本感覺比較快,另一個似乎更可靠,接著就出現回歸問題:skill 沒有被觸發、跳過了必要步驟,或留下多餘的檔案。
本質上,skill 是為 LLM 設計的一組有組織的提示與指令。想要長期穩定地改進 skill,最可靠的方法就是把它當成其他任何 LLM 應用的提示一樣去評估。
Evals(evaluation 的簡稱)用來檢查模型輸出與產生步驟,是否符合你的預期。與其問「這樣是不是比較好?」(或憑感覺),evals 讓你能問更具體的問題,例如:
- Agent 有沒有啟用 skill?
- 有沒有執行預期的指令?
- 產出的內容是否符合你關心的慣例?
更具體地說,一次 eval 就是:一個提示詞 → 一次被記錄的執行(trace + artifacts)→ 一小組檢查 → 一個可以長期比較的分數。
實務上,針對 agent skills 的 eval 很像輕量版的端對端測試:你執行 agent、記錄發生了什麼,再用一小組規則來評分結果。
本文會示範一個清楚的流程,用 Codex 來做這件事:先定義成功,接著加入可重現的檢查與 rubric-based grading(依評分量表的評分),讓改進(與回歸問題)都清清楚楚。
1. 在撰寫 skill 前先定義成功
在寫 skill 本身之前,先把「成功」用可量測的方式寫下來。一個實用的方法是把檢查拆成幾個類別:
- 結果目標: 任務有完成嗎?應用程式能跑嗎?
- 流程目標: Codex 有沒有啟用 skill,並遵循你預期的工具與步驟?
- 風格目標: 輸出有符合你要求的慣例嗎?
- 效率目標: 它是否避免亂試(例如不必要的指令或過量的 token 使用)?
這份清單要小而精,聚焦在必須通過的檢查。目標不是一開始就塞進所有偏好,而是先抓住你最在意的行為。
例如本文的示範會評估一個用來建立 demo app 的 skill。有些檢查是具體的:有沒有跑 npm install?有沒有建立 package.json?同時也會搭配結構化的風格評分量表來評估慣例與版面配置。
這種混搭是刻意的。你希望有快速、聚焦的訊號,能及早揭露特定回歸問題,而不是最後只得到一個通過/失敗的結論。
2. 建立 skill
Codex 的 skill 是一個包含 SKILL.md 的目錄,內容有 YAML front matter(name、description),接著是定義 skill 行為的 Markdown 指令,以及可選的資源或腳本。name 和 description 的重要性往往比你想像中更高:它們是 Codex 判斷是否啟用 skill、以及何時把 SKILL.md 其餘內容注入 agent context 的主要訊號。如果這兩個欄位含糊或過度泛用,skill 就很難穩定觸發。
最快的起手式是用 Codex 內建的 skill creator(它本身也是一個 skill)。它會引導你完成:
$skill-creator
Skill creator 會問你 skill 的用途、該在什麼情境觸發,以及它是純指令型還是有腳本支援(預設建議是純指令型)。想了解更多建立 skill 的方法,可以參考文件。
一個範例 skill
本文使用刻意極簡的例子:一個會以可預測、可重複方式建立小型 React demo app 的 skill。
這個 skill 會:
- 使用 Vite 的 React + TypeScript 模板建立專案
- 用官方的 Vite plugin 方式設定 Tailwind CSS
- 強制最小且一致的檔案結構
- 定義清楚的「完成條件」,讓評估更直接
以下是一份精簡草稿,你可以貼到:
.codex/skills/setup-demo-app/SKILL.md(repo 範圍),或~/.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 刻意採取比較有主見的立場。沒有明確限制,就沒有可評估的具體項目。
3. 手動觸發 skill 以暴露隱藏假設
由於 skill 是否被啟用很依賴 SKILL.md 裡的 name 和 description,第一件要檢查的事就是 setup-demo-app 是否會在你預期時觸發。
一開始就明確啟用 skill,透過 /skills 斜線指令或使用 $ 前綴,並在真實 repo 或臨時工作目錄裡觀察它會在哪裡出錯。這裡能揭露各種缺口:skill 完全不觸發、觸發過頭,或是雖然有跑但偏離了預期步驟。
在這個階段,你不需要追求速度或精緻度。你要找的是 skill 在做哪些隱含假設,例如:
-
觸發假設: 像「快速建立 React demo」這種應該要觸發
setup-demo-app的提示詞卻沒有觸發,或比較泛的提示詞(「加 Tailwind 樣式」)反而意外觸發。 -
環境假設: skill 假設它是在空目錄執行,或假設系統上有
npm且偏好它勝過其他套件管理器。 -
執行假設: agent 因為假設依賴已經存在而跳過
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 設定、或收緊觸發描述,都會成為未來 eval 的候選,讓你在擴大評估前先把行為鎖定。
4. 用小而精的提示詞集合提早抓回歸問題
你不需要很大的基準測試才能從 eval 得到價值。對單一 skill 來說,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)
這個提示詞直接點名 skill。它確保 Codex 能在被要求時啟用setup-demo-app,並確認對 skill 的名稱、描述或指令的調整,不會破壞直接使用方式。 -
隱含觸發(
test-02)
這個提示詞精準描述了 skill 的目標場景:建立最小的 React + Tailwind demo,但完全不提 skill 的名字。它測試SKILL.md的名稱與描述是否足夠強,能讓 Codex 自行選用這個 skill。 -
情境化觸發(
test-03)
這個提示詞加入了領域背景(Responses API),但仍需要相同的底層建置。它檢查 skill 是否能在更貼近真實、略帶雜訊的提示詞中觸發,並確保產出的 app 符合預期結構與慣例。 -
負向對照(
test-04)
這個提示詞 不應該 觸發setup-demo-app。它是常見的相鄰需求(在既有 app 加 Tailwind),很容易不小心和 skill 的描述(React + Tailwind demo)撞到。至少加入一個should_trigger=false的案例,可以抓出 false positives:Codex 過度熱心地啟用 skill、去 scaffold 一個新專案,但使用者其實想要在既有專案上增量修改。
這種混搭是刻意的。有些 eval 用來確認 skill 在被明確啟用時能正確運作;另外一些則要測試它在使用者根本沒提 skill 的真實提示詞中能否正確啟用。
當你發現觸發失敗、或輸出偏離預期的案例時,就把它們加到新的列。久而久之,這份小 CSV 會變成一份活的紀錄,記下 setup-demo-app 必須持續做對的場景。
5. 從輕量、可判定的評分器開始
這是評估步驟的核心:使用 codex exec --json,讓你的評估流程可以評分「實際發生了什麼」,而不只是看最終輸出長得對不對。
當你啟用 --json 時,stdout 會變成 JSONL 的事件串流。這讓你可以直接對你在乎的行為寫可判定的檢查,例如:
- 有沒有執行
npm install? - 有沒有建立
package.json? - 是否以預期順序執行了預期指令?
這些檢查刻意保持輕量。它們能在你加入任何模型評分之前,先提供快速、可解釋的訊號。
一個極簡的 Node.js runner
「夠用」的方法大概是這樣:
- 對每個提示詞執行
codex exec --json --full-auto "<prompt>" - 把 JSONL trace 存到磁碟
- 解析 trace,對事件執行可判定的檢查
// evals/run-setup-demo-app-evals.mjs
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 做質性檢查與 rubric-based grading(依評分量表的評分)
可判定的檢查回答了「基本動作有做嗎?」但回答不了「有照你想要的方式做嗎?」
像 setup-demo-app 這樣的 skill,很多要求是質性的:元件結構、樣式慣例,或 Tailwind 是否按預期配置。這些很難只靠檔案存在與否或指令數量就捕捉到。
一個實務做法是在評估流程中加入第二步、由模型協助的檢查:
- 先執行 setup skill(它會把程式寫到磁碟)
- 對結果 repo 執行唯讀的風格檢查
- 要求結構化回應,讓你的評測腳本能一致評分
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、逐項結果),你可以把它們組合、diff,並長期追蹤。
風格檢查的提示詞
接著跑第二次 codex exec,只檢視 repo,並輸出符合評分量表的 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 物件,讓你的評測腳本能對多次執行結果做一致評分。
如果你之後把這套 eval 移進 CI,Codex GitHub Action 也明確支援把 --output-schema 透過 codex-args 傳入,因此你可以在自動化流程中強制同樣的結構化輸出。
7. 隨著 skill 成熟擴展 evals
當核心迴圈建立之後,你就可以朝最重要的方向延伸 evals。先從小範圍做起,只有在能提升信心時再加深檢查。
舉幾個例子:
-
指令數量與亂跑: 統計 JSONL trace 內的
command_execution事件數,抓出 agent 開始打轉或重複執行指令的回歸問題。turn.completed事件也提供 token 使用量。 -
Token 預算: 追蹤
usage.input_tokens與usage.output_tokens,觀察意外的提示詞膨脹,並比較不同版本的效率。 -
Build 檢查: skill 完成後跑
npm run build。這是一個更強的端對端訊號,可以抓出壞掉的 import 或不正確的工具設定。 -
執行期 smoke checks: 啟動
npm run dev,再用curl打到 dev server,或如果你已有 Playwright,就跑輕量測試。選擇性使用,它會增加信心但也會花時間。 -
Repo 乾淨度: 確認執行後沒有多餘檔案,且
git status --porcelain是空的(或符合明確的允許清單)。 -
沙盒與權限的回歸問題: 驗證 skill 仍能在不提升權限的狀況下運作。當你開始自動化時,最小權限原則尤其重要。
模式是一致的:先從能解釋行為的快速檢查開始,再只在能降低風險時加入較慢、較重的檢查。
8. 重點整理
這個小型的 setup-demo-app 例子說明了從「感覺更好」到「可證明」的轉變:執行 agent、記錄發生的事,再用小規模的檢查來評分。只要這個迴圈存在,每次微調就更容易驗證,每次回歸問題也更清楚。重點如下:
- 衡量重要的東西。 好的 eval 讓回歸問題清楚、失敗可解釋。
- 從可檢查的完成定義開始。 先用
$skill-creator起手,再收緊指令,讓成功變得毫不含糊。 - 以行為為準。 用
codex exec --json捕捉 JSONL,並對command_execution事件寫可判定的檢查。 - 規則不足的地方交給 Codex。 用
--output-schema加入結構化的評分量表式評分,可靠地檢查風格與慣例。 - 讓真實失敗驅動覆蓋。 每次手動修正都是訊號,把它變成測試,讓 skill 持續做對。