改了一行 prompt,要知道結果有沒有變差,就用一個命令把整份 dataset 重跑一遍,每趟在乾淨環境裡跑、逐個欄位給分、每筆重跑同樣的次數;報表也要記下實際跑了幾趟,才分得出沒通過和沒跑完。
需求釐清 agent 會讀一張 ticket 的描述與驗收條件(AC),整理成 Goal、Scope in、Scope out、AC 標註四個區塊,AC 標註是它替有問題的驗收條件標上的問題類別。
前面量過它標的問題每三個只有一個是真的,於是決定改 prompt,把 unverifiable(不可驗證)與 mismatch(與描述不符)兩行定義寫窄,條件是 recall 不能掉破七成,recall 是人工標好的問題有幾成被 agent 找到。
改完之後只挑優惠碼那張卡看一眼,看不出另外 15 筆的 recall 有沒有掉。要回答這個問題,就得把 16 筆 case 在同樣的條件下各重跑幾次、逐筆給分再匯總。這些步驟前面已經分別做好,也已經由 npm run run-all 接成一個命令,這篇把這個命令拆開,看每一步排除了什麼,以及它的總表還少記了什麼。
假的 ticket store(本機)代替真正的看板讓 agent 讀寫卡片,跑完就丟;fixture 保存錄製時查到的外部回應,有檔就重播、沒檔就錄製;goal state 是人工事先寫好的通過條件,scorer 依它逐個欄位比對。給分只用 scorer 的規則,模型裁判 judge 還沒有接進這個命令。
npm run run-all -- --k 3 會把 dataset/ 裡的 16 筆 case 各跑 3 趟,k 是同一筆 case 重跑的次數。每一步各自排除一種讓紅字不可信的情況:
| 步驟 | 程式 | 排除的情況 |
|---|---|---|
| 每趟現建一張卡 | FakeStore.fromTicket |
上一趟寫回的內容留到這一趟 |
| 重播外部回應 | openFixture、ReplayingSource |
外部資料這次和上次查到的不同 |
| 依 goal state 給分 | scoreCase |
人眼逐張比對時看漏 |
| 同一筆跑 k 趟 | run-all 的迴圈 |
只跑一次,通過可能是碰巧 |
| 匯總成一張總表 | run-all 最後寫出的總表 |
只挑幾張卡看 |
重播時若查了 fixture 沒錄過的內容,ReplayingSource 會拋出 MissingFixtureError,錯誤裡帶著工具名 tool 與參數 args,這一趟當場中斷。
前三步在 runOnce 裡,第四步是 src/cli/run-all.ts 在它外面加的一層迴圈,第 50 到 79 行是這樣:
const rows: CaseRow[] = [];
for (const { path, case: testCase } of ready) {
const annotation = expectedFlags(testCase.goalState);
const counts = [];
let passes = 0;
let flagged: string[] = [];
for (let i = 1; i <= k; i++) {
try {
const run = await runOnce({ casePath: path, outDir: join(batchDir, testCase.id, `run-${i}`) });
flagged = run.actual.ac_flags ?? [];
counts.push(compareFlags(annotation, flagged));
if (run.score.pass) passes += 1;
} catch (err) {
if (!(err instanceof MissingFixtureError)) throw err;
console.error(` ${testCase.id} run ${i} 停在 fixture 對不上:${err.tool}`);
}
}
rows.push({
caseId: testCase.id,
category: testCase.category,
k,
passes,
counts: sumCounts(counts),
annotation,
flagged,
});
console.log(` ${testCase.id} ${passes}/${k} 過`);
}
迴圈跑完後,同檔第 81 到 87 行把每筆的結果寫成總表 runs/<時間>-all.md,另存一份 all.json。完成的每一趟,最終卡片、執行紀錄與分數都寫進 runs/<批次>/<case>/run-<i>/,總表上的分數可以回頭打開那一趟的產出核對。
用 gpt-5.4-mini 跑過一次,這批執行紀錄的總表第 3 行是:
model `gpt-5.4-mini` prompt `cd590b9ba593` 16 筆 每筆 k=3
其中 cd590b9ba593 是 prompt 的版本編號,這批從第一趟開始到最後一趟結束是 3 分 9 秒,有 12 筆 case 在這批才開始錄 fixture,各有一趟是錄製,其餘 33 趟都是重播。
第 5 行是整份的 precision 34%、recall 78%,precision 是 agent 標的問題有幾成是對的。把兩行定義寫窄之後重跑同一個命令,就有兩張總表可以逐列對照,但要用來判斷 recall 有沒有掉破七成,還得先核對兩批實際納入計算的 case 與趟數。
同一張總表寫每筆 k=3,目錄裡卻只有 45 趟。merged-concerns-001 和 noise-over-signal-002 缺 run-2,irreversible-push-003 缺 run-3,這三列在表上仍寫 0/3。0/3 的分母是表頭的 k,缺的那趟沒有產出、沒經過 scorer,只是沒加進 passes,看起來和判定失敗一樣;同一列的 precision 與 recall 卻只算完成的兩趟,表上看不出分母換了。
若改 prompt 後缺的是不同 case 或不同趟數,納入計算的樣本也會改變,recall 的升降就不能直接歸因於 prompt。列出缺趟只是讓差異看得見,缺趟尚未處理前,完成部分的 recall 即使超過七成,也不足以確認整份 dataset 守住了門檻。
缺的那趟為什麼停下,沒有檔案可以查。迴圈的 catch 只接住 MissingFixtureError,在終端機印一行就接著跑下一趟,其他錯誤會讓整批中止、不寫總表;runOnce 又要等 agent 跑完才建立該趟目錄,所以停下的那趟不留任何檔案。
merged-concerns-001 和 noise-over-signal-002 的 fixture 是這批第一趟錄的,irreversible-push-003 的則在這批執行前就已錄好,三份都是 calls: [],錄製那趟沒查任何外部資料,之後只要有一趟去查就會對不上。總表寫得出來,所以最可能是這三趟查了 fixture 沒錄過的內容,但終端機輸出沒有保存,這只是從程式推得的。
harness 是把 case 逐筆跑起來的那段程式,在這裡就是 run-all。討論同一筆 case 要重跑幾次時,已經定過 harness 要記下要求的 k、實際完成的次數與參與計算的次數,對不上就列出缺哪一次,run-all 的總表並沒有照這條記。
每一列要分開寫要求、完成、通過幾趟,比值欄改叫 Passed / Completed,表示通過趟數除以完成趟數,停下的那趟另列未完成。照這個格式,兩趟失敗、一趟中斷要寫成 0/2,另列未完成 1 趟;0/3 才表示三趟都完成、都判定失敗。
catch 也要把 case、趟次和錯誤裡的 tool、args 寫進 all.json,不能只印在終端機。
改一行 prompt 之後,可以用 npm run run-all -- --k 3 重跑整份 dataset,每趟現建卡片、有 fixture 就重播、依 goal state 給分,每筆跑 3 趟再匯總,整批 3 分 9 秒,改前改後的兩張總表可以逐列對照。
這張總表還要如實記下每筆要求、完成、通過幾趟,並把中斷的原因寫進檔案。前後缺趟不同時,不能只憑完成部分的 recall 判斷改動是否守住七成門檻。能重跑之後,還沒決定的是拿結果做什麼,哪些動作一律擋下,分數多少才算可以上線。