vatt'ghern jaskier's ballads
本文 2 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

給 agent 換上更完整、維護更好的共用工具,理論上是一次乾淨的升級。GitHub 內部的 benchmark 卻量到相反的結果——同一批審查任務,成本變高,抓到的問題反而變少。

共用工具換上了,Copilot Code Review 卻變差——問題不在工具,在指令

agent 換上更好的工具,理論上該做得更好,這是很自然的直覺。GitHub 把 Copilot code review 原本自製的探索工具,換成與 Copilot CLI harness 共用、維護更完整的一組工具:list_dir、search_file、search_dir、read_code 換成 glob、grep、view。這聽起來像一次沒有懸念的升級——共用工具持續有人維護,功能界面也更統一,理論上只會更好,不會更差。但內部 benchmark 量到的結果剛好相反:同一批審查任務,成本變高,抓到的問題卻變少。工具明明升級了,行為卻退步了,這就是這篇文章要追的謎題——而追下去之後,答案跟「工具本身好不好」幾乎沒有關係,反而牽出一個更根本的問題:換工具的時候,到底還有什麼東西該一起換。

換了更好的工具,審查成本反而上升

GitHub 在文章裡把結果寫得很直接:換工具之後「the cost of reviews was higher and fewer issues were being caught」——審查成本變高,抓到的問題也變少。文章沒有給出換工具當下的精確百分比,但這個方向已經足以構成一個值得追的謎題:工具明明更完整,agent 的表現卻不如從前。真正能拿來對照的具體數字,出現在後面把問題修好之後的 production 量測——調整過指令的版本,比對照組平均審查成本低了大約兩成,同時審查品質沒有掉。下面這張圖先把「修好之後」的落差畫出來,後面幾節再回頭解釋中間到底發生了什麼事。

點擊長條看該情境下 agent 的搜尋行為 · 2 組長條

約兩成的落差——調整指令前後的平均審查成本 0 25 50 75 100 審查成本指數 100 control(對照組) ~80 tuned(調整過指令)
control 對照組:agent 搜得廣、猜可能的路徑、讀得廣,越搜找到越多東西可以再搜,還把多餘的上下文一路帶著走——像在瀏覽整個 repo,不是在查一個 PR。
tuned 調整過指令:從 diff 出發先形成具體的審查問題,路徑不確定就用 glob,要找候選檔案、symbol、呼叫點就用 grep,批次做完便宜的搜尋,只有確定位置時才用 view 讀取。
production A/B 對照:tuned 版本平均審查成本比 control 低約兩成,品質維持不變。原文:「roughly 20% lower average review cost, while maintaining the same review quality」。

兩成的落差不是換工具那一刻就有的——它是後來調整指令之後才量到的改善幅度。真正要回答的問題是:剛換上新工具的那一刻,成本為什麼會先變差,而不是直接變好?

這種「換了更好的東西,結果反而變差」的模式,在自建 agent 系統時其實不算少見——升級一個依賴、換一個 SDK、把自製的 wrapper 換成官方維護的版本,理論上都是風險比較低的動作。這篇文章有意思的地方,不是「工具換錯了」這種簡單的失誤,而是工具本身完全沒有問題,問題出在換工具這件事觸發的連鎖反應——GitHub 花了力氣去追這條連鎖反應,而不是先假設一定是工具的問題就急著換回去,這個追查順序本身就值得借鏡。

文章沒有把「成本」拆解成 token 數、API 呼叫次數還是 wall-clock 時間,通篇只用「higher」「lower」這種相對詞描述,連最後量到的兩成落差也只寫「roughly」,沒有換算成絕對金額或秒數。這種留白不是缺點,反而提醒一件更重要的事:追這個謎題的人真正在意的不是那個數字本身,而是「同一套 benchmark 能不能穩定量出方向對不對」——只要能重複量到「變高」還是「變低」,就足以判斷一次指令調整是不是往對的方向走,不需要先把成本模型拆到每個 token。

先懷疑:新工具是不是「變笨」了

第一個直覺很合理。文章交代了舊工具的行為:舊的自製工具「不是薄封裝」——搜尋目錄或讀一段程式碼的時候,會自動多帶一段周邊上下文回來(原文:the existing review tools were not thin wrappers... they could return the matched or requested lines plus extra surrounding code context)。換句話說,agent 過去不用主動多問一次,就能順便拿到脈絡。

