長會話穩定性:上下文壓縮、簽名快取與工具結果壓縮
你在用 Claude Code / Cherry Studio 之類的客戶端跑長會話時,最煩的不是模型不夠聰明,而是對話跑著跑著突然開始報錯:Prompt is too long、400 簽名錯誤、工具呼叫鏈斷了、或者工具迴圈越跑越卡。
這一課把 Antigravity Tools 為這些問題做的三件事講清楚:上下文壓縮(分三層逐步介入)、簽名快取(把 Thinking 的簽名鏈續上)、工具結果壓縮(避免工具輸出把上下文塞爆)。
學完你能做什麼
- 說清楚三層漸進式上下文壓縮分別在做什麼、各自的代價是什麼
- 知道簽名快取存了哪些東西(Tool/Family/Session 三層)以及 2 小時 TTL 的影響
- 理解工具結果壓縮的規則:何時會丟掉 base64 圖片、何時會把瀏覽器快照變成頭+尾摘要
- 需要時能透過
proxy.experimental的閾值開關調節壓縮觸發時機
你現在的困境
- 長對話後突然開始 400:看起來像簽名失效,但你不知道簽名從哪來、丟在哪
- 工具呼叫越來越多,歷史 tool_result 堆到上游直接拒絕(或變得極慢)
- 你想用壓縮救場,但又擔心破壞 Prompt Cache、影響一致性或讓模型丟資訊
什麼時候用這一招
- 你在跑長鏈路工具任務(搜尋/讀檔案/瀏覽器快照/多輪工具迴圈)
- 你在用 Thinking 模型做複雜推理,且會話經常超過幾十輪
- 你在排查客戶端能重現但你說不清為什麼的穩定性問題
什麼是上下文壓縮
上下文壓縮是代理在偵測到上下文壓力過高時,對歷史訊息做的自動降噪與瘦身:先裁掉舊的工具輪,再把舊的 Thinking 文字壓成佔位符但保留簽名,最後在極端情況下生成 XML 摘要並 Fork 一個新會話繼續對話,從而降低 Prompt is too long 和簽名鏈斷裂導致的失敗。
上下文壓力是怎麼計算的?
Claude 處理器會用 ContextManager::estimate_token_usage() 做一個輕量估算,並用 estimation_calibrator 校準,然後用 usage_ratio = estimated_usage / context_limit 得到壓力百分比(日誌裡會列印 raw/calibrated 值)。
🎒 開始前的準備
- 你已經跑通本地代理,並且客戶端確實在走
/v1/messages這條鏈路(見啟動本地反代並接入第一個客戶端) - 你能檢視代理日誌(開發者除錯或本地日誌檔案)。倉庫裡的測試方案給了一個範例日誌路徑與 grep 方式(見
docs/testing/context_compression_test_plan.md)
配合 Proxy Monitor 更好定位
如果你要把壓縮觸發與哪類請求/哪個帳號/哪輪工具呼叫對上號,建議同時開著 Proxy Monitor。
核心思路
這套穩定性設計不是直接把歷史全刪了,而是按代價從低到高逐層介入:
| 層級 | 觸發點(可配置) | 做了什麼 | 代價/副作用 |
|---|---|---|---|
| Layer 1 | proxy.experimental.context_compression_threshold_l1(預設 0.4) | 識別工具輪,只保留最近 N 輪(程式碼裡是 5),把更早的 tool_use/tool_result 對刪掉 | 不改剩餘訊息內容,對 Prompt Cache 更友善 |
| Layer 2 | proxy.experimental.context_compression_threshold_l2(預設 0.55) | 把舊的 Thinking 文字壓成 "...",但保留 signature,並保護最近 4 條訊息不動 | 會修改歷史內容,註釋裡明確會 break cache,但能保住簽名鏈 |
| Layer 3 | proxy.experimental.context_compression_threshold_l3(預設 0.7) | 呼叫後台模型生成 XML 摘要,然後 Fork 一個新訊息序列繼續對話 | 依賴後台模型呼叫;若失敗會返回 400(有友善提示) |
接下來按三層拆開講,順便把簽名快取和工具結果壓縮放在一起。
Layer 1:工具輪裁剪(Trim Tool Messages)
Layer 1 的關鍵點是只刪整輪工具互動,避免半刪導致上下文不一致。
- 一輪工具互動的識別規則在
identify_tool_rounds():assistant裡出現tool_use開始一輪,後續user裡出現tool_result仍算這一輪,直到遇到普通 user 文字結束這一輪。 - 真正執行裁剪的是
ContextManager::trim_tool_messages(&mut messages, 5):當歷史工具輪超過 5 輪時,刪掉更早的輪次裡涉及的訊息。
Layer 2:Thinking 壓縮但保留簽名
很多 400 問題並不是 Thinking 太長,而是 Thinking 的簽名鏈斷了。Layer 2 的策略是:
- 只處理
assistant訊息裡的ContentBlock::Thinking { thinking, signature, .. } - 只有在
signature.is_some()且thinking.len() > 10時才壓縮,把thinking直接改成"..." - 最近
protected_last_n = 4條訊息不壓縮(大致是最近 2 輪 user/assistant)
這樣可以省掉大量 Token,但仍把 signature 留在歷史裡,避免工具鏈需要回填簽名時無從恢復。
Layer 3:Fork + XML 摘要(極限兜底)
當壓力繼續升高時,Claude 處理器會嘗試重開會話但不丟關鍵資訊:
- 從原始訊息裡擷取最後一個有效的 Thinking 簽名(
ContextManager::extract_last_valid_signature()) - 把整個歷史 +
CONTEXT_SUMMARY_PROMPT拼成一個生成 XML 摘要的請求,模型固定為BACKGROUND_MODEL_LITE(當前程式碼是gemini-2.5-flash) - 摘要裡要求包含
<latest_thinking_signature>,用於後續簽名鏈延續 - Fork 出一個新訊息序列:
User: Context has been compressed... + XML summaryAssistant: I have reviewed...- 再附上原請求最後一條 user 訊息(如果它不是剛剛的摘要指令)
如果 Fork + 摘要失敗,會直接返回 StatusCode::BAD_REQUEST,並提示你用 /compact 或 /clear 等方式手動處理(見處理器返回的 error JSON)。
旁路 1:三層簽名快取(Tool / Family / Session)
簽名快取是上下文壓縮的保險絲,尤其是客戶端會裁剪/丟棄簽名字段時。
- TTL:
SIGNATURE_TTL = 2 * 60 * 60(2 小時) - Layer 1:
tool_use_id -> signature(工具鏈恢復) - Layer 2:
signature -> model family(跨模型相容性檢查,避免 Claude 簽名被帶到 Gemini 家族模型上) - Layer 3:
session_id -> latest signature(會話級隔離,避免不同對話污染)
這三層快取會在 Claude SSE 串流解析與請求轉換時被寫入/讀取:
- 串流解析到 thinking 的
signature會寫入 Session Cache(以及快取 family) - 串流解析到 tool_use 的
signature會寫入 Tool Cache + Session Cache - 在把 Claude 工具呼叫轉換為 Gemini
functionCall時,會優先從 Session Cache 或 Tool Cache 把簽名補回去
旁路 2:工具結果壓縮(Tool Result Compressor)
工具結果往往比聊天文字更容易把上下文塞爆,所以請求轉換階段會對 tool_result 做可預期的刪減。
核心規則(都在 tool_result_compressor.rs):
- 總字元上限:
MAX_TOOL_RESULT_CHARS = 200_000 - base64 圖片塊直接移除(追加一段提示文字)
- 如果偵測到輸出已儲存到檔案的提示,會擷取關鍵資訊並用
[tool_result omitted ...]佔位 - 如果偵測到瀏覽器快照(包含
page snapshot/ref=等特徵),會改成頭 + 尾摘要,並標註省略了多少字元 - 如果輸入像 HTML,會先移除
<style>/<script>/base64 片段再做截斷
跟我做
第 1 步:確認壓縮閾值(以及預設值)
為什麼 壓縮觸發點不是寫死的,來自 proxy.experimental.*。你得先知道目前閾值,才能判斷為什麼它這麼早/這麼晚才介入。
預設值(Rust 側 ExperimentalConfig::default()):
{
"proxy": {
"experimental": {
"enable_signature_cache": true,
"enable_tool_loop_recovery": true,
"enable_cross_model_checks": true,
"enable_usage_scaling": true,
"context_compression_threshold_l1": 0.4,
"context_compression_threshold_l2": 0.55,
"context_compression_threshold_l3": 0.7
}
}
}你應該看到:你的設定裡存在 proxy.experimental(欄位名與上面一致),並且閾值是 0.x 這樣的比例值。
設定檔位置不在這一課重複講
設定檔的落盤位置與修改後是否需要重啟,屬於設定管理範疇。按這套教學體系,優先以設定全解:AppConfig/ProxyConfig、落盤位置與熱更新語義為準。
第 2 步:用日誌確認 Layer 1/2/3 是否觸發
為什麼 這三層都是代理內部行為,最可靠的驗證方式是看日誌裡是否出現 [Layer-1] / [Layer-2] / [Layer-3]。
倉庫的測試方案給了一個範例指令(按需調整為你機器上的實際日誌路徑):
tail -f ~/Library/Application\ Support/com.antigravity.tools/logs/antigravity.log | grep -E "Layer-[123]"你應該看到:當壓力升高時,日誌出現類似 Tool trimming triggered、Thinking compression triggered、Fork successful 的記錄(具體欄位以日誌原文為準)。
第 3 步:理解淨化和壓縮的差別(不要混用預期)
為什麼 有些問題(比如強制降級到不支援 Thinking 的模型)需要淨化而不是壓縮。淨化會直接刪掉 Thinking block;壓縮會保留簽名鏈。
在 Claude 處理器裡,後台任務降級會走 ContextManager::purify_history(..., PurificationStrategy::Aggressive),它會把歷史 Thinking block 直接移除。
你應該看到:你能區分兩類行為:
- 淨化是刪掉 Thinking block
- Layer 2 壓縮是把舊 Thinking 文字替換成
"...",但簽名還在
第 4 步:當你遇到 400 簽名錯誤,先看 Session Cache 是否命中
為什麼 很多 400 的根因不是沒有簽名,而是簽名沒跟著訊息走。請求轉換時會優先從 Session Cache 補簽名。
線索(請求轉換階段的日誌會提示從 SESSION/TOOL cache 恢復簽名):
[Claude-Request] Recovered signature from SESSION cache ...[Claude-Request] Recovered signature from TOOL cache ...
你應該看到:當客戶端丟簽名但代理快取還在時,日誌裡會出現 Recovered signature from ... cache 的記錄。
第 5 步:理解工具結果壓縮的會丟什麼
為什麼 如果你讓工具把大段 HTML / 瀏覽器快照 / base64 圖片塞回對話,代理會主動刪減。你需要提前知道哪些內容會被替換成佔位符,避免誤以為模型沒看見。
重點記三條:
- base64 圖片會被移除(改成提示文字)
- 瀏覽器快照會變成 head/tail 摘要(帶省略字元數)
- 超過 200,000 字元會被截斷並追加
...[truncated ...]提示
你應該看到:在 tool_result_compressor.rs 裡,這些規則都有明確的常數和分支,不是憑經驗刪。
檢查點
- 你能說清楚 L1/L2/L3 的觸發點來自
proxy.experimental.context_compression_threshold_*,預設是0.4/0.55/0.7 - 你能解釋為什麼 Layer 2 會 break cache:因為它會修改歷史 thinking 文字內容
- 你能解釋為什麼 Layer 3 叫 Fork:它會把對話變成 XML 摘要 + 確認 + 最新 user 訊息的新序列
- 你能說明工具結果壓縮會刪掉 base64 圖片,並把瀏覽器快照改成 head/tail 摘要
踩坑提醒
| 現象 | 可能原因 | 你可以怎麼做 |
|---|---|---|
| 觸發了 Layer 2 之後感覺上下文沒那麼穩了 | Layer 2 會修改歷史內容,註釋裡明確它會 break cache | 如果你依賴 Prompt Cache 的一致性,盡量讓 L1 先解決問題,或提高 L2 閾值 |
| Layer 3 觸發後直接返回 400 | Fork + 摘要呼叫後台模型失敗(網路/帳號/上游錯誤等) | 先按錯誤 JSON 裡的建議用 /compact 或 /clear;同時檢查後台模型呼叫鏈路 |
| 工具輸出裡圖片/大段內容不見了 | tool_result 會移除 base64 圖片、截斷超長輸出 | 把重要內容落到本地檔案/連結裡再引用;別指望把 10 萬行文字直接塞回對話 |
| 明明用的是 Gemini 模型卻帶了 Claude 簽名導致報錯 | 簽名跨模型不相容(程式碼裡有 family 檢查) | 確認簽名來源;必要時讓代理在 retry 場景下剝離歷史簽名(見請求轉換邏輯) |
本課小結
- 三層壓縮的核心是按代價分級:先刪舊工具輪,再壓縮舊 Thinking,最後才 Fork + XML 摘要
- 簽名快取是讓工具鏈不斷的關鍵:Session/Tool/Family 三層各管一類問題,TTL 是 2 小時
- 工具結果壓縮是避免工具輸出把上下文塞爆的硬限制:200,000 字元上限 + 快照/大檔案提示特化
下一課預告
下一課我們聊系統能力:多語言/主題/更新/開機自啟/HTTP API Server。
附錄:原始碼參考
點擊展開查看原始碼位置
更新時間:2026-01-23
| 功能 | 檔案路徑 | 行號 |
|---|---|---|
| 實驗性設定:壓縮閾值與開關預設值 | src-tauri/src/proxy/config.rs | 119-168 |
| 上下文估算:多語言字元估算 + 15% 餘量 | src-tauri/src/proxy/mappers/context_manager.rs | 9-37 |
| Token 用量估算:遍歷 system/messages/tools/thinking | src-tauri/src/proxy/mappers/context_manager.rs | 103-198 |
| Layer 1:識別工具輪 + 裁剪舊輪次 | src-tauri/src/proxy/mappers/context_manager.rs | 311-439 |
| Layer 2:Thinking 壓縮但保留簽名(保護最近 N 條) | src-tauri/src/proxy/mappers/context_manager.rs | 200-271 |
| Layer 3 輔助:擷取最後一個有效簽名 | src-tauri/src/proxy/mappers/context_manager.rs | 73-109 |
| 後台任務降級:Aggressive 淨化 Thinking block | src-tauri/src/proxy/handlers/claude.rs | 540-583 |
| 三層壓縮主流程:估算、校準、按閾值觸發 L1/L2/L3 | src-tauri/src/proxy/handlers/claude.rs | 379-731 |
| Layer 3:XML 摘要 + Fork 會話實作 | src-tauri/src/proxy/handlers/claude.rs | 1560-1687 |
| 簽名快取:TTL/三層快取結構(Tool/Family/Session) | src-tauri/src/proxy/signature_cache.rs | 5-88 |
| 簽名快取:Session 簽名寫入/讀取 | src-tauri/src/proxy/signature_cache.rs | 141-223 |
| SSE 串流解析:快取 thinking/tool 的 signature 到 Session/Tool cache | src-tauri/src/proxy/mappers/claude/streaming.rs | 766-776 |
| --- | --- | --- |
| 請求轉換:tool_use 優先從 Session/Tool cache 補簽名 | src-tauri/src/proxy/mappers/claude/request.rs | 1045-1142 |
| 請求轉換:tool_result 觸發工具結果壓縮 | src-tauri/src/proxy/mappers/claude/request.rs | 1159-1225 |
工具結果壓縮:入口 compact_tool_result_text() | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 28-69 |
| 工具結果壓縮:瀏覽器快照 head/tail 摘要 | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 123-178 |
| 工具結果壓縮:移除 base64 圖片 + 總字元上限 | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 247-320 |
| 測試方案:三層壓縮觸發與日誌驗證 | docs/testing/context_compression_test_plan.md | 1-116 |