TL;DR
- Spotify 所說的「90%」,是在 Java monorepo 上以估算輸入 token 衡量大量讀取情境後得出的平均值。文中沒有給出金額數字,也沒有品質分數。
- 用純 Claude Code 重建(一個 PreToolUse hook、兩個便宜的 subagent、一條三行的路由規則),在 Fastify 上跑四個情境、共 16 次執行,這個模式讓主模型的 context 減少 59.6%,總成本減少 33.1%。
- 在實測的執行中,deny hook 一次都沒有觸發。節省來自 CLAUDE.md 裡的路由規則;hook 是模型哪天無視規則時的安全網。
- 委派每次都比較慢,平均耗時增加 +65.3%。在規模較小的撰寫測試情境中,成本還多了 2.6%。
- 兩個陷阱:hook 在 subagent 內也會觸發,所以要把你的 worker 排除在外;而
sed -n範圍讀取會直接穿過只盯著cat、head、tail的 hook。 - Haiku 讀取者的摘要,在八次委派執行中有兩次出現事實錯誤。請保留主模型的驗證回合。
測量結果說明了什麼
Spotify 的外掛 Shunt 透過兩種「模式」把大量工作從主模型分流出去:bulk-reader 和 code-writer,範例中兩者都跑 Gemini 2.5 Flash,model 欄位可以接受 Portal 實例中設定的任何模型 s1。路由分三層。check-file-size hook 在每次 Read 時觸發,攔下超過可設定行數門檻(預設 350)的檔案,並引導模型改用 bulk-reader skill;check-bash-read hook 則攔截對大檔案使用的 cat、head、tail、less 和 more,而串接管線的指令會放行 s1。hook 原始碼和兩個 skill 都在公開 repo 裡 s2,大小檢查也能單獨閱讀 s3。模式本身存在於 Portal,也就是 Spotify 的內部平台,因此這個外掛照原樣出貨時無法在公司外執行 s4。
基準測試的說法證據薄弱。Spotify 在 Java monorepo 上測了四個情境,「衡量 Claude 直接讀檔與讀取 bulk-reader 摘要各自會消耗的 token」,並回報大量讀取的平均節省約 90% s1。文章本身也承認:寫程式的情境更難用 token 衡量,worker 的摘要沒有可靠的行號,所以編輯無法委派,worker 漏掉了主模型抓到的一個細微的執行緒安全 bug,而且每次委派都會增加 10 到 30 秒,Portal 還把單次呼叫限制在 30 秒 s1。Hacker News 的討論串對 90% 到底在衡量什麼,也提出了同樣的疑問 s7。
重建版本把 Portal 模式換成兩個 Claude Code subagent,由定義檔固定模型:跑在 Haiku 上的 Explore 讀取者,以及跑在 Sonnet 上的 code-writer s6。deny 是一個 PreToolUse hook,以目前的 hook JSON 格式回傳 deny 決定 s5。受測 repo 是 fastify/fastify,commit ac28821d,294 個 .js/.ts 檔案、78270 行、63 個檔案超過 350 行。session JSON 回報的模型 id:主對話 claude-opus-5[1m],讀取者 claude-haiku-4-5-20251001,寫入者 claude-sonnet-5。每個情境每種設定跑兩次,共 16 次實測,皆為單回合 claude -p session,只套用專案設定,讓兩邊的 system prompt 完全相同 s5。
這個模式贏的地方:S2 是跨 lib/route.js(691 行)、lib/reply.js(1090 行)和 lib/request.js(398 行)三個檔案的呼叫圖問題,主 context 平均從 357165.5 token 降到 73440.0(-79.4%),成本從 0.5810500000000001 USD 降到 0.21823605000000001 USD(-62.4%)。沒贏的地方:S4 是參照一個 19 行的範例,為 45 行的原始碼寫測試,沒有委派時成本是 0.29465575 USD,委派後是 0.3022213 USD(+2.6%),原因是 Sonnet 等於第二份完整 context(cache 讀取 token 從 13004 增加到 18729),而主模型仍然重讀了產生的檔案並跑了測試 s6。
比百分比更重要的有三項發現。第一,在 16 次實測中 hook 一次都沒觸發:有了 CLAUDE.md 裡的路由規則,主模型自己跑 wc -l 然後委派。唯一觀察到的 deny 出現在沒有 CLAUDE.md 的驗證執行中,模型被拒絕 Read,接著被拒絕 cat -n,最後只靠 grep -n 回答,完全沒呼叫 Agent 工具 s5。第二,hook 會在 subagent 內執行:在兩次被捨棄的執行中,Haiku 讀取者自己被大小檢查擋下,退回用分段的 offset/limit 讀取。解法是在 hook 最上方加一段 case "$agent_type" in Explore|code-writer) exit 0 的豁免,欄位名稱以記錄下來的 stdin 為準 s5。第三,在基準設定中,主模型根本沒用過 Read 工具。它每個檔案都是透過 Bash 讀的(cat -n、sed -n '1,200p'、sed -n '200,560p'),所以只監看 Read 的 hook 什麼都抓不到,而只比對不帶管線的 cat、head、tail 的 Bash hook,仍然會放過 sed -n 範圍讀取 s3。
品質是用 grep 對照原始碼檢查的。基準版本在一次 S2 執行中產生了錯誤的行號(它用 sed -n 倒出沒有行號的檔案,再用手數)。委派版本在另一次 S2 執行中出現三個事實錯誤,在一次 S3 執行中出現兩個,全都追溯到照單全收 Haiku 的摘要:buildRequest/buildReply 的呼叫者錯誤、一個未匯出的常數被列為 export、一個已涵蓋的 iterator 被標成未涵蓋。主模型花輸出 token 用 grep 重新驗證的地方(S3,輸出 token 從 4534 到 4738),答案是正確的 s6。在撰寫測試的情境中,所有產生的檔案都通過:12/12、6/6、7/7 和 10/10 個測試。一則 Reddit 回報展示了相關的失敗模式:沒有東西固定模型時,主模型會用錯誤的模型去啟動 worker s8,而 require-model hook 和 agent 檔案裡的 model: 欄位正是用來防範這一點。
測量數據
每個格子是 2 次執行的平均。「Main context」是整個 session 中計費給主模型的 input + cache_creation + cache_read token,也就是可以和 Spotify 所說「主 context 的 token」相比的數字。A = 純 Claude Code,B = hook + subagent + CLAUDE.md 規則。
| 情境 | main context A | main context B | 變化 | main output A | main output B | 變化 | total cost A | total cost B | 變化 | duration A s | duration B s | 變化 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| S1 | 88693.0 | 51551.5 | -41.9% | 1424.5 | 1060.5 | -25.6% | 0.13910675 | 0.08653685 | -37.8% | 21.817500000000003 | 43.799499999999995 | +100.8% |
| S2 | 357165.5 | 73440.0 | -79.4% | 3835.0 | 2555.5 | -33.4% | 0.5810500000000001 | 0.21823605000000001 | -62.4% | 51.637 | 129.036 | +149.9% |
| S3 | 303807.5 | 114135.5 | -62.4% | 6192.0 | 4636.0 | -25.1% | 0.451037 | 0.3738534 | -17.1% | 93.321 | 124.64099999999999 | +33.6% |
| S4 | 143431.5 | 121818.0 | -15.1% | 5275.5 | 3340.0 | -36.7% | 0.29465575 | 0.3022213 | +2.6% | 65.7125 | 86.857 | +32.2% |
| 四項合計 | 223274.375 | 90236.25 | -59.6% | 4181.75 | 2898.0 | -30.7% | 0.366462375 | 0.2452119 | -33.1% | 58.122 | 96.08337499999999 | +65.3% |
測試流程:兩份位元組完全相同的 fastify/fastify ac28821d 淺層 clone;repo-shunt 只加了 .claude/(設定、兩個 agent 檔案、三個 hook)和一條 CLAUDE.md 路由規則,其他什麼都沒動。每個 session:claude -p "<prompt>" --output-format json --setting-sources project --strict-mcp-config,搭配空的 MCP 設定、不帶 --model、逾時 600 秒。四個 prompt 在兩邊完全相同:S1 是 lib/reply.js 的 export,S2 是跨三個 lib 檔案的呼叫圖,S3 是 lib/hooks.js 的方法與 test/hooks.test.js 的涵蓋範圍對照,S4 是仿照 test/noop-set.test.js 撰寫 test/head-route.test.js。數字取自 session JSON 的 modelUsage 和 total_cost_usd,未經四捨五入。答案用 grep 對照原始碼檢查,產生的測試以 node --test 執行。
週一就動手
- 在你的 repo 上跑
wc -l,數一數超過 350 行的檔案有幾個。如果數量接近零,就在這裡停下:設這個門檻是因為在小檔案上,委派的成本比省下的還多。 - 在 CLAUDE.md 加一條三行的路由規則:超過門檻的檔案交給讀取者 subagent,照著既有模式寫的程式碼交給寫入者 subagent,除錯和架構留給主模型。在測量中,這條規則完成了所有工作。
- 建立
.claude/agents/Explore.md,frontmatter 寫model: haiku,以及.claude/agents/code-writer.md,frontmatter 寫model: sonnet,讓 worker 的模型固定在檔案裡,而不是交給 orchestrator 決定。 - 把 Read 上的 PreToolUse hook 當作安全網寫起來,以目前的 hook JSON 格式回傳 deny 決定,並讓它的開頭幾行在
agent_type是你的 worker 之一時直接 exit 0。 - 把 Bash hook 擴大到
cat、head、tail之外:比對大檔案上的sed -n範圍和cat -n,讓帶管線和 grep 的指令通過。 - 用
claude -p --output-format json,針對一個真實問題分別在有和沒有.claude/資料夾的情況下各跑一次,比較total_cost_usd和duration_ms,不要只看輸入那一欄。 - 在信任讀取者的摘要之前,先用 grep 對照原始碼檢查兩個委派的答案;把主模型的驗證回合當成成本的一部分來編列預算。
- 也測一次多回合的 session:單回合的結果顯示,主 context 在委派時是 50k 到 119k token,沒有委派時是 84k 到 414k,所以第二個問題應該會以更低的成本起步,但這一點沒有實測。
延伸閱讀
- 在從部落格文章複製 hook 之前,先讀官方參考文件裡的 hook 格式和
agent_type欄位;deny 的形式和 stdin 上的欄位,正是讓 subagent 豁免得以成立的關鍵 s5。 - subagent 文件涵蓋了
modelfrontmatter 欄位和工具限制,這就是讓讀取者保持唯讀又便宜的方法 s6。 - Spotify 自己的「What doesn't work」一節是整篇文章最有用的部分:不能委派編輯(摘要裡沒有可靠的行號)、不能委派推理(漏掉的執行緒安全 bug)、一趟來回要 10 到 30 秒 s1。
- Shunt README 展示了三層結構(hook、腳本、skill),以及告訴模型何時該委派的 skill 文字;值得改編的是 skill 的文字,而不是 hook s2。
- Portal 模式是疊在模型加 system prompt 之上的設定層;同樣的概念可以對應到帶有
model欄位的 Claude Code agent 檔案 s4。 - Hacker News 討論串是測量方式的疑問最早被提出的地方,也是檢驗任何省 token 說法時該問哪些問題的好清單 s7。
- 一個 Reddit 討論串記錄了 orchestrator 用自己昂貴的模型啟動了五個 worker;請把模型固定在 agent 檔案裡,如果想要硬性保證,就 deny 沒有
model欄位的 Agent 呼叫 s8。
來源
- Portal by Spotify cut my Claude Code token usage by 90%, Spotify Engineering。為什麼值得讀:原始的說法、三層設計,以及一段坦誠、削弱了標題說法的限制章節。
- Shunt plugin (spotify/portal-ai-plugins), GitHub。為什麼值得讀:真正的 hook、腳本和 skill 文字,短到可以從頭讀完。
- check-file-size hook source, GitHub。為什麼值得讀:用幾行 shell 寫成的 350 行檢查,是你自己寫 deny 時的範本。
- Portal Modes documentation, Spotify。為什麼值得讀:了解「模式」是什麼,就能明白它為什麼對應到 subagent 檔案。
- Claude Code hooks reference, Anthropic。為什麼值得讀:目前的 deny 格式和 stdin 欄位,包括用來辨識 subagent 的那一個。
- Claude Code subagents, Anthropic。為什麼值得讀:
modelfrontmatter 欄位,以及便宜的唯讀 worker 所需的工具允許清單。 - Hacker News discussion of the Spotify post, Hacker News。為什麼值得讀:在有人重新測量之前,就已經提出的「90% 到底在衡量什麼」的疑問。
- Fable spawned five Fable agents instead of Opus (r/ClaudeCode), Reddit。為什麼值得讀:固定
model欄位所能防止的失敗模式。
FAQ
90% 這個數字是錯的嗎?
它只衡量一件事:在大型 Java 檔案的大量讀取情境中,主 context 的估算輸入 token。以同類指標來看,重建版本在讀取情境中得到 41.9% 到 79.4%。它對成本、時間或答案品質都沒有說任何話,文章也沒有這樣宣稱。
我需要 Portal 才能做到嗎?
不需要。路由存在於一條 CLAUDE.md 規則、兩個固定模型的 agent 檔案,以及一個 PreToolUse hook 裡。在 Spotify,Portal 負責提供 worker 模型;在純 Claude Code 裡,一行 model: haiku 就能做到同樣的事。
委派什麼時候反而更貴?
檔案很小的時候。45 行的撰寫測試情境在委派後成本多了 2.6%,因為寫入者是第二份完整 context,而主模型仍然重讀並測試了結果。每一次委派的執行也都比較慢,平均 +65.3%。
為什麼 hook 從來沒有觸發?
因為 CLAUDE.md 裡的路由規則讓主模型在嘗試讀取之前,就先檢查 wc -l 並委派。hook 只在模型無視規則時才有用,而這發生在沒有 CLAUDE.md 的驗證執行中。
AIDive