換成共用的 glob、grep、view 之後,這三個工具各自對應舊工具的哪一段職責,可以攤開來看——list_dir 換成 glob,負責找出候選檔案與目錄;search_file、search_dir 換成 grep,負責搜尋符合的文字;read_code 換成 view,負責讀出真正相關的檔案內容。表面上看,新工具的功能跟舊工具一一對得上,但輸出的「順手程度」不一樣:如果新工具讀寫的時候不會像舊工具那樣自動帶上額外脈絡,agent 要嘛得自己多發幾次請求去補齊上下文,要嘛就得習慣用比較少的資訊做判斷。這個假設看起來足以解釋成本上升——工具「變笨」了,agent 只好花更多力氣去補回原本白拿的脈絡。

舊工具新工具負責什麼
list_dirglob找出候選檔案與目錄
search_file、search_dirgrep搜尋符合的程式碼文字
read_codeview讀出相關檔案內容
Copilot code review 從自製探索工具換成與 Copilot CLI harness 共用的一組工具——三個舊工具對應到三個新工具,職責沒有變,變的是輸出的形狀。

共用工具的維護權跟 Copilot CLI harness 綁在一起,代表以後任何一邊修 bug、加功能,理論上另一邊都能一起受益——這大概也是當初決定換工具的部分原因,只是這個好處要建立在指令跟著配套更新的前提上,不然共用出來的只是同一套容易被誤用的介面,兩邊都得各自摸索一次。

新工具的功能對照舊工具沒有少一塊,但「輸出的形狀」不一樣這件事,本身已經足以讓 agent 的行為出現落差——如果原本每次搜尋、每次讀取都會順便帶回一圈上下文,agent 等於是在不知不覺被餵飽的狀態下做判斷;換成新工具之後,這一圈免費的上下文消失了,agent 要嘛自己多問幾次,要嘛在資訊不足的情況下硬做決定。這也是為什麼「新工具是不是變笨了」聽起來是很自然的第一個懷疑——表面上看,像是新工具「少給」了東西,換工具本身看起來就是嫌疑最大的變因。

這個懷疑合理到幾乎可以直接動手修——最省事的做法,是回頭把新工具也加上「自動帶上下文」這個行為,讓 glob、grep、view 模仿舊工具的習慣,一樣在搜尋或讀取的時候順便多回傳幾行周邊程式碼。如果謎題真的停在這一步,故事會變成「新工具功能不夠完整,補齊之後解決」,是一個純粹關於工具規格的問題。但 GitHub 選擇先不急著動工具,而是回頭去看 benchmark 留下的紀錄——這個選擇後來證明是對的,因為真正的落差不在工具回傳了多少行程式碼,而在 agent 決定要搜尋多少次、讀取多少次。

trace 攤開後:agent 在瀏覽全庫,不是在審查 PR

真正推翻「工具變笨」這個假設的,是 benchmark 本身的設計。GitHub 在文章裡強調,他們的內部 benchmark 之所以有用,不是因為只給一個最終分數,而是因為能還原 agent 走過的完整路徑——呼叫了哪些工具、每次輸出量多大、哪裡出錯,而且能看出 agent 是在收窄證據還是在擴大搜尋範圍。把這條 trace 攤開之後,看到的畫面跟「工具能力不足」完全對不上。

agent 表現得像是在瀏覽整個 repo,而不是在查一個 PR——搜得廣、猜可能的路徑、讀得廣,越搜找到越多東西可以再搜,還把多餘的上下文一路帶著往下走。GitHub 的描述是:廣泛探索在「理解這個 repo」這種任務上很有用,但那不是 reviewer 通常審查一個 PR 的方式。問題不是新工具讀不到東西,是 agent 把「熟悉一個陌生 repo」的探索姿態,直接套用到「審查一個已知範圍的 diff」這個任務上——範圍設定錯了,工具再完整也會越用越貴。

「理解這個 repo」這種任務,廣泛探索完全合理——第一次接觸一個陌生專案,agent 本來就需要到處看、確認架構、找出關鍵模組在哪裡,範圍越搜越廣是預期中的行為,甚至是任務要求的一部分。審查一個 PR 剛好相反:diff 已經把要看的範圍框定死了,agent 要判斷的是「這幾行改動有沒有問題」,不是「這個專案長什麼樣子」。同一套探索能力,套用在兩種目標不同的任務上,合理的行為模式完全不一樣——這正是「廣泛探索有用,但不是 reviewer 通常審查 PR 的方式」這句話背後的道理:工具沒有變,變的是它被套用到哪一種任務框架裡。

