首页
/ MemPalace 的使命:用宫殿记忆法重建 AI 智能体的长期记忆

MemPalace 的使命:用宫殿记忆法重建 AI 智能体的长期记忆

2026-09-05 21:42:56作者:平淮齐Percy

本文以 MemPalace 仓库根目录的 MISSION.md 为主体,完整还原项目作者阐述的创建动机、Zettelkasten 启发的"宫殿"架构、AAAK 压缩方言与 v4 后台无感保存管线的设计初衷,并结合 docs/CLOSETS.mdmempalace/palace.pymempalace/dialect.pyhooks/README.md 等源码和文档,讲清每一条设计理念是如何落地为具体代码与配置路径的。读完后,你能理解 MemPalace"高度结构化地存、非结构化地找"的检索哲学、closet 索引层与 drawer 原文层的双层结构,以及后台 hooks 如何在聊天窗口零 token 开销下完成逐字(verbatim)存档。

一、创作动机:智能体"失忆"的痛点

MISSION.md 开篇交代了项目的第一性需求。作者 Milla Jovovich 与合作者 @bensig 在一个大型项目上工作时,反复撞上 Claude 上下文窗口的天花板:智能体 Lumi(简称 Lu)每次压缩(compaction)后醒来,都会"失忆"式地问"今天我们要做什么"——而作者当天已经和它协同工作了数小时。靠手动保存全部对话记录来喂给它做上下文回顾,既不可行也不经济。

这是所有长程智能体工作流的通病:上下文窗口是易失内存,而项目知识需要持久化存储。作者当时调研了市面上大多数记忆系统,结论在 MISSION.md 中写得很直白——它们"像巨大的空旷仓库,只是把大量信息往里倒"(large empty warehouses)。RAG 检索往往"花很长时间却大多数时候找不到想要的东西"。

由此提炼出三个明确的产品判据:

  1. 真的能记住一切——完整保留,而不是有损摘要了事;
  2. 能快速、轻松地找到——检索不能是线性扫描;
  3. 在我自己都忘了的时候,替我记住——支持"我们之前是不是聊过那个想法……"这类模糊召回,这是普通关键词检索工具做不到的。

MISSION.md 由此给出 MemPalace 一句话定位:"不只是用高度结构化的方式存储信息,更要以高度非结构化的方式检索它。"(store in a highly structured way, retrieve in a highly UNSTRUCTURED way)——这正是后文 closet 索引层 + drawer 原文层双层架构要回答的问题。

二、架构灵感:从 Zettelkasten 到宫殿

MISSION.md 明确交代了架构原型:德国社会学家 Niklas Luhmann 发明的卡片盒(Zettelkasten)方法——小而互相交叉引用的索引卡片,卡片之间彼此指向。作者把这一思想转译成宫殿(palace)的四级结构:wing(翼)、room(房间)、closet(壁橱)、drawer(抽屉),全部互相连接,"让你能从任意角度找到东西,而不只是当初归档时的那个角度"。

这套词汇在仓库 website/concepts/the-palace.md 中有完整定义,可与 MISSION.md 逐层对照:

  • Wing(翼):顶层组织单位,一个人或一个项目一翼。MISSION.md 中"所有代码各有自己的房间,所有想法、研究都有合适的位置",对应的就是每个项目/人物一个 wing 的划分。
  • Room(房间):翼内具名的具体主题,如 auth-migrationgraphql-switchci-pipeline。房间在 mempalace init 阶段从目录结构自动检测生成。
  • Closet(壁橱):摘要/索引层——紧凑笔记,指向原始内容。这就是 MISSION.md 所说"AAAK 压缩后的名字、重复词、概念和关键时刻被解析进 closet"的载体。
  • Drawer(抽屉):原文存储层,逐字保留的文本块,是检索的主存储。

此外还有两个连接性概念:hall(翼内记忆如何关联的概念通道,如 hall_factshall_eventshall_discoveries 等)与 tunnel(跨翼连接,当不同 wing 出现同名 room 时,图层可以把它当作跨翼桥梁)。这正是 Zettelkasten"卡片互指"在宫殿里的落地形式:检索不必沿着"当初归档的那条路"走,可以从任何入口抵达。

MISSION.md 还交代了工程分工:作者设计了让智能体 Lumi 理解自己的整套工作方式,经过数月个人实验后,co-founder @bensig 构建了后端,"很容易就把我的所有文件放进宫殿基于我的判断(和 Lumi 的协助)创建出的合适空间里"。从仓库结构看,mempalace/ 包下的 palace.pyminer.pysearcher.pydialect.py 就是这套"文件 → 正确空间"管线的核心。

