外掛的 eval 跑出滿分,最常見的意思是它根本沒出手。

這句話聽起來很怪。分數 1.00,每條檢查都綠的,不就代表你的 skill 做對了?不一定。你量到的是「模型手上拿著你的外掛,把這件事做對了」。中間少一個資訊:沒有你的外掛,它本來就會做對嗎?

一盆黃金葛澆了兩週營養液,抽了三片新葉。營養液有效嗎?不知道。這兩週陽光照了、水澆了、天氣也轉暖了。你需要旁邊那盆一模一樣、但沒澆營養液的,兩盆擺在一起,差多少才是營養液的功勞。

claude plugin eval 這個指令,有相當大一部分的設計就是在幫你養第二盆。

第二盆植物

一套 eval suite 住在外掛裡的 evals/ 目錄。每個 case 是一個子目錄,裡面一份 prompt.md(使用者真的可能打出來的那句話)加上一個以上的 grader,grader 就是跑完之後的一條 pass/fail 檢查。每次跑,Claude Code 會開一個全新、隔離的非互動 session,只載入你的外掛,把那句話送進去,讓它做到收工、或撞上這個 case 的回合數與時間上限。

跑一次非決定性的 agent,你得到的資訊近乎沒有。所以每個 case 預設跑三次,一次 run 的分數是通過的 grader 佔全部 grader 的比例(設了 weight 就加權),case 的分數是這幾次 run 的平均。

真正的機關在後面那半。只要解析得到外掛,同一組 case 預設還會在完全不載外掛的狀態下再跑一輪,次數一樣。一個 case,六次 agent run。跑完的摘要長這樣:

1
2
CASE        WITH  W/OUT Δ      RUNS COST    NOTES
first-case 1.00 0.33 +0.67 6 $0.41

WITH 是載了外掛的分數,W/OUT 是沒載的,Δ 是兩者相減。COST 是那些模型呼叫的定價估算,NOTES 會顯示 with 那一組裡權重最高的失敗 grader 的解釋,或是那次 run 的錯誤訊息。

兩欄都 1.00 的那一刻,你的外掛就不是讓它通過的原因。文件把這兩組叫做 with-arm 跟 without-arm,中文講起來就是有的跟沒有的那一組。

有些檢查在兩邊都不准算分

這裡是整個設計最不直覺、也最值得看懂的地方。

你替 case 寫的第二條 grader,八成會是「我的 skill 有沒有被叫起來」,寫成 type: tool_usedtool: Skill,再用 input_match 比對 skill 名稱。合理。問題是這條檢查在沒載外掛的那一組,永遠不可能通過,因為那組根本沒有這個 skill。

把它算進分數會發生什麼事?without-arm 被一條註定失敗的檢查往下壓,壓向 0;而 Δ 是兩組相減,於是 Δ 被灌水。你會看到一個漂亮的正數,而那個正數是計分方式造出來的,不是外掛做出來的。

Claude Code 的處理方式是把這類 grader 從兩組的計分裡都拿掉,只在 with 那一組當成一盞指示燈:亮了代表出手過,沒亮代表沒出手,但兩邊都不進分數。報告裡這種 grader 會掛一個 plugin-fired indicator 的徽章。這也是為什麼你可能看到一次 run 明明有 grader 顯示 passed: false,分數卻還是 1.0。它的 scoredfalse

規則整理起來是這樣:

grader 在兩組裡怎麼算
tool_usedtoolSkill 兩組都排除計分,只在 with 那組當指示燈
任何標了 arm: with-only 的 grader 同上
標了 arm: both 強制兩組都算分
一個 case 裡的 grader 全部屬於前兩類 例外,照常計分(不然沒東西可算)

最後那條例外很務實:都排除掉就沒分數了,所以乾脆都算。而 arm: both 的用途也很具體,你要寫一條「這句話不准觸發我的 skill」的檢查(tool_used 配上 min: 0max: 0),那條就該在兩組都算分,因為沒有外掛的那組本來就該通過。

兩把不同的尺

上面那套排除規則有個邊界,文件沒有直接點破,但從規則本身推得出來,而且會咬人。

--ablation none 只跑 with 那一組,成本砍一半,迭代 grader 的時候很好用。問題是單組模式底下沒有 without-arm 可以對照,那些排除規則也就全部不生效,什麼都照常計分——包括那條 tool_used: Skill。它從一盞指示燈變回一條會算分的 grader,而且在 with 那組它通常會過。

於是同一套 suite、同一份 case,兩種模式跑出來的絕對分數不一樣。拿 --ablation none 的分數去跟上週兩組模式的分數比較,你比的是兩把不同的尺。

