上一篇看 Hindsight 時,已經看到一種做法:把 Memory 從 Agent Runtime 抽出來,放進獨立的 Memory Service。
今天看 akitaonrails/ai-memory,是它選擇把長期 Memory 做成一份人也能直接閱讀、修改,而且能用 Git 留下版本歷史的 Project Wiki。
例如航空客服 Project 累積一段時間後,Memory 可能真的就是:
flight/
├── decisions/
├── gotchas/
├── procedures/
├── concepts/
└── sessions/
以 Claude Code 為例,ai-memory 會安裝兩條整合:
ai-memory install-mcp \
--client claude-code \
--apply
ai-memory install-hooks \
--agent claude-code \
--apply
兩者分工很簡單:
MCP
→ Agent 主動查 / 寫 Memory
Hook
→ Agent 工作時,自動把事件送進 ai-memory
例如使用者問:
之前這個 Project
有沒有碰過 cancellation code 的問題?
Agent 可以透過 MCP 呼叫:
memory_query
去查既有 Memory。
另一方面,Agent 工作過程中的:
SessionStart
UserPromptSubmit
PreToolUse
PostToolUse
SessionEnd
會透過 Hook 自動送進 ai-memory,不需要 Agent 額外決定「現在要記住這件事」。
所以一次 Session 可以簡化成:
User 問問題
↓
Agent 工作
↓
Prompt / Tool Call / Tool Result
│
├── Hook → ai-memory 記錄工作過程
│
└── MCP → Agent 查詢或寫入既有 Memory
install-hooks 會依 Agent 的 lifecycle API,建立事件對照。例如 Claude Code 的 PostToolUse 會接到 ai-memory 的 post-tool-use hook;Codex 也有自己的事件對照表。這些 mapping 寫在 render_shared.rs。
ai-memory 是把 Agent Runtime 原本提供的 Lifecycle Event 接進自己的 Memory pipeline,這些由 Hook 收進來、還沒整理成長期知識的工作紀錄,在 ai-memory 裡叫做 Observation。
例如:
PostToolUse
tool: query_ticket
result:
{
"status_code": 3
}
這時它只是:
Agent 做過什麼的原始紀錄
還不是:
gotchas/cancellation-code.md
也就是:
Hook 先負責收集;後面的 Consolidation 才決定哪些 Observation 值得整理成長期 Memory。
Hook 收到的 Observation 只是原始工作紀錄,不會每次都立刻呼叫 LLM。
只有在需要進行 Consolidation 時,ai-memory 才會把這些 Observation 交給 Consolidator 整理。
常見觸發方式包括:
| 觸發方式 | 什麼時候發生 | 是否需要額外設定 |
|---|---|---|
memory_consolidate |
使用者要求整理 Memory,Agent 透過 MCP 呼叫這個 Tool | 不需要改程式碼,但要有 MCP |
PreCompact |
Agent Context 要進行壓縮前,由 Hook 觸發 Consolidation | 需要安裝 Hook,並有 LLM Provider |
SessionEnd |
Session 結束時自動整理 | 要另外開 AI_MEMORY_CONSOLIDATE_ON_SESSION_END=true,預設關閉 |
真正觸發 Consolidation 後,才會進入:
crates/ai-memory-consolidate/src/consolidator.rs
Session Observations
↓
Consolidator
↓
讀既有 Project Wiki
↓
呼叫設定的 LLM
↓
產生 Structured Memory
↓
寫回 Wiki
其中 consolidate_session(...) 就是負責把一段 Session 的 Observation 整理成長期 Memory 的主要入口。
所以可以把兩者的關係記成一句話:
觸發條件決定「什麼時候要整理」,
Consolidator負責「真的怎麼整理」。
LLM 整理 Session 時,可以把 Memory 分成什麼?
這裡 ai-memory 沒有完全交給 Prompt 自由決定。
它先固定了一組 PageKind:
crates/ai-memory-consolidate/src/types.rs
pub enum PageKind {
Rule,
Decision,
Gotcha,
Procedure,
Fact,
}
| Kind | 意義 |
|---|---|
Fact |
一般值得留下的事實 |
Decision |
Project 已經做出的決策 |
Gotcha |
已知陷阱、容易踩到的坑 |
Procedure |
可以重複執行的流程 |
Rule |
Project 應長期遵守的規則 |
例如剛才的航空 API:
status_code=3
容易被開發者誤認成 customer cancellation
LLM Consolidation 可以判斷:kind = gotcha
最後形成像:gotchas/cancellation-code.md
# Cancellation code 3
在 provider X 中,`status_code=3`
代表 flight cancellation。
不能直接套用一般 customer-initiated
change flow。
這個 PageKind 不是 Prompt 裡隨便寫的文字,而是 Rust enum,也會進 Structured Output Schema。
因此使用者不能只設定:
memory_types = [
"fact",
"preference",
"customer_profile"
]
就讓系統多出:
Preference
CustomerProfile
這是 ai-memory 對 Memory Extraction 的第一個限制:
可以調整整理內容,但不能只靠設定任意新增 Memory Kind。
ai-memory 有一種特殊 Wiki Page:_slots/,適合放少量、之後 Session 經常需要知道的 Context。
例如:
使用者偏好:
技術文章不要中英混雜,
Code 名稱除外。
可以做成:_slots/writing-preference.md
Markdown frontmatter 寫:
---
slot_kind: invariant
---
ai-memory 原始碼對 Slot 的分類是:
pub enum SlotKind {
Invariant,
State,
}
兩種意思很簡單:
Invariant
→ 比較穩定的背景 / Preference
→ 不應該隨便被覆寫
State
→ 目前工作狀態
→ 本來就會一直變
例如:
---
slot_kind: state
---
目前正在修改 Day 20。
下一步要確認 Git / SQLite。
所以PageKind和SlotKind回答的是不同問題。
| 設計 | 回答的問題 |
|---|---|
PageKind |
這份長期 Knowledge 是 Fact、Decision、Gotcha 還是 Procedure? |
SlotKind |
這個少量 Context 是穩定資訊,還是目前工作狀態? |
實際使用 Slot 時,改的是 Markdown 的:
slot_kind: invariant
或:
slot_kind: state
_prompts/consolidation.md 又是什麼?它不是 Memory,它是在告訴 Consolidator:
這個 Project 整理 Memory 時,什麼東西比較重要?
例如航空客服可以建立:
_prompts/consolidation.md
內容:
優先保留:
- 航空公司 API 的特殊 mapping
- 已確認的票規
- User 長期偏好
- 曾經造成錯誤的 API 行為
忽略:
- package install log
- 一次性的 debug output
- routine command output
Consolidation 時,ai-memory 會把這段文字放進 Prompt,影響 LLM 選擇與整理資訊的方式,程式裡直接定義:
pub const PROJECT_INSTRUCTIONS_PATH: &str =
"_prompts/consolidation.md";
但是它只是整理偏好,它不能把:PageKind = Fact / Decision / Gotcha / Procedure / Rule,改成:PageKind = Fact / Preference / CustomerProfile
| 東西 | 做什麼 | 使用者怎麼改 |
|---|---|---|
PageKind |
限制 LLM 最後輸出的主要 Memory 類別 | 不能靠一般 config 新增 |
_slots/*.md |
保存穩定 Preference 或目前 State | 編輯 Markdown + slot_kind |
_prompts/consolidation.md |
告訴 LLM 整理時要重視 / 忽略什麼 | 直接編輯 Markdown |
所以如果需求是:「航空客服特別重視票規與 API 踩坑。」,寫_prompts/consolidation.md就可以。
Consolidation 最後會呼叫 Wiki::write_page(),把整理好的 Memory 寫進 Project Wiki。例如:
workspace = customer-service
project = flight
path = gotchas/cancellation-code.md
實際會產生:
wiki/
└── customer-service/
└── flight/
└── gotchas/
└── cancellation-code.md
這份 Markdown 才是 Memory 的 Source of Truth;Git 負責留下它的修改歷史,SQLite 則負責讓 Agent 快速搜尋這些 Markdown。
<data_dir>/wiki/ 本身就是一個真正的 Git Repository,Wiki::new() 會透過:
GitAdapter::open_or_init(&root)
初始化 Git,而 commit_all() 負責建立 Commit。ai-memory 使用 Rust git2 / libgit2 操作 Git,不是每次 shell out 執行 git commit,但產生的仍是一般 Git 歷史。
例如 Memory 一開始是:
status_code=3 代表 flight cancellation
後來修正成:
只有 provider X 的 status_code=3
才代表 flight cancellation
這兩個版本會留在 Git history,因此可以直接:
cd <data_dir>/wiki
git log
git diff
git show <commit>
查看 Memory 什麼時候被修改、前後差了什麼。
如果 Wiki 已經有幾千份 Markdown,Agent 查:
之前有沒有碰過 flight cancellation mapping?
不需要逐一打開所有 .md。
ai-memory 會同步維護:
<data_dir>/db/memory.sqlite
SQLite 裡保存搜尋需要的索引,例如 FTS5 全文搜尋、Entity、Link,以及有設定 Embedding Provider 時的 Vector 資訊。memory_query 先透過這些 Index 找到相關 Page,例如:
gotchas/cancellation-code.md
再讀真正的 Markdown。
SQLite 的更新也有兩條路:
ai-memory 自己寫 Memory
→ Wiki::write_page()
→ Markdown + SQLite 一起更新
使用者直接修改 .md
→ Watcher 偵測變更
→ 更新 SQLite
所以即使直接用 VS Code、Obsidian 或 vim 修改 Markdown,watcher.rs 仍會把變更同步回搜尋索引;如果 SQLite 損壞,也能重新掃描 Markdown,透過 Reindex 重建。
整條流程可以縮成:
Observation
↓
Consolidator + LLM
↓
Wiki::write_page()
↓
gotchas/cancellation-code.md
│
├── Git
│ → 留版本歷史
│
└── SQLite
→ 建搜尋索引
→ memory_query 找回這份 Markdown
所以 Git-backed Markdown Wiki + derived SQLite index 的意思很直接:Markdown 是真正要保留的 Memory,Git 記錄它怎麼改過,SQLite 則讓 Agent 能快速找到它。
現在才需要回到「跨 Agent」。
假設早上是 Claude Code 發現:gotchas/cancellation-code.md,下午換 Codex。
Codex 不需要知道 Claude Code 的原始 Session State,它只要透過 MCP 呼叫:memory_query查:
flight cancellation
status code
ai-memory 使用 SQLite Index 找到候選 Page,再回到真正的 Markdown Wiki。
概念是:
Codex
│
│ memory_query
▼
SQLite Search Index
│
│ 找到
▼
gotchas/cancellation-code.md
│
▼
回傳 Project Knowledge
所以兩個 Agent 真正共享的是外面的:Project Wiki
這也解釋了為什麼 ai-memory 可以接不同 Runtime。
共同介面是:MCP → 查 / 寫 Memory
而自動 Capture 的部分則依 Agent 使用它自己的 integration。
例如:
Claude Code
→ Lifecycle Hook
Codex
→ Lifecycle Hook
OpenClaw
→ Generated TypeScript Plugin
OpenClaw 的 integration 實作在:
crates/ai-memory-cli/src/commands/openclaw_plugin.rs
所以不是所有 Agent 都硬套同一份 Hook Script。
不同 Runtime 各自接到同一個 ai-memory Server,最後讀寫同一套 Project Wiki。