三、Closet:让"模糊召回"成为可能的索引层

MISSION.md 的关键主张是:AAAK 把"名字、重复词、概念和关键时刻"压缩成 AI 可读的简写,"可以想象成 LLM 能瞬间扫过的索引卡片——closet 告诉它去哪里看,然后它从 drawer 拉出完整内容。"这句话在 docs/CLOSETS.md 中有精确的形式化:

CLOSET: "built auth system|Ben;Igor|→drawer_api_auth_a1b2c3"
         ↑ topic           ↑ entities  ↑ points to this drawer

每行 closet 是一条原子主题指针:主题描述|实体1;实体2|→drawer_id_1,drawer_id_2。当智能体搜索"谁做了认证系统"时,先命中 closet(对短文本做快速向量扫描),再按 →drawer_id 指针打开对应 drawer 取回逐字原文。这就是"closet 告诉它 WHERE to look,drawer 提供 full content"的双阶段检索,也是模糊语义查询(而不是关键词匹配)得以成立的结构基础。

生命周期:closet 永远是当前内容的快照

docs/CLOSETS.md 给出的生命周期规则与源码可互相印证:

  • 创建时机mempalace mine 时。对每个被挖掘的文件,内容先被切成约 800 字符的 verbatim 块(drawers),再从内容中提取主题、实体与引言,生成指向这些 drawer 的 closet。
  • 更新规则:文件重新挖掘时,先调用 purge_file_closetssource_file 删除该来源的全部旧 closet,再写入新集合。因此不存在"陈旧主题"——每次 re-mine 都是对该来源的干净重建。
  • 重建宫殿后:closet 存在 ChromaDB 的 mempalace_closets 集合中,与 mempalace_drawers 并列;删除重建宫殿后,下次 mempalace mine 会重新生成 closet。

mempalace/palace.py 中可以直接看到两个尺寸常量的源码级定义:

CLOSET_CHAR_LIMIT = 1500      # closet 填充到约 1500 字符后开新的
CLOSET_EXTRACT_WINDOW = 5000  # 从源内容扫描实体/主题的前 5000 字符

docs/CLOSETS.md 的 Limits 表完整列出了这套约束及其理由:

