去年我在复盘一个客服问答机器人项目时,发现一个特别扎心的现象:单轮问答的准确率已经做到 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()方法。我一开始不理解这个抽象有什么好,直到我把所有业务都往里面塞,才意识到必须区分两种 Agent:
- DialogAgent:纯对话,适合闲聊、答复、解释类场景,只需要人设和模型配置。
- ReActAgent:带工具调用的推理循环,适合需要查询订单、查用户画像、调内部系统接口的场景。它内部维护"思考—行动—观察—再思考"的循环,直到收敛出最终答案。
我们的客服 Agent 用的是 ReActAgent,原因很简单:用户问"我上周的订单到哪了",模型不可能编出一个物流状态,它必须去调用订单查询工具。下面是一个简化版的初始化示例,工具直接用普通函数注册。
from agentscope.agent import ReActAgent def query_order_status(order_id: str) -> str: """根据订单号查询物流状态,返回 JSON 字符串""" # 内部调用订单中台的 HTTP 接口 return '{"status": "已发货", "eta": "明天下午"}' agent = ReActAgent( name="assistant", sys_prompt="你是一名售后客服助理。回答用户问题前,先判断是否需要查询工具。", model_config_name="qwen_plus", tools=[query_order_status], memory=memory, )这里要注意一个细节:工具函数的 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( query=query, top_k=5, threshold=0.7, namespace=current_user_id, ) return format_memory_hits(hits)这里有个很重要的取舍:top_k不要贪多。我在调优时发现 top_k 从 5 调到 10,召回率上升不到 3%,但 prompt 里噪声增加带来的混淆率明显上升。生产环境里我们固定 top_k=5,并且在召回结果里附上记忆的写入时间,让模型有判断依据。
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_model=embedding_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 字无重叠 | 10 | 61% | 1.8s |
| 512 字,重叠 64 字 | 8 | 74% | 2.1s |
| 384 字,重叠 48 字 + 重排 | 5 | 82% | 2.6s |
最终线上选了 384 字切块、重叠 48 字、top_k 5 加简单规则重排的方案,准确率和耗时的平衡最好。
5. 从 Demo 到生产:持久化、可观测性与容错设计
5.1 会话状态不能只靠框架内存
AgentScope 的记忆类默认跑在进程内存里,这对开发调试没问题,但上线必须改造。生产环境我们的做法是:每轮对话产生的 Msg 经过序列化后写入 Redis,保留最近 30 轮;同时定期把关键记忆快照写入 MySQL,用于重建和审计。
import json import redis r = redis.Redis(host="...", port=6379, decode_responses=True, db=1) key = f"agent:session:{user_id}" # 追加一条消息并裁剪到最近 20 轮 r.rpush(key, json.dumps(message_dict, ensure_ascii=False)) r.ltrim(key, -40, -1)为什么要保留 40 条再裁剪?因为用户可能翻聊天记录后说"昨天关于发票那件事怎么说来着",如果只保留最近 10 轮,这类问题就答不上来。Redis 里多存 30 轮的成本很低,但带来的回溯能力提升很明显。
5.2 可观测性:没有追踪,记忆型 Agent 出问题只能靠猜
记忆型 Agent 的可观测性比普通 API 复杂,因为一个回答可能依赖历史消息、记忆召回、工具结果三部分。我在项目里给每个请求生成一个 trace_id,然后在关键节点打结构化日志:模型输入、工具调用、记忆检索结果、最终回复。出问题的时候,第一件事就是看 trace_id 串起来的时间线。
我长期监控的几个核心指标:
| 指标 | 说明 | 告警线 |
|---|---|---|
| 模型调用 P95 延迟 | 回答卡顿的直接反映 | > 3.5s |
| token 消耗/请求 | 成本控制的核心 | 随预算走 |
| 记忆召回为空率 | 长期记忆无效的征兆 | > 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
上线第二周用户报了一个诡异的 bug:A 用户询问退款,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 项目真正的难点不在"记忆"两个字,而在"生产级"三个字:隔离要做好、持久化要可靠、指标要能监控、失败了要有降级。把这些地基打牢,哪怕你后来换了模型、换了框架,这套设计照样能平移过去。我重构两次才想明白这个道理,希望这篇复盘能帮你少走一次弯路。