Agent多轮对话记忆缺失怎么办?Memory分层设计与工程落地实践

发布时间:2026/9/29 17:10:47
Agent多轮对话记忆缺失怎么办?Memory分层设计与工程落地实践 我在做客服类 Agent 的时候碰到过一个印象特别深的场景用户第一轮说“我住在杭州孩子上小学三年级”第五轮问“这周末带娃去哪儿玩合适”Agent 直接给出一个“推荐您飞往三亚度假”的方案。产品经理当场反问我们这个 Agent 是不是没有记忆这个问题不解决后面所有对话体验都白搭。说白了大模型每次调用都是无状态的它自己不会记住任何东西所谓的 Agent 记忆完全靠外围的 Memory 记忆体系去实现。这篇文章我想把在 Agent 扩展范式、记忆体系分层、以及多轮记忆改造这三块上的实操经验完整梳理一遍适合正在做 Agent 开发、被“聊着聊着就失忆”问题折磨的同学参考。1. Agent 记忆缺失的根源无状态模型与三种遗忘很多人第一次接触 Agent 时会有一个错觉模型上下文窗口这么大把历史消息全塞进去不就有记忆了吗实际跑过之后会发现根本不是这么回事。大模型的每次请求都是独立的输入一段文本输出一段文本调用结束之后它的状态就清零了。所谓的“记忆”完全是 Agent 外围工程在替模型补课。1.1 上下文窗口是上限不是记忆以常见的长上下文模型为例就算支持 128K token 的窗口看起来很大但真实使用中消耗得极快。假设用户每轮说 200 token助手回复 300 token50 轮对话就是 25,000 token再加上系统提示词、检索回来的参考资料、工具定义、Agent 的思考链很快就能吃掉一大半窗口。如果业务里还要塞进商品资料或文档片段窗口会在你毫无感知的情况下被撑爆。更麻烦的是窗口满了之后的处理方式。大多数框架默认“截断最旧的消息”但这恰恰是最蠢的做法——早期对话里往往藏着用户最核心的诉求和偏好。我见过不少项目Agent 在 20 轮内表现正常过了 20 轮就开始反复问用户已经回答过的问题原因就是早期信息被滑动窗口挤出去了。这种“伪记忆”策略虽然简单但会在长对话中系统性失效。1.2 短期、长期、永久遗忘分别发生在哪一层把实际项目中遇到的遗忘问题归类后会发现它们根本不是同一个层面的问题短期遗忘同一场会话还没结束早期信息就被窗口截断丢了。表现是“刚才说过的话转头就忘”。长期遗忘跨天、跨会话之后完全不记得用户是谁、之前聊过什么。表现是“换个 session 就重新认识”。永久遗忘用户的核心偏好和事实性信息没有被固化下来后续所有会话都无法复用。表现是“用户反复提供同样的个人信息”。我在做改造前一直把这三类问题混在一起处理结果就是又加摘要又加向量库却说不清每层到底解决什么。后来把记忆体系按这三层拆开每层对应一套独立的存储、读写和管理策略问题一下子就清晰了。短期记忆管“当下的连贯”长期记忆管“跨会话的上下文召回”永久记忆管“用户画像的沉淀”。2. Memory 分层设计Working Memory、长期记忆与永久画像的实现细节记忆体系分层的思路可以用一个类比来理解短期记忆像你手边的草稿纸随写随扔长期记忆像你的笔记本重要内容会记下来用时翻一翻永久记忆像档案柜里的身份证复印件关键信息必须长期保存且不能搞错。Agent 的 Memory 设计本质上就是把这三层分别落地。2.1 Working Memory窗口内即时上下文的组织方式Working Memory工作记忆解决的是“当下这场对话怎么组织上下文”的问题。它不一定要把全部历史消息都塞进模型而是要保证模型在每个时刻都能看到它决策所需的关键信息。我的做法是给对话上下文设定 token 预算然后按优先级分配系统提示词约 20% 的预算里面放 Agent 的角色设定、记忆使用协议。即时对话约 60% 的预算保留最近 N 轮完整对话。记忆检索结果约 15% 的预算放从长期记忆和画像中查回来的相关内容。工具定义与中间输出约 5% 的预算。当对话超过预算时不要直接删最旧的消息而是先把旧消息里仍然重要的信息提炼成一句摘要放进上下文的开头位置再丢弃原文。这样一来模型即使看不到完整历史也能通过摘要保持上下文连续。我在项目中实测同样的任务使用摘要压缩后的窗口管理方式60 轮对话内的追问率比纯截断方式降低了大概 40%。2.2 长期记忆摘要抽取与向量检索的组合拳长期记忆的核心是“跨会话还能想起来”。纯粹的拼接历史行不通因为存不下也检索不动纯粹的向量检索也不够因为用户的问题往往带有时间上下文。我最终采用的方案是“滚动摘要 向量检索 重排序”的组合。滚动摘要的触发时机很关键。我的经验是每累积 20 轮对话或者当前工作记忆接近 token 阈值时触发一次异步摘要任务。摘要必须包含四类信息用户当前的目标、已经确认的事实、尚未解决的问题、明确表达过的偏好。摘要生成后作为一条独立记忆写入向量库同时保留时间戳和会话 ID。检索侧用户每发来一条新消息先用 embedding 模型把这条消息向量化在向量库里召回 top_k 条相关记忆然后再用 LLM 做一次 lightweight 重排序选出真正对当前问题有参考价值的 3 条左右。之所以加一步重排序是因为 embedding 的相似度并不完全等价于“语义相关”有时候召回的前几条都是噪音必须让模型再筛一遍。2.3 永久记忆结构化用户画像与事实库永久记忆适合用结构化数据存我用的是一张用户画像表字段包括用户身份、所在地区、默认选项、明确禁忌、历史偏好等。这层记忆必须和长期记忆分清楚长期记忆是“可能有用”的背景资料永久记忆是“必须遵守”的事实约束。这里有一个非常容易踩的坑画像更新时直接覆盖旧值。用户说“我不吃香菜”过了几天又说“其实我能吃一点”如果直接覆盖那没问题但如果用户只是某次情绪化表达直接覆盖就会造成误判。我的做法是更新画像时保留历史值只改当前值并记录更新时间和来源。模型读取画像时看到的是“当前值 历史变更记录”这样既能适应用户偏好变化又能在上下文冲突时回溯原因。实际上这套设计帮我解决了不少“用户明明改口了Agent 还拿旧偏好说事”的投诉。3. 扩展范式重构让工具、Skill 与记忆体系协同工作标题里的“Agent 扩展范式”指的是 Agent 能力扩展的方式给 Agent 加工具、加技能、加子 Agent。这一层如果不和记忆体系打通就会出现一种尴尬局面——记忆是有了但 Agent 根本不知道什么时候该去查记忆、什么时候该把工具结果写进记忆。扩展范式必须跟着记忆体系一起做重构。3.1 工具扩展层的记忆感知设计工具的调用场景天然和记忆强相关。以天气查询工具为例用户第一次说“我在北京”Agent 调用了带地区参数的天气接口但第二次、第三次还要不要反复问“您在哪座城市”不应该。做法是在工具调用前加一个“记忆回填”阶段从永久画像里提取工具所需的关键参数如果画像里有就直接填上没有才向用户询问。这个“记忆回填”听起来简单但在工程上涉及工具入参的 schema 改造。我给每个工具定义里加了一个memory_source字段标注哪些参数可以从画像元数据中自动获取哪些参数必须由用户显式确认。Agent 在规划工具调用时会先查询记忆层能填充的字段直接填充不能填充的才进入追问流程。这套机制落地后“为了一个城市信息反复追问”的体验问题基本消失。3.2 Skill 封装把记忆操作变成可复用组件Skill技能/能力单元和工具的区别在于工具通常是纯函数式的接口输入输出确定Skill 可以封装完整的状态处理逻辑包括读写记忆。在我改造后的架构里以下三个高频操作被封装成了 Skill偏好提取从用户消息中抽取结构化事实并更新永久画像。对话摘要对工作记忆中的历史做滚动压缩写入长期记忆。记忆检索结合用户画像、长期记忆和工作记忆组装成上下文。判断何时该调用哪个 Skill 则由主 Agent 的规划链路负责不需要业务侧手动调。这样做的好处是任何新接入的子 Agent 或工具只要声明需要这些 Skill就能立刻拥有记忆读写能力。多轮记忆改造不再是一次性补丁而是变成框架层的基础能力。3.3 多 Agent 协作中的记忆共享与隔离多 Agent 场景下记忆不是越共享越好。我的实践原则是全局共享层放用户画像和高频稳定事实局部工作区放单个子 Agent 当轮对话的上下文隔离层放敏感信息和临时状态。举个例子一个客服系统拆成导购、售后、物流三个子 Agent。用户地址、会员等级这类信息放在全局共享层所有子 Agent 都能读取但“当前售后工单的详细信息”只放在售后子 Agent 的 Working Memory 里其他子 Agent 不需要也不应该读取。这样设计避免了两个问题一是全局什么都存导致检索时互相干扰二是子 Agent 之间的临时数据互相污染。记忆的写入和读取权限一定要跟着业务边界走而不是简单地把所有记忆堆到一个共享池里。4. 多轮记忆改造的完整落地链路从场景盘点到底层中间件这一部分进入实操。前面讲的是设计思路现在讲的是改造步骤。我复盘了多个项目的改造过程沉淀出的标准链路是场景盘点 → 记忆分层定位 → 读写触发点设计 → Prompt 改造 → 中间件落地 → 回归验证。4.1 场景盘点先判断你的 Agent 到底需要哪层记忆不是所有业务都需要三层记忆全上。单轮查询类场景翻译、计算、简单问答只需要基础的用户默认项多轮任务类场景客服、导购、编程助手需要 Working Memory 加长期记忆跨会话个性化场景理财顾问、健康助理、学习伴学才需要三层全上。判断矩阵可以参考业务模式需要短期记忆需要长期记忆需要永久画像单轮查询类弱弱部分默认项多轮任务类强中中跨会话个性化强强强我见过不少团队一上来就搞向量库加了一堆基础设施结果业务只需要短期记忆加用户默认项。先花半天时间盘点场景比先花两周搭记忆框架要划算得多。4.2 记忆的写入、读取与更新触发点设计读写触发点是记忆体系能不能真正跑起来的关键。我的经验是写入时机、读取时机、更新策略三件事必须分开设计不能混在一起。写入时机有四个高价值节点用户提供了新事实且经模型确认时、对话轮次达到摘要阈值时、用户显式表达偏好变更时、工具返回了影响后续决策的关键结果时。读取时机有两个用户新消息进入且 Agent 规划下一步时、调用工具之前的参数准备阶段。更新策略上永久画像走“合并式更新”长期记忆走“追加式写入”工作记忆走“滑动窗口 滚动摘要”。4.3 Prompt 层改造让模型学会“查记忆”而不是“背上下文”记忆中间件就算写得再好如果 Prompt 里没有对应的使用协议模型也可能把检索来的记忆当作上下文随手堆叠。我给记忆改造项目写了一套统一的 System Prompt 模板核心是给模型一套记忆使用顺序你是智能助手回答问题时按以下顺序使用记忆首先读取用户画像中的身份、偏好、禁忌字段涉及个性化问题时必须引用其次从长期记忆中检索与当前问题相关的历史结论、未完成事项最后参考当前对话最近几轮内容但不要超出画像与长期记忆范围做推断如果记忆中没有所需信息直接询问用户不要编造。这段协议真正起作用的是第 4 条。加了这一条之后Agent 在记忆不足时不再“硬答”而是选择询问。别小看这个改变它直接决定了记忆体系是“辅助决策”还是“误导决策”。4.4 回归验证五个关键指标改造完成后必须做量化对比。我每次都用同一批业务数据做回归重点看以下五个指标改造成果一目了然指标我的观测方式改造前典型值改造后典型值追问率用户重复提供同一信息的次数 / 总轮次12% 左右5% 以下信息一致性抽取用户陈述过的事实核对 Agent 回复中的引用正确率较低90% 以上任务完成率核心业务目标的闭环完成比例中等明显提升Token 成本单会话推理 token 均值偏高持平或略降端到端延迟单轮响应时间 P95波动大稳定有一个反直觉的发现加了记忆体系之后整体 Token 成本不一定上升。因为模型不需要靠反复追问来补齐缺失信息反而省掉了大量无效对话轮次。只要窗口管理和摘要触发做得好成本是可以做到不升反降的。4.5 一段最小可用的记忆中间件核心逻辑最后放一段我自己项目中精简出来的核心代码它实现了“写入画像 滚动摘要 按需检索”三个最基础的能力。框架无关只保留主干逻辑方便你抄作业后根据需要扩展import json class MemoryMiddleware: def __init__(self, profile_store, memory_store, llm): self.profile_store profile_store # 永久画像存储 self.memory_store memory_store # 长期记忆向量存储 self.llm llm # 对话模型 self.summary_threshold 20 # 每20轮滚动摘要一次 async def on_message(self, session_id, user_msg, assistant_msg): profile await self.profile_store.load(session_id) # 1. 抽取结构化事实并合并进永久画像 facts await self.llm.extract_facts(user_msg) for fact in facts: profile[fact.key] { value: fact.value, updated_at: fact.timestamp, source: user, } await self.profile_store.save(session_id, profile) # 2. 记录本轮对话 await self.memory_store.append(session_id, user_msg, assistant_msg) # 3. 达到阈值时对旧历史做滚动摘要写入长期记忆 recent await self.memory_store.load_recent(session_id, limit21) if len(recent) self.summary_threshold: summary await self.llm.summarize(recent[:-1]) await self.memory_store.save_summary(session_id, summary) await self.memory_store.clean_old(session_id, keep1) async def retrieve(self, session_id, query): # 按优先级组织画像 长期记忆相关片段 profile await self.profile_store.load(session_id) memories await self.memory_store.search(query, top_k3, min_score0.7) return {profile: profile, memories: memories}代码里有两个细节需要注意。min_score0.7是检索阈值具体值取决于你使用的 embedding 模型我建议先跑一批真实数据看分数分布再定。clean_old(session_id, keep1)表示滚动摘要后只保留最新一轮的原始对话避免历史无限膨胀。这两个参数都要在回归验证阶段调优不能拍脑袋定死。5. 记忆框架选型与真实踩坑记录最后聊框架选型。这个话题很容易被带偏因为市面上的方案太多而且新概念一个接一个。我的经验是先弄清楚自己的记忆需求属于哪一层再决定是用现成框架还是自研中间件。5.1 现成框架 vs 自研中间件的取舍我接触过的方案大致可以分成四类方案类型适用场景改造成本维护复杂度框架内置 Memory 模块原型验证、简单多轮任务低低向量库 自研中间件生产级长会话业务中中知识图谱 / 关系库记忆强结构化偏好、合规要求高高高长期记忆自动调度框架超大上下文、复杂规划高高对于大多数业务我建议走“向量库 自研中间件”理由很简单现成框架的 Memory 模块通常只解决“短期上下文拼接”这一件事对长期记忆和永久画像的适配不够灵活而自研中间件虽然要写一部分存储逻辑但能把记忆分层、读写触发点、业务字段更新完全掌握在自己手里。等业务形态确定之后再考虑引入更重的框架也不迟。5.2 向量检索的典型坑阈值、噪声与过期内容向量检索是三块里最容易出问题的部分。第一个坑是相似度阈值拍脑袋。阈值设太低召回一堆无关记忆模型上下文被污染阈值设太高该召回的内容又召不回。我见过有项目用默认的 0.8 阈值结果几乎所有记忆都被过滤掉了效果还不如不用。解决方式是先采样一部分真实对话看查询和正确记忆之间的分数区间再定阈值。第二个坑是召回结果不做时效性处理。用户三个月前的偏好和三天前的偏好权重应该不一样。我的做法是给记忆打时间戳检索评分时乘一个时间衰减因子。第三个坑是混合检索的必要性。用户往往用口语化表达纯向量检索召回不稳定混合关键词检索再合并排序会稳定得多。5.3 记忆污染与安全防线写好“记忆写入防火墙”记忆污染是记忆体系里最隐蔽的问题。用户完全可以通过对话诱导 Agent 记住错误信息比如“请记住我喜欢红色”然后过一阵子用这条被篡改的记忆误导 Agent 做出错误决策。这在多轮对话里相当于一次轻量级的提示注入攻击。我改造时的做法是加一层“记忆写入防火墙”画像字段白名单化只有预定义的字段类型允许写入写入前做事实一致性校验和已有画像冲突的值标记为低置信度敏感信息先脱敏再入库。记忆隔离也要做好不同用户 session 之间的向量检索和画像读取必须严格隔离否则会出现 A 用户的信息被 B 用户的会话召回的严重事故。这点在自研中间件时尤其容易被忽略一旦上线就是生产事故。最后分享几条实际体会这套改造做完我自己印象最深的一点是记忆体系的收益不是“让 Agent 记住更多”而是“让 Agent 少问一遍”。用户感知最强的瞬间不是 Agent 背诵式地复述他的资料而是他发现自己不用再把说过两遍的话说第三遍。如果你正准备做类似改造我建议不要一上来就上重框架。先花半天把业务场景盘清楚画出记忆分层图想清楚每层解决哪个问题再做最小改动验证效果。另外记忆写入的安全防线一定不能省宁可少存几条也不能让错误信息或越权数据进入长期记忆。改造完成后坚持跑一段回归数据用追问率和信息一致性这类硬指标说话你才会真正知道这套记忆体系值不值。