AIDive

Spotify 說 Claude Code 省 90% token,我重建實測只有 60%

AIDive · 發布

編碼 agentAI 模型

九成,還有賣出它的那句話

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%。最後,八次執行裡有兩次,讀取器的摘要帶著錯誤,是主模型的驗證回合抓到的。跳過那個回合,這些錯誤就會進到你的編輯裡。

來源

常見問題

Spotify 真的把 Claude Code 的 token 用量砍了 90% 嗎?
Spotify 的 90% 是一個 Java monorepo 裡三種批次讀取情境的平均值,以每四個字元一個 token 的方式估算輸入 token,沒有品質評分,也沒有金額數字。用純 Claude Code 在 Fastify 上重建同一個想法,主模型的 context 減少 59.6%,總成本減少 33%。
Spotify 的 Portal 是什麼?我可以拿它搭配 Claude Code 用嗎?
Portal 是 Spotify 建立在 Backstage 上的內部開發者入口;它的 Modes 功能會在暫時性執行環境上跑宣告式 agent。把 Claude Code 路由到那些 agent 的 Shunt plugin 公開在 GitHub 上,但它要向一個你沒有的 Portal 實例做驗證,所以你只能照抄這個模式,不能執行這個 plugin。
要怎麼讓 Claude Code 把大檔案的讀取委派給比較便宜的模型?
建立一個名為 Explore 的專案 subagent,把 model 欄位設成 Haiku,它會覆寫內建的 Explore agent(內建的那個現在會繼承你的主模型)。在專案指令檔加上三行規則(超過 350 行的檔案交給 explorer、樣板程式碼交給寫手、每一次 agent 呼叫都要設定模型),再加一個掛在 Read 上的 PreToolUse hook,deny 大型讀取,並在 reason 裡點名那個 subagent。
現在的 Claude Code 裡,PreToolUse hook 要怎麼擋下一次工具呼叫?
hook 把決定放在 hookSpecificOutput 物件裡,欄位叫 permissionDecision,有四種可能的值:allow、deny、ask 和 defer。reason 字串會顯示給 Claude 看,如果有好幾個 hook 同時回答,deny 勝出。Spotify 的 script 用的是舊的頂層 decision,名為 block。
把讀取委派給 Haiku subagent,Claude Code 會比較便宜嗎?
走 API 的話,四個情境合計帳單少了三分之一,但讀取器自己的 token 不是免費的:在一個 45 行的寫測試任務上,總成本反而多了 2.6%。用 Pro 或 Max 方案的話,省下的會反映在五小時和每週額度上,而不是金額,而且輸出 token 完全沒有變化。
為什麼 Claude Code 的 hook 會在 subagent 裡觸發?
你設定裡定義的 hook 會對每一個 agent 執行,包括你派出去的 subagent。因此一個封鎖讀取的 hook 也會 deny Haiku 讀取器自己的讀取,除非 script 檢查呼叫者是誰,並放行那些 worker agent。

相關影片