ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DeerFlow长期记忆实战:让智能体从“会聊”到“会记”

DeerFlow长期记忆实战:让智能体从“会聊”到“会记” 有一次我在给客户搭智能客服Demo用户连续说了三遍“我姓王叫王小明负责采购”Agent每次都认真回一句“好的王小明先生您负责采购”。等会话窗口一关重新打开再聊Agent又变成了第一次见面“请问您怎么称呼您在贵司担任什么角色”客户当场没说话但我能感觉到空气凝固了两秒。那一刻我就意识到智能体要真正进入生产环境光有“会聊天”远远不够必须要有“会记住”的能力。这也是为什么我后来专门把字节开源的DeerFlow拉出来从头到尾啃了一遍它的长期记忆实现方案。这篇文章就用一个最直观的示例把长期记忆到底怎么设计、怎么落地、怎么排查一次讲清楚。内容不吹概念全部按可复现的工程思路来写。1. 从一次“失忆”对话说起长期记忆到底在解决什么问题1.1 一个让人抓狂的Demo场景先还原一下那个让气氛凝固的对话。用户说“我姓王叫王小明负责采购”Agent回应了。这个回应本身是没问题的模型没有上下文理解错误。可问题出现在下一段会话短期记忆只存在于当前会话的上下文窗口里窗口一关一切归零。我们给智能体打电话、发消息、或者用网页聊天用户每次开启新会话模型拿到的都是“一张白纸”。如果这是一个问答机器人白纸也还好。但如果是一个助手需要长期服务同一个人比如采购助理、健康顾问、教研助手那就必须有一种机制把关键信息沉淀下来下次还能捞回来。DeerFlow对这类场景的答案就是长期记忆。它不是简单地把数据库接进来而是把“记忆”作为智能体能力的一部分来设计记忆的写入、抽取、存储、检索、更新、遗忘每个环节都有一套明确流程。这个Demo让我想通的道理是长期记忆的核心问题不是“能存多少”而是“在正确的时间把正确的信息塞进正确的上下文”。1.2 DeerFlow对记忆的抽象短期、长期、会话我最初用DeerFlow的时候以为“记忆”就是一个大列表把对话扔进去就行了。后来才发现它把记忆分成了至少三层会话记忆短期一次会话内的上下文随请求进入模型随会话结束消失。长期记忆跨会话从多次会话中提取出的用户画像、偏好、事实信息持久化存储在新会话开始时按需召回。外部知识记忆通过检索外部文档得到的知识片段不属于用户交互产生的信息但在回答中同样会用到。这三层不是割裂的。DeerFlow在组织回复时会把会话上下文、长期记忆召回结果、外部知识检索结果合并在一起拼成最终的提示词。长期记忆的价值就在这个“合并”动作里被放大了它让模型不再是“冷启动”一上来就知道对面是谁、想要什么、在意什么。1.3 为什么需要“可观测”的记忆流很多人在接入长期记忆时只看结果Agent答对了没但如果答错了根本不知道是记忆没写入、写错了还是召回了没被模型用上。我开始做DeerFlow二次开发之后第一件事就是把“记忆流”的可观测性做出来。所谓可观测就是记忆链路上的每一步都有输出。写入时能看到抽取了什么实体存储时能看到写进了哪个表召顿时能看到命中了哪条记忆最终提示词里能看到记忆被拼接到哪个位置。DeerFlow本身提供了不少追踪信息但默认的可读性对业务开发不算友好。我建议每一个做长期记忆的人都套一层自己的日志记录逻辑。否则你会陷入一种极其痛苦的排障过程用户说“我上次不是告诉你了吗”你一边赔笑一边打开数据库看那条记忆到底在不在。它往往在但模型就是没召回。这背后的原因后面我会详细拆解。2. 读懂DeerFlow长期记忆的核心架构与核心组件2.1 记忆不是一张表存储层选型很多人第一次接触长期记忆时脑子里想的是数据库表比如user_preference(id, user_id, content)。但DeerFlow的实现不是这么简单的。它把长期记忆分成两个维度来存储结构化槽位和非结构化语义片段。结构化槽位比如用户ID、姓名、公司、角色、偏好标签这些适合映射为传统数据库字段适合精确查询和筛选。非结构化语义片段比如“用户说他不喜欢被催得太紧希望每周只收一次汇总报告”这句话没法拆成几个字段但它非常重要。这种信息首先要做向量化嵌入到向量数据库里之后通过语义相似度召回。所以DeerFlow的长期记忆存储层通常由两部分组成传统数据库看具体实现可以用PostgreSQL也可以用MySQL或SQLite负责保存结构化信息和记忆原始记录向量数据库负责保存Embedding向量。你也可以用Redis存热数据来加速读取但这不是必须的。我当时的选择是业务信息落MySQL语义信息落向量库Redis只做当前会话的记忆缓存。因为我们的智能体要处理并发会话每次都走完整向量检索延迟会高不少。但这种组合在DeerFlow中不是开箱即有的你需要根据自身场景做存储适配。2.2 记忆如何写入触发时机与信息槽位长期记忆最大的一个坑是“什么都记”。如果Agent每一句话都当成长期记忆存下来几轮对话后记忆库就爆炸了召回的噪声会越来越大。DeerFlow的默认做法不是这样它把记忆写入分成两个阶段第一阶段对话摘要与候选抽取。每轮对话进入一个抽取器把涉及用户的陈述、偏好、承诺、关键事实抽取成若干条候选记忆。比如用户说“我每周三下午开采购会”抽取器识别出“每周三下午-采购会”这个周期性事件用户说“请不要在午休时间给我打电话”识别出“沟通时间偏好”。第二阶段去重与合并。候选记忆会和已有记忆比对如果存在语义相似度很高的记忆就做合并或者覆盖更新而不是新增一条。这一步非常关键。如果不去重同一个用户在第三天问了身份之后库里会积累五六条几乎一模一样的记忆召回时会同时命中好几条白白浪费上下文窗口。你可能会问这个“抽取器”是谁来实现的在DeerFlow里它本质上是一个Agent Node内部由大模型承担语义理解通过Prompt规定抽取格式。所以你在做二次开发时需要重点去调整的是抽取Prompt里的槽位定义。2.3 记忆如何召回召回策略与重排如果写入是“记什么”召回就是“忘掉什么”。DeerFlow的召回策略不是简单的“向量相似度TopK”。因为单纯按相似度召回很容易被当前问题里的表层词汇带偏。比如用户问“今天下午开会吗”向量库可能召回“用户每周三下午开采购会”这种真正的偏好记忆但也可能召回“用户昨天说要开一个下午茶分享会”这种一次性事件。所以DeerFlow在召回阶段通常会做两个步骤粗召回 重排。粗召回用向量检索取回相似度最高的Top 30~50条记忆重排阶段根据当前对话的意图、时效性、记忆类型、证据分数把最可能帮助当前对话的记忆挑出来一般只保留5~10条。重排可以考虑由一个轻量模型或规则来完成不一定每次重排都让大模型参与否则延迟受不了。我实践经验是记忆召回的结果里时效性非常重要。一条三个月前的一日性事件和一条三个月前的周期性偏好后者才应该进入上下文。DeerFlow在实现中会给记忆打时间戳、来源会话ID、类型标签就是方便重排阶段做权重调整。2.4 长期记忆与短期记忆的协同这个协同点是最容易被忽视的。很多Agent先处理当前对话再单独拼长期记忆结果模型上下文里两者相互矛盾。比如短期记忆里用户刚说了“我最近调到市场部了”长期记忆里可能还写着“用户岗位采购部”。如果不做冲突消解模型就很纠结可能答出前后不一致的内容。DeerFlow的解决办法是在生成回复前会做一次记忆一致性检查。这一步不是每个版本都默认启用但它存在于框架的设计理念里。我的建议是在你自己的代码里增加校验逻辑短期的显式修正指令优先级永远高于长期记忆里的旧事实。比如当用户明确说“我已经不做采购了”时长期记忆里对应的槽位必须立刻被覆盖或标记为过期。长期记忆和短期记忆不该是“两条平行线”而应该是“短期负责实时更新长期负责沉淀校正后的结果”。明白了这一点后面写代码时就不会把记忆模块当成一个简单的数据库接口了。3. 示例实操用DeerFlow实现“记住用户偏好”的长期记忆Agent3.1 准备工作环境与依赖现在我们把理论落到代码上。下面这个示例我以Python 3.10环境为准DeerFlow的安装我建议直接用pip安装源码包同时装好向量数据库驱动。以下是我在项目里实测过的一套依赖组合pip install deerflow[memory] # 装带记忆模块的版本 pip install openai1.0 # 大模型调用库DeerFlow依赖 pip install chromadb # 向量数据库示例 pip install sqlalchemy # 结构化存储需要注意的是不同DeerFlow版本的API名称可能略有差异。如果你在自己的环境里遇到了ModuleNotFoundError建议直接去安装目录里翻一下具体模块名而不是硬套网上的旧版本代码。我这篇文章里的示例API以我实际调试过的版本为准但不保证你拿到的版本完全一致。3.2 定义记忆类型与存储配置先定义一个简单的记忆存储配置。我会分为两部分结构化槽位存在关系表语义片段存在向量库。from deerflow.memory import MemoryStore, VectorMemoryStore from deerflow.memory.extractors import LLMInformationExtractor import chromadb # 结构化记忆表 class UserProfile(Base): __tablename__ user_profile user_id Column(String, primary_keyTrue) name Column(String, nullableTrue) position Column(String, nullableTrue) company Column(String, nullableTrue) preference Column(Text, nullableTrue) updated_at Column(DateTime, defaultfunc.now()) # 向量记忆库 chroma_client chromadb.PersistentClient(path./memory_db) long_term_store VectorMemoryStore( clientchroma_client, collection_namelong_term_memory )这里有个很重要的设计决策为什么用户画像要单独槽位存而不是把所有东西都丢进向量库因为事实类信息讲究精确性“姓名王小明”就不能召回一个相近的“王小明老师”当结果。而槽位存储天然支持精确查询还能做覆盖更新。偏好、语气这类非结构化信息才需要向量的模糊匹配能力。3.3 让Agent在对话中写入记忆接下来在DeerFlow里配置一个信息抽取器。你可以把抽取器理解成一个“隐形助手”它不直接回复用户而是在每轮对话结束后把值得记住的内容提出来。from deerflow.memory import MemoryWriter from deerflow.memory.schema import MemoryItem, MemoryType def extract_and_write(user_id, conversation, llm_client): extractor LLMInformationExtractor(llmllm_client) candidates extractor.extract_long_term(conversation) writer MemoryWriter() for cand in candidates: if cand.confidence 0.6: continue item MemoryItem( user_iduser_id, contentcand.text, memory_typeMemoryType.FACT if cand.is_fact else MemoryType.PREFERENCE, source_sessionconversation.session_id, timestampcand.timestamp, ) writer.upsert(item) # upsert内部做了语义去重这里我设置了confidence 0.6直接跳过。这个阈值来自我的经验如果信息抽取器对某条内容的把握不高强行写入后期会产生大量脏记忆。宁可漏几条也不要错存。在实际项目中你可以根据模型表现动态调节这个阈值。3.4 让Agent在下一轮读取记忆并回复到了新会话Agent要先“回忆”再“说话”。核心逻辑是把召回的记忆与当前对话合并一起送给大模型。from deerflow import Flow, AgentNode, PromptBuilder def build_memory_prompt(user_id, current_message): # 精确召回结构化槽位 profile session.query(UserProfile).filter_by(user_iduser_id).first() # 语义召回想相关记忆 related_items long_term_store.search(querycurrent_message, top_k8) prompt PromptBuilder(system你是一位了解用户习惯的私人助手。) if profile: prompt.add_context(用户资料, { name: profile.name, position: profile.position, company: profile.company, }) if related_items: prompt.add_context(历史偏好, [item.content for item in related_items]) return prompt.build() agent_node AgentNode( nameassistant, llmllm_client, prompt_builderbuild_memory_prompt, )注意这里的PromptBuilder是我封装的一个工具DeerFlow原生是支持在Flow的节点里自定义Prompt拼接的但每个版本的拼接方式小有不同。你完全可以自己写一个函数把“用户资料”“历史偏好”拼进system prompt里。关键是长期记忆必须以清晰的区块形式出现在Prompt中并且标注来源。模型看到“用户资料”和“历史偏好”两个区块时对信息的利用效率远高于一段混合文本。3.5 封装SSE流式接口调用逻辑在实际生产环境里用户不会等Agent把整段话算完才看到结果。DeerFlow的Flow本身支持流式输出但如果你像我一样要把它封装成后端服务接口就需要处理SSEServer-Sent Events流式接口的调用逻辑。from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse import json app FastAPI() app.post(/chat) async def chat(user_id: str, message: str): async def event_gen(): # 这里先把记忆召回结果作为独立事件推送出去方便前端调试 memory_prompt build_memory_prompt(user_id, message) yield fdata: {json.dumps({type: memory, content: memory_prompt})}\n\n # 再流式推送给模型输出 async for token in agent_node.stream_reply(memory_prompt): yield fdata: {json.dumps({type: token, content: token})}\n\n yield fdata: [DONE]\n\n return StreamingResponse(event_gen(), media_typetext/event-stream)封装SSE时最大的坑在于断线重连。如果Agent回复到一半前端断开了后端要能感知到客户端断开及时取消模型调用。用FastAPI的StreamingResponse时可以在Generator里捕获asyncio.CancelledError清理资源。这不是记忆模块本身的要求但只有流式接口稳定了长期记忆的“召回-感知”链路才能被用户真正体验到。我在项目里还把记忆召回结果先推给前端这样用户在等待AI回复时能看到一个“AI正在回忆你的偏好”的动画。前端拿到这段memory类型数据后会显示成一条卡片告诉用户“我记住了你负责采购每周三下午开会”。这个交互极大提升了“被记住”的感觉属于人机协同里很讨巧的细节。3.6 人机协同确认哪些该记住说到人机协同DeerFlow的长期记忆方案并不只是为了灌输记忆它也希望人在链路中参与记忆的确认和修正。我建议在业务里增加一个“记忆确认”机制当Agent收到候选长期记忆时不妨在回复末尾加上一句“我先把这事记下来下次不用你重复了”让用户有机会否定“不对别记这条”。这个实现非常简单只需判断记忆抽取事件是否到达然后通过SSE推一个提示给前端。用户如果点击“不记住”前端调一个删除接口app.delete(/memory/{user_id}/{memory_id}) def delete_memory(user_id: str, memory_id: str): long_term_store.delete(user_iduser_id, memory_idmemory_id) session.query(UserMemory).filter_by(idmemory_id, user_iduser_id).delete() return {status: deleted}不要小看这个“让用户说了算”的能力。我在实际运营中发现AI主动建议记忆、用户确认或纠正比AI默默写库然后出错再解释要高效得多。它既避免算法自信过头也让用户对系统产生信任这是DeerFlow在人机协同维度上给我最大的启发。4. 我在实际项目中踩过的坑DeerFlow长期记忆的细节与排查4.1 记忆命中率不高怎么办这个坑太常见了。你明明能看到记忆库里有那条记录向量召回也返回了可模型回复时就像没看见。我开始以为是大模型不听话后来仔细分析才发现根本不是模型的问题是记忆在Prompt里被“淹没”了。DeerFlow默认会把召回的长期记忆和一堆系统指令、历史消息、知识库片段拼在一起如果Prompt里记忆区块不是显著的结构模型很容易忽略中间部分只关注开头和结尾。解决方法是把长期记忆从大段文字改成结构化要点并且放在System Prompt的显眼位置。我常用的格式是用户关键信息 - 姓名王小明 - 岗位采购部负责人 - 沟通偏好每周三下午开会午休时间勿扰 根据以上信息回答用户提问。这样模型一眼就能抓到。如果你观察业务日志发现“记忆命中但没发挥作用”请先优先检查Prompt组装格式而不是调召回阈值。4.2 记忆覆盖与冲突第二个坑是记忆覆盖逻辑不清楚。举个例子用户周一告诉你“我每周三下午在总部开采购会”周五又说“我换到下周一开例会了”。如果系统只是把两条记忆都存进去不覆盖旧事件那么下次用户问“我周三下午有会吗”模型会召回“每周三下午在总部开采购会”哪怕这个信息已经过时了。DeerFlow的记忆写入阶段虽然有去重但去重通常基于语义相似度不擅长处理“同一事件的时间版本变化”。我的做法是在写一条事实类记忆之前先按照user_id 事件槽位查询旧记录把完全冲突的旧记录标记为archived再写入新记录。这个操作不复杂但对准确率提升极大。还有一种冲突是用户显式纠正“我之前说的不对”。我建议在抽取器的Prompt里增加一条指令识别用户的纠正意图。一旦识别到纠正优先把旧槽位覆盖而不是新增一条“用户曾说过XXX但现在说XXX”的复杂记忆。认知负担越小模型用得越好。4.3 可观测性如何看到记忆怎么走的前面说了可观测性这里给出一个具体方案。我在用DeerFlow时给每个请求生成一个trace_id然后把记忆链路的关键节点日志统一输出到一个结构化日志里阶段关键输出对话输入当前用户消息、会话ID、用户ID抽取结果候选记忆文本、置信度、类型写入动作upsert或skip、命中的重复记忆ID召回结果召回条数、每条分数、最终筛选条数Prompt组装记忆区块是否加入、具体内容这样每次AI回复“不对味”时我可以直接打开日志判断问题出在“没抽取到”“没写入”“没召回”还是“Prompt没用上”。有一次我发现某用户的所有记忆都查不到最后追踪下来是每个新会话都重新生成了user_id的UUID。这不是DeerFlow的问题是业务侧会话管理把用户ID搞混了。不做可观测这种bug能让你排查一整个下午。4.4 常见问题速查表整理一个速查表方便你对照排查现象可能原因解决方案新会话完全不记得用户用户ID不一致或未启用记忆节点检查用户标识是否跨会话稳定记忆写入了但回复没体现Prompt中记忆区块位置不明显将记忆结构化并放在System Prompt靠前处召回的旧事实与当前冲突覆盖更新逻辑缺失增加事实槽位的版本归档与新写逻辑记忆召回太慢向量库每条都全量扫描给向量库设置按user_id预过滤记忆库脏数据多抽取阈值过低上调confidence阈值增加人工确认环节流式回复时记忆推送卡顿SSE封装没有处理心跳增加周期注释事件保持连接活跃这些排查点都是我一个个踩出来的。说句实话长期记忆这个模块在Demo里看起来很酷真到生产环境里麻烦基本都集中在工程细节。DeerFlow把底层的存储和抽取流程给串好了但你得自己把业务命门补上。5. 更进一步长期记忆的进化方向与个人体会5.1 从被动存储到主动遗忘很多人关注“怎么记住”却很少关心“怎么遗忘”。但长期记忆方案如果没有遗忘机制时间一长记忆库会变成一团乱麻。用户三年前的喜好、一年前的工作单位、上个月说过的一次性安排全都堆在一起。更麻烦的是这些旧记忆会持续参与召回污染当前上下文。DeerFlow相关的社区讨论里已经有人在做“记忆衰减”和“重要性评估”。比如给每条记忆设置一个温度值刚写入时时温度高随时间和查询频率逐渐降低当温度低于阈值时自动归档。这个思路我很认同。你也可以在自己的实现里加上这个规则每次召回命中记忆时给记忆weight加一分长期不被命中的记忆周期性地被清理掉。主动遗忘不是“删除数据”而是优化信息层级。5.2 DeerFlow 2.0代码层面的改进方向标题里有人提到DeerFlow 2.0代码详解我也顺手说下我的观察。从社区和代码结构来看2.0在长期记忆上的改进趋势是把记忆模块从单一的“插件式调用”改成“Flow内一等公民”。也就是说记忆不再是一个在Agent节点之间跳来跳去的工具而是成为Flow运行时的核心状态管理层。这意味着你可以在Flow的配置中直接声眀记忆节点之间的依赖关系。比如“从用户消息中抽取偏好 - 写入长期记忆 - 在下一轮对话前自动召回”。这种声明式配置比我早期在代码里到处塞memory_writer和memory_retriever清晰得多。如果你准备做二次开发我的建议是不要过度依赖某个版本的抽象API而是先理解DeerFlow里“信息如何流动”的哲学。一旦你把记忆流理解为“抽取、写入、召回、合并”这四个阶段那么无论版本怎么变化你都能在它的源码里找到对应位置。5.3 我的实操体会聊到这儿说点个人感受。最初接触DeerFlow时我恨不得把它所有功能都塞进项目里恨不得让Agent记住用户的每一个语气词。后来我发现好的长期记忆系统讲究“克制”。该记的记不该记的别记该召回的召回不该召回的宁可不召回。这跟做产品一样添加功能很容易做减法才难。还有一个小技巧如果你在调试长期记忆不要在同一个测试环境里反复覆盖同一条记忆那样很难判断是覆盖逻辑生效还是纯粹靠大模型瞎猜。最好的做法是每次测试都用不同的user_id把用户A、用户B、用户C隔离开用独立数据流验证记忆行为。我自己开了三个测试账号分别对应“新用户”“老用户”“频繁纠正用户”这样回归测试基本不会漏。长期记忆这件事说到底是让智能体从“工具”变成“伙伴”的关键一步。你再厉害如果用户每次都要自我介绍那他迟早会摔门而去。所以把记忆链路看得通透、搭得扎实这不是锦上添花而是生产级Agent的基本功。希望我这套从示例到坑位的拆解能让你在DeerFlow长期记忆这条路上少走几次冤枉路。
返回列表