DeepSeek Harness · 上下文工程

Compaction 雙路徑與 replaceGeneration

上下文快滿了就主動收拾,真撐爆了就先收拾再重試。重試前先對一遍世代號,收拾沒起效就不許重試。

課程目標讀完你能說清兩個思路:DSH 為什麼把自動壓縮拆成主動和被動兩個觸發器,各管一段、互不重疊;以及溢位後允許重試的憑證,為什麼是 replaceGeneration 這個只增不減的世代號,外掛自己的返回值為什麼不算數。
互動演示 · 行李箱收納模擬器

把上下文視窗想成一隻行李箱:每條訊息是一件衣物,虛線是八分滿警戒線。左下角的收拾次數就是 replaceGeneration(世代號),只增不減。三個情景對應壓縮的三種命運,每一步都有字幕解說。

八分滿線(閾值 0.8)
請求被拒 · CONTEXT_WINDOW_EXCEEDED
收拾次數 replaceGeneration3
選一個情景點播放,或滾動到此處自動播放情景 A。
邏輯軌跡(行號對應 compaction-basic/src/index.ts,動畫走到哪一步,哪一行亮)
PRESSURE 路徑
on('agent/pre-step')L147
measure().totalTokens ≥ thresholdTokens ?L304
剪枝 → 摘要 → surface 替換L308-323
CONTEXT-OVERFLOW 路徑
on('agent/request-error')L179
code ≠ CONTEXT_WINDOW_EXCEEDED → next()L183
generation = surface.replaceGenerationL191
compactIfNeeded('context-overflow')L194
replaceGeneration > generation ?L218-219
是 → return { kind: 'retry' }L222
否 → return next(),保留原始錯誤L219
演示是教學化模擬:容量條、衣物塊與世代號都是課程化抽象。情景 C 裡謊報成功的壓縮後端是教學假設,真實的 compaction-basic 不會謊報,只是 compaction 是一個開放接縫,第三方後端接進來之後什麼都可能發生,世代號對帳防的就是它們。出處:packages/compaction/compaction-basic/src/index.ts 第 147 至 223 行。
設計思路一 · 收拾東西要分兩個觸發器

它解決什麼問題

假設只做主動閾值這一條路:每次請求前量一下,超過八成就收拾。聽起來夠了,實際不夠。token 數是估出來的,估算和 provider 的真實計數總有出入。一條超大的工具結果突然塞進來,測量還沒到閾值,請求已經超限,provider 直接拒絕。這時候沒有任何補救邏輯接手,turn 就地報錯終止,使用者看到的是一次莫名其妙的失敗。

一次算錯就直接撞牆,沒有第二道防線。這就是單觸發器的問題。

思路是什麼

DSH 把這件事拆成兩個獨立的觸發器。快滿了主動收,撞牆了被動救,兩條路掛不同的事件、用不同的條件、有不同的失敗語義。

pressure 路徑掛在每個 Step(一次模型請求)開始之前。先量總 token,超過容量的 0.8 就動手收拾,收拾時給最近的對話留 16% 的原文尾巴。它的失敗語義很鬆:收拾中途出了錯,日誌裡記一句就繼續走,提前收拾失敗了天塌不下來。

context-overflow 路徑掛在請求報錯之後,只認轉接器規範化過的錯誤碼 CONTEXT_WINDOW_EXCEEDED,其他錯誤一律放行。它不看閾值,保留預算直接清零,強制做一次真實的縮減。它的失敗語義很嚴:必須給出決定,要麼重試,要麼保留原始錯誤上報。重試上限預設 1 次,每收到一條成功的模型回覆就清零計數,正常幹活的會話不會被卡住。

同一件事,兩個觸發器,各管一段 快滿了(請求前測量) 總 token 超過容量的 80% 主動收拾 留 16% 最近對話原文 旅程繼續 失敗只記一句日誌 撞牆了(請求被拒) CONTEXT_WINDOW_EXCEEDED 強制收拾 不看閾值,能壓全壓 對帳後決定 重試,或上報原始錯誤
上排是預防,下排是兜底。兩條路各自獨立,一條失效了另一條照常工作。

出處:pressure 監聽器在 packages/compaction/compaction-basic/src/index.ts 第 147 至 165 行,overflow 監聽器在第 179 至 223 行;0.8 與 0.16 兩個預設值在同包 config.ts 第 20 與 23 行,重試上限預設 1 次在第 93 行,都可按 provider 加 model 的組合逐一覆蓋。

為什麼長期成立

把預防和兜底分開,是可靠性工程的通則。備份和恢復是兩套系統,限流和熔斷是兩道閘門,道理相同:預防路徑追求便宜、常跑、失敗無所謂;兜底路徑追求可靠、少跑、失敗必須有交代。這兩種訴求塞進同一段邏輯裡必然互相遷就。所以哪怕換個語言重寫整個 harness,只要模型有上下文上限、token 靠估算,這兩個觸發器就都得在。

設計思路二 · 重試要出示證據

它解決什麼問題

