起因

先講結論:在我自己的 Rails 專案裡,光是幫 RSpec 包一層精簡輸出的 wrapper,跑 60 次 Coding Agent 修 bug 的任務下來,總成本完全沒有下降。要把 wrapper 的輸出格式也寫清楚告訴 Agent,費用才降了大約 17%,而且這個差距在統計上還站不太穩。

我在社群上看到一篇貼文,作者說他靠著「軟體工程習慣」整理指令輸出,讓 Coding Agent 省下 99.75% 的 tokens,而且 Agent 沒有因此多跑幾輪。他順便提到 rtk 這類輸出過濾器,並引用了 Quesma 的文章〈RTK reports huge token savings, but our cost benchmarks disagree〉:過濾器單次看起來很省,但資訊被丟掉以後,Agent 可能要多繞好幾輪才補得回來,最後總成本反而更高。

作者沒有公開做法,推測就是 Unix 的老原則:成功時安靜,失敗只印重點,完整 log 存檔。概念很合理,但「99.75%」這種數字我不想直接相信。剛好手邊有一個用 RSpec 測試的 Rails 專案,就決定自己量量看,分開看兩件事:單次輸出能壓多少,以及整個任務的總費用有沒有下降。

實驗怎麼設計

測試對象是我的個人專案:Rails 8、RSpec,共 6 個 spec 檔、45 個 example。全部通過時輸出只有幾行。

我埋了三種 bug,讓測試失敗:

情境 埋了什麼 失敗數
S1 一個統計函式的 .min 改成 .max 1
S2 兩個檔案各一個邏輯錯誤(方向判斷寫反) 6
S3 service 裡呼叫不存在的方法,NoMethodError 一路傳到 controller 7

分成四組對照,每組、每個情境各跑 5 次:

  • A 原始:直接跑 bundle exec rspec
  • B 聰明版 wrapper:bin/agent-test。成功時印一行;失敗時印位置、錯誤訊息和第一個 app 層的 backtrace,同樣的錯誤只列一次,完整 log 存檔
  • C 只給行號:只留失敗的檔案和行號,不給錯誤訊息,模擬 rtk 那種把資訊丟掉的做法
  • B’ 聰明版加說明:wrapper 跟 B 一模一樣,只是在 prompt 裡先說明它的輸出格式(這組是看完 A/B/C 的結果才加的)

B、C、B’ 三組都用 --disallowedTools 擋掉直接跑 rspec,強制 Agent 只能用 wrapper。

每次都在全新的專案副本裡,用 headless 的 claude -p 跑,模型是 Opus 5.5,prompt 只換測試指令。Agent 結束後,我自己再跑一次原始 rspec 確認真的修好;費用、tokens 和輪數直接讀 claude -p 回傳的數字。

結果一:單次輸出確實變小,但本來就不大

wrapper 能把測試輸出壓縮 27% 到 91%,但原始輸出最多也才 3,568 tokens。下表單位是 tokens,用 Anthropic 的 count_tokens API 精確計算:

情境 A 原始 B 聰明版 B 省下 C 只給行號
全部通過 45 16 64% 10
S1(1 個失敗) 251 173 31% 42
S2(6 個失敗) 1,259 921 27% 204
S3(7 個 exception) 3,568 322 91% 237

輸出本來就小,是因為 rails_helper 預設會濾掉 Rails 內部的 backtrace,而 expected / got 是修 bug 必要的資訊,沒什麼可刪。只有 S3 這種同一個錯誤重複 7 次的情況,去重才省得多。在這個專案裡,99.75% 不可能出現。

結果二:整個任務的成本沒有差別

三組各跑 15 次,全部修好。平均費用都落在 $0.08 左右,精簡輸出並沒有讓任務變便宜。

組別 成功率 平均費用 平均總 tokens 平均輪數
A 原始 15/15 $0.0815 37,520 4.40
B 聰明版 15/15 $0.0826 42,486 4.93
C 只給行號 15/15 $0.0841 39,416 4.67

