4.1. 陈述性记忆通过双文件结构分离了环境事实与用户偏好

陈述性记忆通过双文件结构分离了环境事实与用户偏好

2025 年的某个深夜,我在终端里对着一个号称“能记住一切”的 Agent 狠狠敲下 quit。它确实记住了很多——我的 OS 版本、项目路径、最近讨论过的那段代码——但它也把“请用英文回复我”和“项目强制使用 Python 3.11”搅在一起,在我询问 Makefile 里为什么不用 f-string 时,用一口标准伦敦腔回了一大段 Python 2 风格的字符串替换建议。那种感觉就像让一个把工作计划和早餐食谱写在同一张便签上的助理去协调一个架构评审:他可能很诚恳,但你不敢再用。

2026 年春,Hermes Agent 向我展示了一种截然不同的方案。它把关于环境和关于人的知识分成了两个文件:MEMORY.mdUSER.md。这个看似简单的双文件设计,让我在第二次会话开头就发现,它既记得我上周五改过的端口号,也记得我讨厌被动语气的回复。没有混淆,没有覆盖,没有尴尬的“您可能是想问”。

这正是 Hermes 陈述性记忆设计的起点:环境事实用户偏好之所以必须分开,不是为了让文件看起来干净,而是因为它们进入 Agent 决策链条的路径不同、生命周期不同、可信度也不同。把它们放在同一个记忆池里,是早期 Agent 最隐蔽的设计债——而双文件结构,就是还债的方式。


对比表格:单文件记忆 vs. 双文件分离

在深入实现细节之前,我们先通过一张表格,看清“所有记忆放一个地方”和“按性质拆成两个文件”究竟差在哪里。

维度 单文件记忆(典型早期 Agent) 双文件分离(Hermes 陈述性记忆) 作者的结论
内容分类 环境配置、用户偏好、项目约定、临时上下文全部混杂 MEMORY.md 存客观事实;USER.md 存主观偏好,严格分离 分类不是锦上添花,是解决“修了一个 bug 却改了用户语气”的唯一途径
注入方式 所有记忆作为系统提示整体注入,无法精细控制 两个文件分别注入系统提示的不同位置,Agent 可据此判断来源 让 LLM 知道“这个信息来自环境事实模块”本身就是一个重要的推理线索
更新影响范围 更新一条记忆可能导致系统提示重新计算,破坏前缀缓存 冻结快照机制:会话内修改不反映在当前会话,保护缓存,下一会话生效 性能与一致性可以兼得,前提是你接受“最终一致性”而非“实时一致性”
容量管理 通常只有一个总容量限制,类型混杂容易先装满噪声 各自独立容量限制(MEMORY.md 2200 字符,USER.md 1375 字符),迫使 Agent 按性质剪枝 分离的容量限制是一种强制性的自清理信号,避免“环境信息吃掉偏好空间”
信任等级 Agent 对所有记忆一视同仁 环境事实更稳定、更少被修改;用户偏好更频繁、更个人 不同的更新频率和可信度,天然适合分成两个文件
备份与版本控制 单文件混入个人偏好,难以共享项目记忆 环境事实可随项目 Git 管理;用户偏好留在 HOME 目录,不污染项目 团队协作中只分享环境事实而不泄露个人偏好,是双文件结构带来的意外红利

解读这张表格的关键在于:分离不是结果,而是使能条件。只有分开了,你才能对两类信息施加不同的存储策略、不同的容量限制、不同的注入时机,甚至不同的共享边界。这也是为什么 Hermes 没有提供一个“大记忆文件 + 标签系统”,而是直接在文件系统层面用两个物理文件做了硬隔离——意图明确,没有灰色地带。


MEMORY.md:项目约定与环境事实的载体

在 Hermes 的陈述性记忆层中,MEMORY.md 扮演着一个“永远在线的环境笔记本”的角色。它记录的不是“关于你”的信息,而是“你和我共同所处的世界”。典型内容长这样:

# Project Structure
- Backend: ./services/api/ (Python 3.11 + FastAPI)
- Frontend: ./web/ (React 18, TypeScript)
- Shared types: ./packages/types/

# Environment
- PostgreSQL 15 runs on localhost:5432, dbname "mydb_dev"
- Redis 7 is used for session caching, port 6379

# Learned Conventions
- All API endpoints must include OpenAPI decorators
- Database migrations are handled by Alembic, migration files in ./migrations/
- PR titles follow conventional commits: feat:, fix:, chore:, docs:

你一眼就能看出这些内容的特征:它们是可验证的、可共享的、较少受个人情绪影响的。一条环境事实是否正确,可以通过执行一个命令或读一个配置文件来验证。当 Hermes Agent 需要调试一个测试失败时,它从 MEMORY.md 里提取的是“migrations 目录在哪儿”,而不是“用户今天心情如何”。

