對話長到開始變鈍:Claude Messages API 的隨選壓縮,跟你自己寫摘要差在一個簽章
你自己寫的 agent loop 跑到後段,回答開始變鈍。
它重複問一個前面已經確認過的欄位,或是把早就被否決掉的方案再提一次。你翻 log,一個錯誤都沒有,請求全部成功,狀態碼整排 200。唯一在變的是 input_tokens 每輪都比上一輪大一點。然後某一輪開始,答案就是不對勁。
官方的壓縮總覽頁把這件事寫得很直白:response quality degrades as a conversation grows。撞到 context window 上限才失敗算是好結局,品質通常在那之前就先掉了。
一個前提先講清楚:如果你的東西是一問一答、一輪就結束的形態,下面整篇都可以跳過,你沒有這個問題。會踩到的是那種一路跑幾十輪工具呼叫的東西。背景自動化、長任務 agent、會一直接話的客服 bot,歷史一直往上疊,而且疊得比你估的快。
第一條路:放著不管,撐到它自己炸
什麼都不做,讓對話一路長下去。這條路的結局官方講得很乾脆:超出 context window,請求就失敗。
失敗其實不是最糟的,失敗你看得到,錯誤率會亮燈。難受的是失敗之前那一段路,品質已經在掉,卻沒有任何訊號,監控只會告訴你請求都成功。
第二條路:自己寫個 prompt 叫它總結
這是最直覺的一條路,也是看起來最自由的。寫一段 prompt 把舊對話丟進去,叫 Claude 寫成一份摘要,然後把歷史紀錄裡那些舊訊息換成這段文字。省 token、內容自己控制,聽起來沒毛病。
在會保留推理過程(preserved thinking)的新款模型上,這招會壞,而且壞的方式不太直覺。
機制其實不複雜。你拿一張紙從上往下算一道長題目,算到一半那些草稿,是靠紙上更前面幾行才成立的。你寫下「所以 x 一定大於 3」,是因為上面第四行推出了某個式子。現在有人把上面那幾行擦掉,換成一句「前面大致算到這裡」。你的草稿還留在紙上,一個字都沒被動過,但它們已經不知道自己在算什麼了。
Claude 判定 thinking block 有沒有效,用的就是這個邏輯。官方文件寫成一句規則:Nothing before the thinking block has changed. 一個 thinking block 的「前綴」包含最上層的 system、tools,以及排在它前面的所有 messages。前綴跟當初產生這個 block 的時候長得不一樣,這個 block 就無效,而且連帶它後面每一個 thinking block 一起無效。
你自己抄的那份摘要,就是「前綴變了」。換成官方的說法:
If you write the summary yourself, it breaks the rule: the kept assistant turns still carry thinking blocks that were produced when the original turns, not the summary, came before them. Those blocks fail.
失效之後兩種下場,看你怎麼設定:API 回 400 把整個請求拒掉,或是把那些無效的 block 默默丟掉。
所以「自己寫摘要」在實務上只有一種活得下去的用法,官方也直接寫出來了:整段歷史全砍,把整場對話總結成一則 user message,只送那一則加上你的下一個指令。前面什麼都不重播,自然就沒有 thinking 可以被判無效,模型從摘要開始重新想。
這招跑得動,代價是那些推理全部歸零。你花錢買的 extended thinking,每壓縮一次就重來一次。
那為什麼 API 自己寫的摘要就不會垮
差別在誰認可這次替換。
你自己抄的摘要,系統沒辦法分辨你抄對了沒、中間有沒有偷改一句。分辨不出來,它只能採最保守的策略:前綴跟當初不一樣,一律當作無效。
隨選壓縮(on-demand compaction)換了個做法。摘要不由你寫,由伺服器端產生,產生完附一段 signature。下一次請求你把整個 block 原封不動送回去,系統核對簽章,確認這份摘要就是它自己寫的那份、你一個字都沒動過。文件沒有把中間這層因果攤開寫,它只寫了結果:壓縮之後,最近幾輪連同裡面的 thinking 可以留著。從這個結果回推,站得住的解釋只有一個——替換是它自己蓋章放行的,所以後面那些 thinking 不會被判死。
用法是在請求裡多一個 compaction 參數,配上 beta header compact-2026-09-04。官方文件的 curl 範例:
1 | curl https://api.anthropic.com/v1/messages \ |
這一次請求不會得到一般的回覆。stop_reason 回 "compaction",content 裡只有一個 block(以下為官方回應範例的節錄):
1 | { |
最上層的 input_tokens 跟 output_tokens 固定是 0,因為這輪沒產生一般回覆,真正的用量記在 usage.iterations 那筆 compaction 裡。這通呼叫照樣計費、照樣佔 rate limit,官方寫的是 The summarization call is billed and rate-limited like any other request。摘要不是免費的,它只是比「整段重想」便宜。
內建的摘要 prompt 也可以換掉。compaction 底下多給一個 instructions 字串(上限 16,384 個字元),它會整個取代官方預設的指令,你能在裡面要求「已經談定的欄位名稱一個都不准漏」。
換回去的時候有兩種錯,它一個都不會告訴你
拿到 block 之後,下一次請求要把它擺在 messages 最前面,然後把被摘要掉的那些訊息拿掉。結構長這樣:
1 | { |
會報錯的那幾種其實都很好處理。block 前面還留著被摘要過的訊息,回 400 compaction_block_misplaced;內容或簽章對不起來,回 compaction_signature_invalid 或 compaction_content_mismatch;messages 裡根本沒有可摘要的內容,回 compaction_nothing_to_summarize;伺服器一時產不出來或讀不到,回 529 compaction_unavailable,重試就好。
真正會咬人的是官方特別標出來的那兩種。它們不報錯。
官方原文:Two mistakes in the swap raise no error. If summarized messages remain after the block, the API sends them to Claude again. If a later request leaves the block out, Claude gets no summary.
摘要 block 後面還留著已經被摘要掉的訊息,那些訊息會原封不動再送一次給 Claude,你以為省下來的 token 一個都沒省。後續某個請求漏掉了 block,Claude 就是沒看到摘要,安安靜靜少了一段記憶。
這兩種都是 200。對一個沒人盯著的 loop 來說,這比 400 危險得多。400 會讓你的錯誤率儀表板亮起來,這兩種不會,它只會讓帳單慢慢變胖,或是讓 agent 在某一輪突然忘記一件明明講過的事,而你會以為是模型變笨了。
最近幾輪想留逐字的,跟不想停下來等的
兩個延伸用法,解的是不同的痛。
keep-tail 是只把比較舊的回合送去壓縮,最近幾輪原封不動接在 block 後面,模型還是逐字看得到最後那幾次交手,官方那一頁的標題就叫 Compaction that keeps recent turns。這是跟「自己寫摘要」差最多的地方:保留尾段又不弄壞 thinking,自己寫的做不到。
background 是把壓縮請求先丟出去,對話繼續跑在完整歷史上,等 block 回來再換。官方的描述是 the compaction request runs while the conversation continues on its full history, and the swap waits until the block arrives。好處是 loop 不用卡在那邊等摘要寫完,代價是同一時間有兩個請求在飛,rate limit 多吃一份。
如果你已經在用舊的門檻式壓縮(edit type compact_20260112,輸入 token 一碰到你設的門檻,API 自己壓),這兩套不能混。compaction 跟 context_management 在同一個請求裡互斥,只能二選一。門檻式那套也沒有背景模式,官方比較表那一格寫得很硬:No: it runs inside the request that reaches the threshold。
我的立場是能用隨選就用隨選,官方自己也直接建議 Use on-demand compaction wherever it is available。有一種情況我會反過來勸你別碰:如果你的 loop 完全沒開 thinking,也不在乎壓縮發生在哪一輪、卡不卡住,門檻式省事得多,設好就不用再管,沒必要為了「可控」去扛 swap 的複雜度。門檻式那條路我寫過一篇context editing 與 memory tool 的教學,兩篇對著看,看得出設計者為什麼是把「省事」跟「可控」拆成兩條路,不是拿新的取代舊的。
不想自己管 swap 的話,還有一條更省事的。官方 SDK 的 Tool Runner(Python、TypeScript、C#、Go、Java 都有)把這件事包好了,方法名各語言自己一套:Python 是 compact_before_next_turn(),TypeScript 跟 Java 是 compactBeforeNextTurn(),C# 跟 Go 是 CompactBeforeNextTurn()。呼叫之後 runner 會等這一輪跟工具呼叫跑完,自己送壓縮請求、自己換歷史。建 runner 的時候要記得把 compact-2026-09-04 帶上,它不會幫你加。
壓過一次又長回去也沒關係,再送一次 compaction,新 block 會把舊摘要連同後面的內容再摘一遍。
它沒幫你解掉的部分
摘要範圍裡的圖片、文件、container_upload block、抓過的 URL 內容,被 block 換掉之後就沒了,官方的說法是 Restate or re-upload anything a later turn still needs。你的 agent 如果會在第 30 輪回頭看第 3 輪上傳的那張圖,壓縮之前得自己想好怎麼把它帶著走。
平台支援也還不齊。截至 2026-09-23 我看文件的當下,Claude API、AWS 上的 Claude Platform、Google Cloud、Microsoft Foundry 都列為 beta 可用,Amazon Bedrock 標的是 not available,要在 Bedrock 上做類似的事就只剩門檻式那條路。模型也不是全支援,可以用 Models API 帶同一個 beta header 查每個模型的 capabilities.compaction 欄位確認。
還有 beta 本身。header 叫 compact-2026-09-04,日期直接寫在名字裡,這種東西改版不保證相容,正式上線前記得把它當成會變的東西處理。
另外一個很容易搞混的點:Claude Code 那個 /compact 指令跟這裡講的 compaction 參數不是同一層東西。前者是 CLI 產品給終端使用者用的(搭配 autoCompactWindow 跟 PreCompact / PostCompact 兩個鉤子,我之前寫過一篇),後者是給自己寫 agent 的人用的 API 能力。至於兩者底層是不是同一套邏輯,我沒在官方文件看到任何說法,別自己把它們接起來。
誠實邊界:我只讀了官方文件,沒有實際送過任何一次帶 compaction 參數的請求。上面所有 curl 與 JSON 都是官方文件的範例,不是我跑出來的輸出。錯誤碼實際觸發時的行為、簽章長什麼樣、背景模式真正的延遲落在哪個量級,我都沒有第一手資料。
所以你的 loop 要不要動
問題從來不是「紙不夠大」。是你換紙的時候,前面那些推到一半的草稿還算不算數。自己抄一份摘要,系統沒有理由相信你,於是草稿一律作廢,你只能整張撕掉重算。API 自己寫的摘要帶著簽章,等於它替這次換紙背書,草稿就還站得住。
那個變鈍的 loop 要不要動它,看你有沒有開 thinking。沒開就別碰這套,門檻式設一設收工,可控性你用不到。有開的話,在自己的迴圈裡挑一個「上一輪工具呼叫剛結束、還沒開始下一輪」的乾淨位置送 compaction,拿到 block 存起來,把被摘要掉的訊息真的刪乾淨,然後在每一次後續請求都把 block 帶上。
再補兩個 API 不會幫你做的檢查:送出前確認 block 前後都沒有殘留已被摘要掉的訊息,以及每次請求都恰好帶著一個 compaction block。這兩件事寫錯都是 200,儀表板看不到,只有你自己的斷言看得到。
這套東西要你自己拿主意的就兩件:壓縮什麼時候發生,那份摘要誰背書。參數名字查文件就有,這兩件文件不會替你決定。
參考來源:Compaction 總覽、Compaction on demand、保留最近回合、背景壓縮、Preserved thinking












































































































































































































