【Agent Memory篇】08:MemPalace MCP 服务、CLI、Hooks 与实战指南

本文是「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 是主体分桶。
  • 06miner.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 篇要解决的就是最后一公里:

  1. MCP 服务:让 AI 直接调用 19 个工具,无需人工中转;
  2. Hooks:让"保存"这件事在 AI 毫无察觉的前提下每 15 条消息自动发生;
  3. 引导式 setup:让第一次跑的人在 2 分钟之内有一个已经知道"你是谁、家里几个孩子、手头几个项目"的 palace;
  4. 多 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()

三个细节值得注意:

  1. parse_known_args() :因为 MCP harness 可能会追加它自己的 --stdio 之类旗标,用 known_args 保证我们不会因为未知参数报错;
  2. 用环境变量传递配置MEMPALACE_PALACE_PATH 被写进 os.environ,这样后续 MempalaceConfig()searcherKnowledgeGraph 任何地方 os.getenv 都能看到;
  3. 全局共享 _kgKnowledgeGraph 只实例化一次,底层 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 方法论的浓缩,五条协议对应五个行为要求:

  1. 上线自检:wake-up 先 status;
  2. 先查后答:任何涉及事实的回答,必须先 kg_query / search;
  3. 不确定就明说 :"let me check" 是合法响应,幻觉比慢更糟
  4. 下线写日记:diary_write 让下一次的自己有东西读;
  5. 事实变化要 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 和 capabilities
  • notifications/initialized → 无响应(fire-and-forget)
  • tools/list → 返回所有 19 个工具的 schema
  • tools/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) 的确定性哈希------同样内容写多少次都是同一个 idcol.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 ~/chatsmempalace mine ~/chats 等价。Hook 脚本里用的是前者,因为 python -m 不依赖 mempalace 命令是否在 PATH 里------对于 bash hook 这是最可靠的写法。

2.1 命令总表

cli.pymain() 用 argparse 构建了一棵命令树,完整清单:

命令 作用 主要参数 背后函数
init <dir> 扫描目录、检测 entities、检测 rooms、生成配置 --yes 非交互 cmd_initentity_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_mineminer.mineconvo_miner.mine_convos
search "query" 语义检索 --wing, --room, --results cmd_searchsearcher.search
compress 把 drawer 用 AAAK Dialect 压成 closet 级 --wing, --dry-run, --config cmd_compressdialect.Dialect
wake-up 打印 L0+L1 唤醒上下文 --wing cmd_wakeuplayers.MemoryStack.wake_up
split <dir> 拆分 concat 过的 transcript mega-file --output-dir, --dry-run, --min-sessions cmd_splitsplit_mega_files.main
status 概览 - cmd_statusminer.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_hookhooks_cli.run_hook
instructions <name> 打印 skill 说明 md name ∈ {init,search,mine,help,status} cmd_instructionsinstructions_cli.run_instructions

2.2 双层子命令:hook run / instructions <name>

hookinstructions 是两个带二级子命令的分支------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 提供了两层:

  1. Shell 脚本版hooks/mempal_save_hook.sh + hooks/mempal_precompact_hook.sh------零依赖,任何 harness 都能用;
  2. 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

插件安装会做 三件事

  1. 注册 MCP server(等价于 claude mcp add mempalace -- python -m mempalace.mcp_server);
  2. 注册 5 个 slash command skill,内容从 mempalace/instructions/*.md 动态读取;
  3. 如果用户同意,写入 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.txtwing_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. 📖 参考文献

  1. 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.mdexamples/gemini_cli_setup.mdexamples/HOOKS_TUTORIAL.md
  2. MemPalace README(2026-04-07 "Note from Milla & Ben")------对 AAAK 压缩率、LongMemEval 结果、fact_checker 状态、ChromaDB pinning 的官方澄清。
  3. LongMemEval 论文 --- 96.6% R@5 基准的评测集。
  4. MCP 协议规范modelcontextprotocol.io------JSON-RPC over stdio 的工具/资源/提示词接口。
  5. ChromaDB 文档 --- PersistentClient、metadata filter、HNSW 索引重建流程。
  6. Anthropic Claude Code Hooks 文档 --- Stop / PreCompact / SessionStart 事件,decision: "block" 语义,stop_hook_active 无限循环防护。
  7. Google Gemini CLI 文档 --- PreCompress hook、MCP server 注册命令。
  8. 系列前作
    • 05《【Agent Memory篇】05:MemPalace 架构与四层存储》
    • 06《【Agent Memory篇】06:MemPalace 挖掘管线 --- miner / convo_miner / general_extractor》
    • 07《【Agent Memory篇】07:AAAK 方言、Knowledge Graph 与分层检索》
  9. OpenClaw 系列 01-04:agent 本体的四篇,构成 agent + memory 的完整上下文。

相关推荐
Light605 小时前
从“应答机器”到“提问专家”:解锁AI主动思考的Prompt设计艺术
ai agent·ai主动提问·prompt设计·审计顾问·角色设定·苏格拉底提示法
J_Xiong01177 小时前
【Agent Memory篇】05:MemPalace 整体架构、宫殿隐喻与四层记忆栈
ai agent·agent memory
zhangshuang-peta9 小时前
MCP 与 Prompt Engineering:协议出现后,Prompt 还重要吗?
人工智能·prompt·ai agent·mcp·peta
JaydenAI9 小时前
[FastMCP设计、原理与应用-02]以命令行和客户端与MCP服务器交互
ai编程·ai agent·mcp·fastmcp
LucaJu10 小时前
Hermes Agent爆火,聊聊与OpenClaw 到底区别在哪
agent·ai agent·智能体·openclaw·龙虾·hermes agent
非科班Java出身GISer3 天前
国产 AI IDE(Agent) 颠覆传统开发方式:codebuddy 介绍,以及简单对比 trae、lingma、Comate
人工智能·ai编程·ai agent·ai ide·ai 开发工具·ai 开发软件
行者-全栈开发3 天前
腾讯地图 Map Skills 快速入门:从零搭建 AI 智能行程规划应用
人工智能·typescript·腾讯地图·ai agent·mcp 协议·map skills·智能行程规划
AI自动化工坊4 天前
工程实践:AI Agent双重安全验证机制的技术实现方案
网络·人工智能·安全·ai·ai agent
comedate4 天前
【OpenClaw】一文说明 OpenWebUI / Ollama / OpenClaw 的区别与关系
ai agent·ollama·openwebui·openclaw