☰
DeerFlow长期记忆实现方案:从多轮失忆到跨会话记忆的工程实践
2026/10/7 22:57:32 网站建设 项目流程

1. 从一次多轮对话的“失忆”说起

前阵子我在做一个基于智能体的客服辅助项目,底层框架选的是字节开源的 DeerFlow。功能跑通得挺顺利,单轮问答、工具调用、流式输出都没问题,但一进入多轮场景就露馅了:用户第一轮说“我上周买的那台咖啡机漏水”,第三轮再问“那台机器保修多久”,智能体完全接不上,像换了个人。这个现象做智能体的同行应该都不陌生——长期记忆没做对。

DeerFlow 是字节跳动开源的一个深度研究类智能体框架,它把规划、检索、工具调用、报告生成串成一条流水线,社区里讨论比较多的还有 deerflow 可观测、deerflow 人机协同、基于 deerflow 智能体进行二次开发这些方向。但真正让一个智能体从“玩具”变成“能用”的,恰恰是长期记忆这一块。我翻了不少 deerflow 2.0 代码详解的文章,也自己啃了一遍源码,发现它的记忆方案设计得挺克制,没有堆一堆向量库和花哨的架构,而是用一套清晰的“短期上下文 + 长期存储 + 检索注入”的组合拳解决问题。

这篇就借一个具体示例,把 DeerFlow 的长期记忆实现方案拆开讲透。我会说清楚它为什么这么设计、核心数据结构长什么样、检索是怎么触发的、二次开发时哪些地方最容易踩坑。适合正在做智能体、准备基于 DeerFlow 做二次开发、或者单纯想搞懂“长期记忆到底怎么落地”的开发者。看完你应该能自己动手把记忆模块接进自己的项目里。

2. 长期记忆到底难在哪:先想清楚三个问题

2.1 为什么不能只靠上下文窗口

很多人第一反应是:现在模型上下文都 128K 甚至更长,把历史对话全塞进去不就行了?我一开始也这么想,实测下来问题有三个。第一是成本,每轮请求都带上几万 token 的历史,token 费用线性上涨,做商业项目根本扛不住。第二是注意力稀释,历史越长,模型对关键信息的召回越差,中间那段经常被“忽略”,这就是常说的 lost in the middle。第三是跨会话,用户今天聊完明天再来,上下文窗口早就清空了,你不可能把昨天的完整对话一直挂着。

所以长期记忆的本质不是“存更多”,而是“在对的时候取出对的那一小块”。这跟人脑很像:你不会记住跟朋友聊过的每一句话,但会记住“他养了只猫叫豆豆”,下次聊到宠物自然就想起来了。

2.2 记忆的粒度:存原文还是存摘要

这是设计长期记忆时第一个要拍板的决策。存原文的优点是信息无损,缺点是检索时噪声大、存储膨胀快;存摘要的优点是紧凑、检索精准,缺点是摘要过程本身会丢信息,而且摘要质量依赖模型。

DeerFlow 的思路是分层的:短期用原文(当前会话的 message 列表),长期用结构化提取后的记忆条目。也就是说,它不会把整段对话原封不动塞进长期库,而是经过一轮“记忆抽取”,把值得长期保留的事实、偏好、结论提炼成条目。这个取舍很关键,后面讲实现时会看到它具体怎么抽。

2.3 检索时机:什么时候该去翻记忆

第三个问题是触发时机。有的方案是每轮都检索,简单但浪费;有的是让模型自己决定要不要查记忆,灵活但不可控。DeerFlow 更偏向后者——把记忆检索做成一个可被规划器调用的能力,而不是无脑前置。这样做的好处是,闲聊类问题不会触发无谓的检索,只有涉及“之前说过”“我的偏好”“上次那个”这类信号时才去翻库。

把这三个问题想清楚,再看 DeerFlow 的实现就不会迷路。它整套方案就是围绕“分层存储、结构化抽取、按需检索”这十二个字展开的。

3. DeerFlow 记忆方案的整体架构拆解

3.1 三层结构:会话态、记忆库、检索器

我把 DeerFlow 的记忆相关代码梳理了一遍,抽象出来是三层。

第一层是会话态(Session State),也就是当前这次对话的 messages,包含用户输入、模型回复、工具调用结果。它是易失的,会话结束就没了,作用范围就是当前这一轮任务。

第二层是长期记忆库(Memory Store),通常落在向量数据库或者带向量索引的关系库里。每条记忆是一个结构化对象,包含内容、类型、时间戳、来源会话 ID、embedding 向量等字段。它是持久的,跨会话共享。