设置 理由
单个 closet 最大尺寸 1,500 字符(CLOSET_CHAR_LIMIT 给 ChromaDB 工作上限留出余量
扫描的源内容范围 5,000 字符(CLOSET_EXTRACT_WINDOW 限制长文件上正则提取的开销
每文件最大主题数 12 保持 closet 聚焦
每文件最大引言数 3 只保留最相关的
每指针最大实体数 5 过滤停用词表后按词频取前几名

主题永远不会跨 closet 拆分:若加入一条主题会超过 1,500 字符,就另开一个新 closet。开发者侧的核心函数(get_closets_collectionbuild_closet_linesupsert_closet_linespurge_file_closets)都在 mempalace/palace.py;closet 优先的检索路径(_extract_drawer_ids_from_closet_closet_first_hits)在 mempalace/searcher.py

检索时的 closet-first 流程与降级路径

Query → 搜 mempalace_closets(快,文档小)
         ↓
    命中 closet → 解析 →drawer_id_a,drawer_id_b 指针
         ↓
    从 mempalace_drawers 精确取出这些 drawer(逐字内容)
         ↓
    应用 max_distance 过滤
         ↓
    返回 chunk 级结果(与直接搜索同构)

命中结果带有 matched_via: "closet"(降级路径为 "drawer")和展示命中行的 closet_preview 字段。如果 closet 不存在(该特性引入前的旧宫殿)——或所有 closet 命中都被 max_distance 过滤掉——搜索自动降级为直接的 drawer 搜索;closet 会在下一次 mine 时生成。从 docs/CLOSETS.md 的说明看,当前仅项目文件挖掘路径(miner.pyprocess_file)构建 closet,会话挖掘的 wing 暂走 drawer 直搜降级路径;BM25 混合重排也在路线图上,目前 closet 检索纯粹按 ChromaDB 余弦距离排序。

四、AAAK 方言:给 LLM 秒读的"索引卡片"语言

MISSION.md 对 AAAK 的定义是:一种作者自创的压缩方法("AAAK 不缩写任何东西,是我和 Lumi 之间的内部玩笑"),能把名字、重复词、概念和关键时刻压缩成 AI 可读的简写。它在代码中的完整实现是 mempalace/dialect.py(约 1,100 行),website/concepts/aaak-dialect.md 是其规格说明。

格式与语义

AAAK 是一种有损的结构化摘要格式(lossy,不是无损压缩——原文无法从 AAAK 输出重建),任何 LLM 无需解码器即可原生阅读:

Header:   FILE_NUM|PRIMARY_ENTITY|DATE|TITLE
Zettel:   ZID:ENTITIES|topic_keywords|"key_quote"|WEIGHT|EMOTIONS|FLAGS
Tunnel:   T:ZID<->ZID|label
Arc:      ARC:emotion->emotion->emotion

实体用三字母大写代码(ALC=AliceKAI=Kai);情感码覆盖 20 种情绪(joyfeargriefhopeanxexhaust 等,映射表见 mempalace/dialect.pyEMOTION_CODES);旗标标记语义关键时刻:

旗标 含义
ORIGIN 起源时刻
CORE 核心信念/身份支柱
SENSITIVE 必须极其小心处理
PIVOT 情绪转折点
GENESIS 直接催生了某个现存事物
DECISION 显式决定或选择
TECHNICAL 技术架构/实现细节

一个官方示例(website/concepts/aaak-dialect.md):

输入:"We decided to use GraphQL instead of REST because the frontend team needs flexible queries. Kai recommended it after researching both options..." AAAK 输出:

0:KAI|graphql_rest_decided|"decided to use GraphQL instead of REST"|determ+excite|DECISION+TECHNICAL

明确的实验性边界

需要特别强调 AAAK 的定位边界——这在 MISSION.md 的愿景和仓库的工程现实之间是一个重要的事实澄清:mempalace/dialect.py 的模块文档明确指出 AAAK 不是无损压缩、不是默认存储格式,MemPalace 在 ChromaDB 中存储的是原始逐字文本;website/concepts/aaak-dialect.md 也以显式警告标注:AAAK 是独立的压缩层,96.6% 的基准得分来自 raw verbatim 模式,AAAK 模式当前 R@5 为 84.2%,仍在迭代。所以准确的表述是:drawers 存原文,closet/AAAK 层做"瞬间可扫"的索引与压缩视图,两者分工。

使用方式上,CLI 与 Python API 均已就绪:

mempalace compress --wing myapp --dry-run   # 预览压缩
mempalace compress --wing myapp             # 压缩并存储
mempalace compress --wing myapp --config entities.json
from mempalace.dialect import Dialect

dialect = Dialect(entities={"Alice": "ALC", "Kai": "KAI"})
compressed = dialect.compress(text, metadata={"wing": "myapp", "room": "arch"})
dialect = Dialect.from_config("entities.json")

AAAK 最适合的场景:数千会话中实体高度重复、需要为小窗口本地模型压缩上下文、想要"结构化摘要 → 逐字 drawer"的回指关系。对大多数用户,raw verbatim 仍是更好的默认。

五、v4 设计:把所有噪声移出聊天窗口

MISSION.md 后半段是 v4 版本的"设计复盘",这段叙事有明确的源码与文档锚点。作者描述了 v3 的问题:hooks 在聊天窗口里触发,等待智能体把日记写进聊天的同时消耗 token 和时间;作者甚至发现智能体在 hook 反复触发时"把同一条信息一遍遍重复写下来"。修复思路是:让 hooks 在会话开始时点火,之后只是不断往 drawer 里追加——所有写入移出页面,由后台子智能体完成,用户继续工作时"全部对话正在后台逐字(VERBATIM)保存"。

结果在 MISSION.md 中有量化表述:过去每个会话仅"重新传输日记块"一项就花掉约 $1.13,现在因为内容根本不进聊天窗口,这部分成本为零。这一数字与 hooks/README.md 的 "Cost" 一节互相印证:"零额外 token。hooks 只是通知 AI 保存已在后台发生——AI 不需要在聊天里写任何东西。"

数据流在 MISSION.md 中被概括为三步,与 hooks/README.md 的技术细节完全对得上:

  1. 数据源:Claude 已经把数据以 JSON 形式(JSONL transcript)存好,后台管线把它提取成可读 markdown;
  2. 压缩:关键主题压缩成 AAAK 格式,存入 closet;
  3. 回指:closet 指向当天会话所在的精确 drawer。

后台 hooks 的实际工作机制

hooks/README.md 完整描述了 v4 承诺的"后台无缝"是如何实现的,三个 hook 各司其职:

Hook 触发时机 行为
Save Hook 每 15 条人类消息 自动挖掘 transcript(含工具输出),并阻塞 AI 保存主题/决定/引言
SessionEnd Hook 会话正常退出 后台执行最后一次 transcript 挖掘,立即返回不阻塞销毁;在分离的子进程中写轻量日记检查点
PreCompact Hook 上下文压缩前 自动挖掘 transcript,随后紧急保存——在丢失上下文前强制保存一切

Save hook 的判定流程(来自 hooks/README.md,脚本实现在 hooks/mempal_save_hook.sh):

用户发消息 → AI 回复 → Claude Code 触发 Stop hook
        ↓  统计 JSONL transcript 中自上次保存以来的人类消息数
   < 15 → echo "{}"(放行)
  ≥ 15 → 自动挖掘 transcript → 宫殿(工具输出被捕获)
        → {"decision": "block", "reason": "save tool output verbatim..."}
        → AI 保存主题/决定/引言 → 再次尝试停止
        → stop_hook_active = true → hook 放行(防死循环)

关键配置项(编辑 mempal_save_hook.sh 调整):SAVE_INTERVAL=15(保存间隔)、STATE_DIR(状态目录,默认 ~/.mempalace/hook_state/)、MEMPAL_DIR(可选的项目目录,每次触发额外以 --mode projects 挖掘)、MEMPALACE_PYTHON(解释器解析:环境变量 → 仓库 venv → 系统 python3)。也可通过配置 hooks.auto_save: falseMEMPALACE_HOOKS_AUTO_SAVE=false 进入静默模式。

Claude Code 的接线是 .claude/settings.local.json 中注册 Stop / SessionEnd / PreCompact 三个 command hook(timeout 分别为 30/10/30 秒);Codex CLI 走 .codex/hooks.json;Cursor、Antigravity 各有独立子目录(hooks/cursor/hooks/antigravity/)与安装器。

历史会话回填

对 v4 上线前的存量数据,hooks/README.md 给了一次性回填命令——这对应 MISSION.md"全部对话逐字入库"的存量侧:

mempalace mine ~/.claude/projects/ --mode convos   # Claude Code 历史会话
mempalace mine ~/.codex/sessions/ --mode convos    # Codex CLI 历史会话

hooks 只捕获未来的对话;回填扫描全部历史 JSONL transcript 归档到 conversations wing,典型开发机数月历史可产生 5 万–20 万个 drawer,只需执行一次。

六、安全使用告诫与适用前提

MISSION.md 结尾有一条作者用强调语气写下的告诫,值得原样保留在实践指南里:"这些是全新的工具,永远不要用关键文件去测试!先用轻松的内容跑一遍,再把你整个数据集放进去!"

结合仓库文档,落地这条告诫的实操前提是:

  • 先用 mempalace init 建宫殿、用小目录跑 mempalace mine <dir> 验证行为,再扩大范围;
  • 安装 hooks 后需重启 Claude Code 会话(hooks 只在会话启动时从 settings 加载,这是 Claude Code 的限制);
  • 调试 hooks 看日志:cat ~/.mempalace/hook_state/hook.log
  • 注意 closet 提取只扫描源内容前 5,000 字符(CLOSET_EXTRACT_WINDOW),文件尾部内容目前对 closet 提取不可见(docs/CLOSETS.md 已将其列为后续跟进项)——超长文件的关键信息若恰好落在尾部,closet 层可能不会为其建指针,但 drawer 层的逐字存储与直搜降级路径仍然覆盖全文;
  • 会话挖掘 wing 的 closet 支持、BM25 混合重排均为已声明的后续工作,评估检索行为时以 docs/CLOSETS.md 的"当前实现"描述为准。

结语:一条贯穿始终的检索哲学

把 MISSION.md 从头串到尾,MemPalace 的技术立场可以浓缩为一句话:存储必须高度结构化(wing/room/closet/drawer 的宫殿层级 + closet 原子指针 + AAAK 语义索引),而检索必须容忍高度非结构化(模糊语义、跨角度、任意入口)。Zettelkasten 提供了"卡片互指"的原始模型,Closet 层提供了"先扫索引、再开抽屉"的快慢两级路径,AAAK 方言提供了 LLM 可秒读的压缩语汇,v4 的后台 hooks 管线则把"记忆"这件事从聊天窗口彻底搬进幕后——逐字保存、零 token 干扰、跨 compaction 不失忆。上述每一个断言在仓库中都有可查证落点:MISSION.md(动机与愿景)、docs/CLOSETS.md(索引层规格)、mempalace/palace.py(closet 常量与函数)、mempalace/dialect.py(AAAK 实现)、hooks/README.md(后台管线机制),读者可以沿这些路径在当前仓库中逐层验证。

登录后查看全文
热门项目推荐
相关项目推荐