trace 裡還有一個細節值得放大看:舊的探索姿態不只是「範圍設定錯了」,遇到一次搜尋失敗的時候,反應方式也不一樣。GitHub 提到,重寫後的指令「nudged the agent away from letting a small tool failure turn into a larger exploration loop」——把 agent 推離「一次小小的搜尋失敗,滾成一整輪更大的探索」這種反應模式。也就是說,瀏覽式的行為不只出現在一開始設定任務範圍的時候,也出現在中途出錯之後怎麼補救:舊的直覺是出錯就擴大範圍再試一次,新的指令要求先用簡單的方式重試,而不是把一次錯誤當成理由去搜更廣的地方。

這種「看 trace 而不是只看分數」的除錯方式,對想自己接 LLM agent 工具鏈的工程師來說,是比追蹤一次成本數字更好用的方法——分數只能告訴你這一輪變好還是變差,trace 才能告訴你 agent 到底做了什麼、卡在哪一步、搜尋範圍是變寬還是變窄。搜得越廣,代表工具被呼叫的次數越多,每一次呼叫的輸出又會疊進後續的上下文裡,讓下一輪決策要處理的資訊量跟著變大——這條因果關係雖然文章沒有明講,但跟一般 LLM agent 的成本模型是一致的:多繞一次路,通常就是多一次工具呼叫、多一段要塞進上下文的輸出。GitHub 能夠很快排除「工具變笨」這個假設,靠的正是這一層可觀測性,而不是重新跑一次更精細的分數比較。

拖曳分隔線比較兩種搜尋形狀 · before 與 after

before:像在瀏覽全庫 1 · 搜得廣 2 · 猜可能的路徑 3 · 讀得廣,越搜越多 4 · 帶著上下文往下走 範圍一路變廣,像在熟悉整個 repo 越搜找到越多東西可以再搜 after:從 diff 窄化到證據 1 · 從 diff 出發 2 · grep/glob 窄化 3 · view 讀證據 範圍一路變窄,只在確定位置才讀 start from the diff, narrow, then view

互動圖表

舊工作流程讓 agent 越搜越廣、把多餘上下文一路帶著走;新指令逼它從 diff 出發,先用 grep、glob 窄化,再用 view 讀取確切證據。

問題不在工具,在指令給的直覺

定位到行為之後,結論寫得很直接:「The tools were not the problem. The instructions were.」共用工具給 agent 的,是錯誤的直覺去做一次有效率的審查,而不是工具本身能力不夠。這也解釋了為什麼「換更好的工具」這個直覺會失效——工具規格表看起來越完整,越容易讓人以為升級是自動發生的,但實際上舊工具自動帶上下文,等於替 agent 隱性地做了一部分「該讀多少」的判斷;換成新工具之後,這個隱性判斷消失了,卻沒有人在指令裡明確補上,agent 只好用自己最保守的方式去補——也就是搜更廣、讀更多。工具能力沒有退步,退步的是「工具能力變了、指令卻沒有跟著變」這件事本身。

GitHub 把這個教訓講得更抽象一層:「Tool descriptions and system instructions are closer to API documentation. Unclear API docs can leave a developer confused and lead to inefficient or wrong decisions.」工具描述跟系統指令,本質上更接近 API 文件;文件寫得不清楚,開發者會做出沒效率甚至錯誤的決定,agent 也是一樣。共用工具本身的介面規格沒有變得比較差,但配套的「使用說明」——也就是系統指令——換工具之前是照著舊工具的行為習慣寫的,換工具之後沒有跟著重寫,等於是拿舊版 API 文件去用新版 API:介面對得上,用法的默契全部要靠 agent 自己重新摸索。

把這個結論套回一般的 agent 系統會更清楚:換工具、換模型、換 SDK 版本,都可能悄悄改變 agent 拿到的資訊形狀——不只是「能不能拿到」,還包括「拿到的時候順不順手」。如果指令是照著舊的行為習慣寫的,換底層元件之後最先出問題的,往往不是功能對不上,而是 agent 原本仰賴的隱性默契消失了卻沒人發現,一路要等到 benchmark 的數字掉下來,才會有人回頭去查。這也是為什麼 GitHub 把兩者放在同一句話裡講:工具跟指令是配套的,只換一邊,另一邊沒跟上,出來的行為就是四不像。

把指令改寫成 reviewer 的工作流程,成本降了兩成