費用是 claude -p 依定價換算的金額。總 tokens 包含 input、cache read、cache write 和 output。

B 甚至比 A 多了 13% 的 tokens。我用 Welch t 檢定比較各組,費用、tokens、輪數的 |t| 都小於 2,差異不顯著。同一組自己跑五次,費用就會上下差 20% 到 36%,組間的小差距完全被蓋過去。Agent 每一輪光是 system prompt 和對話就有兩三萬 tokens,S3 省下的 3,000 tokens 放進去很難看得出來。

翻 transcript 看到的三件事

數字沒差別,我就去翻 Agent 實際做了什麼。

一、Agent 會先去讀 wrapper 的原始碼。 B 和 C 組的 30 次裡,每一次的第一步都是 cat bin/agent-test。對它來說這是一支沒看過的腳本,先看看在做什麼很合理。問題是聰明版 wrapper 本身有 2.5 KB,大約 700 tokens,而且讀進 context 之後每一輪都會重送。省下的測試輸出,就這樣被這筆成本抵銷掉了。

二、Agent 本來就會自己過濾。 A 組 15 次裡,每一次跑測試都自己加上 | tail -40 或 | head -40。所以所謂的「原始輸出」,其實已經被 Agent 過濾過了。Opus 5.5 已經會這招,wrapper 能加分的空間又更小了。

三、只給行號也沒有讓 Agent 多繞路。 C 組看不到任何錯誤訊息,但 Agent 直接拿失敗的行號去讀 spec 和程式碼,一樣修得好,輪數也沒有明顯增加。所以 Quesma 描述的「資訊不足、多繞好幾輪」,在這種簡單的 bug 上沒有重現。可能要更難的 bug,或是錯誤訊息本身就是關鍵線索的情境,才會看到差別。

加一組 B’:告訴 Agent wrapper 會輸出什麼

既然問題出在 Agent 先去讀 wrapper,我就加了一組 B’。wrapper 完全不變,只在 prompt 裡多一段說明:它接受哪些參數、成功和失敗時各印什麼、完整 log 在哪裡,以及不需要再加 tail。這就是平常會寫在 CLAUDE.md 裡的內容。

行為馬上就變了:讀 wrapper 原始碼的次數從 15/15 降到 1/15,自己加 | tail 的從 15/15 降到 2/15。平均費用比 A 低 17%,但 Welch t 是 −1.40,還不夠顯著,各組的誤差範圍都重疊。差距最大的是 S3,也就是去重最有用的情境:A 平均 $0.068,B’ 只要 $0.046,低了大約三成。

B’ 的總 tokens(37,842)跟 A(37,520)幾乎一樣,費用卻比較低。 原因是費用主要看 cache write 和 output。cache read 的量雖然最大,單價卻只有大約十分之一。B’ 每次平均寫進 cache 的量是 5,703 tokens,其他三組都在 7,200 左右,也就是新進入 context 的東西變少了。如果只比 token 總數,會以為完全沒效果。

我自己的 takeaway

  1. 先量測試輸出有多大。 如果失敗時也只有幾百到一兩千 tokens,花力氣做 wrapper 不太划算。測試數上千、log 很吵、同一個錯誤會大量重複的專案,效果應該會好很多。
  2. 在 CLAUDE.md 或 AGENTS.md 說明輸出格式。 不然 Agent 會先去讀腳本,省下的都貼回去了。
  3. 比費用,不要只比 token 總數。 cache read 的量很大但很便宜,只看總數會被它誤導。
  4. 跑 A/B,而且每組要跑好幾次。 同一個任務的費用會上下差兩三成,只跑一次得不到結論。

這次實驗的限制:專案很小(45 個 example),bug 都很簡單,只測了 Opus 5.5 一個模型,每組也只有 15 次。B’ 是之後單獨跑的一批,跟其他組不是同一時間交錯執行。這些結論最多只能推到「小型 Rails 專案、簡單 bug」的情境。