前面幾天,除了 Script 的單元測試,我們也補上針對 Skill 本身的 Eval,作為後續異動時的回歸防線。
Day 12 那場單一 MR 審查,換算成本是 12 美金。(我自己使用訂閱制;這個數字是以 token 用量乘上模型 API 牌告價回推。)
這個數字會隨 MR 的異動檔案數、異動量與原始專案大小而變化。
今天這篇要跑 50 個案例,數字會大很多。完整的帳放在明天下集的文末。
今天要回答的問題是:這套 Skill 在 benchmark 上表現如何?
這兩天的東西全部收錄在我的 Repo 裡:benchmarks/code-review-bench。50 個 PR 的原始輸出、驗證與配對的逐條判定、計分腳本、每一格分數都在那裡,下面出現的每個數字都可以自己重算。
我選用這個 Repo 來進行 Code Review Skill 的 Benchmark:https://github.com/withmartian/code-review-benchmark
這次會把 repository 中的 50 個情境全部跑過,預期執行時間很長,因此我選用 Claude Code 雲端環境。
使用之前請綁定你的 GitHub 帳號,並且選定自己含有 Code Review Skill 的 Repo
我真正下的第一則 prompt 只有這樣:
使用我 skill 中 nathan code review
然後找 https://github.com/withmartian/code-review-benchmark.git 裡面合適 Python 主戰場的情境
跑一下分數,然後也可以分析下這個專案的盲點,例如難道人的 comment 不會漏嗎?所以應有原始分數跟校正後分數(也就是你自己派獨立審查員下去驗證)
做完 Python 主線,也請你做其他語言的,同時報告中也要跟其他業界 Reviewer 去做比較
除了 Raw Data 也生成類似網誌的語言來介紹我們這個作為。
請你先分析 benchmark 專案,用你自己或是 subagent 取代其中呼叫外部 LLM 的動作。
記得要公平!
它照這份 prompt 依規則抽了 19 個案例就收工:我只寫了「找合適的情境」,沒寫「全部都要跑」。補到 50 個,是我中途追問「我們抽 19個是全部了??」之後加跑的。
跑完之後我回頭把第一則重寫了一版。下面這份沒有執行過,它是把這趟踩到的坑全部提前寫進去的「如果重來」版:
使用我 skill 中的 nathan-code-review,跑 https://github.com/withmartian/code-review-benchmark.git
**先分析這個 benchmark 專案**:不只讀 README,methodology 文件和 offline scorer 的原始碼都要讀,搞清楚它的 precision/recall 到底怎麼算、哪些緩解措施是文件寫了但程式碼沒做。然後用你自己或 subagent 取代其中呼叫外部 LLM 的動作。
**50 個案例全部都要跑,不要抽樣。** Python 是主戰場,其他語言一樣要跑完整,不然分語言的結論會被單一 PR 翻轉。
分數要有兩份:**原始分數**,以及**校正後分數**:你自己派獨立審查員下去驗證。人的 comment 難道不會漏、不會寫錯嗎?所以校正時要把人寫的 golden comment 和各工具沒命中的候選**混進同一個匿名池**,審查員不能知道哪條是人寫的。少了這一步,「golden set 有問題」就只是我們的自我主張。
**記得要公平,而且公平要能被檢驗:**
- 動手前先證明你抓的 diff 跟同業審的是同一份:這個 benchmark 的目錄結構有玄機,別想當然
- 選案規則和判準在讀任何一條 golden comment 之前就寫死在腳本裡
- 你的 judge 要先拿同業的資料去對上游已公布的結果校準,證明沒引入新誤差,順便量出雜訊底線
- recall 的分母不要由我們自己的輸出決定
- 每個階段的產物用檔案系統驗證,不要採信 agent 的自我回報
- clone 用每個 repo 一份共用,不要每個 PR 各一份
- 沒跑到的、跑錯的、以及意外對我們有利的地方,全部寫進報告
其中有一條這次並沒有做到:「clone 用每個 repo 一份共用」。實際是每個 PR 各 clone 一份,同一個 sentry 被 clone 了 10 次,光 clone 就吃掉好幾 GB 磁碟。這只影響成本,不影響結果,但這條是寫給下一次的,不是這一次的紀錄。
重寫版裡那句「目錄結構有玄機,別想當然」是踩出來的。直覺做法是:benchmark 說這是 sentry#77754,那就去抓 sentry/sentry 的 PR #77754 的 diff。我照做了,拿同業的候選清單來對,對不上:他們講的東西有些根本不在我的 diff 裡。回頭翻 benchmark 的目錄才看懂,它為每一個工具各 fork 了一份 PR,工具審的是 fork 裡的那份,base 可能跟上游差很遠。沒發現的話,整份比較就是拿我對 A 的答案去比同業對 B 的答案:分數會有、名次會有、圖會很好看,而且完全沒有意義。修法是把這次審查使用的 fork 釘死(DIFF_FORK_TOOL = "augment"),再拿同業候選清單提到的識別字去 grep 我的 diff,確認我抓到的是 benchmark 提供的案例,而不是直接抓錯上游 PR。這份表重現的是 benchmark 當時提供的資料與計分方式;這項抽查能排除明顯錯版,但不等於逐一證明所有工具 fork 都是完全相同的 bytes。
這條反射後來救了我第二次。有個審查 agent 回報「這份 diff 根本不是它標題說的那個 PR」,我沒直接信,也沒直接否定,去查同業對這個 PR 的候選,全部都在我的 diff 裡。標題錯了,diff 是對的,上游自己的註記也寫著 there is no such PR, it is a mix of many PRs。留下、照常計分。
50 個案例補滿的成果:
先用直覺讀這三個指標:
Precision 越高,代表工具提出的候選中,對上 golden comments 的比例越高;
Recall 越高,代表 golden comments 被工具找到的比例越高;
F1 則把兩者合在一起,任一邊太低都會把分數拉下來。這裡先列 benchmark scorer 的彙總結果,後面再拆它的計數方式。
| 工具 | precision | recall | F1 |
|---|---|---|---|
| cubic-v2 | 56.8% | 67.2% | 61.5% |
| augment | 47.4% | 59.1% | 52.6% |
| greptile-v4-1 | 40.6% | 48.9% | 44.4% |
| coderabbit | 26.3% | 56.9% | 35.9% |
| claude-code | 32.9% | 39.4% | 35.9% |
| nathan-code-review | 13.0% | 73.0% | 22.0% |
最後一名 🫣。
而且不是差一點,是 precision 只有第二差的一半 🤯🤯🤯
第一反應當然是想找藉口 🤣
但先看一下數字的形狀:recall 最高,precision 最低。這至少不是單純「找不到問題」;更明顯的訊號是它講得很多。至於多講的內容是有效發現還是雜訊,光看原始分數還不能確定。
50 個 PR,137 條 golden comments,我的工具講了 775 件事。
再看一眼 benchmark 的 precision 定義:
這套 scorer 會把每條 golden comment 最多計為一次命中;沒有對上 golden 的候選,原則上會被列為 false positive。我的工具提出了 775 條候選,卻只有 137 條 golden comments,因此發言量遠高於標準答案能涵蓋的範圍。
彙總時共有 100 個 true positive、671 個 false positive;另有 4 條候選重複命中已被其他候選命中的 golden,依 scorer 的配對計數方式不另加 true positive,也不列為 false positive。因此表中的 13.0% 是 100 / (100 + 671) 四捨五入後的結果,不是 100 / 775(上游每個 PR 的公式是用候選總數當分母;這裡是 50 個 PR 的 TP、FP 各自加總後再算,兩種算法差不到 0.1 個百分點:12.97% 與 12.90%)。
這不代表其餘候選都是對的,而是原始評分無法區分「真的說錯」與「找到 golden set 沒有收錄的問題」。這正是下一步要另外量測的地方。
有可能。而且老實說,第一批審查報告回來、還沒計分的時候,它就先回報了一件事:每個 PR 約 10 條 finding,golden 平均不到 3 條,在這套計分方式下,發言量本身就會壓低 precision。說白了,這個 skill 話很多。我沒有改指標,也沒有預先寫任何辯解,先照它的規則跑一遍,拿到那個數字,再來談那個數字在說什麼。順序反了,就變成先射箭再畫靶。所以下一步也不是辯解,是量測。
原始 scorer 的計分方式是:沒有對上 golden comment 的候選,在分數上會被列為 false positive。
我的假設是:那 671 個「false positive」裡,有相當一部分是真的問題,只是寫 golden comments 的 人 沒寫下來。
這兩個假設都可以驗證,而且驗證方式必須盡量壓縮我偏袒自己工具的空間。
做法是這樣的。每個 PR 我蒐集一個池子,裡面放兩種東西:
然後把來源標籤全部拔掉,打散順序,編號 c001、c002……丟給一個獨立的驗證 agent。它拿到真實的 diff 和完整的 repo checkout,逐條判斷:這句話對這份程式碼來說,是不是真的?
驗證員拿不到來源標籤,無法直接知道哪些是人寫的。這是整件事的關鍵。 同一個讀者、同一把尺,同時量「標準答案」和「被標準答案否決的東西」。少了這一步,「golden set 漏了東西」永遠只是我自己工具的自我主張。
池子最後共有 1,395 條 claim:137 條 golden comments,加上六個工具合計 1,258 條未命中候選,涵蓋 50 個 PR。
原始分數把我排在最後一名,而且 precision 只有第二差的一半。
但數字的形狀先顯示的是「講得很多」,不是「找不到問題」:recall 最高、precision 最低。至於那些沒有對上 golden 的候選究竟是說錯,還是找到了 golden set 沒有收錄的問題,不能只靠原始分數判定。
不過「指標有問題」不能只是我的自我主張,所以我做了一個匿名盲測池,1,395 條 claim 已經送進去了。明天公布結果,包括一個我沒預期到的數字:人類寫的 golden comments,有多少條沒通過驗證 🤔。