六個框架的表面差異很大,但把它們的機制攤開對照,會發現底層是同一組五個元件的不同組態:記憶層次、寫入管線、召回管線、壓縮與遺忘、以及量測。這一篇把那組共通結構抽出來,寫成一份可以套到新專案上的藍圖——先用決策樹決定要哪些部分,再照參考架構組裝。
這是本專欄的核心產出。 前面八篇的「可抽取的模式」在這裡被組合成一套完整設計;介面與資料結構的具體形式見 schema 草案。藍圖裡的每一個結構決策都標明它來自哪個框架,方便回頭查證原始設計。
第一步:決策樹
多數專案不需要完整的記憶系統。這棵樹的目的是盡快排除不需要的部分——每一個「不需要」都省下大量實作與維運成本。
Q1. 同一個對話中斷後需要續接嗎?
否 → 不需要任何持久化。到此結束。
是 → 需要 checkpointer(thread state)。這不是記憶問題,
多數 orchestration 框架已內建。繼續 Q2。
Q2. 跨對話需要記得任何東西嗎?
否 → 到此結束。你需要的是 checkpointer,不是記憶框架。
是 → 繼續 Q3。
Q3. 要記的東西裡,有「必須被遵守、不能漏」的規則嗎?
是 → 需要【常駐層】:全量載入、精確定址、唯讀。
絕對不要放進向量庫。
→ 繼續 Q4。
Q4. 要記的東西裡,有「量大、只在相關時才需要」的事實嗎?
是 → 需要【召回層】:向量+關鍵字混合檢索、按需注入。
→ 繼續 Q5。
Q5. 這些事實會隨時間改變,而且「以前是什麼」有價值嗎?
否 → 覆寫即可(Mem0 式的 UPDATE/DELETE)。
是 → 需要【雙時間軸】:標記失效而非刪除。(Zep 式)
→ 繼續 Q6。
Q6. 需要回答「A 關聯到 B、B 關聯到 C」這類多跳問題嗎?
否 → 不要用圖。向量+metadata 過濾就夠,運維成本低一個量級。
是 → 需要【圖層】,並準備好承擔實體消解的品質風險。
→ 繼續 Q7。
Q7. 需要向人類或稽核者解釋「系統為什麼記住這件事」嗎?
是 → 需要【provenance】:保留不可變的原始事件,
衍生事實指回原始事件。
→ 繼續 Q8。
Q8. 記憶需要被人類直接讀取、審閱、修正嗎?
是 → 真源選【檔案】(Markdown),索引作為衍生物。
否 → 真源選資料庫。
→ 繼續 Q9。
Q9. 有多個 agent 需要共用記憶嗎?
是 → 需要【共享層】與明確的併發寫入語意。
這一塊生態最不成熟,準備自己處理。
→ 繼續 Q10。
Q10. 系統會跑很久(數月以上)嗎?
是 → 必須設計【遺忘】與【容量上限】,而且要有成長監控。
否 → 可以延後,但要留介面。
實務觀察:大多數專案在 Q4 或 Q5 就結束了。 一個典型的對話助理需要「常駐層 + 召回層 + 覆寫式更新」,不需要圖、不需要雙時間軸、不需要 provenance。過度設計記憶層是這個領域最常見的浪費,而它的成本不只是實作時間——每一個多出來的層都是一個新的失效模式。
第二步:參考架構
通過決策樹之後,把需要的部分照下面的結構組裝。每一層都是可選的,但層與層之間的介面應該固定——這是之後能替換單一層而不重寫全部的前提。
┌─────────────────────────────────────────────────────────┐
│ Context Assembler(上下文組裝器) │
│ 每輪推理前執行,從下面各層組出 working memory │
│ 有 token 預算,按 block 配額分配 │
└───────┬──────────────┬──────────────┬───────────────────┘
│ │ │
┌────▼────┐ ┌─────▼──────┐ ┌───▼────────┐
│ 常駐層 │ │ 召回層 │ │ Session 層 │
│ Resident│ │ Retrieved │ │ Checkpoint │
├─────────┤ ├────────────┤ ├────────────┤
│規則(唯讀)│ │語意事實 │ │ thread 狀態│
│使用者畫像│ │情節記憶 │ │ 最近訊息 │
│agent人格 │ │文件知識 │ │ │
│全量載入 │ │混合檢索top-k│ │ 自動持久化 │
└────┬────┘ └─────┬──────┘ └────────────┘
│ │
│ ┌────▼──────────────────────────┐
│ │ Memory Store(記憶儲存) │
│ │ ─ facts (語意,可更新) │
└────────►│ ─ episodes(情節,不可變) │
│ ─ rules (程序,人工管理) │
│ ─ index (向量+FTS,衍生物)│
└────▲──────────────────────────┘
│
┌──────────────┴───────────────┐
│ Write Pipeline(寫入管線) │
│ extract → dedup → reconcile │
│ → index │
│ 非同步執行,不在 hot path │
└──────────────▲───────────────┘
│
┌──────────────┴───────────────┐
│ 觸發源:對話結束 / 週期 nudge │
│ / 顯式指令 / 文件攝取 │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Maintenance(維護,背景排程) │
│ 壓縮 / 失效 / 歸檔 / 容量檢查 │
└──────────────────────────────┘
三個結構決策值得單獨說明,因為它們是這份藍圖與「直接用某個框架」的主要差異:
Context Assembler 是一個顯式的元件,不是散落在各處的字串拼接。 它有一個明確的職責:給定當前狀態,產出這一輪的 context,並且這個產出應該是可記錄、可重播的。Letta 稱之為 compile,這個用詞很準確。沒有這個元件時,「上一輪 agent 到底看到了什麼」會變成一個無法回答的問題,而那是 debug 記憶問題時最需要的資訊。
常駐層與召回層必須分開,而且用不同的機制。 常駐層全量載入、精確定址、有 token 配額;召回層按需檢索、經過相似度篩選。把兩者混在一個檢索路徑上是最常見的架構錯誤——它讓「必須被遵守的規則」變成「有機會被召回的建議」。
Episodes 不可變、facts 可更新。 原始事件只 append,抽取出的事實可以被覆寫或標記失效,並指回產生它的 episode。這個分離帶來三個好處:provenance 免費取得、抽取邏輯改進後可以重跑歷史、以及「刪除事實」與「刪除發生過的事」在語意上被區分開。這是把 event sourcing 套在記憶上,與 Event Sourcing/CQRS 的推理完全一致。
第三步:寫入管線的四個階段
這是從 Mem0 的 extract-update 管線擴充而來,補上前後兩端。每個階段都應該是可測試的純函式,而且每一次寫入都應該留下一筆決策紀錄。
階段 0:Gate(把關)——這批內容值得進入管線嗎?
最便宜也最常被跳過的階段。判準可以是規則(訊息長度、是否包含第一人稱陳述、是否為工具輸出)或一次輕量的 LLM 判斷。Hermes 的 periodic nudge 本質上就是把整條管線放在一個 gate 後面。 對常駐層的寫入,這個 gate 應該嚴格(預設不寫);對召回層,可以寬鬆。
階段 1:Extract(抽取)——把原始內容轉成候選記憶單元。
用受 schema 約束的結構化輸出,而不是自由生成(Cognee 的 ontology 模式)。輸出應該同時包含事實文字、型別(語意/情節/程序)、範圍鍵、以及時間資訊(這件事何時為真)。抽取階段就要決定型別,因為不同型別走不同的儲存與去重邏輯。
階段 2:Dedup(去重)——移除完全重複的內容。
先做內容雜湊比對(OpenClaw 的 SHA-256 快取 模式),這一層零成本且處理掉相當比例的重複。把它放在 LLM 對帳之前,可以省下大量呼叫。
階段 3:Reconcile(對帳)——決定 ADD/UPDATE/DELETE/NOOP。
取回向量相似的鄰域,把候選與鄰域一起交給 LLM 判斷操作。三個必要的擴充:
- 輸出必須包含理由,而不只是操作。理由是之後 debug 與稽核記憶行為的唯一依據。
- DELETE 的預設實作應該是標記失效而非物理刪除(Zep 模式)。真正的物理刪除只保留給使用者的刪除請求。
- 對帳只在同一個範圍鍵與同一個型別內進行。 跨使用者對帳是資料洩漏,跨型別對帳是把規則與事實搞混。
階段 4:Index(索引)——寫入真源,更新衍生索引。
真源寫入與索引更新應該分開,而且索引必須是可從真源完整重建的。這個性質在 embedding 模型換版時價值巨大:重建索引不需要重跑階段 1-3 的 LLM 呼叫。
整條管線應該非同步執行。 只有一個例外值得走 hot path:使用者剛剛明確說出的偏好或剛剛糾正的錯誤——判準是「這條記憶如果晚十分鐘生效,會不會造成可見的體驗問題」。
第四步:召回管線
召回的目標函式直接採用 Anthropic 的定義:能最大化目標結果機率的、最小的高訊號 token 集合(見 context engineering)。因此召回管線的每一個階段都在做同一件事——在保留有用資訊的前提下減少 token。
階段 1:Resident load(常駐載入)——無條件載入規則與畫像,按 block 配額截斷。這一層不做檢索,因為經過檢索的東西就不叫必須遵守。
階段 2:Candidate generation(候選生成)——三路並行取回:
- 語意(向量)——抓措辭不同但意思相近的。
- 關鍵字(BM25 / FTS5)——抓專有名詞、代號、版本號。純向量在這類查詢上表現很差,這是被反覆驗證的已知弱點,補上關鍵字檢索的成本很低。
- 結構(metadata 過濾、圖遍歷、或直接的 key 取回)——抓「必須靠關係才能到達」或「明確知道要哪一條」的。
三路合併是 Graphiti 與 Cognee 獨立收斂到的同一結論,應該當成預設而非優化。
階段 3:Temporal filter(時效過濾)——只保留在目標時點有效的事實。有雙時間軸時這是一個廉價的 SQL 條件;沒有時間模型時這個階段做不到,而漏掉它的後果是模型同時看到矛盾的新舊事實。
階段 4:Rerank & budget(重排與預算)——依相關性重排,然後按 token 預算截斷。預算應該是每個 block 各自的配額,而不是一個總量(Letta 的 block 配額 模式)——否則一次大量召回會把使用者畫像擠出 context。
階段 5:Assemble(組裝)——填入模板,記錄這一輪實際注入了什麼。記錄這件事不是可選的,它是 可觀測性 的唯一資料來源。
兩個可選的優化,在記憶量大時效益明顯:
摘要粗篩——每個記憶單元同時存摘要與原文,階段 2 檢索摘要,階段 5 才取回需要的原文(Cognee 模式)。
漸進揭露——context 裡只放識別符,讓 agent 用工具按需取回內容(Anthropic 的 just-in-time 模式)。但官方也指出這是用速度換自主性,且混合策略往往最有效——不要把它當成一律更好的選項。
第五步:壓縮與遺忘
這是最容易被延後、也最容易變成技術債的部分。四種機制解決不同的問題,不能互相替代。
Compaction(對話壓縮)——把長對話歷史摘要成短版本。這是 session 層的機制,處理的是 working memory 的膨脹。調校順序:先最大化召回、再改善精確率(Anthropic 的明確建議)——因為壓縮丟掉的資訊無法恢復。
Invalidation(失效)——事實被新事實取代時標記失效,不刪除。它處理的是正確性問題,不是容量問題。
Archival(歸檔)——把失效或長期未召回的記憶搬到冷儲存,從主索引移除。它處理容量與召回品質問題:失效的邊留在索引裡會稀釋檢索結果。Zep 這一塊沒有內建,需要自己設計,這是採用它時必須事先規劃的事。
Deletion(刪除)——真正的物理移除。只有兩個正當觸發:使用者的刪除請求,以及確認為污染的記憶。 其餘情況都該用失效或歸檔。
一組可用的預設策略:失效即時執行(在對帳階段);歸檔按排程執行(例如失效超過 90 天、或連續 N 天未被召回);刪除只由顯式請求觸發;壓縮由 token 門檻觸發。 同時必須有成長監控——記憶條目數、索引大小、平均召回 token 數三條曲線,缺一不可。
第六步:量測
沒有量測的記憶系統無法被改進,因為記憶的失效是靜默的:它不會報錯,只會讓回答變差。完整的評測設計見 評測與可觀測性,藍圖層面需要的最小集合是三個:
寫入決策的正確率——抽樣人工標註「這條該不該被記住/操作選對了嗎」。 召回的命中率與精確率——在需要某條記憶的任務上,它有沒有被召回;召回的內容有多少是無關的。 token 效率——同樣的任務成功率下用了多少 token。這一項直接對應召回的目標函式,是最終的驗收指標。
這三項必須從第一天就開始記錄,因為它們需要歷史資料才能看出趨勢,而記憶系統的退化是漸進的。
這份藍圖與各框架的關係
它不是要取代框架,而是要讓框架變成可替換的。 實務上最合理的作法是:照這份藍圖定義自己的介面(見 介面草案),然後用某個框架實作其中一部分——例如用 Mem0 實作寫入管線的階段 1-3、自己實作常駐層與 Context Assembler。
這樣做的成本是一層薄的抽象,換到的是三件事:換框架時只改一個 adapter;不同層可以用不同框架(規則用檔案、事實用 Mem0、關係用 Graphiti);以及最重要的——當框架的行為不符預期時,你知道問題出在哪一個階段。