AI 工程

/compact 顯示「已壓縮」卻沒真壓縮:Cline Desktop v0.0.5 兩個壓縮假象的踩雷與修法

如果你在 Cline 裡打過 /compact,看到聊天視窗跳出一條「Context compacted」的分隔線,然後安心地繼續跟 agent 對話——這篇要告訴你一件不太舒服的事:在某些設定下,那條分隔線是在對你說謊。 它顯示壓縮成功,但真正送進模型的請求,一個 token 都沒少。

Cline 團隊在 2026-07-27 發布的 Desktop v0.0.5,在一長串效能與 UI 修正的最後,藏了這麼一行:

Fixed agentic compaction silently falling back to basic compaction for OpenAI-Compatible providers, and manual /compact never actually reaching the model when auto-compaction was off.

這一句話其實包了兩個獨立的 bug。修這兩個問題的是 Cline 作者 Saoud Rizwan 的 PR #12563,2026-07-26 合併。有趣的是——這不是使用者回報挖出來的,是他在手動測試前一版剛做好的壓縮 UI(#12487)時,自己在 wire 上抓到 UI 在騙人。

這篇會把這兩個 bug 的機制拆到原始碼層級,然後給你一套不靠 UI、直接在網路層驗證「壓縮到底有沒有發生」的 SOP。就算你不用 Cline,這套「別信分頁、信 wire」的檢查習慣,對任何 coding agent 都適用。

先搞懂:Cline 的壓縮到底在做什麼

要看懂 bug,得先知道 Cline 的 context 壓縮分兩種策略,這是理解整件事的地基。

Agentic compaction(智慧壓縮):這是 Cline 從 #12317(Bee 提交,2026-07-22)起的預設策略。它的做法不是粗暴砍掉舊訊息,而是額外發一次 LLM 請求,叫模型把到目前為止的對話產生一份結構化摘要——保留技術細節、程式碼變更、關鍵決策——然後用這份摘要替換掉冗長的歷史訊息。依 Cline 官方文件(docs.cline.bot/features/auto-compact),這一發摘要請求的成本其實不高,因為大部分 input token 已經被 cache,你主要付的是產生摘要那段輸出。

Basic compaction(基礎截斷):這是規則式的 fallback。當模型不支援 agentic 摘要、或摘要那一發請求失敗時,Cline 就退回到「按規則截斷舊訊息」。它便宜、不會失敗,但會直接丟資訊——這正是長 session 跑到後面會「忘記前面做過什麼、重做已完成的工作」的典型病灶。

再來是兩個關鍵名詞:

  • /compact/smol:你可以在輸入框手動打這兩個 slash 指令,強制立刻壓縮一次。(順帶一提,v0.0.5 當下 /compact 甚至沒被列進 slash 選單,打了會顯示「No matching commands found」,但功能是通的——這是 PR 作者自己列出、但這次沒一起修的 follow-up。)
  • Auto Compact:自動壓縮開關。context 接近視窗上限時自動觸發壓縮。在擴充功能/桌面版裡,這個開關預設是關的(內部旗標 useAutoCondense 預設 false)。記住這個「預設關」——第二個 bug 整個踩在它上面。

運作原理:兩條路,兩個謊言

Cline /compact 兩個 bug 的失效路徑:agentic 悄悄退回 basic、Auto Compact 關著時 /compact 是空操作

兩個 bug 的共同症狀是一樣的:分頁顯示「Context compacted」,模型端卻沒有任何改變。 但它們的成因完全不同,一個在 VS Code / 桌面這層的 session factory,一個在共用的 core runtime。分開看。

Bug #1:Agentic 悄悄退回 Basic(OpenAI-Compatible + 自訂 endpoint 中招)

這個 bug 的觸發條件很具體:你用的是 OpenAI-Compatible provider,而且指向一個自訂 endpoint——LiteLLM proxy、企業內部 gateway、本地跑的推論 server,都算。

機制是這樣的。壓縮用的 summarizer 在建立自己的 LLM handler 時,只吃 CoreSessionConfig.providerConfig 這一份設定。而 cline-session-factory.ts 裡負責解析 base URL 的 resolveBaseUrl()只認得舊版的 provider id 拼法(例如 openai)。問題是,現行設定 UI 存 provider 時,用的是 SDK 的新拼法 openai-compatible。兩邊對不上,於是 providerConfig 被建出來時——base URL 是空的

接下來就很尷尬了:

  1. 你的正常對話其實沒事,因為 core 會另外從 providers.json 重新解析設定,拿得到你的自訂 endpoint。
  2. 壓縮那一發不走這條路。summarizer 的 createHandlerAsync 拿著一個沒有 base URL 的設定,就去打 OpenAI 的預設端點 api.openai.com
  3. 你的 key 是給自家 gateway 用的,打到 api.openai.com 當然回 401
