Skip to content

長會話穩定性:上下文壓縮、簽名快取與工具結果壓縮

你在用 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 1proxy.experimental.context_compression_threshold_l1(預設 0.4)識別工具輪,只保留最近 N 輪(程式碼裡是 5),把更早的 tool_use/tool_result 對刪掉不改剩餘訊息內容,對 Prompt Cache 更友善
Layer 2proxy.experimental.context_compression_threshold_l2(預設 0.55)把舊的 Thinking 文字壓成 "...",但保留 signature,並保護最近 4 條訊息不動會修改歷史內容,註釋裡明確會 break cache,但能保住簽名鏈
Layer 3proxy.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 處理器會嘗試重開會話但不丟關鍵資訊:

  1. 從原始訊息裡擷取最後一個有效的 Thinking 簽名(ContextManager::extract_last_valid_signature()
  2. 把整個歷史 + CONTEXT_SUMMARY_PROMPT 拼成一個生成 XML 摘要的請求,模型固定為 BACKGROUND_MODEL_LITE(當前程式碼是 gemini-2.5-flash
  3. 摘要裡要求包含 <latest_thinking_signature>,用於後續簽名鏈延續
  4. Fork 出一個新訊息序列:
    • User: Context has been compressed... + XML summary
    • Assistant: 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()):

json
{
  "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]

倉庫的測試方案給了一個範例指令(按需調整為你機器上的實際日誌路徑):

bash
tail -f ~/Library/Application\ Support/com.antigravity.tools/logs/antigravity.log | grep -E "Layer-[123]"

你應該看到:當壓力升高時,日誌出現類似 Tool trimming triggeredThinking compression triggeredFork 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 圖片塞回對話,代理會主動刪減。你需要提前知道哪些內容會被替換成佔位符,避免誤以為模型沒看見。

重點記三條:

  1. base64 圖片會被移除(改成提示文字)
  2. 瀏覽器快照會變成 head/tail 摘要(帶省略字元數)
  3. 超過 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 觸發後直接返回 400Fork + 摘要呼叫後台模型失敗(網路/帳號/上游錯誤等)先按錯誤 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.rs119-168
上下文估算:多語言字元估算 + 15% 餘量src-tauri/src/proxy/mappers/context_manager.rs9-37
Token 用量估算:遍歷 system/messages/tools/thinkingsrc-tauri/src/proxy/mappers/context_manager.rs103-198
Layer 1:識別工具輪 + 裁剪舊輪次src-tauri/src/proxy/mappers/context_manager.rs311-439
Layer 2:Thinking 壓縮但保留簽名(保護最近 N 條)src-tauri/src/proxy/mappers/context_manager.rs200-271
Layer 3 輔助:擷取最後一個有效簽名src-tauri/src/proxy/mappers/context_manager.rs73-109
後台任務降級:Aggressive 淨化 Thinking blocksrc-tauri/src/proxy/handlers/claude.rs540-583
三層壓縮主流程:估算、校準、按閾值觸發 L1/L2/L3src-tauri/src/proxy/handlers/claude.rs379-731
Layer 3:XML 摘要 + Fork 會話實作src-tauri/src/proxy/handlers/claude.rs1560-1687
簽名快取:TTL/三層快取結構(Tool/Family/Session)src-tauri/src/proxy/signature_cache.rs5-88
簽名快取:Session 簽名寫入/讀取src-tauri/src/proxy/signature_cache.rs141-223
SSE 串流解析:快取 thinking/tool 的 signature 到 Session/Tool cachesrc-tauri/src/proxy/mappers/claude/streaming.rs766-776
---------
請求轉換:tool_use 優先從 Session/Tool cache 補簽名src-tauri/src/proxy/mappers/claude/request.rs1045-1142
請求轉換:tool_result 觸發工具結果壓縮src-tauri/src/proxy/mappers/claude/request.rs1159-1225
工具結果壓縮:入口 compact_tool_result_text()src-tauri/src/proxy/mappers/tool_result_compressor.rs28-69
工具結果壓縮:瀏覽器快照 head/tail 摘要src-tauri/src/proxy/mappers/tool_result_compressor.rs123-178
工具結果壓縮:移除 base64 圖片 + 總字元上限src-tauri/src/proxy/mappers/tool_result_compressor.rs247-320
測試方案:三層壓縮觸發與日誌驗證docs/testing/context_compression_test_plan.md1-116