Agent 如何查询和更新 MEMORY.md

最反直觉的一点是:Agent 从不直接读取 MEMORY.md 文件。因为它根本不需要。每次会话开始时,系统会将 MEMORY.md 的完整内容冻结成快照,注入到系统提示里。Agent 看到这些内容就像看到了自己的一部分长期记忆——它无法区分哪些来自文件、哪些来自系统提示模板本身。

当它在会话中认为自己学到了新的事实,比如发现用户安装了一个新的依赖,或者确认了一个之前怀疑过的端口号,它就会调用 memory 工具:

memory(action="add", target="memory", content="New dependency: pydantic>=2.0 used for API validation")

同样的,它可以 removereplace 某条记忆。这里的 replace 使用了巧妙的子字符串匹配机制——Agent 只需要提供一小段唯一的旧文本,而不需要记住整条条目的原文:

memory(action="replace", target="memory", old_text="pydantic>=2.0", content="pydantic>=2.5, all models use model_validate instead of parse_obj")

这个设计带来的工程红利是:Agent 的记忆操作非常轻量,类似于在一个持续增长的笔记里做“查找并替换”,而不是维护一个结构化的键值数据库。但同时,它也有代价——子字符串匹配可能歧义,如果简短的 "port" 匹配到多条记忆,工具会要求提供更具体的字段。这看似是缺陷,实际上是一种有益的约束:它迫使 Agent 在记忆时就考虑“够不够唯一”,无意中提高了记忆质量。

经验框:当 MEMORY.md 接近容量上限时
我曾在一次长会话中反复修改 API 端点描述,每次都用 add 追加新条目。不到 30 轮对话,Agent 突然收到工具返回的错误:Memory full (2200/2200 characters)。它没有傻掉,而是主动检查了所有条目,然后用一次 replace 操作把三个相关的端点说明合并成一段简洁的列表,释放了大约 400 字符的空间。
启示:这个机制内置了“信息蒸馏”的驱动力——不是因为规则规定“你必须整理”,而是因为不整理就写不进去。Agent 被迫在记忆与遗忘之间做出选择,而每一次替换都是一次隐式的复习和重构。


USER.md:个人偏好与沟通风格存储

MEMORY.md 的冷硬事实相对,USER.md 保存的是“关于使用者”的软知识。它位于 ~/.hermes/memories/ 下的同一个目录,但内容气质截然不同:

# Communication Style
- Prefers direct, concise answers; no passive voice
- Use bullet points for action items, not prose
- Code examples should include type hints

# Preferences
- Default shell: fish
- Terminal theme: dark mode
- Language: Chinese for explanations, English for code comments

# Personal Context
- Role: backend engineer, working on microservices
- Currently learning: Rust
- Meeting times: 10:00-18:00 CST

这些内容直接影响 Agent 在每次回复中的语气、结构和决策路径。当它被要求“解释一下这段代码”时,它会从 USER.md 快照里取出“用中文解释、代码注释用英文、使用项目符号”三条偏好,组合出一个符合你心智模型的回答。没有这条记录,它可能给出一段纯英文的散文式解释——技术上没错,但你在第 40 秒就会关掉终端。

memory 工具对于 USER.md 的操作与 MEMORY.md 完全一致,只是 target 参数换成 "user"。这种统一的操作接口背后,隐藏着一个重要理念:对 Agent 而言,添加一个环境事实和添加一个用户偏好,操作成本是相同的;但系统通过目标文件的不同,让信息的性质自然分流。Agent 不需要一个“判断我学到的这条信息应该归属哪里”的复杂路由逻辑——它只需要继续对话,在必要时调用正确的 target。


存储位置与跨会话持久化

两个文件都存放在 ~/.hermes/memories/ 目录下:

~/.hermes/memories/
├── MEMORY.md   # 环境事实,最大 2,200 字符
└── USER.md     # 用户偏好,最大 1,375 字符

路径的设计本身就表达了意图:~ 是“我的”,MEMORY.mdUSER.md 却是“关于我们”的。对于个人使用,这个目录下只有一个版本;对于团队协作,常见的实践是将 MEMORY.md 通过软链接指向一个项目仓库中的共享文件,而 USER.md 始终保持本地私有。这就在文件系统层面实现了一个微妙但关键的隔离:环境事实可以被团队共享和版本控制,用户偏好则永远不需要离开个人机器

跨会话持久化的可靠性建立在两个机制之上:

  1. 写入即时持久化:Agent 通过工具调用记忆修改后,文件立即写盘。即使会话意外中断,修改也不会丢失。
  2. 注入使用冻结快照:新会话开始时读取文件快照,会话期间忽略文件变更。这意味着同一会话内的记忆更新只在“下一轮对话”生效(严格说是下一会话),但不会因 Agent 改了自己刚读过的上下文而导致前缀缓存失效,从而避免昂贵的 LLM 重新计算。

