ARTICLE DETAIL

资讯详情

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

从零实现生产级记忆型Agent:分层记忆架构与工程实践

从零实现生产级记忆型Agent:分层记忆架构与工程实践 1. 为什么“记忆”是 Agent 从玩具走向生产力的分水岭1.1 无状态 Agent 的“转头就忘”是行业里最被低估的痛点过去两年我搭过不少 AI Agent从最简单的客服问答机器人到多轮任务编排系统踩过最大的坑往往不是模型幻觉也不是工具调用失败而是对话一长Agent 就开始“失忆”。你让它帮你查完上海的天气它记住了等它再执行完一个函数调用刚才说的航班号它已经忘了。用户得多重复两遍体验立刻从“智能”跌回“人工智障”。圈子里很多人喜欢鼓吹 Agent 是多智能体协作、自动规划、自我反思这些概念但对“记忆”这件事一笔带过。可实际上记忆恰恰是 Agent 能否进入生产环境的分水岭。没有记忆的 Agent本质就是一个套了 Prompt 和工具调用的无状态接口它只能在单轮对话里完成任务一旦涉及多轮上下文、跨会话偏好、知识库增量这种架构就彻底撑不住。我做一个具体的对比你就明白了能力无状态 Agent记忆型 Agent多轮对话连续性靠把全部历史塞进 Prompt成本高且易超限分层记忆按需召回关键历史用户个性化每次从零开始记不住偏好长期记忆沉淀用户画像知识库更新依赖模型参数无法细粒度更新记忆与知识分离增量写入多任务协作上下文易混淆任务间相互干扰工作记忆隔离子任务独立上下文生产可观测黑盒看不出“为什么记错了”记忆可追溯、可回滚、可审计所以这次我从零搭一个“生产级”的记忆型 Agent选择 AgentScope 作为主框架核心目标就一个让 Agent 在长会话、多任务、跨时间段的场景下真正“记得住、查得快、忘得对”。1.2 记忆不是一块存储而是三个层级的协同在动手之前先把“记忆”这个抽象概念拆成可实现的工程模块。我参考认知科学里对记忆的划分把 Agent 的记忆拆成了三层短期记忆Short-term Memory当前会话内的上下文窗口对应 LLM 的 context。实现上就是消息队列 Token 窗口管理。长期记忆Long-term Memory跨会话持久化的知识与事实通常放向量数据库通过语义检索召回。对应公司的知识库、历史交互纪要。工作记忆Working Memory任务执行过程中的临时状态比如一个多步骤任务当前进行到第几步、子任务的结果缓存。它存在的意义是让 Agent 在编排复杂任务时不被历史噪音干扰。这个分层非常关键。很多人实现“记忆型 Agent”时直接把所有历史消息和知识一股脑塞进 Prompt这其实是把短期记忆和长期记忆混为一谈。结果是明明长期记忆里已经存了用户的偏好短期上下文里偏偏塞满了三年前的闲聊记录检索召回率差Token 浪费严重模型的注意力还被稀释。正确的设计是短期记忆负责“最近发生了什么”长期记忆负责“这个用户/这个问题历史上是什么样”工作记忆负责“当前这个任务进行到哪了”。三者各司其职互不污染。这是整个项目的架构基石。1.3 哪些场景“必须”上记忆型 Agent哪些可以缓一缓也不是所有 Agent 都需要记忆架构分清适用边界能省掉大量无用功。我给读者一个判断标准如果用户每次会话都要重复提供相同的身份信息/偏好/背景或者任务结果需要依赖历史数据沉淀那就必须上记忆如果只是单轮问答式的 API 封装先别折腾记忆把工具调用做好再说。典型的必须上记忆的场景个人助理类记住用户偏好、日程习惯、家庭信息客服系统跨会话识别老客户自动带出历史工单写作辅助持续记住项目的风格指南、术语表数据分析 Agent跨多次查询沉淀数据口径和业务定义教育辅导跟踪学习进度、错题本、知识薄弱点这几个场景下无状态 Agent 的体验基本是不可用的。我这次项目选的就是个人知识助理 数据分析混合体跑通后你会看到记忆能力如何直接改变 Agent 的可用性。2. AgentScope 凭什么适合做记忆型 Agent核心机制拆解2.1 为什么我没选 LangChain 或 AutoGen而是押注 AgentScope国内做 Agent 开发主流选择无非 LangChain、AutoGen、LlamaIndex 这几条路。它们当然成熟但有一个共性问题框架层对“记忆”的支持非常浅基本停留在“外部向量库 Callback 插件”的拼凑状态。你要做分层记忆、做会话隔离、做记忆回滚得自己去跟框架底层的消息流搏斗。AgentScope 不一样它的设计哲学是“Message 为中心”。所有 Agent 之间的交互、记忆模块与 Agent 之间的数据交换、工具调用的返回值统一抽象为消息对象。消息既是通信单位也是记忆存储的单位。这意味着你要做记忆不需要去看框架源码、不需要 hack 内部数据结构只需要在消息生命周期里插入你的记忆策略。我可以直接抛一个结论选型时不要看谁 star 多要看框架对“状态”的态度。LangChain 把状态当外挂AgentScope 把消息当一等公民——后者显然更适合记忆型应用的长期演进。当然这不是说 LangChain 不能用而是你付出的定制成本完全不同。2.2 Message 驱动的通信模型记忆的实现单元AgentScope 的核心数据结构是Msg。它的基本字段长下面这样from agentscope.message import Msg msg Msg( nameuser, content帮我查一下上季度华东区的销售数据, roleuser, metadata{ session_id: sess_2024001, timestamp: 1700000000, intent: query_sales } )metadata字段是我认为记忆模块最值得利用的入口。你可以在消息进入记忆层之前往 metadata 里塞会话 ID、用户 ID、时间戳、意图标签、甚至 token 数。这样记忆模块在写入和检索时就能基于 metadata 做时间衰减、会话隔离、意图过滤而不是像传统方案那样对整个消息文本做粗暴的 top-k 向量召回。从工程角度理解消息不再是“一去不复返”的一次性数据它是记忆仓库里的一个可检索实体。每一条用户消息、每一条 Agent 回复、每一次工具调用结果都可以被打上标签后持久化。这个理念贯穿 AgentScope 的整个服务端设计2.0 版本开始引入 Runner 和 Server 模式目的就是帮开发者把“瞬时交互”沉淀为“长期状态”。2.3 Pipeline 与 Runner 的调度机制记忆如何参与每一轮AgentScope 2.0 引入的 Pipeline 机制把我的工作简化了一大截。以前做多步记忆写入需要自己封装“监听器/回调”现在可以直接在 Pipeline 里声明式地编排from agentscope.pipeline import Pipeline pipe Pipeline( steps[ retrieve_memory_step, # 检索长期记忆 build_prompt_step, # 组装上下文 llm_inference_step, # 模型推理 memory_write_step, # 写入本轮记忆 response_rerank_step # 结果后处理 ] )这个 Pipeline 的每个 step 就是一个可编程的函数可以在 step 之间传递上下文对象。记忆模块在 step1 里读取在 step4 里写入。最妙的地方是如果某个 step 挂了整个 Pipeline 的状态是可观测的不会出现“记忆写了一半就崩了导致脏数据”的隐性问题。Runner 在 2.0 里则负责把 Agent 发布成服务支持 HTTP/RPC 调用。它解决了生产环境的另外一个痛点——记忆状态必须跨进程存活。如果你的 Agent 只是跑在 Jupyter Notebook 里的局部变量那谈不上生产级Runner 让 Agent 常驻记忆写入外部存储进程重启后状态不丢。3. 亲手实现记忆模块分层设计 代码级落地3.1 短期记忆的工程实现滑动窗口 摘要压缩短期记忆的目标是“效率和连贯性的平衡”。你不能无限塞对话历史也不能只留最后几条导致上文丢失。我的做法是一个两段式结构消息队列用双端队列保存当前会话最近 N 轮消息我设了 10 轮可配。滚动摘要当消息超过窗口阈值调用 LLM 把旧消息压缩成一个 300 字以内的摘要替换进上下文。这背后的逻辑是旧的细节往往不重要但旧的意图和结论很重要。摘要保留的是后者丢弃的是前者。你没必要每轮都把三天前的原文 Prompt 一遍但你需要让 Agent 知道“用户一直在纠结预算问题”。from collections import deque from agentscope.message import Msg class ShortTermMemory: def __init__(self, window_size: int 10, summarizerNone): self.window deque(maxlenwindow_size) self.summarizer summarizer self.running_summary def recall(self) - list[Msg]: msgs list(self.window) history .join(m.content for m in msgs) if self.summarizer and (len(history) 6000): self.running_summary self.summarizer(history) return list(self.window)看到没短期记忆里就已经需要一个轻量的“遗忘机制”。这个遗忘不是删除而是压缩。窗口外的信息以压缩形态存活而不是彻底消失。这点和人的记忆策略其实很像。3.2 长期记忆的实现向量存储 语义检索 元数据过滤长期记忆我用的方案是向量数据库具体选型是Chroma 一个自定义 metadata 索引。为什么不直接上 Milvus 或 ES因为单机开发阶段没必要Chroma 在单台机器上跑得很稳后期需要扩展时再替换存储后端即可。架构上把长期记忆封装成接口换后端只改一个工厂函数。长期记忆的读写路径是写入用户与 Agent 的高价值对话结论、偏好、知识点经过一个MemoryFilter判断是否值得长期保存。不是所有消息都进长期记忆否则会污染检索质量。读取当前轮次的消息先做意图分析提取关键词然后从长期记忆里检索语义相近的历史记录再按 metadata 里的 session/user 维度过滤最后把命中结果注入 Prompt。值得强调的是写前过滤这个环节。我见过太多人把“All 消息全存向量库”当默认操作结果检索时混入大量“今天天气怎么样”这类毫无长期价值的对话召回质量一塌糊涂。长期记忆的写入应该有门槛我设计的过滤规则是包含明确的结论性词汇“最终决定”“原因是”“我的偏好是”消息长度超过 20 个汉字过滤闲聊与用户画像相关身份、偏好、禁忌任务执行完成时产生的结构化结果如“查询返回华东区 Q3 销售额 2300 万”这四条规则用简单的规则引擎 LLM 二重判断过滤精度能到 85% 以上。剩下 15% 的漏网之鱼靠定期人工复盘清洗。3.3 工作记忆的实现任务状态机 局部上下文工作记忆这块我起初是忽略的直到项目跑到第三个版本才意识到它的必要。场景是这样的Agent 执行一个“先查数据、再做可视化、最后写分析报告”的多步任务每一步的工具调用结果都需要在下一步中被引用。如果这些中间结果都放在短期记忆里会有一个麻烦短窗口被工具输出占满用户会话的历史反而被挤出去。所以我把任务执行期间的中间产物单独放进一个TaskStateStoreclass TaskStateStore: def __init__(self): self.states {} def set(self, task_id: str, key: str, value: any): self.states.setdefault(task_id, {})[key] value def get(self, task_id: str, key: str): return self.states.get(task_id, {}).get(key) def cleanup(self, task_id: str): self.states.pop(task_id, None)工作记忆的生命周期绑定任务本身任务结束就清理避免长期占用内存。在 Pipeline 里每一步的 step 函数都从 TaskStateStore 里取上下文处理完再写回。这样做的好处是子任务之间不会串数据任务结束后内存不会泄漏调试的时候可以用 task_id 精确还原每一步的状态。3.4 组装记忆模块的完整上下文构建流程把三层记忆组装起来一次完整请求的上下文构建流程长这样用户进入新消息 - 工作记忆取任务状态如果有 - 短期记忆取最近 N 轮摘要 - 长期记忆按语义检索到 top 5 条相关历史 - Pipeline 拼接成最终 Prompt - LLM 推理 - 短期记忆追加本轮消息 - 长期记忆过滤后写入高价值信息 - 工作记忆更新任务状态这个流程看起来平平无奇但真正跑起来就知道顺序不能乱。如果先读长期记忆再做意图分析检索出来的结果就没有上下文语义召回质量会大幅下降。如果短期记忆写在长期记忆前两条消息里的冗余信息会让模型分不清“刚说的”和“以前说的”。顺序即架构这一点在 Agent 开发里尤为重要。4. 生产级改造持久化、会话隔离与并发控制4.1 记忆的持久化为什么不能只存在内存里开发阶段记忆放内存没问题但一旦上了生产三个场景会让你崩溃进程重启发版、崩溃恢复、扩容缩容任何一次重启都会清空内存记忆。用户第二天来打招呼Agent 完全不记得他是谁。多实例部署横向扩到两台机器用户请求负载均衡到机器 A下次又到机器 B记忆各存各的等于没有记忆。审计追溯业务方想查“上个月某用户为什么收到这个推荐”你没法从内存里翻历史。所以必须外部化。我的方案是把三层记忆统一抽象成一个MemoryStore接口底层分别对接记忆层级存储介质理由短期记忆RedisList Hash低延迟支持 TTL 自动过期长期记忆向量库 PostgreSQL向量检索 关系属性查询工作记忆Redis Hash高性能读写任务级 TTL短期记忆和工作记忆用 Redis 是天然合理的因为它们的读取频率极高且都需要过期机制短期的会话结束即过期工作的任务结束即清除。长期记忆用 PostgreSQL 存 metadata 做结构化查询向量库只负责向量检索两者通过 ID 关联避免向量库承担它不擅长的关系查询。4.2 会话隔离多用户的记忆绝对不可串线这是记忆型 Agent 生产化最容易做错的一环。很多人在概念阶段做单人 Demo记忆模块没有用户维度一切平安一上线多用户同时使用用户 A 的偏好被用户 B 检索到形成严重的隐私事故。我的做法是所有记忆实体从一开始就带user_id和session_id双维度标签。写入时强制校验读取时强制过滤并且在检索阶段把这两个字段作为不可绕过的过滤条件def recall(user_id: str, session_id: str, query: str): # 先按 user_id session_id 做硬过滤再做语义检索 candidate_ids db.query( user_iduser_id, session_idsession_id ) vectors vector_store.search(query, filter{(id: candidate_ids)}) return vectors顺序问题再次出现先过滤后检索而不是先检索后过滤。如果你在向量库做全量检索再在代码里按 user 过滤一方面浪费算力另一方面向量相似度排序阈值会被跨用户的相似内容污染——两个用户问过相似的“报销流程”A 的内容会把 B 的顶下去。先过滤候选集小且纯净召回质量才有保障。业务上我还做了一个防御性的审计日志所有记忆写入操作记录到独立的 audit 表包含 user_id、写入时间、消息哈希、写入源头模块。一旦出现数据异常或用户投诉能快速定位是哪条链路写入了错误记忆。生产系统不怕出错怕的是出错后查不到根因。4.3 并发与负载高频记忆读写是性能瓶颈不是模型推理多数人以为生产级 Agent 的瓶颈在 LLM 推理实际跑起来才发现记忆层的并发读写往往先成为瓶颈。原因很朴素一次 LLM 推理可能要 2~5 秒期间记忆层的读写可能发生 4 到 6 次如果每个用户每秒产生几十条消息记忆层 QPS 就是请求数的好几倍。应对方案我用三招缓存同一 session 近 30 分钟的检索结果加 Redis 缓存过期时间 30 分钟。用户高频追问同一个主题时不需要每次都向量检索。批量写入短期记忆和工作记忆的写入走批量接口攒 10 条消息或 500ms 内批量 flush 一次减少磁盘/网络往返。异步化长期记忆的写入操作放进消息队列主流程不等写入完成就返回用户。记忆写入失败通过死信队列重试不阻塞对话响应。这三招组合下来压测数据32 并发模拟用户、每用户 50 轮对话P95 响应时间从 8.2 秒压到 3.5 秒其中模型推理占 2.1 秒记忆层损耗从 3 秒降到 0.4 秒。生产可用的标准线是 P95 低于 5 秒这个方案是达标的。4.4 记忆的容错与回滚写坏了怎么办记忆系统最隐蔽的风险是“写坏了”。典型情况有两种错误记忆污染Agent 在一次错误推理中脱口而出“你的生日是 1990 年 3 月 5 日”被 MemoryFilter 误判为高价值信息写进了长期记忆。之后每次对话这个错误事实都被召回Agent 反复确认一个假信息用户越解释它越坚持。记忆膨胀某次任务产生大量中间状态写入长期记忆导致后续检索基准线被污染原本最相关的历史排不上。所以生产级记忆系统必须有回滚机制。我的实现是在长期记忆的每条记录上加version和deprecated标记。允许业务方通过管理接口将某条记忆置为废弃甚至回滚到指定 versiondataclass class MemoryRecord: id: str user_id: str content: str version: int deprecated: bool False created_at: int实际运营中我每周会抽样检查一遍系统的记忆写入日志看有没有 Agent 自己“编造”的记忆混进来。一旦发现异常直接在管理后台废弃掉不需要改代码、不需要重新部署。这个能力在开发期看不出价值运营一个季度就知道有多救命。5. 踩坑实录从 Demo 到稳定运行的五个大坑5.1 坑一记忆污染——Agent 把自己幻觉出来的内容当事实存了进去这是我遇到的第一个严重事故。某个用户问“能否介绍下你们的产品定价”Agent 回复时顺带编了一个“我们提供开发者社区免费高级版年费 199 元”然后被 MemoryFilter 规则 1结论性词汇拦进去存了长期记忆。第二天该用户又问“你们有免费版吗”Agent 基于这条错误记忆一本正经地回答“有高级版免费”差点造成商务事故。事后反思问题出在过滤器太依赖文本特征缺少“记忆来源可信度”判断。修复方式凡是 Agent 自己生成的结论性文本写入长期记忆前必须经过额外的二次校验——要么有工具调用的结构化结果背书要么有用户明确的确认语句。用户说了“对”“是的”“可以”才算数。这个教训让我意识到记忆系统的“写权限”比“读权限”更敏感。生产环境下宁可不存不可错存。清洗脏数据的成本永远是预防成本的十倍以上。5.2 坑二向量召回“看似相关实则无用”爬过向量检索的都知道语义相关的 top-k 结果并不等于有用的结果。最常见的情形用户问“上季度的销售报告”向量库召回了一条用户半年前说的“我想看季度销售数据”还召回了一条别人说的“销售数据口径按出库计”。第一条是历史意图不是知识第二条是别的用户的业务口径——后者简直是灾难因为 Agent 会把 A 用户场景下的定义套到 B 用户头上。解决方案是上文提到的“先过滤后检索 metadata 硬条件”。具体执行上我把召回结果的过滤条件写成强制约束不再只是加分项。同时把召回结果的数量从 5 条收紧到 3 条每条结果经过一个RelevanceReranker二次排序。Reranker 是轻量级模型只判断“本条记忆与当前问题是否同一主题”过滤掉那些“字面相关、逻辑无关”的噪音。这里我想多说一句RAG 方案的成败一半在数据质量一半在检索策略模型只占一小部分。如果你把向量库的 embeddings 调得再漂亮但召回前没有做用户级/会话级过滤结果还是垃圾进垃圾出。5.3 坑三上下文串线——两条平行任务的数据互相干扰这个坑在工作记忆实现不当的时候特别明显。早期版本我把 TaskStateStore 的 key 设计成全局的current_task结果两个任务并行跑时互相覆盖。用户在任务 A 里上传了一个 CSVAgent 正在分析用户又发起了任务 B 问“翻译一下这句话”任务 B 的 LLM 调用居然拿到了任务 A 的中间结果导致翻译结果里混入了“CSV 行数 1024”这种莫名其妙的信息。修复方案很直接给每个任务生成唯一 task_id并在消息的 metadata 里显式携带。Pipeline 的每个 step 通过task_id从任务状态里取对应上下文而不是取一个全局的“当前状态”。这个修复也让调试变得舒服——每个任务的上下文可以独立回放看到底是哪一步把数据写乱了。5.4 坑四记忆膨胀导致响应变慢上线一段时间后用户的长期记忆库越存越多。你猜会发生什么向量检索的延迟从 30ms 涨到 150ms但真正拖垮系统的是召回结果注入 Prompt 后上下文中无关记忆的增多稀释了模型注意力推理质量下降甚至回答变含糊。解决办法不是买更好的 GPU而是做记忆的“季节性清理”过期策略超过 90 天未复用的长期记忆打入冷库不在常规检索范围内。嵌套摘要同一主题的多条记忆每隔一周被压缩成一条综述性记忆旧细节降权。动态预算长期记忆召回条数根据当前 Prompt 剩余 token 自适应上下文紧的时候只召回 2 条宽裕时召回 4 条。这套清理策略跑起来之后响应延迟回落到接近初始水平回答的清晰度也回来了。记忆不是越多越好而是越“精”越好。5.5 坑五测试难写——记忆型 Agent 的回归测试比普通开发难一个量级普通 Agent 的测试可以预设输入输出断言某一个回复是否包含关键词。但记忆型 Agent 存在状态同一个输入在不同记忆状态下结果是不同的。我早期写测试遇到最大的挫败是昨天跑通的用例今天因为长期记忆库里被写入了测试产生的脏数据结果变了。现在的解决方案是三层测试隔离单元测试纯函数级别的测试只测记忆模块的读写和不涉及外部状态的算法逻辑用 Mock 数据。集成测试用内存向量库和历史快照文件每个测试用例启动一个全新记忆环境测完立刻销毁。回归测试手工构造 20 个标准的“记忆状态 问题”组合跑完整 Pipeline把输出结果与基准版本做 diff。最后重点强调测试数据与生产数据必须物理隔离。我吃过一次亏测试用例把“用户生日是 1970 年涨到 110 岁”写进了共享开发库结果演示环境把 110 岁当真了。现在所有测试环境都有独立的 Redis 实例和向量库目录做假数据真隔离。6. 学习路线如何从零到这个水平6.1 从跑通 Demo 到理解架构再回扣业务场景很多读者问我我也想做记忆型 Agent应该从哪开始我按自己走过的路径给一条比较省时间的路线第一步1~2 周把 AgentScope 官方示例跑一遍理解 Msg、Agent、Pipeline 三个核心概念。这个阶段不要追求深度目标是“手能跟着代码动”。第二步2~4 周拆掉示例里的假记忆自己实现一个最简单的“会话级记忆”——就是一个 deque 存历史消息每轮塞进 Prompt。跑通后你会直观感受到记忆的重要性。第三步1 个月读 AgentScope 2.0 的 Runner 和 RAG 相关源码搞清楚服务化部署时状态如何管理。这个阶段开始看分布式概念缓存、队列、向量检索。第四步1 个月回到业务场景。找一个小而真实的业务比如个人日程助理、学习答疑机器人把它当成“生产系统”来做加用户隔离、加持久化、加监控告警而不是满足于 notebook 里跑通。6.2 关键技术栈清单我把这个项目用到的所有技术栈列出来供新人对照补课模块技术需要掌握的程度框架AgentScope 2.0熟练使用 Msg / Pipeline / Runner模型国内 API 或开源模型理解 context 窗口、temperature 等参数短期记忆Redis数据结构、TTL、批量操作长期记忆Chroma / 向量检索向量化、相似度检索、metadata 过滤存储PostgreSQL基本表设计、索引、事务异步Celery / Redis Stream消息队列、重试、死信处理监测Prometheus Grafana指标采集、告警规则给新人的一句掏心窝子的话不要一上来就学所有的东西。先跑通最简单的“会话级记忆 Demo”然后再一个一个叠加能力。叠加的每一步你都会更理解上一层设计的必要性。6.3 独立博客复盘完整跑完这个项目后给大家分享几篇短视频。项目代码我已经整理成独立仓库注释写得很详细适合对照着学。你也可以按照下面的顺序把主要模块都重新实现一遍动手和看代码完全不是一回事。7. 写在最后的经验沉淀这个项目从最初的简单“对话记忆 Demo”迭代到现在稳定的生产级架构前后花了大概四个月。回头看最初让我最兴奋的还不是代码能跑通而是用户真正感受到“Agent 记得我”的那一刻——你问它“上次说的那个预算问题后来怎么样了”它真的能基于跨会话记忆给你一个连贯的回答。那种体验上的跃迁比任何指标提升都更有说服力。至于项目里踩过的那些坑我想强调一句话记忆型 Agent 的工程难点不在于“存”而在于“存得准、取得到、忘得掉”。存得准靠过滤校验取得到靠分层架构和检索策略忘得掉靠清理和回滚机制。这三件事每一步都不复杂但每一步都容易出错而且错得很隐蔽——因为系统不会立刻崩溃只会在某个深夜给出一个让你怀疑人生的错误回答。所以如果你正在做类似的 Agent 项目我的建议是先别急着堆功能把记忆层的写入策略、隔离策略、回滚机制三件事想清楚再考虑多智能体协作、自动规划这些花活。地基稳了上层才能真的跑起来。希望这篇分享能帮你少踩几个我踩过的坑。
返回列表