让Agent记住用户——跨会话记忆的设计:什么值得记、什么时候记、记多了怎么整合
最近公司在预研陪伴类的 AI 产品。陪伴这件事,核心就是"记得你",所以长期记忆是绕不开的一块。
我先用 Mem0 搭了个雏形,聊几轮之后,很快就遇到了三个问题:
- 琐碎的小事记了一大堆,"今天中午吃了面"也被当成重要信息存了下来;
- 用户上个月说喜欢一部剧,这个月又说看不下去了,两条记忆打架;
- "明天要考试"被原样存了进去,过了那天,这句话就成了谁也看不懂的东西。
记忆不是"存下来"就完事了。于是回头去看 Claude Code 是怎么设计记忆的。
一、没有记忆的 Agent 是一个无状态函数
没有记忆的 Agent,本质上是一个无状态函数:输入一样,输出就一样,用户每次都得把背景重新讲一遍。
Claude Code 里其实有两种"记忆",分工很清楚:
- CLAUDE.md:用户自己写的项目说明和规则,比如"用 bun 不用 npm""提交前跑一遍测试"。这些是不应该被改动的约束;
- 自动记忆:Agent 在工作过程中自己学到的东西,比如用户的偏好、踩过的坑。这些是会演化的知识。
不该变的写进 CLAUDE.md,会变的才交给记忆系统。这条边界很重要,如果把规则也交给记忆系统去"学",哪天它学歪了,规则就跟着歪了。
对应到陪伴产品里,人格设定、说话风格、安全底线这些是"CLAUDE.md",写在系统提示词里,不能被用户的聊天内容改掉;用户的喜好、经历、关系,才是记忆。
二、什么值得记
Claude Code 的记忆分成四类,是一个封闭的集合:
| 类型 | 记什么 | 例子 |
|---|---|---|
| user | 用户是谁:角色、背景、偏好 | 资深 Java 开发者,不熟悉前端 |
| feedback | 用户对 Agent 的纠正和指导 | 集成测试必须连真实数据库,不要 mock |
| project | 进行中的工作、目标、期限 | 下周三前要完成灰度发布 |
| reference | 指向外部资料的链接 | 监控面板的地址 |
其中价值最高的是 feedback。"我们用 PostgreSQL"这种信息,Agent 自己翻翻代码就知道了;但"不要用 mock 测试数据库"这种纠正,是用户用一次线上事故换来的,不记下来,下次它还会犯。
所以判断一条信息值不值得记,有两个标准:以后用得上,并且没法从别的地方查到。
我那条"今天中午吃了面",两个标准都不满足。
照着这个思路,我把陪伴场景的记忆也分了类:
- 画像:称呼、年龄段、职业、作息这类相对稳定的信息;
- 偏好:喜欢和讨厌的东西,带上时间;
- 情绪与关系:最近的状态、在意的人和事;
- 重要事件:考试、面试、生日这类有明确日期的事。
分类的好处是,提取的时候模型知道该找什么,整合的时候知道同一类的信息该合并到一起。
三、什么时候记:高频提取,低频整合
这是 Claude Code 记忆设计里我觉得最精彩的部分:高频增量提取 + 低频全局整合,两个频率互补。
1. 高频:每轮后台提取
每一轮对话结束后,Claude Code 会在后台派一个子 Agent 去提取记忆。这个子 Agent 有几个特点:
- fire-and-forget:主流程不等它,用户感知不到;
- 权限隔离:能读文件,但只能往记忆目录里写,其他工具一律不能用;
- 和主 Agent 互斥:如果主 Agent 这一轮已经主动写过记忆了,它就跳过,避免两边同时改同一个文件;
- 回合数有上限:提示词要求第一轮把可能要改的文件一次性读完,第二轮一次性写完,不要浪费回合去"调查和验证"。
注意最后一条:"不要去验证"。高频提取追求的是快和便宜,允许记错。
2. 低频:定期整合
记错了谁来纠正?交给低频的整合,Claude Code 里叫 Dream,有点像人睡觉时整理记忆。
整合不是每次都跑,要过好几道门槛,从便宜到贵依次检查:
- 总开关有没有开;
- 距离上次整合满 24 小时了没有;
- 这期间有没有至少 5 个会话发生过变化;
- 能不能拿到锁(防止多个进程同时整合)。
都满足了,才按四个阶段执行:
- Orient:先看看现在都记了些什么;
- Gather:从最近的会话记录里找新的信号;
- Consolidate:把新信息合并进已有的文件,而不是再新建一个差不多的;发现矛盾就消除矛盾;把相对日期改成绝对日期;
- Prune & Index:修剪过时的内容,更新索引。
可以看到,我遇到的后两个问题,在第 3 步里都有答案。矛盾的记忆要在整合时消解掉,"明天"要改成具体的日期。提示词里还有一句话:如果今天的发现推翻了一条旧记忆,直接去源头改掉它,而不是再追加一条新的。
简单的说,高频阶段负责"别漏",低频阶段负责"别错"。
四、怎么存,怎么用
1. 三层结构
Claude Code 的记忆存成三层:
- 原始信号:会话记录、日志,频率高、质量低;
- 结构化知识:一个个主题文件,每个文件头部用 frontmatter 标明名称、描述和类型;
- 索引:一个
MEMORY.md,每条记忆一行,指向对应的主题文件。
每次会话开始时,只把索引注入上下文。模型看到索引,觉得哪条相关,再去读对应的主题文件。
2. 索引有硬上限
索引不能无限长,Claude Code 给了一个硬上限:200 行,25KB。先按行截,再按字节截。
截断的时候会追加一条警告,提示"把细节挪到主题文件里"。下次整合时,模型看到这条警告,就会自己去精简索引。用截断来驱动整理,形成自修复,这个设计很妙。
3. 一把锁的小巧思
最后说一个工程上的小细节。整合要防止多个进程同时跑,Claude Code 用的是一个锁文件,这个文件身兼两职:
- 文件的修改时间,表示"上次整合是什么时候";
- 文件的内容,是当前持有锁的进程 PID。
拿锁的时候,看修改时间在 1 小时以内、并且这个 PID 的进程还活着,就算被占用;否则就回收这把锁,写入自己的 PID,然后读回来校验一遍,两个进程同时回收时,后写的那个赢。
一个文件同时表达了"上次什么时候整合过"和"现在谁在用",不需要任何数据库。
五、回到我们的预研
Mem0 本身已经做了不少事:从对话里提取事实,和已有的记忆比对,决定是新增、更新还是删除。照着上面的思路,我们在它的基础上补了几件事:
- 提取时按画像、偏好、情绪与关系、重要事件四类来分,不属于这四类的不记;
- 提取的提示词里明确要求把相对时间转成绝对日期;
- 加了一个定期的整合任务,专门处理矛盾和过期的记忆;
- 人格、说话风格、安全底线这类规则留在系统提示词里,不交给记忆系统。
提取部分的提示词大致是这样的:
从下面这轮对话中提取值得长期记住的用户信息。
只提取以下四类:画像、偏好、情绪与关系、重要事件。
以下内容不要提取:一次性的闲聊(比如今天吃了什么)、可以从其他地方查到的信息。
所有时间都转换成绝对日期。当前日期是 {today},
用户说"明天考试",应记为"{tomorrow} 有一场考试"。
如果新信息和已有记忆矛盾,以新信息为准,并标注变化,
例如"曾经喜欢 X,{today} 起表示不再喜欢"。
最后一条我想了挺久。用户从喜欢变成讨厌,不应该简单地把"喜欢"删掉,因为"曾经喜欢、后来不喜欢了"本身就是一条有价值的信息,聊天时说不定还能提一句。
总结
- 不该变的约束放系统提示词或 CLAUDE.md,会演化的知识才交给记忆系统;
- 值得记的信息要同时满足"以后用得上"和"没法从别处查到",用户的纠正(feedback)价值最高;
- 记忆用高频增量提取加低频全局整合,高频阶段允许记错,低频阶段负责纠正;
- 整合时合并而不是新建、消除矛盾、把相对日期改成绝对日期,推翻的旧记忆要在源头改掉;
- 索引要有硬上限,截断时提示模型去整理,形成自修复。