怎麼認出手上這份報告是哪一把尺量的?看那一列的 RUNS。兩組模式一個 case 是六次,單組是三次;W/OUTΔ 那兩欄也沒有東西可以填。分數本身不會告訴你它是哪一種模式跑的,欄位會。

檔案明明產出來了,分數卻是零

官方的 troubleshooting 收了一個症狀,我覺得每個第一次寫 grader 的人都會撞一次:Claude 該生的檔案都生出來了,你打開工作目錄看得見,分數卻全是 0。

原因在 grader 的 target 寫了 files。那個值給你的是「這次 run 裡 Claude 新建的路徑清單」,一行一個路徑,不是檔案內容,而且 Claude 只是編輯過的檔、或 scaffold script 造出來的檔,都不在裡面。你的 regex 在比對一串路徑,當然對不上內容。要看內容得把 target 寫成 { source: file, path: <path> }

file_exists 那條也踩同一個坑:它只認這次 run 裡新建的檔。想驗一個被改過的檔,改成驗它的內容,或者用 tool_used 去確認 Edit 有被呼叫。

挑哪一種 grader

grader 一共六種:regextool_usedtool_orderfile_exists 是從 transcript 跟檔案直接算出來的,不花錢;llmbaseline 會去打 judge model,會進帳單。沒有自訂程式碼型的 grader,這點官方寫得很明白,不要花時間找。

llm grader 的穩定度跟它要讀的東西長度成反比,讀越長越飄。文件給的建議是長輸出(例如一個產出的檔案)用 regex 去比對檔案內容,這樣每次檢查的方式完全一樣;llm 留給短輸出,而且 rubric 要寫成具體的 PASS 與 FAIL 條件。另外每個 case 配兩條:一條看結果(最後那則訊息或產出的檔案),一條看過程(tool_usedtool_order)。一條告訴你答案對不對,另一條告訴你是不是你的外掛做的。

兩種分數的讀法

跑完第一次,最常見的結果是 Δ 貼近零,而那條 tool_used: Skill 沒過。這通常是真實發現,意思是 Claude 在自然講法底下沒有選你的 skill。要改的是 skill 的 description,讓它長得像使用者會講的話,改完跑同一套 suite 比對。

比較容易誤判的是另一種:tool_used: Skill 過了,Δ 卻是負的。外掛出手了,分數反而更差?先懷疑裁判。llmbaseline 的 judge 預設是一個小而快的模型,它有可能因為答案的格式跟 rubric 描述得不一樣,就把一個正確的答案判成錯的。用 --judge-model sonnet 重跑一次,順便把 rubric 收緊,別讓格式決定判決。

誠實講一下這篇的地基:我沒有實際跑過 claude plugin eval,上面每一個欄位名、每一條規則都是照官方文件整理的,不是我自己量出來的行為。那個 $0.41 是文件範例輸出裡的數字,不是我的帳單。實際的分數穩定度、一個 case 真正會花多少錢,我沒有數據可以給你。

另外兩件也照文件寫:這個指令需要 Claude Code v2.1.269 以上;每一次 eval run、每一個 judge grader 都是真的模型呼叫,算在你方案的用量或 API 帳單上。

進 CI 之前

有個預設值會咬人:--threshold 預設 1.0,任何 case 沒拿到滿分,指令就 exit 1。你看著結果覺得不錯、build 卻紅了,通常是這個。文件建議的 CI 寫法是把兩個 model 都釘死(免得模型換版被當成外掛退步)、加 --trust-plugin 讓 job 不會卡在信任提示、--no-publish 把報告留在本機,再用 --max-cost-usd 壓一個成本上限。

什麼時候不值得寫這套

我的立場:值得投資 eval 的外掛,是那種「靠 description 決定要不要出手」的 skill。它的行為不是你寫死的,是模型讀了一段自然語言之後自己判斷的,而你每次改那段描述,都在改一個你沒辦法直接測的東西。這種情況下,那個 Δ 是你唯一拿得到的回饋。

什麼情況我會改變這個看法:如果你的外掛是使用者明確打指令才跑的 command、或是行為完全由程式碼決定的 hook,那 Δ 量不到什麼有意義的東西。它每次都會出手,也每次都做一樣的事。那種東西用 claude plugin validate 檢查檔案結構、再配一般的測試就夠了,不需要為它燒六次 agent run。

回到那盆植物。整件事真正教你的不是一個 CLI 指令,是一個提問順序:任何人(包括你自己)說「加了這個之後明顯變準」的時候,先問拿掉它跑過了沒有,再問兩邊算分的東西是不是同一套。第一個問題決定你有沒有對照組,第二個問題決定那個對照組有沒有被你自己做壞。

分數是拿來比較的,不是拿來收藏的。