第三层是检索器(Retriever),负责把用户当前的问题转成查询,去记忆库里召回 top-k 相关条目,再拼进 prompt。它是连接前两层的桥梁。

这三层各司其职,好处是解耦。你想换向量库,只动第二层;想改检索策略,只动第三层;会话管理逻辑完全不受影响。二次开发时这一点特别重要,很多人一上来就把三层揉在一起,后面想换组件就得推倒重来。

3.2 为什么用“抽取式”而不是“全量式”

前面提到 DeerFlow 走的是抽取式路线。具体来说,它会在会话的某些节点(比如一轮任务结束、或者消息数达到阈值)触发一次记忆抽取,让模型从最近的对话里提炼出“值得记住的事”,写成结构化条目存进库。

我理解这个设计的动机有两个。一是信噪比,一段十轮的对话里真正值得长期记住的可能就一两句,全量存进去检索时全是干扰。二是可控性,抽取时可以指定类型(事实、偏好、待办等),检索时就能按类型过滤,精度高很多。

代价是抽取本身要花一次模型调用,而且抽取质量直接决定记忆质量。所以抽取的 prompt 设计是这套方案里最需要打磨的地方,后面我会给一个我实测效果不错的模板。

3.3 记忆条目的数据结构长什么样

基于常见实践,一条记忆条目大概包含这些字段:

字段类型说明
idstring唯一标识,一般用 uuid
contentstring记忆正文,一句话或一小段
memory_typeenum事实/偏好/待办/结论等
embeddingvectorcontent 的向量表示
session_idstring来源会话,便于溯源和删除
created_attimestamp创建时间,用于时间衰减
last_access_attimestamp最近被检索时间,用于热度排序
metadatajson扩展字段,比如置信度、标签

这个结构看着简单,但每个字段都有用。比如 last_access_at,你可以用它做“最近常被想起的记忆优先”,模拟人脑的激活效应。session_id 则是合规和隐私的基础,用户要求删除某次会话的数据时能精准定位。

4. 核心实现细节:记忆的写入、检索与注入

4.1 记忆写入:抽取时机与抽取 Prompt 设计

写入是长期记忆的入口,做不好后面全白搭。DeerFlow 里触发抽取的时机我总结为两类:事件驱动和阈值驱动。事件驱动是指某个任务完成、用户明确说“记住这个”时触发;阈值驱动是指当前会话消息数或 token 数超过设定值(比如 20 条消息)时触发一次。

抽取的 prompt 我改过好几版,最后稳定下来的是这个结构:

你是一个记忆抽取器。请从下面的对话中提取值得长期记住的信息。 只提取以下类型: - 事实:用户的客观信息(如职业、设备、项目背景) - 偏好:用户的喜好和习惯(如喜欢简洁回复、常用 Python) - 待办:用户明确要求后续跟进的事项 - 结论:本次对话得出的重要结论 要求: 1. 每条记忆独立成句,不超过 50 字 2. 不要提取寒暄、闲聊、临时性内容 3. 如果某条信息不确定,标注 confidence 为 low 4. 输出 JSON 数组,每项包含 content 和 memory_type 对话内容: {conversation}

这里有几个细节值得说。第一,明确列出类型,模型才知道什么该抽什么不该抽,不然它会把“你好”“谢谢”也存进去。第二,限制长度,太长的记忆检索时匹配度反而低。第三,加 confidence 字段,低置信度的记忆在检索时可以降权,避免错误信息污染。

注意:抽取用的模型不一定要跟主对话模型一样。实测下来,抽取这种结构化任务用小模型(7B 级别)就够,成本能降一个数量级,质量差距不明显。

4.2 向量化与存储:embedding 选型与去重

抽出来的记忆条目要转成向量才能检索。embedding 模型的选择上,中文场景我一般用 bge 系列或者 text-embedding 类的中文优化模型,英文为主就用通用模型。维度上 768 或 1024 都行,维度越高精度略好但存储和检索成本上升,一般 768 是性价比甜点。

存储层用向量库还是关系库加向量扩展,取决于你的规模。几千条记忆用 pgvector 完全够,几十万条以上再考虑专门的向量库。DeerFlow 本身不绑定具体存储,二次开发时这块可以自由替换。

去重是个容易被忽略的点。同一件事用户可能在不同会话里说好几遍,如果每次都存一条,检索时全是重复内容。我的做法是写入前先做一次相似度查询,如果跟已有记忆的余弦相似度超过 0.92,就更新那条记忆的 last_access_at 而不是新增。这个阈值可以调,0.9 到 0.95 之间比较稳。

4.3 检索触发:让规划器决定要不要翻记忆

前面说过 DeerFlow 偏向按需检索。具体实现上,它把“检索长期记忆”包装成一个工具,规划器在拆解任务时如果判断需要历史信息,就调用这个工具。

判断信号我总结了几类:出现“之前”“上次”“我的”“还记得”这类指代词;问题涉及用户个人背景(“我适合用什么”);任务需要延续之前的结论(“接着上次那个方案”)。规划器的 prompt 里可以显式引导这些信号。

这样做的好处是闲聊不触发检索,省成本也省延迟。坏处是可能漏检,用户明明需要历史信息但规划器没识别出来。折中方案是加一个兜底:如果模型回复里出现“我不确定你之前是否提过”这类表述,就强制触发一次检索。

4.4 注入策略:怎么把记忆塞进 Prompt 不喧宾夺主

检索回来的记忆怎么用,也有讲究。直接拼在 system prompt 里是最简单的,但要注意位置和格式。我一般放在 system prompt 的靠后位置,用明确的分隔标记包起来:

以下是与当前问题相关的历史记忆,供参考: <memory> - [事实] 用户使用 Python 做数据分析 - [偏好] 用户喜欢简洁的代码示例 </memory>

这样模型能清楚区分“记忆”和“指令”。数量上一般召回 top-3 到 top-5 就够,太多反而干扰。如果召回的条目相关性分数都很低(比如低于 0.6),宁可不注入,避免误导。

5. 一个完整示例:从对话到记忆再回到对话

5.1 场景设定与初始对话

光讲原理太干,我搭一个最小可跑的示例。假设用户在做一个智能家居项目,分两次会话。

第一次会话:

用户:我在用 ESP32 做一个智能浇花系统,土壤湿度传感器读出来的值波动很大。 助手:波动大可能是电源噪声或者采样间隔太短,建议加电容滤波并做多次采样取平均。 用户:好的,我用的是电容式传感器,采样间隔设成 1 秒。 助手:1 秒可以,配合滑动平均滤波效果更好。

这段对话结束后,触发记忆抽取。

5.2 记忆抽取的实际输出

按前面的 prompt,模型抽出来的结果大概是这样:

[ {"content": "用户在用 ESP32 做智能浇花系统", "memory_type": "事实", "confidence": "high"}, {"content": "用户使用电容式土壤湿度传感器", "memory_type": "事实", "confidence": "high"}, {"content": "用户将采样间隔设为 1 秒", "memory_type": "事实", "confidence": "medium"} ]

注意“土壤湿度值波动大”这条没被抽出来,因为它是临时性问题,已经解决了,不值得长期记。这就是抽取式的好处——自动过滤掉过程性信息。

5.3 第二次会话的检索与注入

三天后第二次会话:

用户:我那套浇花系统现在想加个远程查看功能,用什么方案好?

规划器识别到“我那套浇花系统”是回指,触发记忆检索。查询向量化后召回上面三条记忆,注入 prompt。模型于是知道用户用的是 ESP32、电容式传感器,推荐方案时就会考虑这些约束,比如建议用 ESP32 自带的 WiFi 而不是额外加模块。

如果没有长期记忆,模型只能给一个泛泛的“你可以用 WiFi 模块或者 4G 模块”,体验差一大截。这个对比就是长期记忆价值的直接体现。

5.4 关键参数与阈值实测记录

我把这个示例跑了几十轮,记录了几个关键参数的手感:

参数取值说明
抽取触发消息数15-20 条太少抽取频繁,太多信息混杂
去重相似度阈值0.92低于此值新增,高于则更新
检索 top-k3-5超过 5 条噪声明显上升
相关性过滤阈值0.6低于此值不注入
记忆条目最大长度50 字过长检索匹配度下降

这些值不是金科玉律,但作为起点能省不少调参时间。不同领域要微调,比如法律、医疗这类对事实精度要求高的场景,去重阈值可以提到 0.95,避免把相似但不同的信息合并。

6. 二次开发与可观测:把记忆模块接进自己的项目

6.1 基于 DeerFlow 二次开发的接入点

社区里问得最多的就是“基于 deerflow 智能体进行二次开发”怎么下手。记忆这块的接入点其实很清晰:实现一个 MemoryStore 接口(负责增删查)、一个 Extractor(负责抽取)、一个 Retriever(负责召回),然后在智能体的规划流程里注册检索工具。

我建议先把接口定死,再选实现。比如 MemoryStore 就三个方法:add(entry)、search(query, top_k)、delete(session_id)。这样你后面从 pgvector 换到别的库,业务代码一行不用改。

6.2 可观测性:记忆命中了没有,怎么知道

deerflow 可观测是另一个热词,记忆模块尤其需要。你至少要在日志里记录:本轮是否触发检索、召回了哪几条、相关性分数多少、是否被注入 prompt。没有这些,出了问题你根本不知道是没检索到还是检索到了没用上。

我一般会打这样一条结构化日志:

logger.info("memory_retrieval", extra={ "session_id": sid, "query": query, "hit_count": len(hits), "top_score": hits[0].score if hits else 0, "injected": injected })

有了这些数据,你就能算出记忆命中率、平均相关性分数这些指标,判断记忆模块到底有没有在干活。

6.3 流式场景下的记忆处理

如果智能体走的是 SSE 流式输出(封装 sse 流式接口调用逻辑、完成流式消息解析与处理是常见需求),记忆检索要放在流开始之前,不能边流边检索。因为检索结果要拼进 prompt,而 prompt 必须在请求发出前就确定。所以流程是:收到用户消息 → 判断是否检索 → 检索并拼 prompt → 发起流式请求 → 逐块解析输出。这个顺序不能乱。

7. 常见问题与排查技巧实录

7.1 记忆检索不到,先查这四个地方

检索不到是最常见的问题,我按排查顺序列一下。第一,查 embedding 是否用了同一个模型,写入和检索用不同模型是新手最常犯的错,向量空间都不一致,当然搜不到。第二,查查询文本是否太短或太泛,“那个东西”这种查询向量化后没有区分度。第三,查 top-k 和阈值,可能召回了但被阈值过滤了。第四,查记忆库是否真的写进去了,抽取失败或者写入报错都会导致空库。

7.2 记忆污染:错误信息被反复召回

比检索不到更麻烦的是检索到错的。用户随口说的一句“我可能下周去北京”,被抽成“用户下周去北京”存进库,之后每次聊到行程都被召回。解决办法是给抽取加 confidence,低置信度的记忆在检索时降权或者干脆不注入。另外可以加一个“记忆确认”机制,重要的记忆让用户确认一次再入库。

7.3 性能与成本:别让记忆拖慢响应

记忆检索会引入额外延迟,向量检索一般几十毫秒,但如果库很大或者没建好索引,可能到几百毫秒。优化手段有:给向量字段建 HNSW 或 IVF 索引;把检索和主请求并行(如果架构允许);对检索结果做缓存,相同查询短时间内直接返回缓存。成本上,抽取的模型调用是主要开销,用小模型加批量抽取能压下来。

7.4 常见问题速查表

现象可能原因排查方向
检索结果为空embedding 模型不一致核对写入和检索的模型
召回内容不相关查询太泛或阈值太低优化查询改写,提高阈值
重复记忆多去重没做或阈值太高加相似度去重,阈值 0.92
响应变慢向量索引缺失建 HNSW 索引
记忆不更新只新增不更新加 last_access_at 更新逻辑
抽取质量差prompt 不明确明确类型和长度限制

提示:调试记忆模块时,先把检索结果打印出来人工看,别急着调参数。很多时候问题不在参数,而在抽取出来的内容本身就不对。

8. 我在实际项目里踩过的几个坑

最后分享几个真实踩过的坑,都是文档里不会写的。

第一个坑是时间戳的时区。记忆条目存 UTC,展示时忘了转本地时间,导致“上周”这种相对时间算错,检索时按时间过滤就出问题。统一用 UTC 存、展示层转,这个规矩要一开始就定。

第二个坑是会话 ID 的复用。我一开始用用户 ID 当 session_id,结果同一个用户不同设备的对话混在一起,记忆互相污染。session_id 应该是“一次连续对话”的标识,跟用户 ID 分开,用户 ID 放在 metadata 里。

第三个坑是删除逻辑。用户要求删除数据时,只删了会话表忘了删记忆库,合规上就出问题了。记忆库的删除要跟会话删除绑定,最好做成级联。

第四个坑是抽取频率。我一开始每轮都抽,结果模型调用费用翻倍,而且抽出来一堆碎片。改成任务结束或消息数达标才抽,质量和成本都好了。

这套记忆方案我用了大半年,从最初的多轮失忆到现在能稳定跨会话记住用户背景,中间就是不断调抽取 prompt、调阈值、补可观测。它不复杂,但每个环节都得抠细节。你要是也在做智能体的长期记忆,建议先从最小闭环跑通——能存、能搜、能注入,再逐步加去重、加置信度、加可观测。别一上来就追求完美架构,跑起来比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询