LightVela

USER.md 与 MEMORY.md 为什么不能合并?

摘要

USER.mdMEMORY.md 是 Hermes Agent 长期记忆的"两份贴身笔记",它们不能合并的原因有三条硬约束:一、容量各自独立(USER.md 约 1,375 字符 / 500 tokens;MEMORY.md 约 2,200 字符 / 800 tokens),互不挤压;二、加载策略不同(两份都每次会话必载,但语义角色完全不同——一份稳定描述"你是谁",一份高频记录"我们做过什么");三、更新语义不同(USER.md 偏好类字段以"覆盖旧值"为主,MEMORY.md 以"追加 + 按需 consolidate"为主)。合并成一份 NOTES.md 会同时破坏这三条约束,是典型的"看起来更简单,实际用起来更糟"。


引子:一个正经的反问

上一篇讲过,Hermes 的分层记忆里,USER.mdMEMORY.md 加起来只有 ~3,575 字符 / ~1,300 tokens——放在动辄十万上下文窗口的时代,这个空间简直过分节制

于是很多人第一反应是三个问题:

  • 为什么不合成一个 NOTES.md
  • 为什么不放大到几十 KB?
  • 为什么这两个文件的容量还差一截(2,200 vs 1,375)?

这些问题其实指向的是同一个答案:USER.mdMEMORY.md 是两种性质不同的记忆,塞在一起会互相拖累。下面把这条设计原则拆成三层来看。


一、USER.md:稳定、结构化的用户画像

USER.md关于"你"的稳定描述

  • Identity:名字、职业、所在地。
  • Preferences:沟通风格、常用技术栈。
  • Constraints:作息、语言、禁忌话题。
  • Working style:喜欢结论先行还是过程先行。
  • Long-term goals:长期在做什么、追求什么。

它的关键特征

  • 稳定:几天甚至几周才会更新一次。
  • 结构化:分节、条目化、字段化。
  • 必载:每次会话开始自动加载进系统提示。
  • 有硬上限:~1,375 字符 / ~500 tokens。

你可以把 USER.md 想成 Agent 贴在显示器边框上的便利贴——不多,但抬眼就看到,抬眼就用得上。


二、MEMORY.md:可增长、按需召回的事实档案

MEMORY.md 存的是你和 Agent"一起干过什么"

  • 项目状态:agent-demo 前端跑在 3000 端口。
  • 结论决策:上周把 hero 区改为动态渐变。
  • 具体事件:上周和 A 团队定了 Q4 会做 X。
  • 特定知识点:这个 API 的鉴权走内部 SDK 而非公开接口。

它的关键特征

  • 动态:几乎每次实质会话都会写入 1-2 条。
  • 条目式:一条一条追加,方便召回。
  • 必载 + 按需 FTS5 命中:常用条目每次会话必载,历史条目通过 FTS5 命中拉入。
  • 典型 8–15 条:目标条目数 8-15 条、总量 ~2,200 字符 / ~800 tokens。

你可以把 MEMORY.md 想成 桌上的日记本——比便利贴多,但仍然写得克制,为的是"翻起来快"。


三、"为什么不合成一个 NOTES.md?"—— 三条硬约束

有人会说:"既然都是每次必载的 Markdown,为什么不合并?"

不能合并的三条硬约束如下,每条都独立到足够让这个方案破产。

理由 1:容量各自独立,不互相挤压

USER.md 500 tokens、MEMORY.md 800 tokens。合并之后,一份 1,300 tokens 的 NOTES.md 会立刻遇到一个问题:MEMORY.md 快满时,Agent 是否要为"再记一条事件"而删掉一条"用户是谁"的画像?

答案显然是不该——但一旦合并,这个决策就必须做,且每次记录都做。分开之后,两个字段池各自演化,MEMORY.md 满了只挤 MEMORY.md 自己,不会污染画像。

理由 2:加载策略语义不同

两个文件确实都是每次会话必载,但它们在系统提示里扮演的角色完全不同:

  • USER.md 段回答的是 "你面对的是谁",用于给 Agent 定调(语气、结论粒度、技术栈假设)。
  • MEMORY.md 段回答的是 "你们一起做过什么",用于给 Agent 提供事实抓手。

如果合并,两段内容会互相稀释——Agent 无法快速判断"这段话是我要遵循的沟通风格,还是我要引用的过往事实"。

