
1. 为什么我要从零手搓一个记忆型 AI Agent先说结论市面上大部分所谓“AI Agent 框架”本质上只是把大模型的 API 包了一层壳加了个循环调用再塞几个工具函数进去。你拿它跑个 demo 没问题一旦要上生产环境面对多轮对话、长期记忆、工具编排、流式输出、断线重连这些真实场景立刻就会暴露出架构上的短板。我自己在这个方向上踩了差不多大半年的坑。最开始用现成的编排工具搭了一个客服问答机器人单轮问答效果还行但只要用户隔了几天再来问相关的问题它就完全不记得之前聊过什么。后来我尝试把历史对话全部塞进上下文结果 token 消耗爆炸响应速度从两秒变成十几秒成本直接翻了十倍不止。再后来我接触到了 AgentScope 这个项目它的设计思路让我重新理解了“生产级 Agent”到底应该长什么样。这篇文章不是官方文档的翻译也不是简单的 Quick Start 复述。我想做的是把 AgentScope 这个项目从架构设计到核心实现从记忆机制到流式通信从领域驱动设计到 MCP 协议集成一层一层拆开来讲清楚。无论你是刚接触 AI Agent 的新手还是已经用过 LangChain、AutoGen 这类框架想找一个更适合生产环境的替代方案的老手这篇文章都应该能给你一些可以直接抄作业的东西。AgentScope 的核心定位是一个面向生产环境的多智能体编排框架。它跟那些“玩具级”框架最大的区别在于它从第一天就是按照分布式、可观测、可扩展的思路来设计的。记忆管理、消息传递、工具调用、流式输出每一个模块都有明确的边界和接口定义。你可以在本地用单进程跑一个简单的对话 Agent也可以把它部署到多台机器上做分布式协作代码结构基本不用大改。接下来的内容我会按照“设计思路 → 核心机制 → 实操搭建 → 问题排查”这条线来展开。每一部分我都会尽量说清楚“为什么这么设计”以及“实际用的时候要注意什么”。如果你只想看代码可以直接跳到第 3 节如果你想理解背后的设计哲学建议从头看。2. 核心架构拆解DDD 如何重塑 Agent 的工程结构2.1 为什么 Agent 项目需要领域驱动设计大部分人写 Agent 的第一反应是面向过程定义一个 while 循环里面调模型、解析输出、执行工具、把结果塞回去循环直到模型不再调用工具为止。这种写法在 demo 阶段没问题但一旦业务逻辑变复杂代码就会变成一坨意大利面条。AgentScope 选择用 DDD领域驱动设计来组织代码结构这个决策我觉得是整个项目最有价值的地方之一。DDD 的核心思想是把业务逻辑和技术实现分离用领域模型来表达业务概念。放到 Agent 场景里什么是领域概念Agent 本身是一个领域实体消息是一个值对象记忆是一个聚合根工具调用是一个领域服务。这样拆分的好处是什么举个例子你有一个客服 Agent它需要记住用户的历史订单信息、之前的投诉记录、当前的情绪状态。如果用面向过程的方式写这些状态会散落在各种变量和字典里时间一长你自己都记不清哪个变量存在哪里。但用 DDD 的方式你可以定义一个CustomerServiceAgent实体它聚合了ConversationMemory、UserProfile、EmotionState这几个值对象所有的状态变更都通过实体的方法来驱动。代码的可读性和可维护性完全不是一个级别。AgentScope 的目录结构也体现了这个思路。它把agent、memory、message、model、tool这些概念分别放在独立的模块里每个模块有自己的领域模型和接口定义。你在扩展的时候只需要关注自己需要修改的那个模块不用通读整个代码库。2.2 记忆机制的分层设计记忆是 Agent 从“玩具”变成“工具”的关键分水岭。没有记忆的 Agent 每次对话都是重新开始用户说“帮我改一下刚才那个方案”它只会一脸茫然地问你“什么方案”。AgentScope 的记忆设计我总结为三层短期记忆、长期记忆、工作记忆。短期记忆就是当前会话的上下文窗口通常用消息列表来实现。这部分最直接但坑也最多。你不能无脑地把所有历史消息都塞进去因为 token 是有成本的而且过长的上下文会导致模型注意力分散反而降低回答质量。AgentScope 在这块提供了消息裁剪和摘要压缩的策略你可以配置保留最近 N 轮对话或者当 token 超过阈值时自动触发摘要生成。长期记忆通常需要外部存储支持比如向量数据库或者关系型数据库。AgentScope 把长期记忆抽象成了一个独立的接口你可以接 Pinecone、Milvus、pgvector也可以自己实现一个基于文件系统的简单版本。关键是要把“写入”和“检索”这两个操作解耦写入的时候做 embedding 和索引检索的时候做相似度匹配和重排序。工作记忆是我觉得最有意思的一层。它类似于人类在解决复杂问题时的“草稿纸”用来临时存放中间推理结果。比如 Agent 在规划一个多步任务时会把每一步的计划和中间结果写进工作记忆后续步骤可以直接引用不用重新推理。AgentScope 通过一个可配置的scratchpad机制来实现这个功能你可以控制它的容量和过期策略。2.3 消息传递与 SSE 流式输出生产环境的 Agent 必须支持流式输出否则用户等十几秒才看到第一个字体验会非常差。AgentScope 在通信层选择了 SSEServer-Sent Events作为主要的流式传输协议。为什么是 SSE 而不是 WebSocket这个问题我被问过很多次。简单来说SSE 是单向的服务器推送基于 HTTP 协议实现简单浏览器原生支持自动重连。WebSocket 是双向通信功能更强但复杂度也更高。对于 Agent 场景大部分时候是服务器把模型生成的内容推给客户端客户端只需要在开始的时候发一个请求中间不需要频繁发送数据。所以 SSE 的匹配度更高。当然 SSE 也有它的坑。最常见的问题是连接超时和断线重连。AgentScope 在这块做了心跳保活和断点续传的处理。服务端会定期发送注释行来维持连接客户端断开后可以从上次的消息 ID 继续接收。这个机制在长文本生成场景下特别重要因为一次生成可能持续几十秒甚至几分钟。如果你需要在 Java 技术栈里实现类似的功能Spring 的SseEmitter是一个不错的选择。核心思路是创建一个SseEmitter对象设置超时时间然后在异步任务里不断调用send()方法推送数据。注意要处理好异常和完成回调否则容易造成内存泄漏。2.4 MCP 协议工具调用的标准化尝试MCPModel Context Protocol是最近一年在 Agent 圈子里讨论度很高的一个话题。它的核心目标是标准化模型和外部工具之间的通信协议让不同的 Agent 框架和工具提供方能够互相兼容。AgentScope 对 MCP 的支持主要体现在工具注册和调用这一层。传统的做法是每个工具写一个函数然后在 Agent 配置里手动注册。MCP 的思路是把工具的描述、参数 schema、调用方式都标准化Agent 只需要知道 MCP Server 的地址就能自动发现和调用所有可用的工具。这个设计的好处是解耦。工具提供方不需要关心 Agent 用的是什么框架Agent 也不需要为每个工具写适配代码。你可以在本地跑一个 MCP Server 提供数据库查询能力在另一台机器上跑一个提供文件操作能力Agent 通过统一的协议来调用它们。不过 MCP 目前还在快速演进阶段不同实现之间的兼容性还有待观察。我的建议是如果你的工具生态比较固定直接用 AgentScope 原生的工具注册机制就够了如果你需要对接大量第三方工具或者希望工具能够跨框架复用那 MCP 值得投入时间研究。3. 从零搭建一个带记忆的 Agent完整实操流程3.1 环境准备与依赖安装在开始写代码之前先把环境搭好。AgentScope 是一个 Python 项目推荐使用 Python 3.10 或以上版本因为它的异步特性和类型注解用到了比较新的语法。我习惯用 conda 来管理环境这样不同项目之间的依赖不会互相污染。创建环境的命令如下conda create -n agentscope-demo python3.10 conda activate agentscope-demo接下来安装 AgentScope 本体。如果你只是想做实验直接 pip 安装最新版就行pip install agentscope但如果你打算跟着这篇文章一步步搭建生产级的项目我建议从源码安装这样你可以随时查看和修改内部实现git clone https://github.com/modelscope/agentscope.git cd agentscope pip install -e .安装完成后验证一下是否成功import agentscope print(agentscope.__version__)如果能看到版本号输出说明环境没问题。注意AgentScope 的某些功能依赖外部服务比如模型 API、向量数据库等。在开始之前确保你已经准备好了至少一个模型服务的 API Key并且网络能够正常访问。3.2 定义 Agent 的领域模型按照 DDD 的思路我们先把 Agent 的领域模型定义清楚。一个带记忆的对话 Agent 至少包含以下几个核心概念Agent 实体代表 Agent 本身持有配置信息和行为方法消息对话的基本单元包含角色、内容、时间戳等属性记忆管理消息的存储、检索和压缩模型封装大模型的调用接口AgentScope 已经提供了这些概念的基类我们只需要继承并扩展。下面是一个简化的示例from agentscope.agents import AgentBase from agentscope.memory import TemporaryMemory from agentscope.message import Msg class MemoryAgent(AgentBase): def __init__(self, name, model_config, memory_configNone): super().__init__(namename, model_configmodel_config) self.memory TemporaryMemory(configmemory_config) def reply(self, x: Msg) - Msg: # 将用户消息写入记忆 self.memory.add(x) # 从记忆中检索相关上下文 context self.memory.retrieve(x.content) # 构造提示词 prompt self._build_prompt(context, x.content) # 调用模型生成回复 response self.model(prompt) # 将回复写入记忆 reply_msg Msg(nameself.name, contentresponse, roleassistant) self.memory.add(reply_msg) return reply_msg这段代码看起来简单但每一行背后都有设计考量。TemporaryMemory是 AgentScope 提供的短期记忆实现它内部维护了一个消息列表支持按时间顺序检索和按相关性检索两种模式。_build_prompt方法负责把检索到的上下文和当前问题组装成模型能理解的格式这里涉及到提示词工程的一些技巧后面会详细讲。3.3 配置长期记忆存储短期记忆只能解决单次会话内的问题要实现跨会话的记忆必须引入外部存储。AgentScope 支持多种长期记忆后端我这里以向量数据库为例来说明配置过程。首先安装必要的依赖pip install chromadb然后定义一个基于 ChromaDB 的长期记忆类import chromadb from agentscope.memory import MemoryBase class VectorMemory(MemoryBase): def __init__(self, collection_nameagent_memory, persist_dir./memory_db): self.client chromadb.PersistentClient(pathpersist_dir) self.collection self.client.get_or_create_collection(namecollection_name) def add(self, msg): self.collection.add( documents[msg.content], metadatas[{role: msg.role, timestamp: msg.timestamp}], ids[msg.id] ) def retrieve(self, query, top_k5): results self.collection.query( query_texts[query], n_resultstop_k ) return results[documents][0] if results[documents] else []这个实现的关键点在于写入的时候把消息内容和元数据一起存进去检索的时候用查询文本做相似度匹配。top_k参数控制返回的记忆条数太小可能漏掉关键信息太大则会引入噪声。我的经验是 3 到 5 条比较合适具体要根据你的业务场景来调。实操心得ChromaDB 在本地开发时很方便但生产环境建议换成 Milvus 或者 pgvector因为 ChromaDB 的并发性能和持久化可靠性在数据量大了之后会明显下降。迁移的时候只需要改VectorMemory的内部实现上层代码不用动这就是面向接口编程的好处。3.4 实现流式输出与 SSE 接口Agent 的回复生成通常是逐 token 输出的我们需要把这个过程通过 SSE 推送给客户端。AgentScope 的模型接口支持流式调用返回一个生成器我们可以逐块读取并推送。下面是一个基于 FastAPI 的 SSE 接口实现from fastapi import FastAPI from fastapi.responses import StreamingResponse import asyncio app FastAPI() async def event_generator(agent, user_input): msg Msg(nameuser, contentuser_input, roleuser) # 流式生成回复 for chunk in agent.stream_reply(msg): yield fdata: {chunk}\n\n await asyncio.sleep(0) # 让出控制权 yield data: [DONE]\n\n app.get(/chat) async def chat(query: str): return StreamingResponse( event_generator(agent, query), media_typetext/event-stream )这里有几个细节需要注意。第一media_type必须设置为text/event-stream否则浏览器不会按 SSE 协议解析。第二每条消息以data:开头以两个换行符结尾这是 SSE 的格式要求。第三发送完所有内容后要发送一个结束标记客户端收到后关闭连接。如果你用的是 Java 技术栈Spring 的SseEmitter用法类似GetMapping(/chat) public SseEmitter chat(RequestParam String query) { SseEmitter emitter new SseEmitter(300_000L); // 5分钟超时 executor.execute(() - { try { agent.streamReply(query, chunk - { emitter.send(SseEmitter.event().data(chunk)); }); emitter.send(SseEmitter.event().data([DONE])); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }注意SSE 连接的超时时间要设置得足够长因为模型生成长文本可能需要几分钟。同时要处理好客户端断开的情况否则服务端会一直往一个已经关闭的连接写数据造成资源浪费。3.5 工具调用与 MCP 集成一个只会聊天的 Agent 价值有限真正有用的是能调用外部工具的 Agent。AgentScope 的工具注册机制很直观你只需要定义一个函数加上装饰器然后在 Agent 初始化的时候注册进去。from agentscope.tools import tool tool def query_weather(city: str) - str: 查询指定城市的天气信息。 Args: city: 城市名称如北京、上海 # 实际实现会调用天气 API return f{city}今天晴气温 25 度 agent.register_tool(query_weather)装饰器会自动提取函数的文档字符串作为工具描述参数类型注解用来生成参数 schema。模型在决定是否调用工具时会参考这些信息。所以文档字符串一定要写清楚参数说明要准确否则模型可能会误用工具。如果你要接入 MCP 协议的工具AgentScope 提供了MCPToolkit来简化配置from agentscope.tools import MCPToolkit toolkit MCPToolkit(server_urlhttp://localhost:8080/mcp) agent.register_toolkit(toolkit)MCP Server 会暴露一个工具列表Agent 启动时自动拉取并注册。这样你就不需要为每个工具写适配代码了。4. 生产环境踩坑实录与排查技巧4.1 记忆检索不准的排查思路记忆检索是实际使用中最容易出问题的地方。用户明明之前说过的事情Agent 就是检索不到或者检索到了不相关的内容。这个问题通常有三个原因。第一个原因是 embedding 模型的选择。不同的 embedding 模型对中文语义的理解能力差异很大。我试过用某个英文为主的模型来处理中文对话检索准确率惨不忍睹。换成专门针对中文优化的模型后效果立竿见影。所以选型的时候一定要用你的真实业务数据做测试不要只看 benchmark 分数。第二个原因是分块策略。如果你把一整段长对话作为一个记忆单元存进去检索的时候匹配到的可能是一大段无关内容。更好的做法是把对话按语义边界切分成小块每块只包含一个完整的意思。AgentScope 提供了几种分块策略你可以根据对话的特点来选择。第三个原因是检索参数。top_k设得太小会漏设得太大引入噪声。相似度阈值设得太高会过滤掉有用信息设得太低会返回不相关内容。我的经验是先用默认参数跑一批测试用例观察检索结果然后逐步调整。下面是一个排查用的检查清单问题现象可能原因排查方法完全检索不到embedding 服务异常检查 API 是否可达返回向量维度是否正确检索到无关内容相似度阈值过低提高阈值观察结果变化漏掉关键记忆top_k 太小增大 top_k看是否召回检索结果重复去重逻辑缺失检查写入时是否有重复数据4.2 SSE 断线重连的处理方案SSE 在生产环境最常见的问题是连接中断。网络抖动、代理超时、服务端重启都可能导致连接断开。如果客户端没有重连机制用户就会看到回复突然停止。AgentScope 在服务端做了心跳保活每隔 15 秒发送一个注释行防止中间层因为空闲而断开连接。但客户端也需要配合处理。标准的 SSE 客户端会自动重连但重连后是从头开始还是从断点继续需要服务端支持Last-Event-ID头。实现断点续传的关键是给每条消息分配一个递增的 ID服务端在重连时根据客户端提供的Last-Event-ID决定从哪条消息开始推送。AgentScope 的消息对象自带 ID 字段你只需要在 SSE 推送时把它带上yield fid: {msg.id}\ndata: {chunk}\n\n实操心得如果你的 Agent 回复是流式生成的断线重连会比较复杂因为生成到一半的内容没有完整的消息 ID。我的做法是在服务端缓存最近生成的完整回复重连时先推送缓存内容再继续生成剩余部分。这个逻辑需要根据你的业务场景来定制。4.3 工具调用失败的常见原因工具调用失败通常不是 Agent 框架的问题而是工具本身的实现或者配置有问题。我整理了几种最常见的情况。第一种是参数类型不匹配。模型生成的参数是字符串但工具函数期望的是整数调用时就会报错。解决方法是在工具函数的参数注解里写清楚类型AgentScope 会自动做类型转换。如果转换失败模型会收到错误信息并尝试重新生成参数。第二种是工具描述不清晰。模型不知道这个工具是干什么的自然不会调用或者调用时传错参数。工具函数的文档字符串要写得像给新人看的说明书说清楚功能、参数含义、返回值格式。第三种是超时。外部 API 响应慢工具调用超时Agent 就会卡住。建议给所有工具调用设置超时时间超时后返回一个友好的错误信息让模型决定是重试还是换一种方式。4.4 性能优化的几个关键点Agent 的响应速度直接影响用户体验。我总结了几条优化经验按投入产出比排序。最有效的是缓存。相同的用户问题如果之前已经回答过直接返回缓存结果不用再调模型。AgentScope 支持在模型层加缓存你可以用内存缓存做快速验证生产环境换成 Redis。其次是并发。多个用户的请求应该并发处理而不是排队。AgentScope 的异步接口天然支持并发但要注意共享资源比如记忆存储的线程安全问题。然后是模型选择。不是所有任务都需要用最大的模型。简单的意图识别、参数提取可以用小模型复杂的推理和生成再用大模型。AgentScope 支持配置多个模型根据任务类型路由到不同的模型。最后是提示词优化。精简提示词去掉不必要的示例和说明可以减少 token 消耗加快生成速度。但要注意不要精简过头导致模型理解偏差。5. 我对 AgentScope 学习路径的一些个人建议如果你刚开始接触 AgentScope我的建议是先跑通一个最简单的对话 Agent不要一上来就搞多智能体、长期记忆、MCP 这些高级特性。先理解 Agent 的基本循环接收消息、构造提示词、调用模型、解析输出、返回回复。这个循环跑通了再逐步往上加功能。第二步是加记忆。先用 AgentScope 自带的TemporaryMemory理解消息是怎么存储和检索的。然后换成外部存储理解长期记忆的写入和读取流程。这一步的关键是搞清楚 embedding 和相似度检索的原理不然后面调参的时候会一头雾水。第三步是加工具。从一个简单的工具开始比如查询时间、计算器理解工具注册和调用的完整链路。然后再尝试接入 MCP 协议的工具理解标准化协议带来的便利。第四步是上生产。这一步要考虑的东西就多了并发处理、错误重试、日志监控、性能优化、安全防护。AgentScope 提供了很多生产级的特性但需要你根据实际场景来配置和调优。我在这个过程中最大的体会是不要追求一步到位。Agent 系统是一个复杂的工程问题涉及模型、存储、网络、并发等多个领域。每次只解决一个问题逐步迭代比一开始就设计一个完美架构要靠谱得多。踩过的坑都会变成经验而这些经验才是真正让你从“会用”变成“用好”的东西。另外AgentScope 的中文文档质量在开源项目里算是相当不错的遇到问题先翻文档大部分基础问题都能找到答案。文档没覆盖的可以去项目的 issue 区搜一搜很可能已经有人遇到过类似的问题。实在解决不了的提 issue 的时候把复现步骤、环境信息、错误日志都贴清楚维护者响应还是挺快的。