我第一次把 DeerFlow 跑起来的时候,其实有点不以为然——不就是把大模型的对话流程做成 DAG 图吗,编排节点、串接工具、再加上流式输出,这类框架这两年见得多了。真正让我停下来多看了一会儿的,是里面“长期记忆”那一块的设计。它不只是把聊天记录扔进向量库里就算完事,而是把记忆当成了一套有写入策略、有检索机制、有衰减淘汰规则的独立子系统。
这篇文章就从我自己的理解出发,用一个最小可运行的示例,拆一下字节开源项目 DeerFlow 的长期记忆实现思路。你会发现,长期记忆这件事想要做好,真正麻烦的地方根本不在“存得下”,而在“记得准、忘得对、用得上”。这篇文章适合正在做智能体开发、想给自己 Agent 加上跨会话记忆能力的开发者,也适合对 AI 应用架构感兴趣的人。
1. 先搞清楚:智能体的长期记忆到底解决什么问题
我们先把话说明白。一个没有记忆的智能体,本质上就是一个“每次都被清零重来的对话接口”。你上轮告诉它“我是后端团队的,这边数据库统一用 PostgreSQL,所有 schema 变更都要走评审流程”,过两天你再问它“我改个表结构要注意什么”,它一脸茫然。这不是模型笨,而是状态没有被持久化。
长期记忆要解决的,就是让智能体在跨越多个会话、多个时间点之后,仍然能保持对用户背景、偏好、历史决策的连续理解。这在真实业务里太重要了。我做过的几个智能体项目里,用户最大的抱怨往往不是“模型回答不准”,而是“它老是不记得我说过什么”。尤其是 To B 场景,用户的岗位角色、技术栈、组织流程这些信息,一旦每轮都要重新交代,体验就彻底崩了。
DeerFlow 的长期记忆方案,给我的感觉是它在“记忆”这件事上想了三个层次的问题:
第一,记忆怎么写。不是所有对话都值得长期保存,必须要有筛选和提炼,否则存得越多,噪音越大,检索到的质量就越差。第二,记忆怎么读。不是每次回复都要把所有历史捞出来,要在合适的时机、合适的粒度下召回相关的片段。第三,记忆怎么更新。用户的偏好是会变的,去年的技术栈今年可能就换了,旧记忆必须能被覆盖和纠正。
这三点看着简单,真正落地的时候,每一层都有坑。DeerFlow 的思路是把它们拆成记忆节点,编排进 Agent DAG 里,而不是让模型自己瞎记。这个设计上的克制,我觉得是它最值得学习的地方。
2. 长期记忆实现的核心设计拆解
在 DeerFlow 的工作流里,长期记忆不是一个统一的大黑盒,我更愿意把它拆成三条逻辑链来看:存储层、写入链路、读取链路。
2.1 存储层:记忆数据是怎么组织的
先说存储。DeerFlow 风格的记忆存储不会把所有内容混在一个桶里。我见过很多自研 Agent 项目,把历史对话一股脑塞进向量库,然后每次查询直接 top-k,这种做法初期能用,等到数据量上来就越来越钝。原因很简单:向量检索适合找“语义相似的片段”,不适合找“结构性的事实”。
DeerFlow 给我的启发是,记忆数据要按类型分层:
- 短期会话上下文,保存在对话工作流内部,会话结束就清理。
- 场景记忆,记录类似“用户上次在调试支付回调”这种和当前任务相关的中间状态,保留几周或几个月。
- 长期用户画像,记录“用户是后端开发、偏好 PostgreSQL、变更需要评审”这类稳定的事实,生命周期很长。
对长期用户画像这类数据,我用过两种方案来落存储:一种是用关系型数据库或 KV 存结构化的“属性槽”,比如userId + attribute + value + updatedAt;另一种是用向量库存语义化的记忆片段,按用户 ID 做命名空间隔离。DeerFlow 的做法更偏后者,同时会给记忆条目带上 namespace、时间戳、来源事件 ID 这类元数据。这非常关键,没有元数据的记忆库,后续做过期策略和冲突更新的时候你会很痛苦。
| 存储类型 | 适合什么 | 典型实现 | 生命周期 |
|---|---|---|---|
| 内存缓存 | 当前对话轮次的中间状态 | Redis / 进程内 Map | 分钟级 |
| KV 存储 | 用户属性、偏好槽位 | Redis / MySQL | 月级或年级 |
| 向量库 | 语义化的经验片段、历史结论 | Chroma / Milvus / pgvector | 周级或年级 |
| 图存储 | 实体之间的关系记忆 | Neo4j / NebulaGraph | 长期 |
2.2 写入链路:不是所有对话都值得记住
这是我要重点讲的部分。我见过最典型的翻车做法,是把用户每一轮说的话都原封不动地写入记忆库。结果是什么?召回的时候什么都是“相关”的,又什么都不可靠。用户随口一句“今天天气不错”,也会和“他在考虑迁移数据库”这种重要信息混在一起。
DeerFlow 给我的感觉是,它在记忆写入前加了一个“提炼”动作。说白了,就是在工作流里单独放一个节点,用 LLM 把当前对话中值得长期保留的信息抽出来,格式化成结构化的记忆条目。比如用户说了三句闲聊、一句关键决定,记忆写入节点就只抽出那一句关键决定,并打上意图标签和置信度。
我自己常用的做法是让提炼节点输出这样的格式:
{ "fact": "用户所在团队统一使用 PostgreSQL", "type": "preference", "importance": 0.9, "expire_at": "2026-01-01T00:00:00Z", "source": "session_1243" }这里最重要的两个字段是type和importance。type决定这条记忆会被放到哪一类记忆池,importance决定后面做压缩淘汰时,谁先被挤出局。DeerFlow 这类框架虽然不同版本内部实现不太一样,但把“记忆写入”当成一个独立可编排的节点,这个思路是共通的。
2.3 读取链路:记忆召回要和当前问题强绑定
写入做完了,读取才是决定体验的胜负手。很多团队把读取做成“每次请求都历史全量召回”,这其实很蠢。第一浪费 token,长上下文一旦超过模型窗口,性能反而下降。第二是干扰模型判断,不相关的老黄历会带偏回答。
我会在读取链路里做三步:
第一步是意图判断。先让一个轻量节点判断当前这轮问题需不需要长期记忆。如果用户就是随口问一句“你好”,完全没必要查库。
第二步是查询改写。把当前问题改写成更适合检索的形式。比如用户问“我改个表要注意什么”,如果用户画像里有“PostgreSQL”这个事实,那改写后的查询可能就不只是“改表注意事项”,而是“PostgreSQL 改表注意事项 + 团队流程约束”。
第三步是阈值过滤和重排。向量检索出来的 top-k 并不直接可用,还要过滤掉相似度低于阈值的条目,再按重要度和时间衰减重新排序。这里有个参数值得关注:时间衰减。一年前的一条记忆,即使语义相关,优先级也要降一降,除非它是“团队统一使用 PostgreSQL”这种不会过期的硬约束。
读取链路做好了,你会明显感觉到智能体像是“记得你”的,而不是在靠系统提示词里塞一堆历史记录硬撑。
3. 一个可运行的示例:跨会话记住用户偏好
这里我写一个简化但完整的示例,带你走一遍 DeerFlow 式的长期记忆工作流落地流程。场景是做一个“后端项目小助手”,要求它能跨会话记住用户的技术栈偏好和团队约束。
3.1 工作流整体结构
我在 DeerFlow 里会把工作流设计成这样的 DAG,节点之间有明确的输入输出,不搞隐式共享变量:
input_user_message | v memory_recall_node | v agent_reason_node | v output_stream_node | v memory_write_node有人会问,为什么把memory_write_node放在最后而不是在回复之前?因为你要写入的“事实”,往往要在模型完成推理之后才能确认价值。比如模型通过推理判断出用户那句话确实是一个新的长期偏好,再写库。当然你也可以并行去写,但我建议先用一个独立的写节点,方便做失败重试和日志追踪。
3.2 流程一:用户首次说出关键偏好
假设用户说:“我这边后端主要用 PostgreSQL,以后凡是涉及数据库迁移的改动,都得先在群里同步一下。”
DeerFlow DAG 跑起来大概是这么个过程。
memory_recall_node第一阶段检索,因为这是新用户,记忆库为空,所以没有召回任何条目,返回一个空的上下文片段。此时工作流不会报错,它会把“无召回”当成一个合法状态,只是提示信息减少。
然后agent_reason_node拿到原始用户消息,正常生成回复,可以回应说“明白了,后续涉及 PostgreSQL 迁移的事项,会先提示你走群内同步流程”。
关键动作发生在最后。memory_write_node先做提炼,把上面两段信息拆成两条记忆:
- 事实一:用户后端技术栈是 PostgreSQL,类型为
long_term_attribute,重要度 0.9。 - 事实二:数据库迁移改动需要先在群内同步,类型为
workflow_rule,重要度 1.0。
两条记忆写入向量库,同时更新结构化用户画像表里的db_tech_stack字段。注意,这里我不仅写向量库,还写结构化槽位。当模型需要精确的偏好值时,结构化槽位比向量召回稳定得多。DeerFlow 方案在我理解里也支持这种多模态的记忆写入,所以别只盯着向量库。
3.3 流程二:几天后用户追问数据库迁移问题
过了几天,用户带着几乎完全不同的语气回来问:“我刚看了一下当前库里的分区表,有点想改成分区策略,但怕影响线上,我该注意什么?”
如果是一个无记忆的 Agent,它只能根据这句话进行泛泛回答。但在 DeeerFlow 式的工作流里,memory_recall_node会先做查询改写,生成类似“PostgreSQL 分区表变更注意事项 + 是否涉及迁移 + 团队同步要求”的检索条件。然后从长期记忆库里召回两条高相关条目:
- “用户后端技术栈是 PostgreSQL”
- “数据库迁移改动需要先在群内同步”
这两条并不是以完整聊天记录的形式存在于记忆库,而是经过提炼的独立事实。模型在生成回答时,会被明确告知这两条来自长期记忆,并且带上置信度和时间信息。这样生成出来的回答就会变成:
“你这个分区调整本质上属于一次 DDL 变更,从你之前数据库统一用 PostgreSQL 的背景来看,建议优先用原生分区表而不是中间表切换,同时因为你们内部有‘迁移先同步群’的规则,建议操作前先在群内发个变更计划。”
看到区别了吗?它回答得不再是通用方案,而是贴着你的上下文和流程约束来回答。我认为这是长期记忆最大的价值:让大模型从“万金油”变成“懂行的人”。
3.4 流程三:用户偏好发生变化时怎么覆盖旧记忆
长期记忆还有一个绕不开的场景:用户改主意了。还是同一个用户,过了半年说:“我们现在全面切到 MySQL 了,后面新需求不要用 PostgreSQL 的技术方案了。”
如果记忆系统只能追加不能覆盖,那模型就会同时看到“用户用 PostgreSQL”和“用户用 MySQL”两条矛盾记忆,轻则回答摇摆,重则直接把模型搞懵。DeerFlow 风格的实现里,记忆更新一般不是直接删除旧条,而是采取“版本化”策略。
我在落地时是这样做的:
- 新条目写入时带一个
supersedes字段,指向旧条目的 ID。 - 旧条目不立即物理删除,而是把状态标记为
archived或superseded。 - 检索时默认过滤掉被新条目覆盖的旧条目,但如果用户显式问“我以前是不是说过我用 PostgreSQL”,工作流可以通过
include_archived=true把旧状态也翻出来回答。
这样做的好处是既保持了记忆的时效性,又保留了追溯历史的能力。数据团队做审计或者排查模型“为什么这么回答”的时候,不会因为记忆被物理删除而抓瞎。
4. 长期的实战配置与参数经验
如果说上面还是架构层面的思路,那这一节就是我实际调参过程中沉淀下来的实用经验了。DeerFlow 本身在持续迭代,具体配置项每个版本可能不太一样,但底层那些“长期记忆好不好用”的关键参数,在各家实现里都是相通的。
4.1 向量检索的相似度阈值不要固定死
很多同学会在配置里写死一个 top_k = 5 或者 threshold = 0.7。我踩过几次坑之后,现在会这么调整:
- 把 top_k 故意调大一点,比如先召回 20 条。
- 用重排序步骤把相关度最高的 3 到 5 条选出来。重排序可以用一个小的交叉编码器模型,也可以在 DeerFlow 里单独挂一个 LLM 节点做 rerank。
- 如果某条记忆本身带有高重要度,比如
importance=1.0的团队硬约束,即使向量相似度略低于阈值,也应该强制带上。
你看,这就是为什么记忆条目必须要有元数据。没有元数据,你就只能用相似度一个维度做取舍,维度太单薄了。
4.2 时间衰减和生命周期要按记忆类型分开配置
我给自己的系统配了两套衰减策略。对用户偏好类的长期画像,几乎不衰减,除非被新条目显式覆盖。对场景类的临时记忆,比如“用户本周在排查支付回调问题”,我会设一个较短的过期时间,一到两周后逐渐降低权重,一个月后进入可清理状态。
这里给一个参考配置:
| 记忆类型 | 初始化重要度 | 时间衰减半衰期 | 过期策略 |
|---|---|---|---|
| 用户画像偏好 | 0.9 | 不衰减 | 被新条目覆盖 |
| 团队流程规则 | 1.0 | 不衰减 | 被新条目标记覆盖 |
| 项目临时状态 | 0.5~0.7 | 7 天 | 30 天归档 |
| 对话经验片段 | 0.4~0.6 | 14 天 | 90 天归档 |
按类型区分衰减,而不只是按时间戳一刀切,这样长期记忆才会更接近人类记忆的规律。
4.3 记忆召回和 Prompt 拼接的细节
即使长期记忆工作流已经跑通,召回结果往 Prompt 里拼接的时候,我仍然建议用一个清晰的 XML 式结构把它框起来,而不是直接和用户消息混在一堆。我在 DeerFlow 的 agent 节点里实测下来,下面这种格式最不容易让模型混淆:
<long_term_memory> <item source="..."> <fact>用户后端技术栈是 PostgreSQL</fact> <captured_at>2025-11-01</captured_at> </item> <item source="..."> <fact>数据库迁移改动需要群内同步</fact> <captured_at>2025-11-02</captured_at> </item> </long_term_memory>在 Prompt 里明确告诉模型:这段内容来自长期记忆,可能是早期记录,必要时要能意识到它已过期。别小看这句话,我见过太多次模型把记忆里的旧事实当成当前硬约束,回答完才发现用户早就换了技术栈。
5. 人机协同、流式输出与可观测性
标题开头那个热词列表里出现了“deerflow 人机协同”和“可观测”这几个词,长期记忆和它们并不是割裂的,正好借这一节串一下。
5.1 记忆流提示与人机协同的配合
在人机协同场景里,长期记忆不只是给模型看的,也要给“人”看。我的做法是,在回复内容之外,额外通过一个 debug 通道往前端推送一条“记忆召回摘要”,告诉用户或开发者:“这次回答引用了你三天前说的 PostgreSQL 偏好。”这种透明的记忆展示,能极大降低用户对“AI 怎么知道得这么清楚”的困惑,也让人工审核人员能快速确认记忆召回是否正确。
DeerFlow 支持把多个节点编排成一个 DAG,在记忆召回和模型回复之间,我建议保留一个可配置开关:如果用户处于“专家模式”,可以让它直接看到原始召回条目和置信度;如果用户只需要结果,就只展示一句“已自动参考历史上下文”。
5.2 结合 SSE 流式接口做记忆写入的时机控制
很多 Agent 都采用 SSE 流式接口交互,DeerFlow 里也逃不开“封装 SSE 流式接口调用逻辑,完成流式消息解析”这件事。在长期记忆这里有一个容易被忽略的点:记忆写入一定要等流式回答结束之后触发,或者至少等到模型端出了done事件再触发。
我之前图省事,想一边流式输出一边做记忆写入,结果经常出现模型生成到最后一步突然自我纠正的情况,而前面的“半成品”已经被写进记忆库了,造成脏数据。现在改为:先把流式输出实时推给前端,但记忆提炼节点只监听流结束事件,再把完整回答交给提炼逻辑去抽取事实。这样虽然多了一次等待,但记忆质量高很多。
5.3 长期记忆的可观测性怎么落地
“可观测”这个词在长期记忆里尤其要落地。AI Agent 的记忆出问题,不像普通服务报错那么直接。你说不清是“没写入”“写错了”还是“召回了不该召回的”。我给的排查链路是:
在记忆写入节点记录memory_written_total指标,标注类型和来源。在记忆召回节点记录memory_recalled_count指标和平均相似度。再把每次写入前后的记忆快照作为 trace span 关联到会话 ID 上。这样一旦用户反馈“它怎么不记得我说过 XXX”,我直接查这个会话 ID 关联的 trace,能看到当时记忆库里到底有没有这条数据、召回时阈值够不够、Prompt 里到底拼没拼进去。
操作上,我会把长期记忆工作流单独抽成一个中间件,并对接 OpenTelemetry。DeerFlow 这类支持 DAG 编排的框架,天然能让每个节点输出 trace span,颗粒度比传统单体对话服务好太多了。
6. 常见问题与排查技巧实录
我把自己在项目里踩过的、以及在社区里看到大家讨论最多的几个长期记忆问题,整理成了一张排查表。这比任何理论都实用。
| 问题现象 | 可能原因 | 排查方案 |
|---|---|---|
| 模型“假装记得”但回答错误 | 记忆写入节点没成功提炼,或冗余信息和关键信息混在一起 | 打开记忆写入节点的 trace 看最终写入的 memory item 是什么 |
| 跨会话后什么都召回不回来 | 向量检索阈值设得过高,或者 query 改写没把关键实体扩写进去 | 下调阈值,检查查询改写环节,人工测试召回 top 10 的内容 |
| 模型把过期记忆当成当前状态 | 覆盖机制没生效,旧条目仍在读取范围 | 检查supersedes逻辑,确认已过滤archived条目 |
| 记忆库出现同一事实的多个版本 | 没有按事实指纹去重,写入链路并发执行 | 在设计上引入fact_key,同一事实先查重再写 |
| 写入过多导致 token 暴涨 | 召回策略太粗,把大量低质量记忆拼进 Prompt | 增加重排序步骤,限制最终进入 Prompt 的记忆数量 |
| 用户画像更新后老接口还在用旧值 | 结构化槽位更新和向量库更新没做事务一致性 | 用消息队列或 DAG 事务节点保证两者都成功再返回 |
6.1 我踩得最重的一个坑
要说印象最深的坑,还是“无差别写入”。第一版方案为了省事,把每一轮对话都送进记忆提炼节点。结果模型为了表现得更尽职,把大量低价值信息都提炼成了“长期事实”,包括“用户昨天心情不错”“用户提到很忙没时间做饭”。这些信息占满了向量库的召回空间,真正重要的偏好反而不容易排到前面。
后来我加了一道前置过滤节点,先用规则判断这一轮是否包含潜在的长期价值。比如消息里出现了技术栈关键词、项目名、时间节点、流程约束词,才进入提炼节点;否则直接跳过。这一改,召回准确率肉眼可见地提升了。
这里也可以分享一个个人经验:不要贪心。长期记忆系统的能力边界,不是由“能存多少”决定的,而是由“能否在正确的时候忘掉错误的旧信息”决定的。把记忆当成有生命周期的资产去管理,它才会真的对你有用。
6.2 多租户与数据隔离的补充
最后补一句数据隔离。给企业做 Agent 的时候,长期记忆库里绝对不能只有一个裸集合,至少要做到tenant_id + user_id双层命名空间隔离。DeerFlow 示例里可能只写单用户,但实际部署到多团队场景,没有隔离意识的记忆系统等于给自己挖坑。我在每个记忆条目上都会强制带上tenant_id,查询时也必须在过滤条件里带上,防止跨部门串数据。这是合规问题,不只是技术问题。
7. 写在最后的体会
我自己在完整跑通这套长期记忆方案之后,一个非常强烈的感受是:长期记忆能力,最后拼的不是模型能力,而是工程治理能力。模型再聪明,也不会自动替你决定哪些信息该长期记住、哪些过期信息该丢掉、哪些旧版本和更新版本要如何在大脑里共存。这些规则,全部要靠工作流设计来回答。
以后再做智能体项目,我肯定都会按这个思路来设计记忆层:写入走提炼节点,读取走检索加过滤节点,更新走版本覆盖机制,全程接上可观测系统。这套方法论比单独某一行代码重要得多,你甚至可以把它复用到任何框架里。
如果想让记忆层更完善,下一步可以考虑加上周期性记忆总结节点。比如每天凌晨对近期记忆做一次压缩合并,把重复条目归并成一条高置信度的事实。这个动作我试过,效果很不错,既降低存储成本,又减少检索噪音。你可以从简单的“按 user + 时间窗口跑定时任务”开始试。