你的 Agent 為什麼越跑越貴:Headroom 的 Context Compression 設計
我在跑一個分析 agent,每輪呼叫三個 tool:搜尋財報、取得歷史數據、查分析師評分。跑到第 15 輪的時候,我注意到 token 用量是第 5 輪的三倍多,但 agent 每輪做的事差不多。
打開 trace 才看清楚:messages 陣列裡,前 14 輪的所有 tool output 全都還在。14 輪 × 3 個 tool × 平均 2,000 token,context 裡有接近 85,000 token 是 tool output history。當前問題只需要最近兩輪的資訊。
這不是 agent 設計的問題,而是 context 管理的結構性問題:tool output 預設就是永久留存。每多跑一輪,context 就多一份 JSON。LLM 每次推理都在讀一遍它根本不再需要的歷史記錄。
讀完精華版(2 分鐘),你會理解:
- Headroom 為什麼只壓最新一輪的 tool output,而不重壓整個歷史
- SmartCrusher 保留「代表性樣本」而不是完整資料的邏輯
- 最該防的風險是什麼,以及最直接的應對方式
這篇不是安裝教學,是讓你理解這個壓縮層的設計判斷,夠不夠信任,以及信任到哪裡為止。
精華版
| 元件 | 職責 | 核心設計選擇 |
|---|---|---|
| CacheAligner | 穩定 message prefix | Frozen 已送出 bytes,保護 provider KV cache |
| SmartCrusher | 壓縮 JSON array | 統計取樣 + safety items 永遠保留 |
| CCR | 讓壓縮可逆 | LLM 可呼叫 retrieve 取回原始,Response handler 自動攔截 |
| IntelligentContext | 多輪 context 管理 | 低重要性訊息進 CCR cache,不直接丟棄 |
四個元件的核心定位:
- CacheAligner:確保壓縮不破壞 provider KV cache,已送出的 message bytes 標記為 frozen,壓縮只作用在最新一輪。
- SmartCrusher:對 JSON array 做統計取樣,保留時序頭尾和高重要性項目,error / 異常值 / 數值突變點永遠不壓縮。
- CCR(Compress-Cache-Retrieve):原始資料存本地 cache,LLM 遇到需要更多資訊時主動呼叫
headroom_retrieve取回,對呼叫方完全透明。 - IntelligentContext:context 逼近上限時,按重要性評分(recency、semantic 相關度、error 指標等)排序,低分訊息進 CCR cache 而不是直接刪除。
三個關鍵設計問題:
為什麼只壓最新一輪,不重壓整個歷史? Provider(Anthropic、OpenAI)的 KV cache 依賴穩定的 message prefix。一旦修改已送出的訊息,provider 端 cache 就 miss,反而增加成本和 latency。Headroom 把這個限制變成一個原則:已送出的 bytes 永遠 frozen,壓縮只對當前輪次的新內容生效。
SmartCrusher 怎麼決定 JSON array 留哪幾筆? 統計取樣:保留時序頭部(前 30%)、最新狀態(後 15%)、加上重要性評分最高的那批(55%)。含 error / exception / failed、數值異常、突然轉折的項目屬於 safety items,不進評分流程、永遠保留。壓完如果 token 數不降反升,自動 revert 回原始。
LLM 如果需要被壓掉的資訊怎麼辦?
CCR 機制:原始資料存在本地 LRU cache,LLM 推理時如果需要更多細節,會自動呼叫 headroom_retrieve 取回完整內容。這個 retrieve 由 Response handler 攔截並執行,整個過程對 agent 完全透明。長對話需要注意 TTL,預設 300 秒對超過 5 分鐘的 session 不夠。
以下是完整版,按需取用。
Tool Output 為什麼是 Context Bloat 的主要來源
Tool output 是 context bloat 的主要來源,原因很直接:每次 tool call 的輸出會進入 messages 陣列,然後永久留在那裡,沒有任何機制會主動清掉它。
一個 agentic session 的 context 裡,token 來自幾個地方:system prompt、對話歷史(user + assistant 輪次)、tool output。前兩者有自然的邊界:system prompt 固定,對話輪次隨著任務結束也不會無限增長。但 tool output 不同。
每呼叫一次 tool,輸出就進 messages 陣列,然後永久留在那裡。一個每輪呼叫三個 tool、跑 25 輪的 agent,context 裡會有 75 份 tool output。如果每份平均 1,500 token,那是 112,500 token 的 tool output history。
問題在於內容的形態。Tool output 通常是 JSON array:搜尋結果、資料庫查詢回傳、API response。這類資料高度冗餘,100 筆 log 記錄之間往往有大量重複的欄位結構,真正有資訊量的是少數幾筆。全部留著,大部分 token 對推理沒有貢獻;但直接砍掉,又可能丟失關鍵資訊。
Headroom 的切入點就在這裡:不是「壓縮全部 context」,而是「對 JSON array tool output 做統計取樣,同時保留可逆性」。
Headroom 的三個核心設計原則
Live-Zone Only:只壓最新一輪
Headroom 有一個核心的不變量:已送出的 message bytes 永遠不動。壓縮只作用在當前輪次新進來的 user message 和 tool results。
這個限制來自 provider KV cache 的工作方式。LLM provider 在伺服器端快取已處理過的 message prefix,讓後續 request 可以跳過重新計算。如果 Headroom 修改了第 3 輪的 tool output,第 4 輪 request 送出時,provider 就無法 cache hit 第 3 輪以前的內容——等於讓 provider 替你重算了一遍。Live-Zone Only 確保這個情況不發生。
工程上的含義是:Headroom 解決的是「每輪新增的 token 負擔」,不是「歷史累積的存量」。它讓每輪 context 成長的速度變慢,而不是幫你清掉過去。
CCR:讓壓縮可逆
壓縮有一個根本的張力:壓得越激進,節省越多,但 LLM 遺失資訊的風險越高。Headroom 的解法不是找「最佳壓縮率」,而是讓壓縮結果可逆。
CCR(Compress-Cache-Retrieve)的做法:壓縮時把原始資料存進本地 cache,並給 LLM 注入一個 headroom_retrieve tool。LLM 拿到壓縮版本,如果推理過程中發現需要更多細節,會自動呼叫 retrieve 取回原始內容。這個呼叫由 Response handler 攔截並執行,呼叫方完全不需要感知。
效果是:壓縮不再是破壞性操作。LLM 可以從「摘要」開始推理,在需要時往下鑽。這讓 Headroom 可以在壓縮率和資訊完整性之間取更激進的位置,因為「取回」是保底機制。
Fail-Open:壓縮失敗不傳播
任何壓縮都可能失敗:JSON parse error、壓完 token 數不降反升、content type 辨識錯誤。Headroom 的原則是:失敗直接回傳原始內容,記錄 WARNING,不拋例外給呼叫方。
這個設計讓 Headroom 可以作為 proxy 或 library 安全地嵌入現有 pipeline,不用擔心壓縮邏輯的異常狀況會打掛整個 agent。最壞的情況是「沒省到 token」,而不是「agent 崩潰」。
SmartCrusher 怎麼決定 JSON Array 留哪幾筆?
SmartCrusher 是 Headroom 的主力壓縮器,專門處理 JSON array。它的核心邏輯是統計取樣:從一份大的 JSON array 裡,選出一個代表性子集。
選法:先決定保留多少筆(根據資料的覆蓋曲線找 elbow point,避免留太少或留太多),然後分配來源:保留時序頭部的 30%(上下文建立)、最新的 15%(當前狀態)、以及按重要性評分選出的 55%。
有四類 safety items 不進評分流程,永遠保留:含 error / exception / failed / critical 的項目、數值異常的項目、字串長度異常的項目、以及數值突然轉折的 change point。這些是「罕見但業務關鍵」的資訊,評分模型可能低估它們,所以直接豁免。
壓完之後有一個保底:如果壓縮後的 token 數 ≥ 原始,自動 revert,不送出壓縮版本。
什麼內容適合壓縮、什麼不適合
Headroom 對不同內容類型的效果差異很大,取決於資料的冗餘程度,不是所有 tool output 都值得壓。
| 內容類型 | 壓縮率 | 說明 |
|---|---|---|
| JSON array(logs、search results、tool outputs) | 70–95% | 主力場景,效果最明顯 |
| JSON array of strings / numbers | 60–90% | 效果良好 |
| 多輪 agentic 對話(25–50 輪) | 56–81% | 中等效果 |
| Plain text | 43–46% | 效果差,還增加 latency |
| Code | ~0%(passthrough) | 刻意不壓 |
Code 刻意 passthrough 是一個設計決策,不是限制。理由是:code 通常是使用者正在操作的內容,壓縮可能讓 LLM 在生成或修改時遺失結構細節。JSON array 的冗餘性遠高於 code,壓縮的 ROI 不同。
Plain text 的情況則相反:文字本身已經比較精煉,統計取樣沒辦法找到「哪幾句代表全部」,壓縮率低,overhead 卻一樣存在。RAG 的 retrieval 結果如果是純文字段落,用 Headroom 效果有限。
使用 Headroom 最該注意的兩個風險
Context Dilution 是最核心的風險。 SmartCrusher 保留的是「代表性樣本」,不是完整資料。如果你的業務邏輯有「罕見但關鍵」的 edge case,而且那個 edge case 的數值剛好在正常範圍內(不觸發 safety item 的異常偵測),它可能被靜默丟棄。
最直接的防護是 per-tool profile:對業務關鍵的 tool(例如 database query、financial data API)直接設 skip_compression: True,讓那個 tool 的輸出永遠 passthrough。SmartCrusher 主要應該壓縮「量大但單筆重要性低」的 tool output(搜尋結果、debug log、metrics 歷史),而不是「量小但每筆都要看」的資料。
CCR TTL 過期是第二個容易忽略的風險。 預設 TTL 是 300 秒,對超過 5 分鐘的 session,LLM 超時才去 retrieve 時,原始資料可能已經不在 cache 了。長對話場景直接把 TTL 調高:
HEADROOM_CCR_TTL_SECONDS=3600 # 長對話建議至少 1 小時
其他風險(延遲 overhead、multi-turn amnesia)相對次要,且有對應的設定參數可以調整。在上面兩個問題解決之前,先不用考慮進階調參。
怎麼開始試用 Headroom?
最低門檻的試法是 proxy 模式:
headroom proxy --port 8787
ANTHROPIC_BASE_URL=http://localhost:8787 claude
把 base URL 指向本機 proxy,不改任何 agent code,就能看到效果。http://localhost:8787/stats 可以看 compression stats。
先用 audit mode 觀察幾個 session:
client = HeadroomClient(default_mode="audit") # 只記錄統計,不實際壓縮
觀察 retrieve rate:如果 LLM 頻繁呼叫 retrieve(> 50%),代表壓縮太激進,需要調高 protect_recent(預設保護最後 4 輪不壓)或降低壓縮 aggressiveness。確認行為正常之後再切到 optimize 模式。
結語:Headroom 解決的不是「context 太短」的問題,而是「tool output 累積」的問題——這兩件事看起來相關,但設計解法完全不同。理解這個區別,是判斷它適不適合你的 pipeline 的第一步。