为什么 AI 总是"失忆"?
你有没有遇到过这种情况:跟 AI 聊了半天,换了个新对话窗口,它就把你忘得一干二净。
你得重新告诉它你是做什么的、你喜欢什么风格、你上次让它改的那个 bug 修好了没有。每次开新对话都像第一次见面,非常累。
这不是 AI 笨,而是大语言模型天生就没有"记忆"------每次对话对它来说都是全新的,上一次聊了什么,它完全不知道。
DeerFlow 的记忆系统就是为了解决这个问题。它做的事情用一句话概括:每次聊完自动记笔记,下次聊的时候自动把笔记递给 AI 看。
整体思路:记笔记 + 看笔记
想象你有一个很靠谱的秘书。每次你和客户开完会,秘书会帮你整理一份会议纪要,存进档案柜。下次你再见同一个客户之前,秘书会把之前的纪要放在你桌上,让你快速回忆起之前的上下文。
DeerFlow 的记忆系统就是这个秘书。整个流程分四步:
1. 你和 AI 聊完了
2. 系统在后台悄悄把对话内容做一次"结构化反思"------提取关键信息
3. 提取出来的信息存到一个 JSON 文件里
4. 下次你开新对话,系统把之前提取的信息塞进 AI 的上下文里
就这样,AI 在跟你聊天之前就已经"知道"你是谁了。
捕获:聊完之后悄悄记下来
什么时候记?
每次 AI 回复完你之后,系统会自动触发一次"记忆捕获"。这个过程你完全感知不到,不会影响回复速度。
记什么?
原始对话里有很多"噪音"------工具调用的 JSON、文件上传的记录、AI 的中间推理步骤------这些东西对理解"用户是谁"没有帮助,反而会干扰提取。所以第一步是过滤:只保留你说了什么、AI 最后回答了什么。
过滤之后,系统还会扫描你有没有说出以下两类话:
"你搞错了"------比如你说"不对"、"错了"、"重新来"。系统会标记这是一个"纠正信号",后续提取记忆时会特别注意识别哪些旧信息是错的。
"就是这个"------比如你说"很好"、"就是这样"、"perfect"。系统会标记这是一个"正向强化",说明 AI 之前的理解是对的,值得记住。
一个容易踩的坑
你可能会问:为什么不在 AI 处理的过程中记,而要等到处理完之后?
因为记忆更新不是同步的------它会在后台排队等一会儿再处理。而 Python 的线程之间是互相隔离的,如果等到后台线程处理时才去获取"这是哪个用户",就会拿到空值,导致张三的记忆混进李四的档案里。
所以系统在入队的那一刻就把用户 ID 快照下来,绑在这批消息上,后面不管什么时候处理都不会搞混。
防抖:别每句话都记一次
你和 AI 聊天时经常会连续发好几条消息:"帮我写个接口"、"要支持分页"、"返回格式用 JSON"。如果每条消息都触发一次记忆提取,一来浪费钱------每次调用 LLM 都要花 token 费,二来质量也不好------单看一条消息,LLM 很难判断你到底在做什么。
所以系统用了防抖机制:你发第一条消息时启动一个 30 秒倒计时,如果 30 秒内又有新消息,倒计时重新开始。直到你停下来 30 秒没说话,系统才把这段时间的所有消息打包发给 LLM 做一次提取。
这就好比你不会每说一句话就让秘书记一次笔记,而是等一段话说完了再统一整理。
长对话的保护机制
DeerFlow 有一个上下文摘要功能:当对话太长、超出模型的上下文窗口时,会把早期的消息压缩成摘要,腾出空间。
这带来一个问题:被压缩掉的消息如果还没来得及提取记忆,就永远丢失了。
所以系统加了一个保护钩子:在摘要压缩即将发生的那一刻,先把即将被丢弃的消息紧急塞进记忆队列,确保不会遗漏。这就像秘书发现你要扔掉一叠旧文件,赶紧先翻一遍看看有没有重要的东西。
提取:让 AI 帮你"结构化反思"
防抖时间到了之后,系统调用 LLM 来做记忆提取。这里的设计很有讲究------不是简单地让 AI "总结一下这段对话",而是要求它做一次结构化反思。
反思什么?
LLM 拿到对话内容后,需要回答三个问题:
- 有没有犯错? 对话中 AI 的回答有没有出错?用户有没有纠正它?
- 用户是什么样的人? 用户的技术背景、偏好、工作习惯是什么?
- 用户在做什么? 当前的目标、关注点、项目上下文是什么?
提取出来的记忆长什么样?
LLM 会输出一个 JSON,里面包含两类内容:
摘要段落------对用户画像和历史背景的自然语言描述。比如"后端开发工程师,主要用 Python 和 FastAPI,偏好简洁的代码风格"。这些摘要分六个维度:工作背景、个人偏好、当前关注、近期历史、早期历史、长期背景。
结构化事实------一条一条的独立信息,每条都有分类和置信度评分。比如:
- "偏好 TypeScript + Next.js 做前端"------分类是 preference,置信度 0.92
- "使用 uv 作为包管理器"------分类是 knowledge,置信度 0.95
- "之前给的数据库方案有问题"------分类是 correction,置信度 0.95
置信度是 LLM 自己判断的:用户明确说了的给 0.9 以上,明显暗示的给 0.7 到 0.8,推断出来的给 0.5 到 0.6。后续注入时,置信度高的事实优先展示。
合并:新旧记忆怎么融合?
LLM 返回结果后,系统需要把它合并到已有的记忆里。这个过程有几个规则:
- 摘要段落直接覆盖------新的描述替换旧的
- 新事实要和旧事实去重------如果已经有了"喜欢 TypeScript",就不会再存一条一模一样的
- 置信度低于 0.7 的事实丢弃------不够确定的信息不记
- 事实总数超过 100 条时,淘汰置信度最低的------给新记忆腾空间
- 被用户纠正的旧事实删除------如果 LLM 判断"之前记的方案 X 是错的",就把那条删掉
- 关于文件上传的句子剥离------上传文件是临时的,不应该记住
存储:一个人一个档案柜
记忆数据存在 JSON 文件里,每个用户一个独立的文件,物理上完全隔离。
users/
├── alice/
│ └── memory.json ← Alice 的记忆
├── bob/
│ └── memory.json ← Bob 的记忆
└── default/
└── memory.json ← 未登录用户
为什么用文件而不是数据库?
因为数据量很小------一个用户通常也就几十条 fact,一个 JSON 文件完全够用。文件的好处是简单、没有额外依赖、出了问题直接打开文件就能看、备份就是复制文件。
如果将来数据量大了需要换数据库,也只需要替换存储层的实现,上层逻辑完全不用改------因为存储层用了抽象接口设计。
写入安全:原子操作
写文件时有一个风险:如果写到一半进程崩溃了,文件就会损坏,之前存的记忆全没了。
系统的解决方案是原子写入:先写到一个临时文件里,写完了再一步到位替换掉原文件。在 Linux 和 macOS 上,文件替换是原子操作------要么完全成功,要么完全不发生,不存在"写了一半"的中间状态。
读取加速:内存缓存
每次读记忆都去磁盘太慢了。系统维护了一个内存缓存,读取时先检查文件有没有被改过------通过文件的修改时间判断。没改过就直接用缓存,改过了才重新读文件。
注入:怎么让 AI "看到"记忆
记忆存好了,下一步是在 AI 聊天之前把它"递"给 AI 看。
为什么不直接写进 system prompt?
你可能会想:把记忆写进 system prompt 不就行了?这样每轮对话 AI 都能看到。
问题是:system prompt 是 LLM 处理的第一段文本,所有对话共享。如果把记忆写进去,每个用户的 system prompt 都不一样,LLM 的 prefix cache 就没法复用------这会导致每次请求都要重新处理 system prompt,既慢又贵。
所以系统把记忆放在了 HumanMessage 里------system prompt 保持不变,记忆作为一条独立的隐藏消息插进对话历史。这条消息标记了"前端不显示",你看不到它,但 AI 能看到。
什么时候注入?
- 第一轮对话:注入完整的记忆内容,包括用户画像、历史背景、所有事实
- 同一天的后续轮次:不注入,因为记忆已经在历史里了
- 跨天对话:如果对话跨越了午夜,额外注入一条日期更新提醒,让 AI 知道"今天"变了
ID-Swap:一个精巧的优化
首轮注入时用了一个巧妙的技术:注入的隐藏消息替换了你发的第一条消息的位置,你的原始内容被挪到了后面。
为什么要这样做?因为所有用户的对话都以相同的记忆内容开头,LLM 处理时这部分可以命中 prefix cache,不用每次都重新算。就像你翻开一本书,前几页是目录------不管谁来读,目录都一样,可以直接跳过。
这个优化能把 token 消耗和响应延迟都降下来。
Token 预算:不能塞太多
记忆内容有 token 预算,默认 2000 个 token。系统会优先塞摘要段落,剩下的空间按置信度从高到低塞事实。预算满了就不再塞了------宁可少记几条,也不能把上下文窗口撑爆。
格式化时用 tiktoken 精确计算 token 数,不会超预算。
配置和 API
配置项
yaml
memory:
enabled: true # 总开关,关掉就完全不用记忆
debounce_seconds: 30 # 防抖时间,等多久再提取
model_name: null # 用哪个 LLM 做提取,null 就用默认的
max_facts: 100 # 最多存多少条事实
fact_confidence_threshold: 0.7 # 低于这个置信度的事实不存
injection_enabled: true # 是否注入到对话里
max_injection_tokens: 2000 # 注入的 token 预算
REST API
系统提供了完整的 API,可以手动查看、编辑、导入导出记忆:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/memory |
查看当前用户的所有记忆 |
| DELETE | /api/memory |
清空记忆 |
| POST | /api/memory/facts |
手动添加一条事实 |
| PATCH | /api/memory/facts/{id} |
修改一条事实 |
| DELETE | /api/memory/facts/{id} |
删除一条事实 |
| GET | /api/memory/export |
导出为 JSON 文件 |
| POST | /api/memory/import |
从 JSON 文件导入 |
所有操作都限定在当前用户范围内,不会看到别人的数据。
Q A
为什么异步而不是实时? LLM 调用要 1 到 3 秒,如果同步等待,你每次发消息都会多等这么久。放到后台异步处理,你完全感觉不到。
为什么用 HumanMessage 而不是 system prompt? 为了 prefix cache。所有用户共享同一个 system prompt,cache 命中率高。如果把记忆塞进去,每个用户都不同,cache 就废了。
为什么防抖而不是每条消息都处理? 成本和质量的双重考量。一次对话里的多条消息通常高度相关,打包处理比逐条处理更省钱,LLM 看到更完整的上下文提取质量也更好。
为什么需要保护钩子? 长对话会被压缩摘要,压缩掉的消息如果还没提取记忆就永远丢了。这个钩子确保"即将丢失"的消息被及时捕获。
为什么用 JSON 文件而不是数据库? 数据量小、简单可靠、无需额外依赖。万一出问题,直接打开文件就能看到所有记忆内容,排查成本极低。
总结
DeerFlow 的记忆系统做的事情,本质上和人类的记忆是一样的:
- 听------捕获对话,过滤噪音,识别重要信号
- 想------LLM 做结构化反思,提取关键信息
- 记------存到文件里,一个人一个档案柜
- 忆------下次聊天之前把笔记递给 AI 看
整个过程对你完全透明。你不需要做任何事情,系统在后台默默运转,随着对话越来越多,AI 对你的理解也会越来越深。