理由 3:更新语义不同

  • USER.md 偏好字段以"覆盖旧值"为主:用户说"以后别再总结了",直接覆盖 preference 里的相关行。
  • MEMORY.md 以"追加 + 按需 consolidate"为主:事件是时间序列,历史不能被随意抹掉;容量满了则通过下面的错误响应触发主动整合:
{
  "success": false,
  "error": "Memory at 2,100/2,200 chars. Consolidate now...",
  "current_entries": [...],
  "usage": "2,100/2,200"
}

合并之后,add / replace / remove 三种原子操作要在同一个文件上并存,实现和用户心智负担都会同时变糟


四、一个具体的例子

假设你告诉 Agent:

"我叫 Jasmin,是一名前端工程师,喜欢结论先行的沟通风格。最近在做 agent-demo 项目(一个 AI Agent 应用)的官网落地页,用 Next.js。上周我们把 hero 区改成了动态渐变。"

一个训练良好的 Hermes Agent 会做这样的拆分:

写入 USER.md

## Identity
- Name: Jasmin
- Role: 前端工程师

## Preferences
- 沟通风格:结论先行
- 常用技术栈:Next.js

写入 MEMORY.md

- [2026-07-30] agent-demo 项目:官网落地页 hero 区改为动态渐变。
- 相关项目:agent-demo / 官网落地页

看到区别没?

  • "你是什么样的人"进画像,"我们一起做了什么"进档案。
  • 前者只会随着你本人变化(换工作、换团队)才会改;后者随着每一天的对话都在增长。
  • 分开写,才能各自演化。

五、跳过写入的 5 条黑名单

Hermes 也不是什么信息都往 USER.md 塞。它有一份"跳过黑名单":

  1. 一次性情绪:"今天头有点疼"——不是稳定属性。
  2. 临时任务参数:"这次帮我把标题改成 X"——用完即弃。
  3. 模糊态度:"我大概比较喜欢简洁一点"——不够可复用(要么加限定,要么不记)。
  4. 可从对话上下文实时推断的信息——没必要落盘。
  5. 敏感信息(如未经用户显式确认的邮箱、密码、地址)——涉及隐私默认不写。

有了这份黑名单,USER.md 才能维持"500 tokens 且高质量"的密度。


六、这对产品意味着什么?

对 Agent 产品而言,USER.md / MEMORY.md 的分层不只是工程细节,它决定了三件事:

  1. 能不能给用户看"你对我的印象" —— 需要一份稳定、可读、可编辑的画像(USER.md 的语义)。
  2. 能不能长期不失忆 —— 需要一份可增长、可召回的事实档案(MEMORY.md 的语义)。
  3. 成本能不能被约束 —— 分层加载才能既"记得你是谁"又不烧掉一整个 context。

七、LightVela:把这份"两笔账"做成产品

LightVela 内部的 Agent 记忆体系,借鉴的正是这种分层思路。Hermes 的 USER.md / MEMORY.md 是给开发者看的 Markdown,LightVela 把它做成两件普通用户能直接操作的东西:

  • 画像层可视化:用户能在界面上直接看到"Agent 记得我的哪些画像"、"这些画像来自哪次对话",并支持一键修正。画像不再是黑盒 USER.md
  • 事实层可组织:事实记忆不只是一堆条目,而是能被打上项目、任务、时间等标签,方便召回与清理。团队场景下还能把"团队记忆"和"个人记忆"分层管理。
  • 最短路径:如果你被"分层记忆"这套思路打动,但不想自己去改 ~/.hermes/USER.md、维护 FTS5、备份 SQLite,LightVela 是把这套思路直接产品化后端给你的最短路径。

一句话:Hermes 给了"分层记忆"的工程蓝本,LightVela 把它做成每个人都用得上的产品体验。


小结

  • USER.md:稳定、结构化、每次必载的用户画像(~500 tokens,8-15 条画像字段)。
  • MEMORY.md:可增长、按需召回的事实档案(~800 tokens,8-15 条事件条目)。
  • 二者不能合并的三条硬约束:容量互不挤压 / 加载语义不同 / 更新语义不同
  • 分层是好 Agent 记忆系统的底层能力,也是 LightVela 记忆能力产品化的起点。