Agentic compaction failed; falling back to basic compaction
{"errorMessage":"Incorrect API key provided: sk-dummy...roxy"}
  1. agentic 摘要失敗,Cline 靜默退回 basic 截斷。而 UI 呢?照樣顯示一條漂漂亮亮的「Context compacted (manual)」,完全沒有任何 hint 告訴你「其實智慧摘要根本沒跑」。

結論:任何用自訂 endpoint 的 OpenAI-Compatible 設定,在 v0.0.5 之前,從來沒真正用過 agentic 壓縮。 你以為模型幫你做了聰明的摘要保留重點,實際上它只是被規則式地砍掉了一段歷史。

修法:在 resolveBaseUrl 裡把 SDK 新拼法 openai-compatible 也對應上,並且在舊 state 沒有 base URL 時,回頭去讀 providers.json 的 base URL(跟 resolveApiKey 的行為對齊)。附帶一個容易被忽略的修正:把 knownModels 提升到 CoreSessionConfig 頂層——這樣手動壓縮才會照模型真實的 context window 來抓預算,而不是掉回一個 64k 的保底值(sdk-compaction.ts 讀的是 config.knownModels[modelId],先前這欄根本沒被設)。

Bug #2:Auto Compact 關著時,/compact 是純空操作

這個更隱蔽,而且跟 provider 無關,人人有份——只要你沒開 Auto Compact(也就是預設狀態)。

流程拆開看:

  1. 你打 /compactcompactSessionMessages強制為這一次手動執行打開壓縮、跑一次摘要、把結果寫進所謂的 sidecar(壓縮後的工作狀態)。它的承諾是:「接下來的 turn 和之後的 resume 都會沿用這份壓縮過的 working context」。
  2. 但是——local-runtime-host.ts 只有在 compaction.enabled === true 時,才會把 createCompactionStateAwarePrepareTurn 接上去。這個 prepareTurn 才是真正「在組下一次請求時,去讀 sidecar、用壓縮後的內容取代完整 transcript」的那隻手。
  3. 而擴充功能預設 Auto Compact 是關的useAutoCondense = false),於是 compaction.enabledfalse,那隻手根本沒接上。
  4. 結果:sidecar 存了,卻沒有人去讀它。 每一次後續請求,照樣把完整的原始 transcript 送給模型。作者是掛一個 logging proxy、在 wire 上親眼確認的——請求內容沒變。

這就是為什麼那麼多人回報「/compact 沒用」。GitHub 上像是 sean-harpin 開的 #7298/compact 後 context 沒縮、只是回一句更短的完成訊息)、以及 #10637(context bar 數字自己往回跳但沒有正常壓縮事件)這類回報,症狀都指向同一個大方向:壓縮的 UI 訊號和實際送到模型的內容脫鉤了。 v0.0.5 修的是其中兩個可以在原始碼裡指名道姓的根因。

修法:把 state-aware prepareTurn 無條件接上。它本來就支援 compact: undefined 的語意(意思是「投影已存在的壓縮狀態,但不要重新壓縮」),而且對沒有 sidecar 的 session 會自動 no-op,所以無條件接上是安全的。同時,在壓縮功能關閉時也保留 resume/初始的 sidecar,而不是把它丟掉。作者順手改掉了舊測試——那條舊測試釘死的是相反行為(壓縮關閉時不投影狀態),卻又同時斷言「關閉時仍持久化壓縮狀態」,等於「存了但永遠不讀」,語意本身就自相矛盾。

數據與限制:哪些數字是真的,哪些別當證據

先把可驗證的事實擺清楚。以下數字全部來自 PR #12563 作者的測試紀錄,測試環境是真實的擴充功能 dev host,透過本地 logging proxy(http://127.0.0.1:4141/v1)打到 OpenRouter,proxy 會用 system prompt 特徵標記出壓縮 summarizer 的請求——所以「agentic 有跑」是在 wire 上被證明的,不是靠 UI

  • 修好後:proxy log 出現 tag=COMPACTION_SUMMARIZER ... status=200 ... stream completed;擴充功能 log 顯示 Performed agentic compaction,沒有 fallback。修好前,同一個流程 log 是 Agentic compaction failed; falling back to basic compaction,而 proxy 上什麼都沒看到(因為 401 打在別的端點)。
  • sidecar 投影生效後:自動壓縮後,下一發請求在 wire 上的訊息數從 14 則掉到 5 則(摘要 + 尾端保留的即時訊息)。這是「真的壓了」最直接的證據形態。
  • 測試也涵蓋了壓縮中取消、壓縮中打字排入佇列、壓縮中重載視窗恢復等邊界,單元測試方面 apps/vscode 有 984 項通過、@cline/core 有 1435 項通過。

