你跟一個 AI agent 聊了快一小時,中途它忽然問你一開始就講過的事——那個你以為已經講清楚的需求,它又問了一次。你心裡冒出一句:這傢伙是不是失憶了?
某種意義上,是的。不是模型變笨了,是它能看到的東西被砍掉了。
Context Window 不是記憶,是一次攤開的稿紙
語言模型本身沒有「狀態」。它不是一個持續運作、把對話存在腦子裡的東西。每一次呼叫,系統都要把到目前為止的所有訊息重新攤開、餵給模型讀一次。這份攤開的稿紙就是 context window(上下文視窗),它的大小是固定的,通常用 token 數計算,例如 20 萬 token。
問題是,稿紙不會無限長。對話愈聊愈久,工具呼叫愈疊愈多,token 用量只會往上疊。撐過某個門檻,要嘛請求直接被拒絕,要嘛速度和費用一起變差,要嘛模型開始「lost in the middle」:資訊塞在稿紙中段時,模型對它的注意力明顯比放在開頭或結尾時弱。
所以 context 管理從來不是「要不要做」的選擇題,而是資源永遠不夠用的必答題:稿紙就這麼大,東西一直進來,總有一刻要決定,什麼留在紙上,什麼被劃掉。
取捨:什麼該留,什麼該丟
不是所有塞進 context 的東西都一樣重要。把一段對話拆開來看,其實有清楚的優先順序。
| 類型 | 例子 | 該怎麼處理 |
|---|---|---|
| 系統指令 | System prompt、任務規則 | 全程保留,優先權最高 |
| 近期對話 | 最近幾輪的使用者訊息與回覆 | 保留,維持對話連貫性 |
| 工具輸出 | 搜尋結果、API 回應、檔案內容 | 用完即丟,或壓成摘要 |
| 中間嘗試 | 走錯的推理路徑、失敗的呼叫 | 丟掉過程,只留結論 |
規則其實很直覺:保留結論,丟掉過程。 一個 agent 讀了一份三千行的 log 才找出問題根因,那三千行 log 不需要留在 context 裡——需要留下來的,是「根因是什麼」這一句話。ReAct 這類邊查邊想的 agent 尤其容易踩到這個坑:每一輪工具呼叫的 Observation(搜尋結果、網頁全文、長 JSON)會不斷疊進對話紀錄,跑個幾輪,context 就被這些原始資料撐爆,而不是被真正有用的資訊佔滿。
換句話說,context 管理的第一步不是「怎麼壓縮」,而是先分清楚哪些東西值得占用這塊稀缺空間。
壓縮:換一種形狀留下,而不是直接砍掉
分清楚優先順序之後,中間那一層(工具輸出、舊的對話)不會全部丟掉,而是換一種更省空間的形狀留下來,這就是compaction(壓縮)。
最粗暴的壓縮是truncation(截斷):只留最後 N 輪,前面的直接砍。做法簡單,代價是砍掉的部分徹底消失,如果稍早提到的某個限制條件其實還很關鍵,它就這樣憑空不見了。
更常見的做法是summarization(摘要):不是刪掉舊訊息,而是另外呼叫一次模型,把舊訊息濃縮成一段摘要,取代原本冗長的原文。
def compact_context(messages, keep_recent=10):
old, recent = messages[:-keep_recent], messages[-keep_recent:]
# 用一次額外的模型呼叫,把舊訊息壓成一段摘要
summary = summarize_with_llm(old)
return [{"role": "system", "content": f"先前對話摘要:{summary}"}] + recent
這正是許多 agent 系統(包含 Claude Code 自己)在對話逼近視窗上限時做的事:把早期訊息壓成摘要留在最前面,近期訊息保持原文不動。摘要保留了「發生過什麼、做了什麼決定」,犧牲的是逐字細節——這是有意識的取捨,不是資訊遺失的意外。
截斷丟的是內容,摘要丟的是細節。哪一種划算,取決於被丟掉的東西之後還會不會被用到。
記憶機制:短期留在 context,長期搬出去
壓縮解決的是「context 裡的東西太多」,但有些資訊的問題不是太多,而是活得比一次對話還久——這種情況,答案不是壓縮,是搬家。
把它想成兩層記憶。**短期記憶(short-term memory)**就是 context window 本身:即時、可讀,但一旦對話被壓縮或 session 重啟就會流失。**長期記憶(long-term memory)**是 context 之外的儲存(一個檔案、一個向量資料庫、一份任務清單),內容持久保存,agent 需要的時候才主動去讀,而不是全程佔著視窗的位置。
這也是 RAG(Retrieval-Augmented Generation,檢索增強生成) 的核心邏輯:與其把所有可能用到的資料都塞進 context 常駐,不如把它們存在外部,等真正要用的那一刻再檢索、再讀進來。Context window 是快取,外部儲存是硬碟——快取小而快,硬碟大而慢,兩者分工,誰都不用硬撐全部。
多步驟任務中最實用的一種長期記憶,是把計畫寫成一份外部清單,而不是全靠對話紀錄記住「做到哪了」。任務清單本身不占多少 token,卻能在對話被壓縮、甚至跨 session 之後,讓 agent 讀一眼就找回進度,不用重新從頭推敲。
多步驟任務的策略:把髒活外包給別人的 context
任務一旦拉長,還有一種取捨不是「留什麼、丟什麼」,而是這件事該不該進到我的 context 裡。
典型的例子是 sub-agent(子代理)隔離:主線任務丟出一個「幫我把這幾十個檔案的相關內容找出來」的子任務,交給一個獨立的 sub-agent 去執行。子 agent 自己的 context 裡裝滿了搜尋過程、讀過的檔案、走過的死路,但這些都不會流回主線——主線 context 收到的,只有子 agent 濃縮完的結論。
這跟「用完即丟」的道理一樣,只是換了一個層級操作:與其讓主線的 context 先被塞滿、再事後壓縮,不如一開始就別讓那些原始素材進來。骯髒、龐大、一次性的探索過程,本來就不屬於主線的稿紙。
結語
回頭看開頭那個「忽然失憶」的 agent,它沒有壞掉——它只是在做一件所有 context 有限的系統都得做的事:決定什麼值得留在手邊,什麼該壓縮,什麼該搬到別處保存。
一個 context 管理做得好的 agent,不是視窗開得最大的那個,而是清楚知道哪些東西該忘、哪些東西忘了會出事的那個。稿紙永遠不夠大,能寫得下整個任務的,從來不是靠更大的稿紙,而是靠更懂得取捨的手。