本文是「Agent Memory 系列」第 08 篇,也是 MemPalace 子系列的完结篇。
- 05 讲架构(Wings/Rooms/Closets/Drawers 四层 + AAAK 方言)
- 06 讲挖掘管线(miner、convo_miner、general_extractor、split_mega_files)
- 07 讲 AAAK 方言、知识图谱、分层检索(layers、searcher、palace_graph)
- 08(本篇)把前面所有零件装进真实的 AI 工作流:MCP 服务、CLI、引导式 setup、自动保存 hooks,以及如何把 MemPalace 接到 Claude Code / Gemini CLI / ChatGPT / 本地模型里跑起来。
这是"最后一公里"------把一个单机 Python 包变成 AI 的长期记忆器官。
📑 目录
- [0. 引言:单机包 → AI 工作流的最后一公里](#0. 引言:单机包 → AI 工作流的最后一公里)
- [1. 🔌 Part A --- MCP 服务(mcp_server.py 深读)](#1. 🔌 Part A — MCP 服务(mcp_server.py 深读))
- [1.1 MCP 协议速览](#1.1 MCP 协议速览)
- [1.2 进程启动与配置注入](#1.2 进程启动与配置注入)
- [1.3 19 个工具完整清单](#1.3 19 个工具完整清单)
- [1.4 AAAK auto-teach:一次唤醒教会 AI 方言](#1.4 AAAK auto-teach:一次唤醒教会 AI 方言)
- [1.5 Memory Protocol:把"记忆使用规范"注入每次 status 调用](#1.5 Memory Protocol:把"记忆使用规范"注入每次 status 调用)
- [1.6 JSON-RPC 主循环、类型强制与幂等写入](#1.6 JSON-RPC 主循环、类型强制与幂等写入)
- [2. 🛠️ Part B --- CLI(cli.py +
__main__.py)](#2. 🛠️ Part B — CLI(cli.py + main.py))- [2.1 命令总表](#2.1 命令总表)
- [2.2 双层子命令:hook run / instructions
<name>](#2.2 双层子命令:hook run / instructions <name>) - [2.3
--palace全局选项与配置解析顺序](#2.3 --palace 全局选项与配置解析顺序) - [2.4 compress / repair:两个运维命令](#2.4 compress / repair:两个运维命令)
- [3. ⚙️ Part C --- 引导式 setup(onboarding.py)](#3. ⚙️ Part C — 引导式 setup(onboarding.py))
- [3.1 init 全流程:从 mode 到 wing_config](#3.1 init 全流程:从 mode 到 wing_config)
- [3.2 AAAK bootstrap:aaak_entities.md + critical_facts.md](#3.2 AAAK bootstrap:aaak_entities.md + critical_facts.md)
- [3.3 identity.txt、wing_config.json、entities.json 的角色](#3.3 identity.txt、wing_config.json、entities.json 的角色)
- [4. 🪝 Part D --- Hooks(hooks_cli.py + 两个脚本)](#4. 🪝 Part D — Hooks(hooks_cli.py + 两个脚本))
- [4.1 Save Hook:每 N 条消息触发结构化保存](#4.1 Save Hook:每 N 条消息触发结构化保存)
- [4.2 PreCompact Hook:压缩前的最后一次快门](#4.2 PreCompact Hook:压缩前的最后一次快门)
- [4.3 MEMPAL_DIR 与 auto-ingest](#4.3 MEMPAL_DIR 与 auto-ingest)
- [4.4 安全考量:shell injection(#110)与
_sanitize_session_id](#110)与 _sanitize_session_id) - [4.5 bash 脚本 vs Python hooks_cli:为什么同时提供两套](#4.5 bash 脚本 vs Python hooks_cli:为什么同时提供两套)
- [5. 🚀 Part E --- 实战配置](#5. 🚀 Part E — 实战配置)
- [5.1 Claude Code 插件市场安装](#5.1 Claude Code 插件市场安装)
- [5.2 通用 MCP:ChatGPT / Cursor / 任意 MCP 客户端](#5.2 通用 MCP:ChatGPT / Cursor / 任意 MCP 客户端)
- [5.3 Gemini CLI 集成(PreCompress hook)](#5.3 Gemini CLI 集成(PreCompress hook))
- [5.4 本地模型:wake-up + Python API 离线回路](#5.4 本地模型:wake-up + Python API 离线回路)
- [5.5 完整的
~/.mempalace/目录结构](#5.5 完整的 ~/.mempalace/ 目录结构)
- [6. 🎯 典型对话的一次完整走一遍](#6. 🎯 典型对话的一次完整走一遍)
- [7. 📚 系列总结:MemPalace 四篇脉络回顾](#7. 📚 系列总结:MemPalace 四篇脉络回顾)
- [8. 🔭 未来展望:AAAK closets、fact_checker、ChromaDB pin](#8. 🔭 未来展望:AAAK closets、fact_checker、ChromaDB pin)
- [9. 📖 参考文献](#9. 📖 参考文献)

0. 引言:单机包 → AI 工作流的最后一公里
翻回第 05 篇开头时我们列过一张"memory 系统三件套":
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 存什么 │ → │ 怎么找 │ → │ 谁来用它 │
│ (storage) │ │ (retrieval) │ │ (integration)│
└──────────────┘ └──────────────┘ └──────────────┘
05/06 06/07 ★ 08 ★
前面三篇把 存什么 + 怎么找 讲透了:
- 05:四层结构 + ChromaDB 单集合 + 元数据分层。你知道了 drawer 是逐字存、closet 是压缩存、room 是语义分桶、wing 是主体分桶。
- 06:miner.py / convo_miner.py / general_extractor.py / split_mega_files.py,四条挖掘流水线把代码仓、聊天 export、混合 mega-file 都能灌进 palace。
- 07:AAAK 方言规范、KnowledgeGraph SQLite 时序三元组、MemoryStack 分层 wake-up、palace_graph 跨 wing tunnels。
但到这里,一切还只是一个命令行工具 。你还得手动 mempalace search "XXX",把结果复制粘贴进 Claude 的对话框。这和 2021 年抄 ChatGPT 回答贴进 Notion 没有本质区别------AI 依然"不记得",是你在帮它记。
第 08 篇要解决的就是最后一公里:
- MCP 服务:让 AI 直接调用 19 个工具,无需人工中转;
- Hooks:让"保存"这件事在 AI 毫无察觉的前提下每 15 条消息自动发生;
- 引导式 setup:让第一次跑的人在 2 分钟之内有一个已经知道"你是谁、家里几个孩子、手头几个项目"的 palace;
- 多 harness 集成:Claude Code、Gemini CLI、ChatGPT、Cursor、Llama/Mistral 全覆盖。
读完这篇,你手里会有一套完整的、能接到任意 AI 客户端上的 long-term memory stack。
1. 🔌 Part A --- MCP 服务(mcp_server.py 深读)
文件 :mempalace/mcp_server.py,812 行,本包最重要的单个文件。它把前面所有组件(ChromaDB 集合、KnowledgeGraph、palace_graph、searcher、dialect)一次性暴露成 19 个 MCP 工具,供任何 MCP 客户端直接调用。
1.1 MCP 协议速览
MCP(Model Context Protocol)是 Anthropic 在 2024 年末放出的一个 JSON-RPC over stdio 协议,核心目标是让 LLM harness(Claude Code、Cursor、Gemini CLI ...) 和 外部能力提供者(搜索、数据库、文件系统 ...) 之间用统一的接口对话。它有三种主要资源:
- tools:可被模型调用的"函数",有名字、描述、JSON Schema 参数;
- resources:可被读取的"文件",通常用 URI 引用;
- prompts:预置提示词模板。
MemPalace 只用到了 tools ,并且以最小依赖的方式------不用 fastmcp、不用官方 SDK,直接手写 JSON-RPC。原因很简单:mempalace 想做到"pip install 之后零额外依赖就能跑 MCP",避免把 fastmcp 的 asyncio stack 强加给用户。
1.2 进程启动与配置注入
启动命令通常是:
bash
python -m mempalace.mcp_server [--palace /path/to/palace]
mcp_server.py 开头就做三件事:
python
def _parse_args():
parser = argparse.ArgumentParser(description="MemPalace MCP Server")
parser.add_argument("--palace", metavar="PATH",
help="Path to the palace directory (overrides config file and env var)")
args, _ = parser.parse_known_args()
return args
_args = _parse_args()
if _args.palace:
os.environ["MEMPALACE_PALACE_PATH"] = os.path.abspath(_args.palace)
_config = MempalaceConfig()
if _args.palace:
_kg = KnowledgeGraph(db_path=os.path.join(_config.palace_path, "knowledge_graph.sqlite3"))
else:
_kg = KnowledgeGraph()
三个细节值得注意:
parse_known_args():因为 MCP harness 可能会追加它自己的--stdio之类旗标,用known_args保证我们不会因为未知参数报错;- 用环境变量传递配置 :
MEMPALACE_PALACE_PATH被写进os.environ,这样后续MempalaceConfig()、searcher、KnowledgeGraph任何地方os.getenv都能看到; - 全局共享
_kg:KnowledgeGraph只实例化一次,底层 SQLite 连接常驻进程------避免每次工具调用都 open/close 一次数据库。
ChromaDB 连接则是 懒加载 + cached:
python
_client_cache = None
_collection_cache = None
def _get_collection(create=False):
global _client_cache, _collection_cache
try:
if _client_cache is None:
_client_cache = chromadb.PersistentClient(path=_config.palace_path)
if create:
_collection_cache = _client_cache.get_or_create_collection(_config.collection_name)
elif _collection_cache is None:
_collection_cache = _client_cache.get_collection(_config.collection_name)
return _collection_cache
except Exception:
return None
对 palace 不存在的场景(第一次跑、用户删库)也有兜底:_no_palace() 返回 {"error": "No palace found", "hint": "Run: mempalace init ..."}。模型看到 hint 就会自己去建议用户执行 init------这是一个很精巧的"自我引导"设计。
1.3 19 个工具完整清单
下面这张表是我读完 TOOLS 字典之后手动整理出来的速查表,按 README 的 5 个分组排版,把参数 schema + 内部调用链 + 典型用途放在一起:
| # | 工具名 | 组 | 必填参数 | 可选参数 | 内部调用 | 典型用途 |
|---|---|---|---|---|---|---|
| 1 | mempalace_status |
read | - | - | col.count() + 遍历 metadata 聚合 wings/rooms + 附带 PALACE_PROTOCOL + AAAK_SPEC |
wake-up 第一调用,同时教会 AI 方言和使用规范 |
| 2 | mempalace_list_wings |
read | - | - | 遍历 metadatas 统计 wing → count | 看看 palace 里都有哪些 wing |
| 3 | mempalace_list_rooms |
read | - | wing |
遍历 metadatas(可加 where={"wing": wing}) |
列某 wing 下的 room,或全库 room |
| 4 | mempalace_get_taxonomy |
read | - | - | 构建 {wing: {room: count}} 嵌套 |
一次看完整棵树 |
| 5 | mempalace_get_aaak_spec |
read | - | - | 返回 AAAK_SPEC 常量 |
后悔没看 status?单独拿方言规范 |
| 6 | mempalace_search |
read | query |
limit, wing, room |
search_memories(...) → searcher.py |
语义检索(第 07 篇详述) |
| 7 | mempalace_check_duplicate |
read | content |
threshold |
col.query(...) 做近似匹配,1 - distance ≥ threshold 视为重复 |
写入前查重,避免把同一段话存两次 |
| 8 | mempalace_add_drawer |
write | wing, room, content |
source_file, added_by |
构造 drawer_{wing}_{room}_{md5[:16]} → col.upsert |
AI 主动把新发现归档 |
| 9 | mempalace_delete_drawer |
write | drawer_id |
- | col.delete(ids=[...]) |
误存或过时内容清理 |
| 10 | mempalace_kg_query |
KG | entity |
as_of, direction |
_kg.query_entity(...) |
回答事实前的 verify 查询,支持 as-of 穿越 |
| 11 | mempalace_kg_add |
KG | subject, predicate, object |
valid_from, source_closet |
_kg.add_triple(...) |
学到新事实就写一条三元组 |
| 12 | mempalace_kg_invalidate |
KG | subject, predicate, object |
ended |
_kg.invalidate(...) |
事实失效(升学、离职、康复) |
| 13 | mempalace_kg_timeline |
KG | - | entity |
_kg.timeline(...) |
看一个实体的全部历史节点 |
| 14 | mempalace_kg_stats |
KG | - | - | _kg.stats() |
entity/triple 总量 |
| 15 | mempalace_traverse |
Nav | start_room |
max_hops |
palace_graph.traverse(...) |
从一个 room 出发走图 |
| 16 | mempalace_find_tunnels |
Nav | - | wing_a, wing_b |
palace_graph.find_tunnels(...) |
找两个 wing 之间的桥 room |
| 17 | mempalace_graph_stats |
Nav | - | - | palace_graph.graph_stats(...) |
图连通性概览 |
| 18 | mempalace_diary_write |
Diary | agent_name, entry |
topic |
写到 wing_{agent}/diary room,hall=hall_diary |
AI 写给自己的 session 日记(AAAK 格式) |
| 19 | mempalace_diary_read |
Diary | agent_name |
last_n |
col.get(where={"$and":[{"wing":...},{"room":"diary"}]}) 后按时间倒排 |
下次 wake-up 看上次的自己留了啥 |
五个分组对应五个职能:
- Palace read(1-7):只读、只聚合,给 AI 看库的样貌;
- Palace write(8-9) :写入 drawer,有幂等设计(同 content 同 wing 同 room 的 md5 碰撞即 no-op);
- Knowledge Graph(10-14):结构化事实层,支持 temporal validity;
- Navigation(15-17):走 palace_graph,用来做跨 wing 联想;
- Agent Diary(18-19) :这是 08 篇最想强调的设计------每个 agent 有自己的 diary wing ,写入时
wing = f"wing_{agent_name.lower().replace(' ', '_')}",形成天然的 per-agent 隔离命名空间。
1.4 AAAK auto-teach:一次唤醒教会 AI 方言
AAAK 是 MemPalace 发明的一种压缩方言(见第 07 篇),但对 LLM 来说再好的方言如果没被"教过"就用不起来。MemPalace 的解决方案简单得让人拍大腿------把方言规范作为 status 工具返回值的一部分:
python
def tool_status():
col = _get_collection()
if not col:
return _no_palace()
count = col.count()
wings, rooms = {}, {}
try:
all_meta = col.get(include=["metadatas"], limit=10000)["metadatas"]
for m in all_meta:
w = m.get("wing", "unknown")
r = m.get("room", "unknown")
wings[w] = wings.get(w, 0) + 1
rooms[r] = rooms.get(r, 0) + 1
except Exception:
pass
return {
"total_drawers": count,
"wings": wings,
"rooms": rooms,
"palace_path": _config.palace_path,
"protocol": PALACE_PROTOCOL,
"aaak_dialect": AAAK_SPEC,
}
注意最后两行。AI 第一次调用 mempalace_status 时,连同仓库统计一起收到一份完整的 AAAK_SPEC:
text
AAAK is a compressed memory dialect that MemPalace uses for efficient storage.
It is designed to be readable by both humans and LLMs without decoding.
FORMAT:
ENTITIES: 3-letter uppercase codes. ALC=Alice, JOR=Jordan, RIL=Riley, ...
EMOTIONS: *action markers* before/during text. *warm*=joy, *fierce*=determined ...
STRUCTURE: Pipe-separated fields. FAM: family | PROJ: projects | ⚠: warnings ...
DATES: ISO format (2026-03-31). COUNTS: Nx = N mentions (e.g., 570x).
IMPORTANCE: ★ to ★★★★★ (1-5 scale).
HALLS: hall_facts, hall_events, hall_discoveries, hall_preferences, hall_advice.
WINGS: wing_user, wing_agent, wing_team, wing_code, wing_myproject, ...
ROOMS: Hyphenated slugs representing named ideas (e.g., chromadb-setup, gpu-pricing).
EXAMPLE:
FAM: ALC→♡JOR | 2D(kids): RIL(18,sports) MAX(11,chess+swimming) | BEN(contributor)
Read AAAK naturally --- expand codes mentally, treat *markers* as emotional context.
When WRITING AAAK: use entity codes, mark emotions, keep structure tight.
这就是 auto-teach :不用 system prompt 注入、不用 finetune、不用用户配置------AI 一调用 status 就学会了方言。对于像 Claude / GPT-4 这种 few-shot 能力极强的模型,看到上面这段规范之后紧接着 diary_read 返回的 AAAK 内容,天然就能读懂、也能写出来。
1.5 Memory Protocol:把"记忆使用规范"注入每次 status 调用
同一个 status 响应里还有另一个宝贝:
python
PALACE_PROTOCOL = """IMPORTANT --- MemPalace Memory Protocol:
1. ON WAKE-UP: Call mempalace_status to load palace overview + AAAK spec.
2. BEFORE RESPONDING about any person, project, or past event: call
mempalace_kg_query or mempalace_search FIRST. Never guess --- verify.
3. IF UNSURE about a fact (name, gender, age, relationship): say "let me check"
and query the palace. Wrong is worse than slow.
4. AFTER EACH SESSION: call mempalace_diary_write to record what happened,
what you learned, what matters.
5. WHEN FACTS CHANGE: call mempalace_kg_invalidate on the old fact,
mempalace_kg_add for the new one.
This protocol ensures the AI KNOWS before it speaks. Storage is not memory ---
but storage + this protocol = memory."""
这段文本是整个 MemPalace 方法论的浓缩,五条协议对应五个行为要求:
- 上线自检:wake-up 先 status;
- 先查后答:任何涉及事实的回答,必须先 kg_query / search;
- 不确定就明说 :"let me check" 是合法响应,幻觉比慢更糟;
- 下线写日记:diary_write 让下一次的自己有东西读;
- 事实变化要 invalidate+add:不是覆盖,而是时间线追加。
把这段协议塞进 status 响应有一个巨大的工程好处------不用改 harness 的 system prompt。任何接了 MCP 的 AI 只要在一个 session 里调过一次 status,就相当于被 "自助培训"了一遍。协议的扩散靠的是 MCP 协议本身,而不是任何平台特定的注入点。
1.6 JSON-RPC 主循环、类型强制与幂等写入
python
def main():
logger.info("MemPalace MCP Server starting...")
while True:
try:
line = sys.stdin.readline()
if not line:
break
line = line.strip()
if not line:
continue
request = json.loads(line)
response = handle_request(request)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
break
except Exception as e:
logger.error(f"Server error: {e}")
一个极简的 stdio JSON-RPC 循环------每行一个请求,每行一个响应。handle_request 处理 4 种方法:
initialize→ 返回 serverInfo 和 capabilitiesnotifications/initialized→ 无响应(fire-and-forget)tools/list→ 返回所有 19 个工具的 schematools/call→ 找到 handler 调用
类型强制是一个容易忽略的坑:
python
# Coerce argument types based on input_schema.
# MCP JSON transport may deliver integers as floats or strings;
# ChromaDB and Python slicing require native int.
schema_props = TOOLS[tool_name]["input_schema"].get("properties", {})
for key, value in list(tool_args.items()):
prop_schema = schema_props.get(key, {})
declared_type = prop_schema.get("type")
if declared_type == "integer" and not isinstance(value, int):
tool_args[key] = int(value)
elif declared_type == "number" and not isinstance(value, (int, float)):
tool_args[key] = float(value)
为什么要做这层?因为 模型生成的 JSON 经常把 limit 写成 "5" 或 5.0 。Python 的 ChromaDB 切片要求严格 int,不做这一层类型 coerce 的话工具会在 .get(limit=5.0) 这种看起来无害的调用处炸掉。
写入的幂等性 (tool_add_drawer):
python
drawer_id = f"drawer_{wing}_{room}_{hashlib.md5(content.encode()).hexdigest()[:16]}"
# Idempotency: if the deterministic ID already exists, return success as a no-op.
try:
existing = col.get(ids=[drawer_id])
if existing and existing["ids"]:
return {"success": True, "reason": "already_exists", "drawer_id": drawer_id}
except Exception:
pass
drawer_id 是 wing+room+md5(content) 的确定性哈希------同样内容写多少次都是同一个 id ,col.upsert 保证第二次写入是 no-op。这让 AI 可以"放心地瞎存":宁可多调几次 add_drawer 也不会造成重复污染。
MCP 通信流画成图是这样:
┌─────────────────┐ JSON-RPC over stdio ┌────────────────────┐
│ Claude Code │ ────────────────────────► │ mempalace.mcp_ │
│ (harness) │ ◄──────────────────────── │ server │
└─────────────────┘ └─────────┬──────────┘
│
┌──────────────────────────────┼──────────────┐
▼ ▼ ▼
┌───────────────┐ ┌────────────────┐ ┌─────────────┐
│ ChromaDB │ │ KnowledgeGraph │ │ palace_ │
│ PersistentCl. │ │ (SQLite) │ │ graph.py │
└───────┬───────┘ └────────┬───────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
~/.mempalace/palace/ knowledge_graph.sqlite3 tunnels
chroma.sqlite3 + HNSW
2. 🛠️ Part B --- CLI(cli.py + __main__.py)
文件 :mempalace/cli.py(543 行),mempalace/__main__.py(5 行)。CLI 是人直接操作 palace 的入口------MCP 是给 AI 用的,CLI 是给运维和 onboarding 流程用的。
2.0 __main__.py:为什么要这一层
只有 5 行:
python
"""Allow running as: python -m mempalace"""
from .cli import main
main()
作用是让 python -m mempalace mine ~/chats 和 mempalace mine ~/chats 等价。Hook 脚本里用的是前者,因为 python -m 不依赖 mempalace 命令是否在 PATH 里------对于 bash hook 这是最可靠的写法。
2.1 命令总表
cli.py 的 main() 用 argparse 构建了一棵命令树,完整清单:
| 命令 | 作用 | 主要参数 | 背后函数 |
|---|---|---|---|
init <dir> |
扫描目录、检测 entities、检测 rooms、生成配置 | --yes 非交互 |
cmd_init → entity_detector + room_detector_local + MempalaceConfig().init() |
mine <dir> |
挖掘项目/对话进 palace | --mode {projects,convos}, --wing, --agent, --limit, --dry-run, --no-gitignore, --include-ignored, --extract {exchange,general} |
cmd_mine → miner.mine 或 convo_miner.mine_convos |
search "query" |
语义检索 | --wing, --room, --results |
cmd_search → searcher.search |
compress |
把 drawer 用 AAAK Dialect 压成 closet 级 | --wing, --dry-run, --config |
cmd_compress → dialect.Dialect |
wake-up |
打印 L0+L1 唤醒上下文 | --wing |
cmd_wakeup → layers.MemoryStack.wake_up |
split <dir> |
拆分 concat 过的 transcript mega-file | --output-dir, --dry-run, --min-sessions |
cmd_split → split_mega_files.main |
status |
概览 | - | cmd_status → miner.status |
repair |
从 SQLite metadata 重建 ChromaDB 向量索引 | - | cmd_repair(带 backup) |
hook run --hook X --harness Y |
执行 hook 逻辑(stdin JSON → stdout JSON) | --hook {session-start,stop,precompact}, --harness {claude-code,codex} |
cmd_hook → hooks_cli.run_hook |
instructions <name> |
打印 skill 说明 md | name ∈ {init,search,mine,help,status} | cmd_instructions → instructions_cli.run_instructions |
2.2 双层子命令:hook run / instructions <name>
hook 和 instructions 是两个带二级子命令的分支------argparse 的处理稍微绕一点:
python
p_hook = sub.add_parser("hook", help="Run hook logic (reads JSON from stdin, outputs JSON to stdout)")
hook_sub = p_hook.add_subparsers(dest="hook_action")
p_hook_run = hook_sub.add_parser("run", help="Execute a hook")
p_hook_run.add_argument("--hook", required=True,
choices=["session-start", "stop", "precompact"])
p_hook_run.add_argument("--harness", required=True,
choices=["claude-code", "codex"])
dispatch 时额外处理:
python
if args.command == "hook":
if not getattr(args, "hook_action", None):
p_hook.print_help()
return
cmd_hook(args)
return
if args.command == "instructions":
name = getattr(args, "instructions_name", None)
if not name:
p_instructions.print_help()
return
args.name = name
cmd_instructions(args)
return
这种双层结构的典型用法:
bash
mempalace hook run --hook stop --harness claude-code < transcript_event.json
mempalace instructions init # 打印 mempalace/instructions/init.md
instructions 子命令存在的理由值得单独说一句------它是为了 slash command 生态 。每个 MemPalace Skill(/mempalace:init、/mempalace:mine ...)在 Claude Code 里对应一个 Markdown 文件,里面写的是"给 AI 的流程指令"。CLI 把这些 md 文件暴露成 mempalace instructions <name> 就能让 harness 直接 exec 拉出 prompt 文本,而不用去解析包安装路径。
instructions_cli.py 本体只有 28 行:
python
INSTRUCTIONS_DIR = Path(__file__).parent / "instructions"
AVAILABLE = ["init", "search", "mine", "help", "status"]
def run_instructions(name: str):
if name not in AVAILABLE:
print(f"Unknown instructions: {name}", file=sys.stderr)
print(f"Available: {', '.join(sorted(AVAILABLE))}", file=sys.stderr)
sys.exit(1)
md_path = INSTRUCTIONS_DIR / f"{name}.md"
if not md_path.is_file():
print(f"Instructions file not found: {md_path}", file=sys.stderr)
sys.exit(1)
print(md_path.read_text())
mempalace/instructions/init.md 示例片段:
markdown
# MemPalace Init
Guide the user through a complete MemPalace setup. Follow each step in order,
stopping to report errors and attempt remediation before proceeding.
## Step 1: Check Python version
Run `python3 --version` and confirm the version is 3.9 or higher...
## Step 3: Install mempalace
Run `pip install mempalace`.
### Error handling -- pip failures
If `pip install mempalace` fails, try these fallbacks in order:
1. Try `pip3 install mempalace`
2. Try `python -m pip install mempalace`
...
## Step 6: Configure MCP server
claude mcp add mempalace -- python -m mempalace.mcp_server
这是一种非常聪明的 "prompt 即文档" 工程实践:Markdown 文件同时是人类文档、是 AI 的执行脚本、是 slash command 的源。
2.3 --palace 全局选项与配置解析顺序
所有命令都支持 --palace /path/to/dir:
python
parser.add_argument("--palace", default=None,
help="Where the palace lives (default: from ~/.mempalace/config.json or ~/.mempalace/palace)")
解析 palace 路径的优先级:
1. 命令行 --palace /abs/path (最高)
2. 环境变量 MEMPALACE_PALACE_PATH
3. ~/.mempalace/config.json 中的 palace_path
4. ~/.mempalace/palace (默认兜底)
MempalaceConfig 类(在 config.py)负责这个链,cli.py 每个 cmd_XXX 函数开头都有一行:
python
palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
这样同一台机器可以跑多个 palace:工作一个、家庭一个、测试一个。
2.4 compress / repair:两个运维命令
compress 是把 drawer 用 AAAK Dialect 压到一个并行的 mempalace_compressed collection。批处理逻辑做了 ChromaDB SQLite variable limit(~999)的规避:
python
_BATCH = 500
docs, metas, ids = [], [], []
offset = 0
while True:
try:
kwargs = {"include": ["documents", "metadatas"], "limit": _BATCH, "offset": offset}
if where:
kwargs["where"] = where
batch = col.get(**kwargs)
except Exception as e:
if not docs:
print(f"\n Error reading drawers: {e}")
sys.exit(1)
break
batch_docs = batch.get("documents", [])
if not batch_docs:
break
docs.extend(batch_docs); metas.extend(batch.get("metadatas", []))
ids.extend(batch.get("ids", []))
offset += len(batch_docs)
if len(batch_docs) < _BATCH:
break
压缩完之后的统计会打印总 token 降幅,典型能到 ~30x。注意 README 澄清过 AAAK 并非无损,所以 compress 默认存到 mempalace_compressed 集合而不是覆盖主集合------raw mode 依然保留给 96.6% 的 LongMemEval 结果。
repair 解决的是 macOS ARM64 下 ChromaDB HNSW 索引偶尔 segfault 的问题(README Issue #74):
python
print(" Rebuilding collection...")
client.delete_collection("mempalace_drawers")
new_col = client.create_collection("mempalace_drawers")
filed = 0
for i in range(0, len(all_ids), batch_size):
batch_ids = all_ids[i : i + batch_size]
batch_docs = all_docs[i : i + batch_size]
batch_metas = all_metas[i : i + batch_size]
new_col.add(documents=batch_docs, ids=batch_ids, metadatas=batch_metas)
流程:读旧集合 → backup 整个 palace 目录 → 删除旧集合 → 新建集合 → 分批重灌。这是 ChromaDB 版本升级/索引损坏后的标准 recovery pattern,也是 MemPalace 愿意把"本地数据库"作为核心存储的信心来源------坏了能修。
3. ⚙️ Part C --- 引导式 setup(onboarding.py)
文件 :mempalace/onboarding.py,489 行。这是 mempalace init 第一次跑时的对话式向导,负责把一个空目录变成一个"知道用户世界"的 palace。
3.1 init 全流程:从 mode 到 wing_config
onboarding 有 6 步:
┌──────────────────────────────────────────────────┐
│ Step 1 --- Mode 选择 │
│ work / personal / combo │
├──────────────────────────────────────────────────┤
│ Step 2 --- People 采集 │
│ name, relationship, nickname(aliases 字典) │
├──────────────────────────────────────────────────┤
│ Step 3 --- Projects 采集 │
│ 用来区分 "Lantern 项目" vs "lantern 单词" │
├──────────────────────────────────────────────────┤
│ Step 4 --- Wings 命名 │
│ 从 DEFAULT_WINGS[mode] 预设,可自定义 │
├──────────────────────────────────────────────────┤
│ Step 5 --- Auto-detect from files │
│ 扫描目录,补充额外 people candidates │
├──────────────────────────────────────────────────┤
│ Step 6 --- Ambiguity warnings │
│ 检测和英语常用词同名的情况(e.g. "Riley") │
└──────────────────────────────────────────────────┘
DEFAULT_WINGS 里放的是每种 mode 的默认 wing taxonomy:
python
DEFAULT_WINGS = {
"work": ["projects", "clients", "team", "decisions", "research"],
"personal": ["family", "health", "creative", "reflections", "relationships"],
"combo": ["family", "work", "health", "creative", "projects", "reflections"],
}
典型一次交互:
text
============================================================
Welcome to MemPalace
============================================================
How are you using MemPalace?
[1] Work --- notes, projects, clients, colleagues, decisions
[2] Personal --- diary, family, health, relationships, reflections
[3] Both --- personal and professional mixed
Your choice [1/2/3]: 3
──────────────────────────────────────────────────────────
Personal world --- who are the important people in your life?
Format: name, relationship
Type 'done' when finished.
Person: Jordan, spouse
Nickname for Jordan? (or enter to skip): Jo
Person: Riley, daughter
Nickname for Riley? (or enter to skip):
Person: Max, son
Nickname for Max? (or enter to skip):
Person: done
──────────────────────────────────────────────────────────
Work world --- colleagues, clients, collaborators
Person: Ben, co-founder
Person: done
...
Wings: family,work,health,creative,projects,reflections
Scan your files for additional names? [Y/n]: y
Directory to scan [.]: ~/projects
Found 3 additional name candidates:
Alice confidence=85% (git_author)
Devon confidence=72% (filename)
Pat confidence=70% (code_comment)
Add any of these to your registry? [Y/n]: y
Alice --- (p)erson, (s)kip? p
Relationship/role for Alice? designer
...
──────────────────────────────────────────────────────────
Heads up --- these names are also common English words:
Max, Pat
MemPalace will check the context before treating them as person names.
============================================================
Setup Complete
============================================================
Registered: 6 people, 1 project, 3 aliases
Wings: family, work, health, creative, projects, reflections
Registry saved to: ~/.mempalace/entities/registry.json
AAAK entity registry: ~/.mempalace/aaak_entities.md
Critical facts bootstrap: ~/.mempalace/critical_facts.md
Your AI will know your world from the first session.
注意 Step 6 的消歧警告------Max 既是名字也是形容词,Pat 既是名字也是动词 。COMMON_ENGLISH_WORDS 里存了一批常见词,onboarding 会逐个比对,给用户一个 heads-up。MemPalace 在挖掘阶段会用上下文判别(这部分属于 06 篇的 miner 范畴)。
3.2 AAAK bootstrap:aaak_entities.md + critical_facts.md
onboarding 结束时会调用 _generate_aaak_bootstrap,产出两个文件:
python
def _generate_aaak_bootstrap(people, projects, wings, mode, config_dir=None):
mempalace_dir = Path(config_dir) if config_dir else Path.home() / ".mempalace"
mempalace_dir.mkdir(parents=True, exist_ok=True)
# Build AAAK entity codes (first 3 letters of name, uppercase)
entity_codes = {}
for p in people:
name = p["name"]
code = name[:3].upper()
while code in entity_codes.values(): # collision handling
code = name[:4].upper()
entity_codes[name] = code
...
~/.mempalace/aaak_entities.md 长这样:
markdown
# AAAK Entity Registry
# Auto-generated by mempalace init. Update as needed.
## People
JOR=Jordan (spouse)
RIL=Riley (daughter)
MAX=Max (son)
BEN=Ben (co-founder)
ALC=Alice (designer)
## Projects
LANT=Lantern
## AAAK Quick Reference
Symbols: ♡=love ★=importance ⚠=warning →=relationship |=separator
Structure: KEY:value | GROUP(details) | entity.attribute
Read naturally --- expand codes, treat *markers* as emotional context.
~/.mempalace/critical_facts.md:
markdown
# Critical Facts (bootstrap --- will be enriched after mining)
## People (personal)
- **Jordan** (JOR) --- spouse
- **Riley** (RIL) --- daughter
- **Max** (MAX) --- son
## People (work)
- **Ben** (BEN) --- co-founder
- **Alice** (ALC) --- designer
## Projects
- **Lantern**
## Palace
Wings: family, work, health, creative, projects, reflections
Mode: combo
*This file will be enriched by palace_facts.py after mining.*
这两份文件是 L0 wake-up context 的种子------在 MemoryStack(第 07 篇讲的分层 wake-up)里,L0 就是 identity + critical_facts,大概 170 tokens。所以即使你还没跑 mine,palace 里一条 drawer 都没有,你的 AI 就已经知道 "用户的孩子叫 Riley(女儿)和 Max(儿子),Jordan 是配偶,Ben 是 co-founder"。
这是整套 MemPalace 哲学里最漂亮的一笔 :别让 AI 从零开始读,给它一份 cheat sheet。
3.3 identity.txt、wing_config.json、entities.json 的角色
~/.mempalace/ 目录在 onboarding 之后会有这样一批文件:
| 文件 | 来源 | 作用 | 被谁读 |
|---|---|---|---|
config.json |
MempalaceConfig().init() |
palace_path, collection_name 等 | 所有 CLI/MCP |
identity.txt |
用户手动(可选) | AI 的角色定位:"你是 Ben 的研发助手" | layers.MemoryStack.wake_up L0 |
wing_config.json |
用户手动/Gemini CLI guide | wing ↔ 项目名变体映射 | miner auto-classification |
entities/registry.json |
EntityRegistry.seed() |
人、项目、aliases 的结构化注册表 | miner, entity_detector, dialect |
aaak_entities.md |
_generate_aaak_bootstrap |
AAAK code lookup | AI 通过 wake-up 读 |
critical_facts.md |
_generate_aaak_bootstrap |
L0 facts | MemoryStack.wake_up |
hook_state/<session>_last_save |
hooks_cli._hook_stop |
每 session 上次 save 的 exchange 编号 | save hook |
hook_state/hook.log |
hooks | 滚动日志 | 运维 |
palace/chroma.sqlite3 |
mempalace mine |
ChromaDB 主存储 | 所有读/写 |
palace/knowledge_graph.sqlite3 |
KnowledgeGraph |
时序三元组 | KG 工具 |
palace/entities.json |
mempalace init <dir> 的第二次 |
项目级的 entities 覆盖 | miner |
4. 🪝 Part D --- Hooks(hooks_cli.py + 两个脚本)
Hooks 的目标是"让 AI 在不被用户提示 的情况下自动保存"。MemPalace 提供了两层:
- Shell 脚本版 :
hooks/mempal_save_hook.sh+hooks/mempal_precompact_hook.sh------零依赖,任何 harness 都能用; - Python 版 :
mempalace/hooks_cli.py,通过mempalace hook run --hook X --harness Y运行,与 bash 版等价但更容易测试和扩展。
4.1 Save Hook:每 N 条消息触发结构化保存
触发流程:
User → AI response → Claude Code fires Stop hook
│
▼
┌─────────────────────────┐
│ hook reads stdin JSON: │
│ session_id │
│ stop_hook_active │
│ transcript_path │
└─────────────┬───────────┘
│
┌────────────────┴────────────────┐
│ │
stop_hook_active=True stop_hook_active=False
│ │
▼ ▼
echo "{}" count human messages in JSONL
(let AI stop) │
▼
last_save file → since_last = count - last_save
│
┌────────────────┴────────────────┐
│ │
since_last < 15 since_last >= 15
│ │
▼ ▼
echo "{}" • update last_save file
• _maybe_auto_ingest() (bg)
• emit {"decision":"block",
"reason": STOP_BLOCK_REASON}
│
▼
AI sees reason → saves to palace
│
▼
AI tries to stop again
│
stop_hook_active=true
│
▼
hook echoes "{}" → done
核心的 _count_human_messages 用 JSONL 行式解析,并显式跳过 <command-message>------即 slash command、斜杠命令这种不算"真实用户消息"的东西不计数:
python
def _count_human_messages(transcript_path: str) -> int:
path = Path(transcript_path).expanduser()
if not path.is_file():
return 0
count = 0
try:
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
try:
entry = json.loads(line)
msg = entry.get("message", {})
if isinstance(msg, dict) and msg.get("role") == "user":
content = msg.get("content", "")
if isinstance(content, str):
if "<command-message>" in content:
continue
elif isinstance(content, list):
text = " ".join(
b.get("text", "") for b in content if isinstance(b, dict)
)
if "<command-message>" in text:
continue
count += 1
except (json.JSONDecodeError, AttributeError):
pass
except OSError:
return 0
return count
注意两个鲁棒性细节:
encoding="utf-8", errors="replace"避免 transcript 有奇怪字符时崩溃;- 内层
try: json.loads吞掉损坏行------transcript 是 append-only,偶尔有半行很正常。
触发后发出的 "block reason":
python
STOP_BLOCK_REASON = (
"AUTO-SAVE checkpoint. Save key topics, decisions, quotes, and code "
"from this session to your memory system. Organize into appropriate "
"categories. Use verbatim quotes where possible. Continue conversation "
"after saving."
)
这段文本被 Claude Code 作为"系统消息"呈现给 AI。AI 收到之后调用 MCP 工具把关键信息归档,然后 AI 再次尝试 stop------这时 stop_hook_active=true,hook 直接放行,避免无限循环。
4.2 PreCompact Hook:压缩前的最后一次快门
比 save hook 简单粗暴------永远 block:
python
def hook_precompact(data: dict, harness: str):
parsed = _parse_harness_input(data, harness)
session_id = parsed["session_id"]
_log(f"PRE-COMPACT triggered for session {session_id}")
# Optional: auto-ingest synchronously before compaction
mempal_dir = os.environ.get("MEMPAL_DIR", "")
if mempal_dir and os.path.isdir(mempal_dir):
try:
log_path = STATE_DIR / "hook.log"
with open(log_path, "a") as log_f:
subprocess.run(
[sys.executable, "-m", "mempalace", "mine", mempal_dir],
stdout=log_f, stderr=log_f, timeout=60,
)
except OSError:
pass
_output({"decision": "block", "reason": PRECOMPACT_BLOCK_REASON})
PRECOMPACT_BLOCK_REASON 比 save hook 的措辞更紧张:
COMPACTION IMMINENT. Save ALL topics, decisions, quotes, code, and
important context from this session to your memory system. Be thorough
--- after compaction, detailed context will be lost. Organize into
appropriate categories. Use verbatim quotes where possible. Save
everything, then allow compaction to proceed.
save hook 是异步 Popen(后台跑 mine 不阻塞对话),precompact hook 是同步 subprocess.run with timeout=60------因为压缩一旦发生就回不去了,必须确保 mine 落盘再放行。
4.3 MEMPAL_DIR 与 auto-ingest
MEMPAL_DIR 是一个可选的环境变量(或者 bash 脚本里的硬编码变量)。设置后,两个 hook 都会在触发时额外跑 mempalace mine $MEMPAL_DIR:
python
def _maybe_auto_ingest():
"""If MEMPAL_DIR is set and exists, run mempalace mine in background."""
mempal_dir = os.environ.get("MEMPAL_DIR", "")
if mempal_dir and os.path.isdir(mempal_dir):
try:
log_path = STATE_DIR / "hook.log"
with open(log_path, "a") as log_f:
subprocess.Popen(
[sys.executable, "-m", "mempalace", "mine", mempal_dir],
stdout=log_f, stderr=log_f,
)
except OSError:
pass
典型用法是把它指向你的 Claude Code 会话导出目录------例如 export MEMPAL_DIR="$HOME/.claude/projects/foo/conversations"。这样每次 save hook 触发,不仅 AI 会手动把要点归档,hook 还会把整个 transcript 作为兜底喂进 convo_miner。双保险。
4.4 安全考量:shell injection(#110)与 _sanitize_session_id
早期版本的 bash hook 直接把 $SESSION_ID 拼进文件路径:
bash
LAST_SAVE_FILE="$STATE_DIR/${SESSION_ID}_last_save"
如果 harness 把 session_id 设成 "../../etc/passwd" 之类,会造成 path traversal 甚至命令注入(README 里引用的 issue #110)。修复方案是 两层净化:
bash 版:
bash
SESSION_ID=$(echo "$SESSION_ID" | tr -cd 'a-zA-Z0-9_-')
[ -z "$SESSION_ID" ] && SESSION_ID="unknown"
只保留字母、数字、下划线、短横线------其他字节全部丢掉。
Python 版用正则做同样的事:
python
def _sanitize_session_id(session_id: str) -> str:
"""Only allow alnum, dash, underscore to prevent path traversal."""
sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", session_id)
return sanitized or "unknown"
这是一个典型的"假设输入是对抗性的"防御性编程------哪怕现在所有 harness 都正派传参,也得保持 whitelist。
4.5 bash 脚本 vs Python hooks_cli:为什么同时提供两套
这两套实现做同一件事,但定位不同:
| 维度 | hooks/*.sh |
hooks_cli.py |
|---|---|---|
| 依赖 | bash + python3 | 安装了 mempalace 包 |
| 入口 | /absolute/path/to/mempal_save_hook.sh |
mempalace hook run --hook stop --harness claude-code |
| 可移植性 | 高------复制一个文件就能用 | 需要 PYTHONPATH 正确 |
| 可测试性 | 低------得 fake stdin 跑 bash | 高------直接 pytest |
| 扩展性 | 新 harness 要改 parser | 新 harness 在 _parse_harness_input 加一个分支 |
| 哪种优先 | 用户手动装 hook 时 | 官方 plugin/marketplace 装时 |
并存的好处 :用户下载 repo 直接复制 .sh 就能跑;pip install mempalace 的用户则用更结构化的 mempalace hook run。未来添加 Cursor、Windsurf 支持时只需要改 Python 版。
5. 🚀 Part E --- 实战配置
这一节把上面所有零件拼起来,给出每种 harness 的复制-粘贴配置。
5.1 Claude Code 插件市场安装
这是最省事的一条路径:
bash
# Step 1: 安装 mempalace(plugin 会自动做,但手动装也无妨)
pip install mempalace
# Step 2: 初始化 palace
mempalace init ~/projects/myapp
# Step 3: 安装官方 plugin marketplace
claude plugin marketplace add milla-jovovich/mempalace
claude plugin install --scope user mempalace
# 重启 Claude Code,验证 skills
claude
> /skills
# 应该看到: mempalace:help, mempalace:init, mempalace:mine,
# mempalace:search, mempalace:status
插件安装会做 三件事:
- 注册 MCP server(等价于
claude mcp add mempalace -- python -m mempalace.mcp_server); - 注册 5 个 slash command skill,内容从
mempalace/instructions/*.md动态读取; - 如果用户同意,写入 save + precompact hook 到
~/.claude/settings.json。
如果你不想用 plugin,也可以手动加:
bash
claude mcp add mempalace -- python -m mempalace.mcp_server
然后编辑 ~/.claude/settings.json:
json
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "/absolute/path/to/mempalace/hooks/mempal_save_hook.sh",
"timeout": 30
}]
}],
"PreCompact": [{
"hooks": [{
"type": "command",
"command": "/absolute/path/to/mempalace/hooks/mempal_precompact_hook.sh",
"timeout": 30
}]
}]
}
}
记得 chmod +x hooks/*.sh。
5.2 通用 MCP:ChatGPT / Cursor / 任意 MCP 客户端
任何支持 MCP over stdio 的客户端都可以注册 MemPalace。以 Cursor 为例,编辑 ~/.cursor/mcp.json:
json
{
"mcpServers": {
"mempalace": {
"command": "python",
"args": ["-m", "mempalace.mcp_server"],
"env": {
"MEMPALACE_PALACE_PATH": "/home/you/.mempalace/palace"
}
}
}
}
ChatGPT Desktop 的 MCP 配置同理。注意几点:
- 用绝对路径的
python(或python3),如果你用 venv 就写/path/to/.venv/bin/python,否则 harness 进程拉不到包; env里设MEMPALACE_PALACE_PATH,比--palace更稳,因为不是所有 harness 都原样传参数;- 这些客户端没有 Stop/PreCompact hook ,所以只能靠 AI 自觉调
mempalace_diary_write------建议在 system prompt 里加一条"每次会话结束前调用 mempalace_diary_write"。
5.3 Gemini CLI 集成(PreCompress hook)
Gemini CLI 的 hook 叫 PreCompress 而不是 PreCompact,语义相同。examples/gemini_cli_setup.md 给了完整流程:
bash
# 1. 克隆 + venv 装
git clone https://github.com/milla-jovovich/mempalace.git
cd mempalace
python3 -m venv .venv
.venv/bin/pip install -e .
# 2. 初始化 palace
.venv/bin/python3 -m mempalace init .
# 3. 注册 MCP(注意用绝对路径到 venv python)
gemini mcp add mempalace \
/absolute/path/to/mempalace/.venv/bin/python3 \
-m mempalace.mcp_server --scope user
# 4. 编辑 ~/.gemini/settings.json 加 PreCompress hook
json
{
"hooks": {
"PreCompress": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "/absolute/path/to/mempalace/hooks/mempal_precompact_hook.sh"
}]
}]
}
}
验证:
text
gemini
> /mcp list # 应该看到 mempalace: CONNECTED
> /hooks panel # 应该看到 PreCompress: active
另外手动把自己的 identity.txt 和 wing_config.json 放到 ~/.mempalace/,让 L0 context 更丰富:
text
# ~/.mempalace/identity.txt
You are Ben's engineering assistant. Ben is co-founder of Lantern, an
AAAK-compressing memory system. Ben lives with Jordan and two kids.
Focus on brevity, verifiability, and shipping working code.
json
// ~/.mempalace/wing_config.json
{
"wings": {
"wing_lantern": ["Lantern", "LANT", "lantern-repo"],
"wing_family": ["Riley", "Max", "Jordan", "Jo"],
"wing_ops": ["deploy", "infra", "monitoring"]
}
}
5.4 本地模型:wake-up + Python API 离线回路
Llama、Mistral、Qwen 这类本地模型目前普遍不会说 MCP。MemPalace 给了两条绕行路径:
路径 A --- wake-up 注入 system prompt:
bash
mempalace wake-up > /tmp/context.txt
wc -c /tmp/context.txt # ~680-900 字符,约 170-220 tokens
这份文本就是 L0(identity + critical_facts)+ L1(近期重要 event)拼出来的,直接贴到 ollama、llama.cpp、vllm 的 system prompt 前面:
bash
ollama run llama3 --system "$(cat /tmp/context.txt)"
路径 B --- Python API 按需检索:
python
from mempalace.searcher import search_memories
def ask_llama(user_query: str):
# 每次都先查 palace 拿最相关的 5 条
memories = search_memories(
user_query,
palace_path="~/.mempalace/palace",
n_results=5,
)
context = "\n---\n".join(m["content"] for m in memories["results"])
# 拼进 prompt
prompt = f"""[RELEVANT MEMORIES]
{context}
[USER]
{user_query}
[ASSISTANT]
"""
return your_local_llm.generate(prompt)
这就是 手搓 RAG ------MemPalace 此时扮演 vector store + metadata filter 的角色。注意 search_memories 是第 07 篇里详细讲过的函数,支持 wing=, room= 过滤。
路径 A + B 可以共存:用 wake-up 做 L0 sticky context,每次 query 再 RAG 一次 L2。
5.5 完整的 ~/.mempalace/ 目录结构
一个跑了几周的 palace 典型长相:
~/.mempalace/
├── config.json # palace_path, collection_name
├── identity.txt # L0 身份描述 (用户可选)
├── wing_config.json # wing → project name variants (用户可选)
├── aaak_entities.md # init 生成的 AAAK code 表
├── critical_facts.md # init 生成的 L0 facts
├── entities/
│ └── registry.json # EntityRegistry 序列化
├── hook_state/
│ ├── hook.log # 滚动日志
│ ├── abc123_last_save # session abc123 上次 save 时的 exchange 编号
│ └── xyz789_last_save
└── palace/ # ChromaDB + KG 实际存储
├── chroma.sqlite3
├── chroma.sqlite3-shm
├── chroma.sqlite3-wal
├── <uuid>/ # ChromaDB HNSW 索引目录
│ ├── data_level0.bin
│ ├── header.bin
│ ├── length.bin
│ └── link_lists.bin
├── knowledge_graph.sqlite3 # KG 时序三元组
└── entities.json # 项目级 entities override (可选)
备份策略很简单------整个 ~/.mempalace/ 打成 tar.gz ,restore 就是解包。mempalace repair 在索引损坏时也只需要 palace/ 目录即可重建。
6. 🎯 典型对话的一次完整走一遍
把所有组件串起来看一次真实发生的事情。假设用户在 Claude Code 里问:
"Max 今年读几年级?上次我们是不是聊过他换了学校的事?"
T=0 :Claude Code 启动,session-start hook 触发,初始化 ~/.mempalace/hook_state/<sid>_last_save。
T=1 :Claude 看到 mempalace MCP 已连接,按 protocol 的第一条调 mempalace_status:
json
// Request
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"mempalace_status","arguments":{}}}
// Response (节选)
{
"total_drawers": 2183,
"wings": {"wing_family": 412, "wing_lantern": 1021, ...},
"rooms": {"school": 47, "chromadb-setup": 102, ...},
"palace_path": "/home/ben/.mempalace/palace",
"protocol": "IMPORTANT --- MemPalace Memory Protocol: ...",
"aaak_dialect": "AAAK is a compressed memory dialect ..."
}
此时 Claude 已经学会了 AAAK、记住了协议。
T=2 :用户消息进来。Claude 按 protocol 第 2 条,先查 KG:
json
{"name":"mempalace_kg_query","arguments":{"entity":"Max","as_of":"2026-04-09"}}
返回:
json
{
"entity": "Max",
"as_of": "2026-04-09",
"facts": [
{"subject":"Max","predicate":"child_of","object":"Jordan","valid_from":"2014-01-01"},
{"subject":"Max","predicate":"started_school","object":"Year 7","valid_from":"2026-09-01","ended":null},
{"subject":"Max","predicate":"loves","object":"chess","valid_from":"2023-05-10"}
],
"count": 3
}
咦,started_school: Year 7, valid_from 2026-09-01------但今天是 2026-04-09,valid_from 还没生效。
T=3:Claude 补一次语义搜索确认"换学校"的记忆:
json
{"name":"mempalace_search","arguments":{
"query":"Max switching schools",
"wing":"wing_family", "room":"school", "limit":3}}
返回 3 条 drawer,里头一条:
text
[2026-03-18] Jordan said Max got accepted into Oak Hill --- starting Year 7 in September.
He's nervous about the chess club but excited about the bigger swimming pool.
T=4:Claude 合成回答:
Max 现在还在原来的学校,今年 9 月才会升到 Year 7 并转去 Oak Hill。你 3 月 18 号提过这件事------他对那边的国际象棋社有点紧张,但很期待更大的游泳池。
T=5:对话继续几轮。到第 15 条 human message 时,Stop hook 计数达到阈值:
bash
[14:52:11] Session abc123: 15 exchanges, 15 since last save
[14:52:11] TRIGGERING SAVE at exchange 15
hook 返回 {"decision":"block","reason":"AUTO-SAVE checkpoint..."}
T=6:Claude 看到 block reason,调两次工具做存档:
json
{"name":"mempalace_add_drawer","arguments":{
"wing":"wing_family","room":"school",
"content":"2026-04-09: Confirmed Max starts Year 7 at Oak Hill in September 2026. Ben was unsure about start date."}}
{"name":"mempalace_diary_write","arguments":{
"agent_name":"claude",
"entry":"SESSION:2026-04-09|verified.MAX.school.transition|BEN.asked.about.grade|kg_query.saved.guess|★★",
"topic":"memory_hits"}}
然后 Claude 再次尝试 stop → stop_hook_active=true → hook 放行 → 用户看到回答。
T=7 :用户下线。下次上线,Claude 调 mempalace_diary_read(agent_name="claude"),看到上条 AAAK 日记,一秒钟恢复上次的上下文。
整个过程里用户完全没有手动存任何东西------这就是 MemPalace 作为"AI 记忆器官"的样子。
7. 📚 系列总结:MemPalace 四篇脉络回顾
四篇从底到顶串起来:
┌─────────────────────────────────────────────────────────────┐
│ 08 --- MCP / CLI / Hooks / 集成 (本篇) │
│ "把 palace 接到真实 AI 里" │
│ │
│ mcp_server.py cli.py onboarding.py hooks_cli.py │
│ hooks/*.sh instructions/*.md │
├─────────────────────────────────────────────────────────────┤
│ 07 --- AAAK 方言 / KG / 分层检索 │
│ "怎么找得准 & 怎么压得狠" │
│ │
│ dialect.py knowledge_graph.py layers.py │
│ searcher.py palace_graph.py │
├─────────────────────────────────────────────────────────────┤
│ 06 --- 挖掘管线 │
│ "怎么把原始数据灌进 palace" │
│ │
│ miner.py convo_miner.py general_extractor.py │
│ split_mega_files.py entity_detector.py │
├─────────────────────────────────────────────────────────────┤
│ 05 --- 架构与四层存储 │
│ "MemPalace 到底是个什么东西" │
│ │
│ config.py entity_registry.py │
│ Wings / Rooms / Closets / Drawers │
│ ChromaDB 单集合 + metadata 分层 │
└─────────────────────────────────────────────────────────────┘
用一段话浓缩 MemPalace 的设计哲学:
存储不是记忆。原始信息堆在硬盘上和没有没区别。真正的记忆 = 结构化存储(Wings/Rooms/Closets/Drawers)+ 可检索性(ChromaDB 语义 + metadata filter + KG 时序)+ 协议化使用(Memory Protocol + AAAK auto-teach)+ 自动触发(Hooks + MCP)。MemPalace 把这四样东西一次做齐了,而且全程跑在本地、零 API 调用。
与 OpenClaw 01-08 系列的全局对照
| 篇号 | 系列 | 主题 | 关键文件 | 核心思想 |
|---|---|---|---|---|
| 01 | OpenClaw | Agent SDK 框架总览 | agent.py, tools.py |
"agent = loop + tools" |
| 02 | OpenClaw | Tool use & schema | tool_schema.py |
JSON Schema 驱动的工具契约 |
| 03 | OpenClaw | 多步推理与 subagent | subagent.py |
递归分解任务 |
| 04 | OpenClaw | Prompt 缓存与优化 | prompt_cache.py |
上下文重用降低成本 |
| 05 | Agent Memory | MemPalace 架构 | config.py, entity_registry.py |
四层 palace 结构 |
| 06 | Agent Memory | 挖掘管线 | miner.py, convo_miner.py, general_extractor.py |
Store-everything 哲学 |
| 07 | Agent Memory | AAAK + KG + 检索 | dialect.py, knowledge_graph.py, layers.py |
压缩 + 时序 + 分层 |
| 08 | Agent Memory | MCP / CLI / Hooks / 集成 | mcp_server.py, hooks_cli.py, onboarding.py |
把 memory 接进 AI 工作流 |
前四篇讲"agent 本身怎么跑",后四篇讲"agent 怎么拥有长期记忆"------合起来就是一套完整的:能思考 + 能记忆 + 能使用工具 + 能在多次会话间保持连续性的 agent stack。
8. 🔭 未来展望
README 的 "Note from Milla & Ben" 以及各种 GitHub issue 里提到了几条明确的 roadmap:
8.1 AAAK closets 落地
目前 closet 层是"在 room 和 drawer 之间的 AAAK 压缩层",但 compress 命令把结果存在并行的 mempalace_compressed 集合里,还没和主检索链打通。未来版本会把 closet 作为检索时的"中间粒度候选"------先从 closet 粗筛 room,再 drill-down 到 drawer,从而在大型 palace(>10 万 drawer)上也能保持低延迟。
8.2 fact_checker 接入 KnowledgeGraph
fact_checker.py 已经作为独立工具存在,能检测同一实体的矛盾三元组。下一步计划是把它 wire 到 tool_kg_add 的 hook 里------每次加三元组都自动做一遍 contradiction check,发现冲突时要求 AI 显式 invalidate 旧事实。这会让知识图谱从"append-only 事实库"进化成"consistent 事实库"。
8.3 ChromaDB 版本固化(Issue #100)
早期版本对 ChromaDB 依赖宽松,不同 chroma 版本之间 HNSW 索引格式不兼容导致 segfault(尤其 macOS ARM64 上)。下一版会在 pyproject.toml 里 pin 一个经过充分测试的 chroma 版本范围,并给 mempalace repair 加一个 --upgrade 子命令来做跨版本数据迁移。
8.4 Hook shell injection 彻底修复(Issue #110)
当前 _sanitize_session_id 只白名单了 [a-zA-Z0-9_-]。更激进的修复是把 bash 脚本完全废弃,全部走 mempalace hook run------这样所有字符处理都在 Python 里完成,审计面小得多。hooks_cli.py 已经具备所有能力,只差一个 deprecation 周期。
8.5 更多 harness 适配
hooks_cli.py 里的 SUPPORTED_HARNESSES = {"claude-code", "codex"} 是一个白名单------要加 Cursor、Windsurf、Continue.dev、Aider,只需要在 _parse_harness_input 里加一个分支处理各家的 stdin JSON 字段差异。
8.6 LongMemEval 100% 与 Haiku rerank
README 里提到的"100% with Haiku rerank"是一个真实但未开源的结果------在 raw mode 检索结果上再加一层 Haiku rerank,能把 R@5 从 96.6% 推到 100%。这条流水线未来会进 benchmarks/ 作为可选配置。注意它不是默认路径,因为它违反了"零 API 调用"的承诺,但会作为 opt-in 高精度模式提供。
8.7 Agent diary 的跨 session 对齐
现在每个 agent 有独立的 diary wing,但 agent 不认识"其他 agent 写了啥"。未来会加 mempalace_diary_read_all 让一个 agent 能读整个 agent 群的日记------不同 AI 之间的传话机制,类似 MemPalace 版的 git-log。
9. 📖 参考文献
- MemPalace 源码 ,GitHub:
milla-jovovich/mempalace。本篇重点引用:mempalace/mcp_server.py(812 行,19 个 MCP 工具、AAAK auto-teach、memory protocol)mempalace/cli.py(543 行,全部 CLI 命令)mempalace/__main__.py(5 行,python -m mempalace入口)mempalace/onboarding.py(489 行,引导式 setup、AAAK bootstrap)mempalace/hooks_cli.py(226 行,Python 版 save / precompact hook)mempalace/instructions_cli.py(28 行,skill instructions 分发)mempalace/instructions/*.md(slash command 源文件)hooks/mempal_save_hook.sh(147 行,bash 版 save hook)hooks/mempal_precompact_hook.sh(77 行,bash 版 precompact hook)hooks/README.md(hook 触发图与安装说明)examples/mcp_setup.md、examples/gemini_cli_setup.md、examples/HOOKS_TUTORIAL.md
- MemPalace README(2026-04-07 "Note from Milla & Ben")------对 AAAK 压缩率、LongMemEval 结果、fact_checker 状态、ChromaDB pinning 的官方澄清。
- LongMemEval 论文 --- 96.6% R@5 基准的评测集。
- MCP 协议规范 ,
modelcontextprotocol.io------JSON-RPC over stdio 的工具/资源/提示词接口。 - ChromaDB 文档 --- PersistentClient、metadata filter、HNSW 索引重建流程。
- Anthropic Claude Code Hooks 文档 --- Stop / PreCompact / SessionStart 事件,
decision: "block"语义,stop_hook_active无限循环防护。 - Google Gemini CLI 文档 --- PreCompress hook、MCP server 注册命令。
- 系列前作 :
- 05《【Agent Memory篇】05:MemPalace 架构与四层存储》
- 06《【Agent Memory篇】06:MemPalace 挖掘管线 --- miner / convo_miner / general_extractor》
- 07《【Agent Memory篇】07:AAAK 方言、Knowledge Graph 与分层检索》
- OpenClaw 系列 01-04:agent 本体的四篇,构成 agent + memory 的完整上下文。