撞牆之後收拾了一次,接下來要重發請求。問題是:怎麼確認收拾真的起效了?compaction 是一個開放接縫,第三方可以接自訂後端。假設某個後端每次都報告成功,但從來沒真正改過模型可見的內容:如果只看返回值就重試,請求原樣超限,再報錯,再壓縮,再重試,每一圈都是白花的 API 錢,死迴圈燒到天亮。

思路是什麼

DSH 的答案是一個世代號。先說背景:surface 是會話日誌裡模型可見事件的即時投影,可以理解成模型眼裡的那份對話。replaceGeneration 是它身上的一個只讀計數器,記的是這份對話被替換過幾次。整個程式碼庫只有一處會讓它加一:一段舊訊息真的被摘要替換、真的落盤的那一刻。沒有任何 API 能把它改小或重置。所以世代號前進了,在數學上等價於至少發生過一次真實的、已落盤的替換。

溢位恢復的用法就三步:動手收拾之前先拍快照記下當前值;收拾;收拾完拿新值和快照比。嚴格變大才允許重試,否則放行,原始錯誤原樣上報。外掛說什麼不重要,帳本上的數字變沒變才重要。

重試前先看收拾次數有沒有變 動手前拍快照 收拾次數 = 3 收拾一次 交給壓縮後端執行 再看一眼計數 現在是幾? 變成 4,箱子確實動過 允許重試 還是 3,收拾沒起效 拒絕重試,上報原始錯誤
計數只增不減,全庫只有替換真正落盤的那一處會加一,所以數字變大就是硬證據。

還有一個反方向的細節。就算收拾中途拋了異常,只要前面的免費剪枝已經落盤、世代號已經前進,這份進展照樣夠格授權重試。憑證據放行,憑證據拒絕,兩邊用的是同一條標準。

世代號沒前進,一次重試都不放行。

出處:快照與比對在 packages/compaction/compaction-basic/src/index.ts 第 191 行與第 218 至 222 行,異常後憑已落盤進展重試在第 195 至 208 行;replaceGeneration 的定義在 packages/core/session/src/surface.ts 第 136 至 142 行,全庫唯一的加一處在第 361 至 371 行。專案的 Agent Note(.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)明確否決過只看返回值的寫法,理由是自訂後端可能報告成功卻沒有改變模型可見狀態。

為什麼長期成立

用單調遞增的版本號證明狀態確實變了,這個套路資料庫的樂觀鎖用了幾十年,Git 的 commit 鏈、分散式系統裡的 epoch 也都是它的變體。它的好處是把信任問題變成算術問題:執行者可以撒謊,帳本不會。只要系統裡存在不受信任的擴充點,重試之前核對一個改不了的計數器,永遠是最便宜的防線。

橫向對比 · 三家怎麼防燒錢
Grok Build

主動閾值路徑和 DSH 的 pressure 同構:預設 85% 觸發,另有預設關閉的 two-pass 預摘要,細節見站內 Compaction:85% 閾值與可選 two-pass。請求報錯後的被動恢復加世代號對帳這條路,在已核對的 Grok Build 材料裡沒有見到等價機制。這一條基於已公開證據,保留未知項。

Claude Code

主動方向做得最厚,每次調 API 前要過裁剪、微壓縮、摺疊、全量摘要四道工序。防燒錢的答案是計數熔斷:自動壓縮連續失敗 3 次就停手。這個 3 來自真實事故,原始碼註釋記載曾有 1279 個 session 連續失敗 50 次以上,全球每天浪費約 25 萬次 API 呼叫。出處:claude-code-sourcemap-main/study/chapters/03-context-management.md 第 78 至 81、121 至 124 行。

對比焦點就一個:壓縮失敗會不會迴圈燒錢。Claude Code 數失敗次數,數到 3 就熔斷,止損線是拿事故資料校準出來的,屬於計數器止損:先允許問題發生幾次,再靠上限兜住。DSH 不數次數,要求每次重試都出示世代號前進的證據,一次無效重試都不放行,屬於結構性證明:讓無效重試從機制上發不出去。前者的 3 需要事故餵出來,後者的 0 是推導出來的。再加上雙路徑正交這一點,這兩處是 DSH 在三家對比裡獨有的設計。

課堂練習
01

手推一個謊報成功的後端

設重試上限為 1,裝一個自訂壓縮後端:每次都報告收拾成功,但從不真正替換模型可見的內容。現在第一次溢位報錯發生了。

問題一:DSH 會發起第二次壓縮嘗試嗎?提示:對帳失敗後原始錯誤直接上報,turn 就結束了,重試計數根本沒機會增加。

問題二:如果把判定標準改成只看後端的返回值,同一場景下每圈耗時 5 秒,第一分鐘會發出多少次註定失敗的請求?Claude Code 的 3 次熔斷又會在第幾次請求後止損?

Takeaway:快滿了主動收,撞牆了被動救,預防和兜底各走各的觸發器。溢位重試的憑證是 replaceGeneration 的單調前進,外掛的返回值不算數。要證據,別信口頭彙報。