核心建议框:备份与 Git 集成
对于关键项目的 MEMORY.md,建议将其纳入项目仓库的 .hermes/ 目录,并使用符号链接指向 ~/.hermes/memories/ 中的对应文件。这样,每当 Agent 学到新的项目约定,这些变化可以通过 Git 跟踪和分享,同时你的个人偏好文件始终安全地待在 HOME 目录,不会意外推送到公共 repo。
定期 cp USER.md USER.md.bak.$(date +%Y%m%d) 是一个低成本的良好习惯——如果 Agent 某次误操作清除了偏好,你可以轻松恢复。


核心洞察:从被动记忆到独立事实域

要用一个类比级联来理解双文件结构的本质,可以这样递进:

被动记忆(单文件):像一个把所有东西都塞进一个抽屉的助手。你去问“上次我们讨论的那个 API 端口是 8080 还是 3000?”,他翻遍抽屉,翻出了你的生日、你的饮食习惯、还有三张过期的便签,最后找到了那条被压在下面的端口号。答案是对的,但你等了 30 秒,而且他开始问你“要顺便帮您订个蛋糕吗?”

主动分类(双文件):你给了助手两个抽屉,一个标着“项目环境”,另一个标着“我的习惯”。助手不需要每次都翻遍所有东西,他根据你问题的性质打开对应的抽屉。端口号在环境抽屉,生日在习惯抽屉,两者互不干扰。这已经好了很多。

独立事实域(冻结快照 + 独立容量):真正的飞跃在于,你告诉助手:这两个抽屉里的东西只能在每天早晨交换一次,白天你可以往里面放新东西,但旧的快照不会变;而且每个抽屉有固定的空间,满了就必须丢掉一些旧的。这就是 Hermes 做到的——它不仅分开了信息,还让两类信息进入不同的管理生命周期。环境事实更稳定、容量更大(2,200 字符);用户偏好更动态、容量稍小(1,375 字符)。Agent 不会在“要记住一个新端口号”时担心覆盖掉你爱的主题颜色,也不会因为反复记录偏好而挤掉环境事实。

双文件结构最终解决的,是记忆系统中的信噪比问题。当所有信息都在一个桶里时,任何一条新记忆的加入都在增加噪声,降低下一次检索的精准度。而当信息被按性质分流,每一条记忆都落在一个预期它存在的领域里,Agent 的推理效率自然提高——这不是因为模型变聪明了,而是因为上下文的结构更清晰了。


适合谁,不适合谁

这套双文件陈述性记忆并不是万能方案,它有自己最擅长的场景,也有该主动退让的时刻。

适合

  • 长期项目合作者:你在一个代码库里待数月,Agent 需要记住项目结构、构建命令、测试入口。单文件记忆会在这里积累大量环境事实,而用户偏好可能被挤得只剩一两行。
  • 有明确沟通偏好的人:你希望 Agent 始终用某种风格回复、使用某种语言、避免某些术语。把这些写进 USER.md 后,它能稳定地跨会话复用,不需要每次都重新告知。
  • 团队中有共享环境信息需求:只要把 MEMORY.md 放进 Git,所有同事打开 Agent 时都自动获得相同的项目事实,却互不见对方的主题偏好。

不适合

  • 一次性任务或短期试玩:双文件结构的优势需要跨会话积累,如果每次都是独立的新话题,记忆系统反而增加系统提示长度,没有收益。
  • 极度注重实时一致性者:如果你无法接受“记忆更新要等到下个会话才生效”的最终一致性模型,那你会觉得冻结快照机制是 bug 而非特性。但请理解,这背后是为了保留前缀缓存这一巨大的性能优化——没有这种妥协,模型推理成本会陡增。
  • 记忆内容高度动态且可能冲突的情况:如果同一个环境事实在不同上下文中有不同含义(例如在多租户环境下,端口号在不同客户的会话中不同),双文件记忆的静态快照模型就不那么适合——那可能需要更细粒度的会话级记忆。

从当前调研资料看,Hermes 社区的主流使用者正是那些想要一个“真正记住项目的 Agent”的开发者。对他们而言,双文件结构不是一种过度设计,而是让 Agent 从“能聊天”进化到“能共事”的临界点。


下一章,《严格的 3,600 字符容量上限是一种信息蒸馏策略》,我们将从一个看似刻薄的设计决定切入:为什么 MEMORY.md 和 USER.md 不能无限制生长?那个硬限制背后的逻辑,远比“防止系统提示过长”更深刻——它是一种逼迫 Agent 对记忆进行价值排序的压力装置,一个将机器遗忘转化为知识提纯的蒸馏器。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~