九成,還有賣出它的那句話
Spotify 的 Claude Code 設定來自 Dimitri Mazmanov 的一篇部落格文章,他是 Spotify 的產品經理,程式碼放在 GitHub 上:他說他們團隊使用的這套設定,把他的 Claude Code token 用量砍了 90%。文章第一句話就撐起了整個論點:AI 寫程式 agent 做的事,大多不是思考,而是 I/O。為了回答關於一個方法的問題而讀五個檔案,或是照著旁邊二十個測試檔再寫第二十一個,都會燒掉數千個 token,卻幾乎沒有任何推理。
一則推文靠一句話把這篇文章推到一百五十萬次瀏覽:寫下來的規則只是建議,封鎖不是。Hacker News 把它放上首頁,271 分、173 則留言,其中一半的留言都在問同一個問題:90% 是什麼的 90%?Spotify 自己加的但書是「批次讀取(bulk read)」。這篇文章在純 Claude Code 裡重建這套設定,再實際測量,讓你確切知道那個但書到底換到了什麼。
Portal 到底是什麼(以及為什麼你跑不了)
Portal 不是路由器。它是 Spotify 的內部開發者入口,建立在 Backstage 之上,也就是 Spotify 開源出來的開發者平台。裡面相關的功能叫 Modes:依 Spotify 的定義,一個 mode 是跑在暫時性執行環境上的宣告式 agent,大致上就是給 agent 用的 AWS Lambda。你寫好指令、挑一個模型、設定 temperature、掛上工具。Mazmanov 做了兩個:一個批次讀取器和一個程式碼寫手,都跑在 temperature 0.2 的 Gemini Flash 上,所以兩個都刻意做得又便宜又無趣。
路由邏輯放在一個叫 Shunt 的 Claude Code plugin 裡。它公開在 GitHub 上,兩道指令就能安裝。但第二步是把 Portal 的命令列工具向你的 Portal 實例做驗證,而你根本沒有 Portal 實例。plugin 是公開的,它委派出去的那個東西不是。
所以有用的做法是忘掉 plugin,留下模式。用他自己的話說,它有三層:hook、script、skill。每一層在純 Claude Code 裡都有對應的做法,這正是本文接下來要重建並測量的東西。
第一層:封鎖而不是詢問的 hook
這套設定的第一版,是專案指令檔裡的一段路由規則。用 Mazmanov 的話說,它「勉強算有用」:規則只是建議,沒有強制力,Claude 可以無視它們,而且每個專案都需要自己的一份。第二版把決定從 prompt 移到工具層,用兩個 hook 實作,都在工具呼叫之前觸發。一個盯著每一次檔案讀取,另一個盯著 shell。
讀取 hook 是 33 行的 bash。它從環境變數讀一個門檻值,預設 350 行,然後放行三種情況:
- 帶 offset 或 limit 的讀取,因為 Claude 已經知道自己需要什麼。
- 不存在的檔案。
- 行數等於或低於門檻的檔案,因為委派一個小東西的成本比直接讀還高。
其他一律封鎖,並附上一段 Claude 會讀到、用來代替檔案內容的訊息:這個檔案有幾行、請使用批次讀取器 skill,如果你需要精確內容來做編輯,只重讀那一段就好。shell hook 則攔截對大檔案執行的 cat、head、tail、less 和 more。帶管線的指令會放行,因為接到 grep 的管線是有目標的讀取。
Mazmanov 關於分層的論點才是重點:就算 Claude 從來沒讀過 skill 的描述,hook 仍然會擋下那次昂貴的讀取。skill 讓轉向更順暢;封鎖讓它真的生效。有一個細節之後會用到:這個 script 回傳的是一個名為「block」的頂層 decision。記住這個字。
第二、三層:worker 和它們的數字
兩個 worker 就是兩段 prompt。讀取器:「你是精確的程式碼分析員,只輸出結構化的條列,不打招呼、不寫散文,每一條都以確切的名稱、型別或行號開頭。」寫手:「完全比照既有的模式、命名和風格;只輸出程式碼,不加程式碼區塊標記,不加說明。」少了最後那一句,模型會把所有東西包在 Markdown 裡,Claude 還得再解析一次。
兩個 script 把它們包起來。Bulk-read 接收一個問題和幾個檔案路徑,然後送出去。Code-write 接收一份規格和一個參考檔案,把結果直接寫到磁碟,所以 Claude 從來不會看到產生出來的程式碼。每一次委派都是一次性的:後續追問會把檔案再送一遍。這在關鍵之處是免費的,因為整批內容送到 worker 那裡,從來不會進入 Claude 的 context。
第三層是一個 skill 檔案,告訴 Claude 什麼時候該委派:超過 350 行的檔案、跨三個以上檔案的問題、大型 diff。它的最後一行是「編輯前先驗證行號」。
Spotify 的表格涵蓋一個 Java monorepo 和三種讀取情境。單一檔案的案例從大約 34,000 個 token 降到 6,000 以下,三列的平均節省是 90%。
| Spotify 的基準測試 | 數值 |
|---|---|
| 程式庫 | 1 Java monorepo |
| 情境 | 3 個,全是批次讀取 |
| 單一檔案案例,之前 | ~34,000 tokens |
| 單一檔案案例,之後 | < 6,000 tokens |
| 平均節省 | 90% |
| Token 估算方式 | 每個 token 4 個字元 |
| 寫手的強制機制 | 無(只有讀取者有 hook) |
Spotify 自己印出了兩個但書:token 是以每四個字元一個 token 估算的,而寫手完全沒有任何強制機制。所以那個 90%,是三列批次讀取在估算輸入 token 上的平均值,沒有任何品質評分,也沒有任何一個金額數字。這就是要拿來測的數字。
重建(上):帶 model 欄位的 subagent
Claude Code 內建一個 Explore subagent,而從最近的某個版本開始,它會繼承你的主模型,上限到 Opus,所以「便宜的讀取器」已經不便宜了。文件用一句話給出解法:一個名為 Explore 的專案 subagent 會覆寫內建的那個,並保留自己的 model 欄位。一個 markdown 檔、一段 front matter,model 那一行寫 Haiku。這就是批次讀取器。寫手是第二個檔案:model 設 Sonnet,工具只有 Read 和 Write,內文則直接貼上 Spotify 自己的指令。
它之所以有效,是因為每個 subagent 都從一個全新、隔離的 context window 開始。它讀到的東西落在那裡,而不是主對話裡。這就是 Spotify 的一次性委派,只是少了網路往返。
接著是沒人料到的那部分。這週在 Reddit 上,有人叫 Fable 去開 Opus agent,它卻開了五個 Fable agent:三十分鐘內用掉 73% 的每週額度。最高票的回答是一個 hook,在模型派出 subagent 時執行,強制它明確選擇模型,並告訴它挑能完成任務的最便宜的那一個。這就是第三個 hook:它盯著 Agent 工具,沒有指定模型的呼叫會被拒絕,只回一句話:「請明確選擇模型」。
Spotify 的 skill 變成專案指令檔裡的三行:超過 350 行的檔案交給 explorer,樣板程式碼交給寫手,每一次 agent 呼叫都要設定模型。粗暴的選項也存在:兩個環境變數,把所有 subagent 強制鎖在同一個模型上。老實說的限制是,讀取器是比較便宜的模型,所以它回傳什麼,主模型就只知道什麼。實測那一節會談到這一點。
重建(下):用現行 hook 格式的 deny
還記得「block」這個字嗎。Spotify 的 script 回傳一個頂層 decision,但現行的 Claude Code 文件說的不一樣:PreToolUse hook 把決定放在一個 hook 專屬的輸出物件裡,欄位叫 permissionDecision。它有四種結果:allow、deny、ask 和 defer,這裡要的是 deny。hook 寫在 reason 裡的內容會顯示給 Claude 看,而如果有好幾個 hook 同時回答,deny 勝出。
重建後的讀取 hook 保留同樣的 350 行門檻和同樣的三個例外,但不再回傳「block」,而是回傳一個 deny,reason 裡點名 Explore subagent 和該用的模型。文件明白寫出的一個陷阱:你設定裡的 hook 也會在 subagent 裡執行。沒有例外機制的話,Haiku 讀取器自己的讀取也會被 deny,永遠做不了它的工作,所以 script 會檢查呼叫者是誰,放行那兩個 worker。
接線只需要一個設定檔和三個 matcher:Read、Bash 和 Agent,各自指向自己的 script,門檻值則設成環境變數。實際上,讀取一個 1,090 行的檔案會回傳一個錯誤,附上寫好的那句話:把這次讀取委派給 explorer,模型用 Haiku。委派隨即發生:主模型先數行數,用 Haiku 模型呼叫 explorer,條列回來了,每一條都帶行號。三個回合,44 秒。
Spotify 的說法站得住:分層意味著系統會優雅降級。指令負責路由,hook 是安全網。不過這張網有個洞。想讀整個檔案的模型可以用 offset 和 limit 分段讀,這會放行;或者用 sed 範圍透過 shell 把檔案倒出來,這個 hook 抓不到。實測兩者都算進去。
實測
測試用的程式庫是 Fastify,Node 的 web 框架:294 個檔案,其中 63 個超過門檻。兩個一模一樣的 clone,唯一的差別是 .claude 資料夾和規則檔。主模型 Opus,也就是 CLI 的預設;讀取器 Haiku;寫手 Sonnet。單一 prompt 的 session,不追問,每個情境在每種設定下各跑兩次,總共十六次。四個情境和 Spotify 的一樣:一個大檔案的 exports、三個檔案以及它們如何互相呼叫、一個原始碼檔案對照它的測試,還有從既有測試檔寫出一個新的測試檔並存到磁碟。
| 情境 | 主 context,無設定 | 主 context,有設定 | 變化 | 總成本,無設定 | 總成本,有設定 | 變化 | 耗時,無設定 | 耗時,有設定 | 變化 |
|---|---|---|---|---|---|---|---|---|---|
| 一個大檔案 | 88,693 | 51,552 | -41.9% | $0.139 | $0.087 | -37.8% | 22 s | 44 s | +100.8% |
| 三個檔案 | 357,166 | 73,440 | -79.4% | $0.581 | $0.218 | -62.4% | 52 s | 129 s | +149.9% |
| 原始碼對測試 | 303,808 | 114,136 | -62.4% | $0.451 | $0.374 | -17.1% | 93 s | 125 s | +33.6% |
| 新的測試檔案 | 143,432 | 121,818 | -15.1% | $0.295 | $0.302 | +2.6% | 66 s | 87 s | +32.2% |
| 四個全部 | 223,274 | 90,236 | -59.6% | $0.366 | $0.245 | -33.1% | 58 s | 96 s | +65.3% |
主 context,也就是昂貴的那個模型實際看到的 token,是第一個重要的欄位。在三個檔案的問題上它降了 79%,四個情境合計降了 59.6%。帳單降得比較少,整體大約三分之一,因為讀取器自己的 token 不是免費的,而在寫測試的小任務上,帳單反而多了 2.6%。時間則往另一個方向走:沒有這套設定平均 58 秒,有的話 96 秒。委派每一次都比較慢。
品質是兩種設定差最多的地方。沒有這套設定時,主模型透過 shell 倒出檔案,沒有行號,用手數,結果整篇的行號都是錯的:一個被回報在第 149 行的函式,實際在第 156 行。有這套設定時,四次裡有一次把讀取器的摘要照單全收,帶了三個錯誤的說法,其中一個是讀取器說路由檔從來沒呼叫過的函式,其實有呼叫,在第 553 行。四個產生出來的測試檔全部通過,而 deny hook 在十六次執行裡觸發了零次:只要規則檔在,主模型每一次都會自己檢查行數並委派。
追蹤紀錄裡還有一件事:沒有規則時,主模型從來沒用過 Read 工具。它全部透過 shell 讀,而 shell 的範圍讀取花同樣的 token,又能穿過 hook。所以 Spotify 的表格說 90;這一張說 context 是 60,帳單是三分之一。
留下封鎖。別指望帳單降九成。
有三樣東西值得留下:一個跑 Haiku 的專案 Explore subagent、指令檔裡的三行規則,還有當安全網的讀取 hook。測得的結果是主 context 少 60%、帳單少三分之一、實際耗時多三分之二。
在信任 hook 之前,先修兩件事。hook 會在 subagent 裡執行,所以要把你的 worker 排除在外。還有 shell 的洞:bash hook 抓得到 cat、head 和 tail,但範圍讀取會穿過去,而主模型在沒有規則時用的正是這一招。
Spotify 自己說的限制依然成立。你不能委派編輯,也不能委派推理;worker 漏掉了一個 Claude 幾秒鐘就抓到的執行緒安全 bug,而且每一次委派都是一趟往返。Hacker News 的懷疑者也有一點說對了:輸入 token 不是帳單的主體。輸出 token 更貴,而這套設定對它們完全沒有幫助。
誰能省,取決於你怎麼付費。走 API 的話,省三分之一。用 Pro 或 Max 方案的話,同一套設定動的是你的五小時和每週額度,不是錢。也要留意門檻值:低於門檻時,委派花的比省的多,那個 45 行的測試案例就是證明,多了 2.6%。最後,八次執行裡有兩次,讀取器的摘要帶著錯誤,是主模型的驗證回合抓到的。跳過那個回合,這些錯誤就會進到你的編輯裡。
AIDive