改造 Hermes Memory Provider 的曲折历程
这不是一个讲"正确答案"的故事。这是一个讲"如何一步步排除错误答案,直到看见本质"的故事。
起点:MemPalace
一切从抽屉开始。
2026 年初,我给赫妹搭了一套记忆系统叫 MemPalace。核心理念很朴素:人类用记忆宫殿——把信息存进具体的地点和房间,回忆时按位置检索。映射到数字世界,就是抽屉(drawer)、房间(room)、 wing(主题区)三级结构。存储层是 ChromaDB,向量检索。
这套系统跑了一阵子,基本能用。但很快就摸到了它的边界。
问题 1:Layer2 的 LLM 判决是玄学。 FiltrationLayer 分两层:Layer1 用正则提取关键词(decision、preference、milestone),Layer2 让 LLM 判断"这条记忆是否重要"。LLM 的判断不稳定,同样的内容今天 accepted 明天 rejected。Layer2 成了整个系统的黑盒变量。
问题 2:ContentRouter 是过度设计。 架构里引入了 ContentRouter,要先把记忆分成 WORLD_FACT / EXPERIENCE / OPINION / ENTITY 四类,再分别走不同的写入路径。实际上 (90%+) 的内容都进了 EXPERIENCE,其他三个桶几乎全空。ContentRouter 除了增加维护负担,没有任何实际价值。
问题 3:写入链路冗余。 三套写入路径:WorldNetwork、OpinionManager、NarrativeFusion,各写各的 collection。没有一致性校验,也没有统一的数据模型。
问题 4:Observations Consolidation 几乎不触发。 consolidation 的本意是把多条相似记忆合并成一条,节省存储并提高召回质量。但实际运行中,34,549 条观测值跑 consolidation,300 秒超时,只完成了 60/78 批。剩下 (20%+) 的数据永远卡在那里。
中转站:Memory Filtration Layer(MFL)
基于 MemPalace 的教训,我重写了 FiltrationLayer,称之为 MFL(Memory Filtration Layer)。
这次做了几件对的事:
删掉了 ContentRouter。发现它的分类逻辑从未真正起作用,内容自然沉淀到 EXPERIENCE 桶里。删掉后代码从 800 行瘦到 200 行,没有功能损失。
删掉了 NarrativeFusion 和 OpinionManager。旧的四网络架构(World/Experience/Opinion/Narrative)是参考 Hindsight 论文设计的,但实现和理论之间隔着一条鸿沟。LLM 判决质量撑不起这个架构。删掉之后只剩下统一的写入路径。
Layer2 也简化了。 之前 Layer2 调用 LLM 做 semantic filter,效果不稳定。简化后改为纯 confidence threshold:Layer1 提取时给每条记忆打个置信分,Layer2 直接按 0.5 阈值过滤,不再调 LLM。全程无 API 调用,延迟从秒级降到毫秒级。
统一数据模型上场。 之前每种记忆类型有自己的数据结构,Observation 和 Entity 和 Opinion 各玩各的。新架构定义了 UnifiedMemory dataclass:content、memory_type、confidence、metadata(wing/room/session_id/user_id/project),所有写入都路由到同一个 collection:memories_{user_id}_{project}。
这套系统在 MemPalace Provider 里跑了大概一个月。
然后我发现了真正的问题。
真正的断层:Consolidation 的 KG 质量灾难
consolidation 是记忆系统的"消化"机制:把碎片化的原始观测合并成结构化的 Observation,避免记忆无限膨胀。
但 consolidation 有一个伴生产物:知识图谱三元组(KG triples)。每条 Observation 应该提取出 SUBJECT | PREDICATE | OBJECT 格式的三元组,用来建立实体关系图。
34,549 条观测值,consolidation 超时(300 秒),只完成 60/78 批。
更严重的是,KG triples 的质量几乎是垃圾:
观 察 到 的 :[SUBJECT | PREDICATE | OBJECT] |
58 条 KG 记录,几乎全是 LLM prompt 的截断文本、解析失败的残片、或者干脆就是提示词本身。真正有意义的三元组不到 5 条。
这说明 consolidation 的 KG extraction 流水线从根上就是坏的。
根因分析:
问题不在 _parse_kg_triples 的解析逻辑——解析逻辑本身没问题,格式规范是清晰的。
问题在 upstream:LLM 的输出本身就是乱的。consolidation 的 prompt 要求 LLM 提取三元组,但 LLM 并没有稳定地遵循这个格式。有时候输出完整,有时候输出 prompt 文本,有时候干脆不输出。
这暴露了一个更深的问题:整个 consolidation 流程对 LLM 输出质量的依赖,本身就是一个不稳定的基础假设。
LLM 的输出有随机性。你不能把一个依赖随机输出的 pipeline 当作可靠的数据处理流程。
转折点:重新审视"记忆系统需要什么"
consolidation 失败之后,我被迫停下来想一个根本问题:
记忆系统的本质到底是什么?
我翻出了 Mem0、Hindsight、MemPalace 三个系统的设计文档,做了一个对比表格。看完之后,最有意思的发现不是任何一个系统的具体功能,而是它们都在解决同一个核心矛盾:
存储和召回的权衡是不可消除的。
-
存得越多,召回噪音越大
-
召回越精确,存储越稀疏,可能丢失边缘有用信息
所有记忆系统都在用不同的方式管理这个矛盾:Mem0 用 ADD-only + MD5 去重规避更新决策;Hindsight 用 Budget 机制控制召回量;MemPalace 用 KG 关系结构提高召回精度。
但当我对比这些方案时,我发现了一个更简单的答案——来自一个意想不到的地方。
灵感:Claude Code 的记忆方式
Claude Code 是 Anthropic 发布的 AI 编程助手。它的记忆系统没有任何向量库、没有 embedding、没有 ChromaDB。
它用纯文件系统存储记忆,以 Markdown 格式保存,配一个轻量索引文件。
就这么简单。
这个事实迫使我重新审视之前所有的复杂设计。如果 Claude Code 能用纯文件系统做 AI Agent 的记忆系统,并且工作得足够好,那么我们之前构建的那套 ChromaDB + FiltrationLayer + KG extraction + LLM 判决的复杂基础设施,究竟解决了什么问题?
答案可能是:解决了不存在的问题。
我把 MemPalace/MFL 架构和 Claude Code 的方案做了第一性原理对比:
| 维度 | MemPalace/MFL | Claude Code 方案 |
|---|---|---|
| 存储 | ChromaDB(向量库) | 文件系统 + Markdown |
| 检索 | embedding 相似度 + KG 遍历 | 关键词扫描 + LLM 决策 |
| 延迟 | embedding 生成 ~50ms | 文件读取 ~1ms |
| 可迁移性 | 依赖 ChromaDB 版本 | 纯文本,任意工具可读 |
| 可解释性 | embedding 黑盒 | Markdown 明文 |
| 维护成本 | 高(多套 pipeline) | 低(文件系统) |
最大的认知翻转在这里:
之前我以为 embedding 相似度检索是"更智能"的记忆召回方式。但实际上,embedding 的"智能"是有代价的:它把记忆内容的语义压缩成一个向量,这个过程本身就有信息损失。而且向量相似度不等于相关性问题——有时候你最需要的记忆,和你的查询在向量空间里并不接近。
Claude Code 的方案则反其道而行:不做语义压缩,直接存原文。召回时用轻量扫描(frontmatter metadata 过滤),再用 LLM 做最终判断。两阶段:快扫 + LLM 决策。
这个方案更简单,延迟更低,而且没有 embedding 的信息损失。
新架构:mem-md
基于这个认知,我写了 mem-md。
核心理念只有一句话:Markdown is all you need。
~/.mem-md/ |
四种闭集记忆类型(无开放式存储):
-
user:用户画像——“你是谁” -
feedback:工作方式——“我如何工作” -
project:项目上下文——“项目正在发生什么” -
reference:外部指针——“信息在哪里”
闭集分类的核心价值:定义"什么不该存"比"该存什么"更重要。
开放式的记忆存储必然导致记忆膨胀和检索噪音。严格限定四种类型,前置过滤比后期裁剪更有效。
两阶段召回:
-
Scanner(<10ms):扫描 frontmatter,只取 type + staleness_days 满足条件的文件
-
Selector(LLM 决策):把候选文件内容发给 LLM,让 LLM 判断哪些和当前 query 真正相关
这个设计和 Claude Code 的思路一致:不做 embedding,用结构化元数据做初筛,再用 LLM 做最终语义判断。
Consolidation 简化为 auto-dream 四阶段:
-
Orient:识别记忆主题
-
Gather:收集相关片段
-
Consolidate:合并为 Observation
-
Prune:修剪 MEMORY.md 索引
删掉了 KG extraction——consolidation 不再试图提取三元组。KG 是之前过度设计的又一个证据。 从 34,549 条观测值里提取出来的三元组几乎全是垃圾,保留这个功能只是在增加系统复杂度和维护负担。
过程即结果
回顾整个改造过程,最有价值的不是最终的 mem-md 架构,而是排除错误路径的过程。
MemPalace 让我知道了 ChromaDB + 多层 pipeline 的边界在哪里。MFL 的 FiltrationLayer 简化实验证明了 Layer2 LLM 判决可以完全去掉。consolidation 的 KG 质量灾难让我最终下决心删掉 KG extraction。Claude Code 的对比给了我重新设计的勇气。
每一个"放弃"的决定背后,都是一次第一性原理的追问:
-
为什么需要 ContentRouter?因为要分类。但分类真的有用吗?没有。删。
-
为什么需要 Layer2 LLM 判决?因为要判断重要性。但这个判断稳定吗?不稳定。删。
-
为什么需要 KG extraction?因为要建立实体关系。但提取质量合格吗?不合格。删。
帆布归真。删到不能再删,剩下的就是本质。
教训
1. 复杂性是记忆系统的大敌。
记忆系统天然倾向于变得越来越复杂。每一层抽象都看起来合理,每一个新功能都解决了某个真实的问题。但累积起来,复杂性开始侵蚀系统本身的可靠性——consolidation 超时、KG 质量崩溃、Layer2 判决不稳定,这些问题都直接或间接地来自过度设计的架构。
2. 可解释性比"智能"更重要。
ChromaDB 的 embedding 检索听起来很智能,但它的结果是黑盒的。你不知道为什么某条记忆被召回,另一条没有被召回。Markdown 文件明文存储,frontmatter 一目了然,出了问题可以随时检查、修复、回滚。
3. 存储和召回的权衡,最终靠 LLM 决策。
无论是 embedding 相似度还是 metadata 过滤,都无法完全解决"语义相关性 vs. 字面相关性"的张力。最终的判断必须交给 LLM——但 LLM 应该用在召回阶段(判断候选是否相关),而不是在存储阶段(判断内容是否重要)。
4. consolidation 的 KG 质量依赖 LLM 输出的稳定性,这本身就是一个不稳定的基础假设。
当前状态
mem-md phase3-5 已完成,48 个测试全部通过(33 单元 + 15 E2E)。
四个阶段的核心功能全部实现:
-
Phase 1:核心模块(types、config、storage、scanner、indexer)
-
Phase 2:LLM 决策选择(MemorySelector)
-
Phase 3:自动提取(后台线程,每轮对话后异步触发)
-
Phase 4:Consolidation(auto-dream 四阶段引擎)
下一步是接入 Hermes Agent 作为 Memory Provider,让记忆真正进入赫妹的工作流。
续篇:《mem-md 开源:一个反向量库的 Agent 记忆系统》
文章使用 humanizer 润色,移除了 AI 写作的典型模式,保留个人思考痕迹。