找到真因之後,修法不是一次寫完就上線,而是反覆跑同一批範例去校準。GitHub 的說法是:「We could run the same review examples, compare tool traces, update the instructions, and run again.」用同一組審查範例跑過一輪,比對 tool trace,調整指令的用詞,再跑一次,直到 trace 顯示的行為從「越搜越廣」變成「越搜越窄」為止。這個迴圈呼應了前面提到的方法論——benchmark 有用,是因為它能讓每一次指令調整都有東西可以對照,而不是憑感覺猜哪一句話該加、哪一句話該刪。

修法不是換回舊工具,也不是幫新工具加自動帶上下文的功能,而是把系統提示改寫成專屬於審查 PR 的工作流程。文章把這個流程寫成五步:從 diff 出發、形成具體的審查問題;路徑不確定就用 glob,要找候選檔案、symbol、呼叫點就用 grep;把便宜的搜尋操作批次做完,再開始讀檔;只有確定要讀哪個檔案、哪段範圍時才用 view;用批次讀取取代「搜一次、讀一次」交替進行。濃縮成一句話就是:從 diff 出發,先用 grep、glob 窄化範圍,再用 view 讀取確切的證據。

五步之間其實有一個共通的邏輯:先確定「要問什麼問題」,才決定「用哪個工具」,最後才「花力氣去讀」。舊的瀏覽式行為之所以貴,是因為順序反過來——先讀、邊讀邊決定要不要再搜,搜到什麼算什麼,範圍自然收不住。新流程把 view 放在最後一步,等於是把最貴的動作(讀取完整檔案內容)延後到真正確定值得讀的時候才做;glob、grep 這種便宜的搜尋操作則盡量批次做完,減少來回切換搜尋與讀取的次數。這跟前面提到「小失敗不該滾成大探索」是同一套精神:每一步都先問「這一步真的需要嗎」,而不是先做了再說。

切換查看新工作流程的 5 個步驟 · 5 個分頁

從 diff 出發,形成具體的審查問題——起點是這次變更本身,不是整個 repo。
路徑不確定就用 glob 找候選檔案與目錄;要找候選檔案、symbol、呼叫點就用 grep 搜尋符合的文字。
把便宜的搜尋操作批次做完,再開始讀檔案——不要搜一次就急著讀一次。
只有在確定要讀哪個檔案、哪段範圍時,才用 view 讀取——view 只讀已經鎖定的證據。
用批次讀取取代「搜一次、讀一次」交替進行,減少來回次數。
五步工作流程濃縮成一句話:從 diff 出發,先用 grep、glob 窄化,再用 view 讀取確切證據。

五步工作流程濃縮成一句話:從 diff 出發,先用 grep、glob 窄化,再用 view 讀取確切證據

修好後的五步流程:先鎖定 diff、路徑不定用 glob、找候選用 grep,批次搜尋做完才讀檔,確定位置才用 view,讀取也批次進行,不再一搜一讀交替。

這個順序把 agent 的行為從「越搜越廣」反過來變成「越搜越窄」——同樣三個工具,指令告訴它先鎖定 diff 的範圍再動用搜尋,而不是把整個 repo 當成起點。production 量測的結果是:調整過指令的版本,平均審查成本比對照組低了大約兩成,同時審查品質沒有掉。GitHub 沒有給出品質的具體分數,只確認它「維持不變」——成本下降不是靠少做事情省出來的,是靠少走冤枉路省出來的。合理的推測是,一次批次搜完、批次讀完,比起一搜一讀來回交替,能省下的主要是重複繞路的部分——每多繞一次路,就是多一次工具呼叫、多一段要塞進上下文的輸出,而這些輸出到最後不一定都用得上。整篇文章的收尾寫得也很直接:共用工具要能規模化運作,前提是指令跟 benchmark 都要對齊實際任務,不是工具清單越長越好。

對正在接 LLM agent 工具鏈的工程師來說,這篇文章示範的排查順序值得抄——先確認工具本身的輸出正確,再去看 agent 走過的路徑有沒有跟任務對齊,最後才回頭檢查指令的用詞夠不夠具體,而不是反過來,一遇到指標變差就先換工具。把工具描述跟系統指令當成 API 文件來寫,把 benchmark 當成能重複跑、能拿 trace 互相比對的迴圈,而不是一次性的分數——這是整起事件裡最容易被複製到自己專案上的兩件事,也是這篇文章真正想留給讀者的東西,遠比那個「兩成」的數字本身更值得記住。

Take-away:共用工具能不能規模化,關鍵不在工具清單多完整,而在指令跟 benchmark 有沒有跟著任務一起換——下次看到「換了更好的東西反而變差」,先去看 agent 走過的路徑,不要急著怪換掉的那個東西。