
去年我在复盘一个客服问答机器人项目时发现一个特别扎心的现象单轮问答的准确率已经做到 87%但用户稍微换个话题再绕回来模型就彻底失忆了——它记不住十分钟前自己说过的话更不用说上个月用户咨询过的偏好。这个项目最后被我重构了两次第二次重构时我决定不再自己堆一堆低层组件而是用 AgentScope 从零搭一个带记忆的 AI Agent。这篇文章就是那次重构的完整复盘从记忆机制的选型、RAG 服务的接入到生产环境的容错和监控全部摊开讲。如果你正在做从 0 到 1 搭建 AI Agent或者已经看过不少 AgentScope 教程、想从练手小项目进阶到能上线的系统这篇内容应该能帮你省掉一两个月的弯路。需要提前说明的是AgentScope 版本迭代很快2.0 之后引入了不少新能力我下面写的内容基于我线上使用的分支个别类名和接口可能随版本微调但核心设计思路是稳定不变的。1. 记忆型 Agent 到底在记什么拆掉记忆这个词1.1 会话记忆、事实记忆、知识记忆是三回事很多人一上来就冲记忆二字把短期上下文、长期用户画像、外部知识库混在一起处理结果代码写得一团糟。我的经验是先把记忆拆成三层。会话记忆短期当前这轮对话里已经说过的话。它负责让 Agent 能指代上文比如用户说第二个方案呢你得知道第二个指的是什么。这层记忆通常以消息列表的形式存在跟着会话走。事实记忆长期跨会话沉淀的用户事实。比如用户是铂金会员上周已经退款过一次偏好简短回答。这层记忆是结构化或半结构化的需要写入存储并在后续会话中召回。知识记忆RAG领域知识库比如售后政策、产品文档、工单处理方法。它本质上是查资料不需要把整个数据库塞进模型上下文而是按需检索。这三层如果混在一个代码文件里初期跑通没问题等数据量上来、并发上来你就会发现调一个问题要动三个地方。AgentScope 之所以适合做记忆型 Agent是因为它在框架层面就把消息记忆检索分开了我们可以按层设计而不是在一个 reply 函数里硬塞逻辑。1.2 生产级记忆的三个硬指标Demo 里能记住东西不算本事生产环境要看的指标其实就三个持久化、召回质量、更新与遗忘。持久化服务重启、进程崩溃、发版部署之后记忆不能丢。内存里的 list 或者 dict 是扛不住的必须落到 Redis、MySQL 或者外部向量库。召回质量该想起的想得起不该想起来的别拿出来。召回太宽会把噪声塞进 prompt太窄又等于没记。这不是调一个 top_k 就能解决的事它牵扯到切块策略、向量模型、重排策略。更新与遗忘用户说我把偏好改一下的时候旧记忆要能被替换不是每条都 append 进去。记忆如果只能增不能改时间久了上下文里全是自相矛盾的结论Agent 会越来越蠢。我见过很多项目死在第三点上上线三个月后用户画像库里堆了一万条互相冲突的记录Agent 每次回答都像人格分裂。所以从第一天起记忆管理就要考虑版本和覆盖机制别等出了问题再返工。1.3 为什么我用 AgentScope 而不是自己拼轮子在选框架之前我其实是反对过早引入框架的。但记忆型 Agent 有个特点它要在模型调用—记忆读写—工具调用—状态更新之间反复跳转自己拼的话每一个跳转点都要写胶水代码。等业务复杂到一定程度请求权链路的日志都看不清。AgentScope 解决的是这个结构性问题。它给我提供了几个最关键的东西统一的 Msg 消息协议人和模型对话、工具返回、记忆存储都用同一种数据结构流转可插拔的 Agent 抽象DialogAgent 管普通对话ReActAgent 管推理加工具调用不用自己实现循环自带 TemporaryMemory、LongTermMemory 等记忆类短期和长期从接口上就分开了2.0 里把 RAG 做成了服务知识库接入、切块、召回一步到位不用自己搭向量检索链路。另外 AgentScope 对国内模型的接入做得比较顺官方也有中文文档遇到问题翻文档和社区讨论的效率比纯英文框架高不少。如果你所在团队还有 Java 中台那更要注意生产中通常是 Java 侧通过 HTTP 网关调用 Agent 服务Agent 核心编排留在 Python 侧而不是在 Java 里重写一套 Agent 逻辑。AgentScope 提供 Gateway 这类能力之后Java 侧对接成本可以压得比较低。2. AgentScope 的地基模型封装、Agent 抽象和消息流转2.1 模型封装换模型不换业务代码AgentScope 里调用模型不是直接写 requests而是通过 Model Wrapper 做统一封装。我线上环境主要用的 DashScope 兼容接口和 OpenAI 兼容接口配置写在agentscope.init的 model_configs 里。import agentscope agentscope.init( model_configs{ qwen_plus: { model_type: dashscope_chat, config_name: qwen_plus, model_name: qwen-plus, api_key: sk-xxx, generate_args: { temperature: 0.7, max_tokens: 2048, }, }, qwen_turbo: { model_type: dashscope_chat, config_name: qwen_turbo, model_name: qwen-turbo, api_key: sk-xxx, generate_args: { temperature: 0.3, max_tokens: 1024, }, }, } )如果你用的是其他模型或者公司内部网关只要网关提供 OpenAI 兼容接口把model_type换成openai_chat、填上base_url就行。这个设计的价值在于业务代码里写的是配置名不是具体模型名后面想从主力模型切到轻量模型改一行配置就能灰度不用改 Agent 逻辑。2.2 Agent 抽象把角色和行动分开Agent 在 AgentScope 里的核心是一个reply()方法。我一开始不理解这个抽象有什么好直到我把所有业务都往里面塞才意识到必须区分两种 AgentDialogAgent纯对话适合闲聊、答复、解释类场景只需要人设和模型配置。ReActAgent带工具调用的推理循环适合需要查询订单、查用户画像、调内部系统接口的场景。它内部维护思考—行动—观察—再思考的循环直到收敛出最终答案。我们的客服 Agent 用的是 ReActAgent原因很简单用户问我上周的订单到哪了模型不可能编出一个物流状态它必须去调用订单查询工具。下面是一个简化版的初始化示例工具直接用普通函数注册。from agentscope.agent import ReActAgent def query_order_status(order_id: str) - str: 根据订单号查询物流状态返回 JSON 字符串 # 内部调用订单中台的 HTTP 接口 return {status: 已发货, eta: 明天下午} agent ReActAgent( nameassistant, sys_prompt你是一名售后客服助理。回答用户问题前先判断是否需要查询工具。, model_config_nameqwen_plus, tools[query_order_status], memorymemory, )这里要注意一个细节工具函数的 docstring 会被拼进给模型的工具描述里。所以 docstring 一定要写清楚这个函数什么时候用、输入是什么、输出是什么格式。我见过不少同事写工具函数不写 docstring结果模型经常在错误场景触发错误工具排查半天。2.3 Msg 是整条链路的血管AgentScope 里所有交互都走 Msg 对象字段大致是name、content、role还可以挂额外的 metadata。这个设计在记忆型 Agent 里特别重要因为我们可以给消息打标签。举个例子用户说我不需要物流短信了这条消息既是对话内容也是需要写入用户画像的事实。我在进入 ReAct 循环之前会先把这类消息标记为meta: preference_update然后由记忆模块决定是写入长期记忆还是只留在会话记忆里。这样做的好处是日志可追踪、记忆写入可审计出现问题能一条条回放。另外AgentScope 的 Pipeline 机制可以管理多 Agent 的执行顺序。我们当时虽然只有一个 Agent但后面接工单系统时用到了分支 Pipeline让客服 Agent 和工单 Agent 并行处理再合并结果。这部分的建议是单 Agent 阶段别滥用 Pipeline先用最简单的顺序调用跑通等流程复杂了再上编排。3. 短期记忆落进 ReAct 循环让上下文真正可控3.1 TemporaryMemory 不是没有用的记忆很多人一听短期记忆就急着上向量库其实对于客服场景绝大多数需求用短期记忆就够了。AgentScope 的 TemporaryMemory 本质上是在一个会话范围内维护消息列表并在生成下一次回复前把最近若干轮拼进 prompt。我用它的时候踩过一个认知误区以为短期记忆就是直接把所有历史消息一股脑塞给模型。实际上短期记忆的核心工作是截断和筛选不是全量塞入。假设用户和 Agent 聊了 20 轮如果每一轮都完整放进模型上下文很快就把 token 预算耗光了。实际操作中我会用临时记忆维护最近 6 到 10 轮的消息再额外维护一份本会话关键信息摘要每 5 轮用模型滚动更新一遍摘要。这样 prompt 结构大致是系统人设 会话摘要 最近 N 轮对话 当前问题。AgentScope 的 Msg 可以携带 metadata我把这条消息是否已进入摘要作为标记存在 metadata 里避免摘要重复注入。3.2 ReAct 循环里检索记忆的正确姿势ReActAgent 的推理循环里有一步是观察。这一步特别适合做记忆检索。我们的实现是模型产生 Thought 后如果决定需要查一下用户画像就会调用一个retrieve_user_memory工具。这个工具去长期记忆里做向量召回把命中的用户事实返回给模型。def retrieve_user_memory(query: str) - str: 检索与 query 相关的用户长期记忆返回记忆条目列表 hits long_term_memory.query( queryquery, top_k5, threshold0.7, namespacecurrent_user_id, ) return format_memory_hits(hits)这里有个很重要的取舍top_k不要贪多。我在调优时发现 top_k 从 5 调到 10召回率上升不到 3%但 prompt 里噪声增加带来的混淆率明显上升。生产环境里我们固定 top_k5并且在召回结果里附上记忆的写入时间让模型有判断依据。3.3 上下文窗口管理的实操经验短期记忆的另一个坑是 token 计算。你以为自己控制了 10 轮对话但用户一条消息可能 2000 token工具返回可能 3000 token实际上下文早超了。我总结了一套预算分配规则供你参考假设模型上下文上限是 8K token我会按 50% 留给系统人设和历史对话、30% 留给工具返回和记忆召回、20% 留给模型输出余量来规划。一旦超出优先截断的是工具返回值而不是系统人设和最近几轮对话。工具返回通常在观察环节被消化截断了影响最小。另外如果有用户连续追问的场景比如刚才说的退款具体什么时候到账单靠上一轮对话其实不够因为刚才说的退款指的是更早的一轮。这时候我会从会话摘要里把退款相关信息单独提取出来作为附加上下文注入到当前请求里。这个操作我在 AgentScope 里是通过在 prompt 前追加一栏当前请求的关联历史实现的效果比单纯扩大轮数好很多。4. 长期记忆落地向量检索与 RAG 服务的组合拳4.1 LongTermMemory跨会话的事实仓库用户画像、历史诉求这类跨会话记忆不能放在 TemporaryMemory 里因为会话一结束就清了。AgentScope 的 LongTermMemory 设计是记忆条目写成 MemoryEntry落库并建立向量索引查询时通过 embedding 召回相关条目。我在初始化长期记忆时的核心配置是 embedding 模型的选择。如果生成和查询用的是不同版本或不同类型的 embedding 模型召回质量掉得很快。线上环境我们统一用同一个文本向量模型并且固定了文本归一化方式避免因为前后格式不一致导致想不起来。from agentscope.memory import LongTermMemory long_term_memory LongTermMemory( embedding_modelembedding_config, # 实际实现中会有一个落库和召回用的 memory backend )这里必须提醒一句长期记忆要按用户隔离。也就是说每次查询都要带上 namespace 或者 user_id否则用户 A 的用户画像可能被用户 B 的对话召回出来这是生产事故级别的 bug。后面第 6 节我会详细讲这个坑的排查过程。4.2 AgentScope 2.0 的 RAG 服务知识库即插即用2.0 版本最让我省心的变化是RAG as a service。之前我自己搭知识库链路时要处理文档解析、切块、Embedding、向量存储、召回 API 五个部分任何一个环节出问题都会让回答质量崩盘。AgentScope 2.0 把 RAG 做成了独立服务之后业务方只需要做两件事注册文档、发起查询。我在项目里把售后政策、产品手册、历史 FAQ 全部灌进了 RAG 服务。接入方式大致是这样的import requests # 注册文档把切好的文本块和元数据发给 RAG 服务 resp requests.post( http://127.0.0.1:8000/v1/rag/documents, json{ namespace: tenant_id, chunks: [ {text: chunk_text, meta: {source: refund_policy.md}}, ], }, )查询时走统一的 ask 接口resp requests.post( http://127.0.0.1:8000/v1/rag/ask, json{ session_id: session_id, question: 退货的最晚时间是什么时候, }, ) answer resp.json()[answer]RAG 服务化带来的一个额外好处是中台友好。公司内部如果有 Java 中台或者其他语言写的 BFF 层它们不需要理解 Python 的 Agent 框架只需要调用这个 HTTP 接口就能复用知识库能力。这就是为什么架构上Agent 编排在 Python、能力开放靠网关是比较稳妥的生产形态。4.3 检索质量调优决定想起了什么的三个旋钮切块大小和重叠度我实测下来中文场景切块 300 到 500 字、重叠 50 字左右效果比较好。太长会让一块内容混杂多个主题召回精度下降太短会丢失上下文模型拿到碎片也答不好。top_k 与相似度阈值top_k 大不一定好建议结合阈值一起调。阈值过低会召回大量无关内容过高会让该想起来的想不起来。重排策略如果知识库条目很多先粗召回 20 条再用模型或权重规则重排到 5 条效果比直接取 5 条稳定。重排的开销不小只建议在高价值查询上启用。我调优时做过一次明显的对比切块策略top_k回答准确率平均响应耗时整篇一段1024 字无重叠1061%1.8s512 字重叠 64 字874%2.1s384 字重叠 48 字 重排582%2.6s最终线上选了 384 字切块、重叠 48 字、top_k 5 加简单规则重排的方案准确率和耗时的平衡最好。5. 从 Demo 到生产持久化、可观测性与容错设计5.1 会话状态不能只靠框架内存AgentScope 的记忆类默认跑在进程内存里这对开发调试没问题但上线必须改造。生产环境我们的做法是每轮对话产生的 Msg 经过序列化后写入 Redis保留最近 30 轮同时定期把关键记忆快照写入 MySQL用于重建和审计。import json import redis r redis.Redis(host..., port6379, decode_responsesTrue, db1) key fagent:session:{user_id} # 追加一条消息并裁剪到最近 20 轮 r.rpush(key, json.dumps(message_dict, ensure_asciiFalse)) r.ltrim(key, -40, -1)为什么要保留 40 条再裁剪因为用户可能翻聊天记录后说昨天关于发票那件事怎么说来着如果只保留最近 10 轮这类问题就答不上来。Redis 里多存 30 轮的成本很低但带来的回溯能力提升很明显。5.2 可观测性没有追踪记忆型 Agent 出问题只能靠猜记忆型 Agent 的可观测性比普通 API 复杂因为一个回答可能依赖历史消息、记忆召回、工具结果三部分。我在项目里给每个请求生成一个 trace_id然后在关键节点打结构化日志模型输入、工具调用、记忆检索结果、最终回复。出问题的时候第一件事就是看 trace_id 串起来的时间线。我长期监控的几个核心指标指标说明告警线模型调用 P95 延迟回答卡顿的直接反映 3.5stoken 消耗/请求成本控制的核心随预算走记忆召回为空率长期记忆无效的征兆 30%工具调用失败率中台接口稳定的晴雨表 2%人工抽检好评率答案质量的最终判据 85%5.3 容错与降级模型和 RAG 服务都会挂生产环境最重要的教训是你的 Agent 再聪明也挡不住上游接口抖动。我在代码里做了三层降级模型层主模型超时或者报错时用带指数退避的重试重试两次还不行就切到备用模型我上面配置的 qwen_turbo 就是干这个的。代价是备用模型聪明程度下降但至少用户在等一个回复而不是等一个报错。RAG 层RAG 服务挂了不能连累整个 Agent。我们的降级策略是改用关键词检索本地缓存知识库再不行就直接让模型用系统人设兜底回答并明确告诉用户详细政策请咨询人工客服。记忆层长期记忆服务不可用时自动退化成只用短期记忆的纯对话模式。这会让 Agent 变笨但不会变崩。一个容易被忽略的点是超时设置。ReActAgent 可能一次回答要调 2 次甚至 3 次模型如果每轮都设 60 秒超时用户侧的体验就是转圈两分钟最后失败。我们给最终接口统一设了 12 秒超时内部每次模型调用超时设为 5 秒并严格限制 ReAct 循环最多 4 轮。6. 踩坑实录那些文档里不写但一定会遇到的问题6.1 记忆串号我差点把用户 A 的记忆交给用户 B上线第二周用户报了一个诡异的 bugA 用户询问退款Agent 回答里竟然提了 B 用户的订单号。第一反应是工具调用传错了 user_id查了半天发现不是工具的问题而是长期记忆模块的查询没有按用户隔离。因为我初始化 LongTermMemory 时没有设置 namespace所有用户的事实写进了同一个向量索引召回的自然是全局最相似的记忆。排查链路是这么走的先比对 A 用户的完整对话日志发现 Agent 回答里的订单号确实不在 A 的任何会话里然后查记忆写入记录发现这条记忆的 owner 字段是 B最后定位到记忆查询接口发现传入的 namespace 是空的。问题根因清楚了修复就是所有写入和查询入口强制校验 namespace并加一层白名单校验召回的记忆条目 owner 必须等于当前用户否则丢弃。这个教训让我养成了习惯凡是涉及用户数据的模块第一行先断言租户/用户隔离而不是等出问题再补。6.2 历史消息重复注入Agent 变成了复读机有段时间 Agent 经常重复上一个回答而且用户特别火大。我把模型输入日志拉出来一看发现同一段历史对话出现在 prompt 里两次。原因是我从 Redis 恢复了最近对话同时又把短期记忆里的消息也拼进了 prompt两边加起来等于让模型照着念第二遍。修复方案是在 Msg 里加全局唯一的 message_id拼接 prompt 前先对历史消息做一次按 message_id 去重。AgentScope 的 Msg 是可以挂扩展字段的我把 message_id 放在 metadata 里既不破坏协议又方便日志排查。6.3 RAG 文档更新不及时模型一直用旧政策回答上市政策更新后Agent 还在按旧政策回答而且答得振振有词。一开始我以为 RAG 服务没刷新后来发现是文档切块把新旧政策混在了同一块里召回旧政策时顺带引用了新政策的矛盾语句模型不知道怎么处理就挑了旧信息。这次之后我定了一个流程文档更新时按版本管理RAG 服务支持对同一知识源的不同版本做覆盖替换切块时对版本号敏感的内容强制按段落边界切避免新旧文本交叉。更重要的是给召回结果带上文档更新时间字段让模型在回答时效敏感问题时可以主动判断。6.4 长会话 token 爆炸滚动摘要救了命客服场景里一个工单可能持续一整天对话轮数轻松破百。如果只是简单裁剪最近 N 轮Agent 会丢失前置信息如果全部保留模型上下文迟早爆掉。我的方案是 8 轮一摘要每满 8 轮用模型把之前的内容压缩成结构化摘要后续请求只携带摘要加最近 8 轮。摘要里显式保留用户诉求、已答复结论、待办事项三个字段这样 Agent 在长会话里既不会失忆也不会被历史淹没。7. 从单 Agent 到多 Agent记忆能力再往上走的路线7.1 多 Agent 协作时记忆边界怎么划单 Agent 的记忆系统跑通之后下一步自然是想让多个 Agent 协作比如客服 Agent 负责接待、质检 Agent 负责抽检、工单 Agent 负责记录。这时候最容易犯的错是让所有 Agent 共享同一份长期记忆。协作归协作记忆边界要清晰客服 Agent 只写用户说了什么质检 Agent 只写这次服务评价如何工单 Agent 只写处理结果。我建议用 namespace 区分业务域跨域读取需要显式走网关审核避免一个 Agent 的错误记忆污染另一个 Agent 的判断。7.2 记忆的元数据管理与遗忘机制长期记忆不能只会加不会删。用户可能隔了一个月说我改主意了不要自动扣费这时候旧记忆必须被标记失效。我给每条记忆增加 created_at、updated_at、status 三个字段写入新的事实时会做语义冲突检测旧条目如果有明显冲突就置为 archived而不是追加一条矛盾记忆。遗忘机制目前我用的是时间衰减超过 180 天未被召回的画像类记忆进入待确认队列人工确认后清理。7.3 建立记忆评估集别靠感觉调参数我在项目后期搭建了一个 200 条的记忆评估集专门用来回答三个问题该记住的记住了吗、不该记住的忘了吗、记住了但用对了吗。评估集里每条样本包含模拟多轮对话和测试点跑一次全量评估能拿到召回率、误召回率、回答准确率三个数字。每次调整切块、top_k、重排策略时都跑一遍用数据说话而不是凭感觉改参数。如果你也想从 0 到 1 搭一个 AI Agent 练手我的建议是别贪多先做一个纯短期记忆的客服 Agent跑通 ReAct 循环再接入长期记忆解决跨会话问题最后才上 RAG 服务体感知识库。每一步都有明确的验证方式出了问题也好定位。AgentScope 的中文文档和社区案例都比较友好属于那种照着上手不难、深入要动脑子的框架。最后说点个人体会。记忆型 Agent 项目真正的难点不在记忆两个字而在生产级三个字隔离要做好、持久化要可靠、指标要能监控、失败了要有降级。把这些地基打牢哪怕你后来换了模型、换了框架这套设计照样能平移过去。我重构两次才想明白这个道理希望这篇复盘能帮你少走一次弯路。