上一篇收在一個數字上:二十輪對話裡,之後還用得到的可能只有三句話,而你每一輪都把那三句跟另外九十七句一起重送。
把場景換成代理,這件事會更痛。昨天你讓代理修一個測試,它花了半小時才發現這個專案的測試要用 make test 跑、不能直接跑 pytest,你也糾正過它兩次:不要動 migration 檔。今天開一個新任務,它又從 pytest 開始試,又去改 migration。昨天學到的東西,今天全部歸零。
第 5 篇講過,權重是凍結的,模型本身不會因為用過一次就變聰明。所以對一個代理來說,記憶是它唯一能「學」的地方:這一次學到的東西,只有被寫到某個下一次讀得到的地方,才算學到。
這個動作叫寫入(memory write),而它最難的部分在動手存之前:你得先決定哪些東西值得存。 在寫入的那一刻決定什麼能進記憶,研究裡叫寫入時過濾(write-time gating)。這一篇講的是這道過濾怎麼設,以及怎麼把它接到你自己的代理上。
找一段你剛讓 Claude 做完的任務,最好是除錯、改程式或 code review,至少十輪,中間有它做錯、你糾正過的地方。
第一步:在同一段對話的最後,貼上這段提示詞:
這個任務結束後,對話會被清掉。請列出下次接新任務時,你需要知道的東西。
只能列四種:
1. 事實:關於我、這個專案、這個環境,之後不會隨便變的
2. 約束:我明確說過的規則或偏好
3. 結論:這次最後定案的決定(不是討論過程)
4. 教訓:這次試過、但不行的做法,以及為什麼不行
每一條一行,寫上今天的日期。沒把握的不要寫。
第二步:開一個新對話,把清單貼進去,交給它一個類似的新任務。
第三步:看它有沒有再犯上一次被你糾正過的錯。
它會不會重蹈覆轍,完全取決於第一步有沒有把那件事寫下來。 清單上沒有的,新任務就沒有。代理的記憶能做到多少,上限就是寫入時寫了什麼。
「記憶」這個詞太大了,先拆開。CoALA(Sumers et al., 2023)是一篇替語言代理整理架構的論文,借認知科學的分法,把代理的記憶拆成四種:
| 種類 | CoALA 的定義 | 在你的代理上是什麼 |
|---|---|---|
| 工作記憶(working memory) | 這一個決策週期裡正在用的資訊,跨模型呼叫延續 | 窗口裡的東西,也就是上一篇每一輪重送的那一整段 |
| 情節記憶(episodic memory) | 先前決策週期的經驗 | 「上次試過 pytest,不行」 |
| 語意記憶(semantic memory) | 代理對世界與對自己的知識 | 「這個專案用 make test」「不要動 migration」 |
| 程序記憶(procedural memory) | 模型權重裡的隱性知識,加上代理程式碼裡的顯性知識 | 權重(凍結的)與你寫的系統提示、工具、流程 |
第 5 篇的知識庫,CoALA 也算成語意記憶;上一篇的帳單,是工作記憶的成本。這一層要處理的是中間兩列:情節與語意記憶,怎麼從這一次的工作記憶裡寫出來。
CoALA 對學習的定義很直接:語言代理的學習,就是把資訊寫進長期記憶。 它也提醒,改寫程序記憶(代理自己的程式碼與規則)比寫情節或語意記憶風險更高,容易引入錯誤。
回到第 1 篇那個請求結構。加上記憶之後,新任務的第一個請求長這樣:
{
"model": "claude-opus-5",
"max_tokens": 1024,
"system": "(你原本的系統提示)\n\n<memory>\n2026-09-25 事實:這個專案的測試用 make test 跑\n2026-09-25 約束:不要修改 migrations/ 底下的檔案\n2026-09-25 教訓:直接跑 pytest 會缺環境變數而失敗\n</memory>",
"messages": [
{ "role": "user", "content": "修好 test_checkout 那個失敗的測試" }
]
}
昨天的歷史沒有跟過來,跟過來的只有 <memory> 那三行。這一層加上去的,就是那三行,以及決定寫哪三行的判斷。
第 4 篇的評估集裡本來就有兩題在等這一層:第 5 題「上一個對話說過我負責 order-api」是一個事實,第 6 題「上一個對話說過團隊 review 不寫風格意見」是一個約束。層二的基準線上兩題都沒答對,第 5 篇接上知識庫以後也還是沒答對,因為它們不在任何文件裡,只是你在某一段對話裡順口講過的話。
代理還需要第四種:教訓。Reflexion(Shinn et al., 2023)做的就是這件事:代理做完一次任務、拿到回饋(測試沒過、答案錯了)之後,用文字反省自己哪裡做錯,把這段反省存進一個情節記憶緩衝區,下一次嘗試時帶著它。整件事不更新任何權重,只靠寫下來的文字。在 HumanEval 這個程式碼基準上,它的 pass@1 做到 91%,當時 GPT-4 是 80%。
所以我的判準是這張表:
| 該寫 | 記憶種類 | 不該寫 |
|---|---|---|
事實:測試用 make test、資料庫是 PostgreSQL 16 |
語意 | 為了找出這件事而試過的五個指令 |
| 約束:不要動 migration、review 不寫風格意見 | 語意 | 它第一次怎麼違反、你怎麼糾正它 |
| 結論:最後決定用 cursor 分頁,不用 offset | 情節 | 討論過 offset 分頁的三個缺點 |
教訓:直接跑 pytest 會缺環境變數而失敗 |
情節 | 那半小時裡每一次失敗的完整輸出 |
右邊那一欄的用途在任務結束時就結束了。寫進去有兩個問題。
第一,錯的東西會被當成對的讀回來。 過程裡一定有被推翻的假設,寫進記憶之後,代理分不出那是定案還是當時的猜測,第 6 篇的雪球效應就跨過任務繼續滾。教訓是過程的結論:「試過 X,不行,因為 Y」,不含那一串嘗試。
第二,它會回到上一篇的帳單。 記憶每次任務都要讀,寫進去的過程越多,每次讀回來的也越多。
「沒把握的不要寫」也是這個道理:沒寫,代理會去查或問你;寫錯,它會很有把握地照著錯的做。
這道過濾在寫入時做,比在讀取時做更穩。Selective Memory for Artificial Intelligence: Write-Time Gating with Hierarchical Archiving(arXiv, 2026)直接比了這兩種做法:寫入時依顯著性(salience)分數決定一筆資訊能不能進記憶,被取代的舊版本封存而不刪除,留成一條版本鏈。干擾資料與有用資料的比例拉到 8 比 1 時,讀取時過濾(Self-RAG)的準確率掉到 0%,寫入時過濾仍然是 100%。原因很直觀:雜訊一旦寫進去,讀的時候就得跟有用的東西一起被撈出來比。
決定好寫什麼之後,還有「寫成什麼樣子」。LongMemEval(Wu et al., ICLR 2025)把記憶**存成多大的粒度(granularity)**列為第一個設計控制點,比了整段對話一筆、一輪一筆、壓成摘要或事實三種。結果是取捨:一輪一筆整體最好;壓成事實整體變差(資訊流失),但跨對話推理反而變好。 壓得越小越好串,也越會掉細節,下面三種形狀就站在這條軸的不同位置。
Mem0(Chhikara et al., 2025)的寫入分兩步:從最新的一問一答抽取候選事實,再拿它比對最相近的舊記憶,由模型選一個更新操作:ADD(新的)、UPDATE(補充舊的)、DELETE(推翻舊的)、NOOP(不動)。最值得抄的是 DELETE 和 NOOP:自己做的清單多半只有 ADD,所以才越加越長、新舊打架。
作者有利益關係。 Mem0 也是一家公司的產品,論文裡「省下超過 90% 詞元成本」這類數字是他們自己的量測。設計可以抄,數字打折看。
Wang et al.(2023)把它寫成遞迴(recursive):每次拿上一版記憶加上新的對話,產生新一版,而且能跟長脈絡、檢索增強疊加;更早的 Beyond Goldfish Memory(Xu et al., 2021)也量到摘要再回想的方法勝過當時的標準架構。弱點同樣有記錄:LongMemEval 說摘要會流失細節,Mem0 也因此在摘要之外另給最近幾則訊息。遞迴讓損失累積,第三版掉的,第四版不會知道。
MemGPT(Packer et al., 2023)借作業系統的分層記憶,叫虛擬脈絡管理(virtual context management):窗口是快而小的一層,外部儲存是慢而大的一層,在兩層之間搬資料。A-MEM(Xu et al., 2025)借卡片盒筆記法(Zettelkasten),每筆記憶都帶描述、關鍵字與連結,新筆記進來時還會回頭更新舊筆記。Claude 的記憶工具就是這個形狀:一個目錄底下的檔案,也就是 MemGPT 那個慢而大的一層。
| 面向 | 事實清單 | 滾動摘要(rolling summary) | 檔案 |
|---|---|---|---|
| 代表研究 | Mem0 | Wang et al.;Beyond Goldfish Memory | MemGPT;A-MEM |
| 在粒度軸上的位置 | 最小,一條一個事實 | 整段歷史壓成一段 | 按主題分,一份裡可以很細 |
| 寫入時的決定 | ADD/UPDATE/DELETE/NOOP |
新一版要保留舊一版的哪些部分 | 寫進哪一份、要不要回頭改舊的 |
| 讀的時候 | 整張貼進去 | 整段貼進去 | 只讀跟這次有關的那一份 |
| 論文量到或指出的弱點 | 壓成事實,整體表現下降(LongMemEval) | 摘要會流失細節(LongMemEval、Mem0) | 要有人決定分類與連結,分錯就找不到 |
事實清單:Mem0 的更新那一步,在 Claude 上就是一個工具定義。四個操作做成 enum,再用 tool_choice 強制模型用它回答,拿回來的一定是一個結構化的操作:
update_tool = {
"name": "update_memory",
"description": "比對候選事實與現有記憶,選一個操作",
"input_schema": {
"type": "object",
"properties": {
"operation": {"type": "string", "enum": ["ADD", "UPDATE", "DELETE", "NOOP"]},
"target_id": {"type": "integer"},
"text": {"type": "string"},
},
"required": ["operation"],
},
}
# client.messages.create(..., tools=[update_tool],
# tool_choice={"type": "tool", "name": "update_memory"})
模型只負責選操作,照著它去改那份 JSON 的是你的程式碼。有一個細節要顧:刪掉的 id 不要回收,否則別處還記著舊 id 的地方,會默默指到新的那一條。
滾動摘要:Wang et al. 的遞迴,寫成程式就是一行:
for turns in sessions:
memory = summarize(memory, turns) # 新一版 = f(上一版, 這一段對話)
過去的一切只能透過 memory 這個字串傳下去,某一次沒寫進去的,之後每一次都拿不到。
檔案:按主題拆開,更新時改掉舊的那一條、留一行取代了什麼:
/memories/
├── preferences.md # 2026-10-02 回答用繁體中文(取代 2026-09-24:中英並列)
└── projects/api.md # 2026-09-24 資料庫是 PostgreSQL 16
在 Claude 上,讓模型自己讀寫這個目錄,tools 裡放一行 {"type": "memory_20250818", "name": "memory"} 就夠了(對照官方 Python SDK 0.112.0 的原始碼,2026-09-26 查)。檔案實際存在哪、誰看得到,是你的程式碼決定的,那是下一篇的事。
條目少的時候清單最省事;條目一多、主題一雜,就會長成檔案的形狀。不管哪一種,事實都要寫得夠精確,才經得起壓縮。
上面都是寫入時過濾。Generative Agents(Park et al., 2023)走另一條路:25 個代理住在沙盒小鎮裡,每一筆經驗都存進記憶流(memory stream),寫入當下請模型打一個 1 到 10 的重要性分數(importance score);近期分數加總超過 150,就觸發一次反思(reflection):拿最近 100 條紀錄問出 3 個高層問題,歸納出見解,並標明根據哪幾條紀錄。反思也存回記憶流,所以常被讀到的,是模型自己歸納出來的那幾條。消融實驗顯示觀察、規劃、反思各自拿掉都會明顯變差。
它跟 Reflexion 是同一個動作:讓代理自己把一堆經驗歸納成幾條用得上的話。 最值得抄的是標出處,一條結論指不回它的依據,錯了也查不出來,跟第 8 篇講的是同一件事。150、100 這些數字是小鎮場景的,不要照抄。
LongMemEval 把長期記憶拆成五種能力。它們本來是用來量的,倒過來看,每一種都對應到寫的時候要做對的一件事:
| LongMemEval 的能力 | 寫入時要做對的事 | 用第 5、6 題來看 |
|---|---|---|
| 資訊抽取(information extraction) | 細節要留得夠精確 | 寫「order-api」,不是「一個訂單相關的服務」 |
| 跨對話推理(multi-session reasoning) | 同一個東西每次都用同一個名字 | 不要這次寫 order-api、下次寫 orders 服務 |
| 時間推理(temporal reasoning) | 每一條帶日期 | 「2026-09-24 約束:…」,才答得了「上個月的規則是什麼」 |
| 知識更新(knowledge updates) | 新的要取代舊的,不是再加一條 | 團隊改成要寫風格意見了,舊的那條要改掉 |
| 知道自己不知道(abstention) | 沒把握的不寫,沒寫的就讓它說不知道 | 沒聽你說過負責哪個服務,就不要猜一個寫進去 |
最後兩列在寫的當下看不出問題:知識更新做錯,記憶裡會有兩條矛盾的約束;abstention 做錯,它會很有把握地講出你沒說過的事。兩種錯都不會報錯。
這一篇大部分是通用的。 什麼該記、記成什麼形狀,換成任何一家模型都成立,我用 Claude 做示範。下一節的記憶工具與快取位置,才是 Claude 專屬的部分。
一個有記憶的代理,每一次任務都是同一個循環:開工時讀,工作中用,收工時寫。 真正要設計的是誰來讀、誰來寫、什麼時候寫。常見的接法有三種:
| 面向 | 預先載入 | 讓代理自己管 | 收工後另外整理 |
|---|---|---|---|
| 怎麼讀 | 開工時整張放進系統提示 | 代理自己決定什麼時候去讀哪個檔案 | 同預先載入 |
| 誰決定寫什麼 | 你的程式碼 | 代理,在工作過程中 | 任務結束後,另一次模型呼叫 |
| 適合的形狀 | 事實清單 | 檔案 | 事實清單或檔案 |
| 容易壞在哪 | 清單一長,每次都整張讀 | 代理在任務中途寫下還沒驗證的猜測 | 多一次呼叫的成本 |
預先載入最適合 Claude 的快取。第 10 篇講過快取只認前綴,記憶在同一次任務裡不變,所以開工讀一次放進系統提示,任務中不要動它。
讓代理自己管就是 Claude 記憶工具的做法。官方文件寫明,打開記憶工具之後,Claude 會在開始任務前自動檢查它的記憶目錄,工作中把學到的東西存成檔案(2026-09-26 查)。讀寫都是工具呼叫,發生在 messages 裡,系統提示的前綴不受影響。代價是寫入的判斷交給了代理本身。
收工後另外整理就是 30 秒實驗做的事,最接近 Reflexion:寫的時候結果已經出來,知道哪個做法成功、哪個失敗。
不管哪一種,系統提示裡都該有一份寫入規則。這一篇的判準寫成代理讀得懂的樣子,可以直接拿去用:
<memory_rules>
開工前:先讀記憶。記憶跟這次任務的指示衝突時,以這次為準,收工時把舊的那條改掉。
收工時:只寫四種:事實、約束、結論、教訓(試過什麼、為什麼不行)。
每條一行,開頭寫日期與種類。沒把握、還沒驗證的不寫。
已經有相關的一條,改舊的,不要加新的。不要寫任務過程。
</memory_rules>
第一,寫入要花模型呼叫。 收工整理要讀完整段過程,Generative Agents 那一派還要逐條打分與反思。省下的是之後每次任務的重新摸索。
第二,判斷錯了沒有人會知道。 不該寫的寫了,代理會在你沒在看的時候一直照做:「不要動 migration」寫進去之後,它每次都遵守,包括你真的需要它改 migration 的那一天。
第三,有些記憶會變成規則。 「以後部署都先跑 make check」改變的是代理怎麼做事,碰到了 CoALA 說風險更高的程序記憶。比較穩的做法是讓代理只能提議,由你確認後才寫進系統提示。
第四,記憶要有人維護。 知識更新那一列說的「新的要取代舊的」,不會自己發生。第 5 篇的知識庫會沉默地過期,記憶也會,而且更難發現,因為它是一條一條在不同的對話裡長出來的,沒有人記得是哪一天寫進去的。所以每條都要帶日期。
我現在的習慣是:一段任務結束才寫,一次只寫幾條,寫之前先看一眼現有的清單,有衝突就改舊的,不要加新的。
多了什麼能力:你的代理跨任務學得到東西了。你有 CoALA 的四種記憶當地圖、一個判斷該不該寫的準則(留事實、約束、結論、教訓,不留過程)、三種接法的取捨,以及一份可以直接放進系統提示的寫入規則。
多付了什麼代價:每次任務結束時多一次模型呼叫,以及一個不會自己更新、寫錯了也不會報錯、而且代理會在你沒看的時候照做的狀態。
決定好要記什麼以後,那些東西實際上存在哪裡?又是誰負責存?
在 Claude 上,這個問題的答案有點出乎意料:模型只負責提出「我要寫這一條」的請求,真正把它存下來、存在哪、誰看得到,全部是你的程式碼決定的。下一篇就真的接一次,看那個請求長什麼樣子,以及它把哪些責任留給了你。
ADD/UPDATE/DELETE/NOOP 四個更新操作。