Agent SDK 每句吃 2–3% 額度:我怎麼從失敗修法追到快取問題
擋掉 system-reminder 的 diff,快取仍只有 15–25%。這次追查讓我分清楚少送內容與沿用快取,也留下不能直接照抄到新版的歷史修法。
這篇的段落(6)
用 Claude Agent SDK 跑多代理系統,每則訊息只有幾個字,卻吃掉 2–3% 的五小時額度。同一段對話換到 Claude Code CLI,resume 後只有第一則高,後面卻降到 1% 以下。
這是我在 2026 年 3 月遇到的狀況。我花了幾天逆向約 12MB 的 minified JavaScript,先找到重複注入檔案 diff 的機制,以為擋掉它就好了,結果改完快取數字還是沒變好。那次失敗讓我分清楚,少送了一段內容,和原本的大段內容有沒有沿用快取,要分開量。
版本:SDK v0.2.76、Claude Code v2.1.71(2026 年 3 月)。V2 session API 後來在 SDK 0.3.142 移除,文中的 V2 修法是當時的做法。
幾個字的訊息,為甚麼需要處理那麼多 tokens?
因為每次送出的還有前面的對話、工具定義和環境資訊。 你畫面上打的字,只佔其中一小段。
當時我的程式每送一則訊息,就重新呼叫 V1 query() 並帶上 resume,每次都會啟動新的 cli.js 程序、從 JSONL 對話紀錄還原狀態再組裝請求;CLI 互動模式則是同一個程序持續存活。
這個差異給了我追查方向。Prompt cache 存在服務端,只要送出的可快取前綴相同、還在保存時間內並符合命中條件,換一個本機程序仍可能命中,所以真正要查的,是重建過程把請求內容改了哪裡。
重建狀態時,到底是甚麼改動了請求?
resume 重建檔案狀態時,可能讓檔案 diff 再次塞進請求。
當時的 cli.js 有張 readFileState 記錄讀寫過的檔案,執行環境會比對檔案時間與內容,必要時把變更 diff 包進 <system-reminder> 送給模型。那個版本從歷史重建追蹤資料時,會收集無 offset、無 limit 的完整 Read,以及帶路徑和 content 的 Write;每次呼叫並 resume 都可能重跑這段重建。
這說明了重複注入的來源,但還不算全部額度消耗的原因。依 Claude API 官方快取文件,前綴按 tools → system → messages 組成,變動點以前符合條件的快取仍可能沿用,變動點之後才可能要重新處理,不能把任何差異都說成整包歸零。即使 diff 只多幾百 tokens,改到前面大段前綴影響就大,只追加在最後則前面仍可能命中;如下圖,程序重開只是線索,得核對實際請求的普通輸入、讀取與建立。
把可疑的注入關掉,有改善嗎?
沒有,快取讀取比例仍停在 15–25%,大量重建並未消失。
我先試了兩件事:一是設 CLAUDE_CODE_REMOTE=1,利用該版本的內部條件減少 git status 注入;二是在 resume 前處理 JSONL(下稱 sanitizer),避開部分檔案追蹤的收集條件。後者非官方支援且會改動對話歷史,細節與代價另寫在〈擋掉 diff,為甚麼還沒省到額度?〉。
我在同一個 session 連續送短訊息(間隔約 11–90 秒),留下這組比較:
| 紀錄條件 | cacheRead | cacheCreation | 原紀錄的快取比例 |
|---|---|---|---|
設定 CLAUDE_CODE_REMOTE=1 | 8,624 | 48,661 | 15% |
設定 CLAUDE_CODE_REMOTE=1 | 11,945 | 45,499 | 21% |
| 未設定該變數 | 15,294 | 45,724 | 25% |
| 未設定該變數 | 15,294 | 45,735 | 25% |
讀取快取維持在 8.6K–15.3K,建立快取仍約 45K–49K。我能下的判斷很具體,這次修法沒有用量改善的證據,不能因為 diff 少了,就宣稱成本問題修好了。
在這幾次請求裡,關掉可疑來源沒讓大量重建消失,這逼我改試讓程序持續存活。
後來改用 V2,數字怎麼變?
改成持續存活的 V2 session 後,快取讀取比例在第 4 則升到 84%。
v0.2.76 有實驗性的 unstable_v2_createSession(),讓單一 cli.js 程序常駐並走 stdin / stdout 接收後續訊息,不必每則重新啟動並還原。當時 V2 選項不夠用,為接回原系統,我還直接修補 sdk.mjs,轉發工作目錄、設定來源、thinking 與執行限制、額外參數及 MCP 連線資料,也增加了升級維護成本。
切換後的四列紀錄:
| 訊息 | cacheRead | cacheCreation | 原紀錄的快取比例 |
|---|---|---|---|
| 第 1 則 | 11,689 | 45,974 | 20% |
| 第 2 則 | 69,352 | 46,108 | 60% |
| 第 3 則 | 127,149 | 46,208 | 73% |
| 第 4 則 | 402,087 | 78,011 | 84% |
第四列約等於 402087 / (402087 + 78011),是快取讀取占比,不是費用比例。當時五小時額度消耗也跟著降下來。
這次比較支持當時的整體改法有幫助,但未逐項隔離程序生命週期、選項修補和請求組裝的影響,也未定位唯一造成失效的字元。下圖對照三種模式:CLI(close 再 resume 才重建)與 V2 皆為單一程序常駐,V1 query() 則是每則重新 spawn。
今天遇到相同問題,還要 patch V2 嗎?
不用,V2 已移除;先看目前 SDK 支援的多輪方式,再量你自己的呼叫路徑。
2026-10-29 查閱的 官方 V2 文件與 SDK CHANGELOG都寫明:V2 自 0.2.133 棄用,0.3.142 移除。官方改建議使用 query(),多輪可傳入 AsyncIterable<SDKUserMessage>;接續已保存對話則使用 options.resume。因此「V1 必然 miss,V2 是唯一解」已不能當成選型建議,當年的 patch 也不能只換個套件版本繼續套(同版本的逆向追查見〈我逆向了 Claude Code v2.1.71,找到 token 消耗暴增的根因〉)。
若你仍是每輪重新呼叫並 resume,可先用小型對話,固定模型、工具、工作目錄和訊息間隔,比較這條路徑與官方支援的持續輸入方式,記下各輪實際用量;同時確認工具權限、MCP 與中斷恢復仍正常,別只看命中率變漂亮(如〈AI 幫你寫的測試全綠,為甚麼上線還是壞掉?〉所談,只看單一指標容易漏掉問題;要把本文判準用在工作上,可參考課程與工程手冊的適用範圍)。
怎麼判斷這次真的修到了?
同時看讀取、建立與普通輸入,並比較完成相同任務的結果。
Claude API 的輸入用量可以這樣讀:
全部輸入 = input_tokens
+ cache_creation_input_tokens
+ cache_read_input_tokens
快取讀取比例 = cache_read_input_tokens / 全部輸入SDK 的彙總欄位名稱可能不同,要按你所用版本的定義對應,避免把逐輪值和累計值混在一起。比例低,可能是第一輪、剛換工具、超過保存時間,或任務本來就一直在讀新資料;比例高,也可能只是反覆處理沒幫助的舊內容,因此沒有一條「低於 50% 就是在燒錢」的通用線。
那次追查裡,逆向幫我找到可疑機制,A/B 數據卻逼我承認第一版修法還沒解決問題。下次遇到短訊息異常昂貴,先留一組固定條件的前後用量:改完之後,大量建立快取的情況,有沒有在後續幾輪下降? 這個答案,比「我關掉了幾個注入來源」更能幫你決定要不要保留修法。