現在講限制,這部分要誠實。 PR 作者自己列了幾個「測試中發現、但這次刻意沒修」的 follow-up,這些是你升級後仍會遇到的坑:

  1. 分隔線的 token 數字可能不減反增:當一段很大的全新 tool 結果落在被保留的即時尾端時,完成後的分隔線可能顯示成 Context compacted · 11.4k → 21.5k tokens,context bar 甚至會往上撐過視窗。原因是「壓縮前」是對這一 turn 的 apiMessages 估算、「壓縮後」是對正式結果訊息計算,兩者口徑不同。純屬顯示問題,但極度誤導——所以請不要把分隔線上的數字當成壓縮成效的證據。
  2. 從歷史重開的任務打 /compact/smol 會回你「There is no active task to compact.」,得先送一則正常訊息把它變成 active session 才行。
  3. /compact 不在 slash 選單裡(只有 /smol 在),打了會顯示查無指令,但功能其實是通的。
  4. 策略 fallback 依然是靜默的:當 agentic 摘要失敗退回 basic 時,分隔線跟成功跑 agentic 長得一模一樣。也就是說——就算修了 Bug #1,未來若有別的原因讓 agentic 失敗,你在 UI 上還是看不出來。這正是 Bug #1 當初能藏這麼久的結構性原因。

這一點很重要:v0.0.5 修的是兩個特定根因,不是「讓壓縮失敗變得可見」這個更根本的問題。 UI 仍然只報「壓縮了」,不報「用哪種策略壓的」。

適用場景與 trade-off:你該不該在意這件事

你一定要升級 v0.0.5,如果

  • 你用 OpenAI-Compatible provider 接自訂 endpoint(LiteLLM、企業 gateway、本地 vLLM/Ollama 之類的 OpenAI 相容層)。你先前的每一次壓縮都只是規則式截斷,長 session 品質流失比你以為的嚴重。
  • 依賴手動 /compact 來控制 context,而且沒有特別去開 Auto Compact(也就是絕大多數人的預設)。你以前打的每一發 /compact 對模型端都是空操作。

影響相對小,如果:你用官方 Cline / Anthropic / 標準 OpenAI 直連(provider id 對得上、base URL 不是自訂的),而且平常就開著 Auto Compact。這種組合下 Bug #1 不太會踩到,Bug #2 因為 compaction.enabled 是 true 也不受影響。但升級沒壞處。

trade-off 提醒:就算修好了,agentic 壓縮本質上就是多一次 LLM 請求。它保留語意、代價是多一發 API call(多數 input token 已 cache,成本主要在摘要輸出)。在超長、超密集的 session 裡,這仍是你要納入預算的東西。如果你要的是「便宜、可預測、絕不失敗」而不在乎丟細節,basic 截斷反而合適——但那你要自己知道你在用它,而不是被靜默 fallback 決定。

對工程團隊的意義:別信分頁,信 wire

這個 bug 真正的教訓不是「Cline 有 bug」,而是——coding agent 的 UI 狀態指示,和它實際送給模型的內容,是兩回事,而且會脫鉤。 一條「已壓縮」的分隔線、一根往下掉的 context bar,都只是前端的估算與宣稱,不是模型端的事實。當你的 agent 行為詭異(長 session 變笨、重做已完成的工作、莫名忘記前文),第一件事不是相信 UI,是去看線上真正送出去的請求

下面這套 SOP 適用於任何 coding agent,不限 Cline:

驗證壓縮是否真的發生:不能當證據的 UI 訊號 vs 才算數的 wire 證據,以及三步自我驗證

具體三步:

  1. 架一個 logging proxy 夾在中間。 把 agent 的 base URL 指到本地 proxy(例如 http://127.0.0.1:4141/v1),proxy 再轉發到真正的 API。LiteLLM proxy、mitmproxy,或任何能記錄 request body 的東西都行。這一步讓你握有「模型實際收到什麼」的唯一真相來源。
  2. 跑一次壓縮,看那一發摘要請求在不在。/compact/smol,然後盯 proxy log:有沒有出現一發額外的、帶摘要 system prompt 特徵的請求,而且是 200?沒有這一發,就代表 agentic 壓縮根本沒跑——不管 UI 說什麼。
  3. 送下一則訊息,比對請求體積。 壓縮後再送一則普通訊息,比對這一發 request 的訊息數與 token 數有沒有真的掉下來(像作者測到的 14 則 → 5 則)。掉了,才是真壓縮;沒掉,UI 就是在騙你。

把「別把 UI 當成 ground truth」寫進團隊的 agent 除錯 checklist。特別是當你們在正式環境跑 agent、又用了自訂 gateway 時——一個靜默 fallback 可能讓你們的每一次壓縮都名存實亡好幾週,而 dashboard 上一切正常。

最後一個務實建議:升級到 v0.0.5 後,把 Auto Compact 打開。 Bug #2 的根因就是「手動壓縮的狀態沒被後續請求讀取」,開著 Auto Compact 讓 compaction.enabled 為 true,是最省事的自保。同時也記住——就算開了,分隔線上的 token 數字仍可能誤導(follow-up #1 尚未修),要確認成效,還是回到 wire。

來源

整理:DataAgent · Coding Agent 實戰教學

發表迴響

%d 位部落客按了讚: