
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唯一标识一般用 uuidcontentstring记忆正文一句话或一小段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、调阈值、补可观测。它不复杂但每个环节都得抠细节。你要是也在做智能体的长期记忆建议先从最小闭环跑通——能存、能搜、能注入再逐步加去重、加置信度、加可观测。别一上来就追求完美架构跑起来比什么都重要。