上下文壓縮免疫)
1. 從一次上下文崩潰說起為什么長會話代理總會“失憶”做過 AI 編碼代理的人都遇到過這個場景你花了兩個小時跟代理一起把一個模塊從零搭起來中間討論了接口設計、踩了三個依賴沖突的坑、定下了命名規(guī)范結果對話一長代理突然開始胡言亂語——它把你三小時前明確否掉的方案又提了一遍把已經(jīng)改好的函數(shù)名寫回舊版本甚至開始編造根本不存在的文件路徑。你往上翻記錄發(fā)現(xiàn)它并不是“忘了”而是上下文被壓縮之后那些關鍵決策被摘要成了幾句不痛不癢的廢話。這個現(xiàn)象我管它叫“腦葉切除術”。上下文窗口就像一個容量固定的工作臺當 token 數(shù)量逼近上限系統(tǒng)必須做壓縮要么截斷最早的對話要么用一個小模型把歷史總結成摘要。無論哪種方式被切掉的部分往往是“為什么這么做”的推理鏈留下的只是“做了什么”的結論。而編碼這件事恰恰極度依賴推理鏈——一個變量為什么叫這個名字、一個邊界條件為什么這么處理這些信息一旦丟失代理就會退化成只會補全代碼的自動機。標題里提到的“20 萬 token”不是夸張。一個中等復雜度的重構任務加上反復的報錯日志、文件內容、diff 輸出輕松就能堆到十幾萬 token。我實測過一個跨模塊的接口遷移任務光是來回粘貼的編譯錯誤就有四萬多 token。在這種量級下任何依賴“把全部歷史塞進上下文”的方案都是脆弱的。那怎么辦標題給了兩個看似不相關的線索Unix init.d和《記憶碎片》。前者是操作系統(tǒng)里那套“開機自動拉起服務、崩潰自動重啟”的機制后者是那部倒敘順敘雙線交織的電影。把這兩個東西揉進 AI 代理的設計里就能得到一套對上下文壓縮免疫的架構。下面我把這套思路完整拆開講。2. 核心思路拆解把代理當成一個需要“開機自啟”的系統(tǒng)服務2.1 為什么是 init.d 而不是“更聰明的記憶”大多數(shù)人遇到代理失憶第一反應是“換個更大的上下文窗口”或者“換個更強的摘要模型”。這兩個方向我都試過結論是治標不治本。窗口再大也有上限摘要模型再強也會丟信息因為你無法保證它每次都把“關鍵決策”判定為關鍵。init.d 的思路完全不同。它不關心“記憶”本身它關心的是狀態(tài)的可重建性。一個 Unix 服務重啟之后它不需要記得自己上次運行到哪它只需要讀取配置文件、檢查運行時狀態(tài)、然后繼續(xù)工作。換句話說持久化的不是對話歷史而是對話產生的副作用。映射到 AI 編碼代理上代理不應該依賴“我記得我們討論過用 snake_case”而應該依賴“項目里有一個.editorconfig文件寫著naming snake_case”。代理不需要記得“上次編譯報錯是因為缺了某個依賴”而應該依賴“項目里有一個deps.lock文件記錄了完整依賴樹”。對話歷史可以丟但這些落盤的狀態(tài)不能丟。這就是 init.d 哲學的第一層把易失的上下文轉化為持久的、可被重新讀取的配置和狀態(tài)文件。2.2 《記憶碎片》結構倒敘與順敘的雙軌記憶《記憶碎片》的主角有短期記憶障礙他的應對方式是把關鍵信息紋在身上、拍成照片、寫成紙條。電影的結構也是雙線——一條線倒敘一條線順敘兩條線在中間交匯。這套結構用在代理上非常巧妙。傳統(tǒng)的對話歷史是純線性的從第一條消息到最后一條。壓縮的時候系統(tǒng)傾向于保留最近的、丟掉最早的。但編碼任務的關鍵決策往往發(fā)生在早期——架構選型、接口定義、命名規(guī)范這些都在開頭幾輪就定下了。線性壓縮恰好把最重要的部分丟掉了。雙軌結構是這樣設計的順敘軌正常的對話流記錄“我們正在做什么”。這條軌可以隨意壓縮、截斷因為它只承載過程。倒敘軌一個反向索引的“決策日志”記錄“我們?yōu)槭裁催@么做”。每做一次關鍵決策就往這個日志里追加一條帶時間戳和理由的記錄。這條軌永不壓縮因為它本身就是高度濃縮的。當上下文被壓縮后代理重新加載時先讀倒敘軌決策日志再讀順敘軌最近幾輪對話就能快速重建出“我是誰、我在做什么、我為什么這么做”的完整認知。這就像主角每次醒來先看自己身上的紋身。2.3 兩套機制如何咬合單有 init.d 的持久化還不夠因為狀態(tài)文件會越來越多代理重新加載時怎么知道該讀哪些單有《記憶碎片》的決策日志也不夠因為日志只記錄決策不記錄決策產生的實際文件變更。兩者咬合的方式是決策日志負責“指路”狀態(tài)文件負責“落地”。決策日志里的一條記錄長這樣[2024-XX-XX 14:32] 決策接口層采用適配器模式 理由上游有三個數(shù)據(jù)源格式不一致直接耦合會導致每次新增源都要改核心邏輯 影響文件src/adapters/*.ts, src/core/pipeline.ts 狀態(tài)快照snapshot-014代理重啟后讀到最后一條決策就知道要去src/adapters/目錄看當前有哪些適配器去src/core/pipeline.ts看核心邏輯怎么調用的。它不需要記得討論過程只需要讀文件就能重建認知。這就是“對上下文壓縮免疫”的本質——壓縮掉的是對話不是認知。3. 核心細節(jié)解析決策日志與狀態(tài)快照的具體設計3.1 決策日志的字段設計與寫入時機決策日志不是流水賬不能每輪對話都寫。寫得太密它自己就變成了需要壓縮的長文本寫得太疏關鍵信息又會丟。我的經(jīng)驗是只在“不可逆決策”發(fā)生時寫入。什么叫不可逆決策滿足以下任一條件即可選定了某個方案且該方案會排除其他方案比如選了 REST 就不太可能同時用 GraphQL定義了會被多處引用的命名、接口、數(shù)據(jù)結構確認了某個約束條件比如“這個模塊不能引入新的第三方依賴”踩了一個坑并確定了規(guī)避方式比如“這個庫的某個版本有 bug鎖定到上一個版本”每條記錄的字段我固定為六個字段說明是否必填時間戳精確到分鐘用于排序是決策內容一句話說清選了什么是理由為什么這么選這是最關鍵的是影響文件涉及哪些文件路徑是狀態(tài)快照對應的快照編號是替代方案考慮過但放棄的方案及原因否“理由”字段是靈魂。我見過太多代理在壓縮后把決策內容記住了但把理由忘了結果遇到一個邊界情況就不知道該怎么處理因為它不知道當初為什么這么設計。理由寫清楚了代理就能自己推導出邊界情況該怎么處理。寫入時機上我建議在每輪對話結束時由代理自己判斷是否觸發(fā)寫入而不是每輪都寫。判斷邏輯可以很簡單如果這輪對話產生了文件變更且變更涉及接口或命名就寫一條。這個判斷本身可以用一個輕量規(guī)則實現(xiàn)不需要再調一次大模型。3.2 狀態(tài)快照的粒度與存儲策略狀態(tài)快照不是 git commit。git commit 記錄的是代碼的完整狀態(tài)而狀態(tài)快照記錄的是代理的認知狀態(tài)。兩者互補但不能互相替代??煺盏牧6任以囘^三種每輪快照太密存儲爆炸而且大部分快照沒有區(qū)別每小時快照太疏中間的關鍵變更可能丟失決策觸發(fā)快照每寫一條決策日志就拍一個快照這個粒度最合適快照的內容包括當前工作目錄的文件樹、關鍵文件的哈希值、當前打開的“任務上下文”比如正在改哪個模塊、以及最近 N 輪對話的摘要。注意快照里不存完整對話歷史只存摘要。完整歷史交給順敘軌去管快照只負責“重建認知所需的最小信息集”。存儲策略上我用的是“快照鏈 定期歸檔”。最近 20 個快照保留完整內容更早的快照只保留決策日志和文件哈希具體文件內容從 git 歷史里按哈希找回。這樣既控制了存儲又保證了可追溯性。3.3 重新加載時的“喚醒流程”這是整套機制最關鍵的環(huán)節(jié)。代理被壓縮后重新加載不能直接把所有決策日志和快照都讀一遍——那又變成大上下文了。喚醒流程必須是有序的、增量的。我的喚醒流程分四步讀最后一條決策日志知道當前處于哪個決策節(jié)點讀對應的狀態(tài)快照知道當前文件樹和任務上下文讀最近 3-5 輪順敘對話知道剛才在做什么具體操作按需回溯如果當前任務涉及某個早期決策再定向讀取那條決策日志這四步下來通常只需要 2000-4000 token 就能重建完整認知相比原來十幾萬 token 的上下文壓縮比超過 95%。而且這個重建過程是確定性的——同樣的決策日志和快照每次重建出的認知都一樣不會像摘要那樣每次都有細微差異。注意喚醒流程里第 4 步“按需回溯”需要代理有能力判斷“當前任務涉及哪個早期決策”。這個判斷我建議用一個簡單的關鍵詞匹配 文件路徑匹配來實現(xiàn)不要用大模型判斷否則又引入不確定性。4. 實操過程從零搭一套免疫壓縮的代理工作流4.1 目錄結構約定先定目錄結構這是整套機制的地基。我在項目根目錄下建一個.agent/目錄里面放所有代理相關的狀態(tài)文件.agent/ decisions.log # 決策日志純文本一行一條 snapshots/ # 狀態(tài)快照目錄 snapshot-001.json snapshot-002.json ... current-task.md # 當前任務描述代理每次喚醒先讀這個 constraints.md # 硬約束清單比如“不許引入新依賴”decisions.log用純文本而不是 JSON是為了讓代理能快速 grep。JSON 雖然結構化好但讀取時需要解析而且容易因為格式問題導致整個文件不可讀。純文本一行一條即使某行格式壞了其他行還能用。current-task.md是代理的“當前指令”。每次喚醒時代理先讀這個文件知道“我現(xiàn)在要干什么”。這個文件由人維護或者在任務切換時由代理自己更新。它不需要長三五句話即可。constraints.md是硬約束比如“這個項目用 pnpm 不用 npm”“所有異步操作必須用 async/await 不用回調”。這些約束不常變但一旦違反就會產生大量返工所以單獨拎出來每次喚醒必讀。4.2 決策日志的寫入腳本代理自己寫日志容易格式不統(tǒng)一我寫了一個簡單的 shell 函數(shù)來規(guī)范化寫入# 用法log_decision 決策內容 理由 影響文件 替代方案 log_decision() { local ts$(date %Y-%m-%d %H:%M) local snapshot_idsnapshot-$(printf %03d $(ls .agent/snapshots/ | wc -l | tr -d )) echo [$ts] 決策$1 | 理由$2 | 影響文件$3 | 快照$snapshot_id | 替代方案$4 .agent/decisions.log # 拍快照 mkdir -p .agent/snapshots { echo { echo \timestamp\: \$ts\, echo \decision\: \$1\, echo \files\: \$(find src -type f -newer .agent/snapshots/last_marker 2/dev/null | tr \n ,)\, echo \task\: \$(cat .agent/current-task.md | head -5 | tr \n )\ echo } } .agent/snapshots/$snapshot_id.json touch .agent/snapshots/last_marker }這個腳本做了三件事寫日志、拍快照、更新標記文件。標記文件用于下次拍快照時判斷哪些文件是新改的。實際使用時代理在每輪對話結束時調用這個函數(shù)參數(shù)由代理自己填。實操心得理由字段一定要讓代理用“因為……所以……”的句式寫不要只寫“因為……”。我試過只寫原因不寫結論結果代理喚醒后看到原因自己推導出了另一個結論跟當初的決策不一致。加上“所以……”能強制它把結論也寫清楚。4.3 喚醒流程的實現(xiàn)喚醒流程我封裝成一個腳本代理啟動時先跑這個腳本把輸出作為第一條系統(tǒng)消息注入#!/bin/bash # wakeup.sh - 代理喚醒腳本 echo 當前任務 cat .agent/current-task.md echo echo 硬約束 cat .agent/constraints.md echo echo 最近決策 tail -5 .agent/decisions.log echo echo 當前文件樹 find src -type f -name *.ts -o -name *.js -o -name *.py | head -30 echo echo 最近對話摘要 # 這里從順敘軌讀取最近幾輪摘要具體實現(xiàn)依賴你的對話存儲方式 cat .agent/recent-summary.md 2/dev/null || echo (無摘要)這個腳本的輸出通常只有 1500-2500 token但包含了重建認知所需的全部信息。代理讀完這個輸出就知道自己是誰、在哪、要干什么、有什么約束、最近做了什么決策。4.4 與現(xiàn)有工具鏈的集成這套機制不需要替換你現(xiàn)有的代理框架它是在框架之上加一層“狀態(tài)管理層”。我用的是這樣的集成方式對話存儲仍然用框架自帶的對話歷史但設置一個較短的保留窗口比如最近 20 輪更早的自動丟棄決策日志由代理在每輪結束時調用log_decision寫入喚醒注入在每次新會話開始時先跑wakeup.sh把輸出作為 system message 注入任務切換當任務切換時更新current-task.md并寫一條“任務切換”決策日志這樣即使框架把對話歷史壓縮得面目全非代理也能通過喚醒流程重建認知。我實測過一個 18 萬 token 的重構任務中間經(jīng)歷了 7 次上下文壓縮代理沒有出現(xiàn)一次“失憶”導致的返工。5. 常見問題與排查技巧實錄5.1 決策日志膨脹了怎么辦這是最常見的問題。項目跑久了decisions.log可能積累幾百條記錄喚醒時tail -5只看最近 5 條但代理有時需要回溯更早的決策一 grep 就拉出一大堆。我的解法是分層歸檔。每 50 條決策為一個“紀元”歸檔到.agent/decisions-epoch-N.log主日志只保留當前紀元的記錄。喚醒時只讀主日志需要回溯時再按紀元號去讀歸檔文件。紀元號可以從決策日志的時間戳推算或者直接在current-task.md里標注“當前紀元3”。另一個技巧是給決策打標簽。在決策內容前加一個方括號標簽比如[架構]、[命名]、[依賴]、[坑]?;厮輹r按標簽 grep比全文搜索快得多也準得多。5.2 快照和 git 沖突了怎么辦有人會問既然有 git為什么還要快照兩者不沖突但職責不同。git 記錄的是代碼狀態(tài)快照記錄的是認知狀態(tài)。git 里看不到“我們?yōu)槭裁催x了這個方案”快照里能看到。但確實會有冗余。我的做法是快照里不存文件內容只存文件路徑列表和哈希值。需要文件內容時用哈希值去 git 歷史里找。這樣快照文件很小通常只有幾 KB不會和 git 搶存儲。注意快照里的哈希值要用 git 的 blob 哈希不要用 md5 或 sha256。git blob 哈??梢灾苯佑胓it cat-file找回內容其他哈希還得自己維護映射表。5.3 代理不按格式寫決策日志怎么辦這是提示詞工程問題。我的經(jīng)驗是不要指望代理自覺。在 system prompt 里明確寫“每輪結束必須調用 log_decision參數(shù)格式如下”并給一個完整示例。如果代理還是寫錯就在喚醒腳本里加一個校驗讀最近一條決策日志如果格式不對就在喚醒輸出里加一句“上一條決策日志格式異常請修正”。更狠的辦法是用工具調用強制格式。如果你的代理框架支持 function calling把log_decision定義成一個工具參數(shù)用 JSON Schema 約束。這樣代理不按格式寫就調不通自然會修正。5.4 喚醒后代理仍然“跑偏”怎么排查喚醒后跑偏通常是三個原因之一現(xiàn)象可能原因排查方法代理重復已否決的方案決策日志里“替代方案”字段沒寫清楚grep 決策日志看被否決方案是否記錄在案代理用了錯誤的命名constraints.md里沒寫命名規(guī)范檢查 constraints 是否覆蓋了命名約定代理不知道當前在改哪個文件current-task.md太模糊把任務描述細化到文件級別我踩過最坑的一次是current-task.md里寫“優(yōu)化數(shù)據(jù)處理流程”代理喚醒后把整個數(shù)據(jù)層重寫了一遍因為它不知道“優(yōu)化”具體指哪個環(huán)節(jié)。后來我把任務描述改成“優(yōu)化src/core/pipeline.ts里transformBatch函數(shù)的批處理邏輯目標是把單批耗時從 200ms 降到 100ms 以內”代理就精準多了。5.5 多代理協(xié)作時怎么共享狀態(tài)如果多個代理同時在一個項目上工作決策日志會互相覆蓋。我的解法是按代理分文件.agent/decisions-agentA.log、.agent/decisions-agentB.log喚醒時讀所有代理的日志按時間戳合并排序。但更根本的解法是按模塊分文件.agent/decisions-frontend.log、.agent/decisions-backend.log。每個代理只負責一個模塊只寫自己模塊的日志??缒K的決策單獨放.agent/decisions-global.log由人工維護或由主代理寫入。這樣既避免了沖突又讓每個代理的喚醒上下文更聚焦。6. 一些踩坑之后的個人體會這套機制我用了大半年最大的體會是不要試圖讓代理“記住”要讓它“能查”。記憶是不可靠的尤其是經(jīng)過壓縮的記憶。但查詢是可靠的只要狀態(tài)文件寫得好代理每次都能查到準確的信息。另一個體會是決策日志的質量比數(shù)量重要得多。我一開始每輪都寫結果日志里全是“修改了某文件某行”這種廢話真正關鍵的架構決策反而被淹沒了。后來改成只在不可逆決策時寫日志短了但每條都有分量喚醒效果反而更好。最后分享一個小技巧在current-task.md里加一行“上次中斷時的狀態(tài)”比如“正在等編譯結果”或“剛改完接口還沒跑測試”。代理喚醒后看到這行能更快接上中斷前的節(jié)奏不用從頭推理“我上次干到哪了”。這行由代理在每輪結束時自己更新成本很低但效果立竿見影。