1. 为什么我要从零手搓一个记忆型 AI Agent去年下半年开始我陆续接手了几个 AI Agent 相关的项目从最简单的问答机器人到稍微复杂一点的多轮任务编排踩的坑不算少。最开始我也是直接拿现成的框架往上堆能用是能用但一旦业务方提出“这个 Agent 要记住用户上周说过的话”“要能跨会话保持上下文”“要能接入我们内部的工具链”现成方案就开始捉襟见肘。尤其是记忆这一块大部分框架要么只给你一个简单的对话历史缓冲区要么把记忆做成一个黑盒你根本不知道它什么时候写入、什么时候召回、召回的准不准。后来我决定不再将就干脆从零构建一个生产级的记忆型 AI Agent。选型上我最终落在 AgentScope 这套体系上原因后面会细说。这篇文章就是把我整个搭建过程、架构决策、踩坑记录和实操细节完整地摊开来讲。核心关键词会围绕AgentScope、AI Agent、DDD、SSE、MCP这几个展开适合已经了解大模型基本调用、想往生产级 Agent 方向深入的后端或全栈同学参考。如果你还在纠结“AI Agent 到底怎么落地”或者被记忆管理、流式输出、工具协议这些概念绕得头晕那这篇内容应该能帮你省下不少试错时间。先说清楚这个 Agent 到底要干什么。它不是一个玩具级的聊天窗口而是一个能长期运行、有状态、能调用外部工具、能通过 SSE 把大模型回答实时推给前端、并且用 DDD 思路把领域逻辑和基础设施彻底分开的生产系统。记忆不是简单的聊天记录堆叠而是分层的短期会话记忆、长期用户画像、工具调用结果缓存三者各司其职。MCP 则负责把外部能力以标准协议接进来避免每接一个工具就写一堆胶水代码。我见过太多团队在 Agent 项目上翻车根本原因不是模型不够强而是架构从一开始就没设计好。对话状态散落在各处工具调用逻辑和业务逻辑搅在一起流式输出用轮询硬扛最后维护成本高到没人敢动。所以这次我从零开始每一步都问自己这个决策在生产环境扛不扛得住下面就把整个思路和实现拆开讲。2. 整体架构设计与技术选型背后的取舍2.1 为什么是 AgentScope 而不是自己造轮子市面上 Agent 框架不少我前后试过三四个主流方案。有的偏研究向抽象层次太高想改一个记忆策略得翻半天源码有的偏应用向封装得太死想接一个自定义工具得按它的规矩来灵活性差。AgentScope 吸引我的点在于它的分层比较清晰消息、记忆、工具、模型这几块边界明确而且对多 Agent 协作有原生支持。更关键的是它的中文文档相对完整社区里关于 AgentScope Java 的实践文章虽然不算多但质量还行遇到问题能查到东西。自己造轮子也不是没想过但生产级 Agent 要考虑的东西太多了并发会话管理、记忆的持久化与召回、工具调用的超时与重试、流式输出的断线重连。这些如果全自己写没个两三个月根本稳不下来。AgentScope 把这些基础设施都提供了我只需要聚焦在业务领域逻辑上这才是合理的投入产出比。2.2 DDD 分层把领域逻辑从技术细节里救出来这个项目我坚持用 DDD 来组织代码原因很直接Agent 的领域逻辑和技术实现必须分开。什么叫领域逻辑比如“用户问了一个需要查订单的问题Agent 应该先识别意图再决定调用哪个工具拿到结果后组织语言回复”。什么叫技术实现比如“用 SSE 把 token 推给前端”“用 Redis 存会话记忆”“用 MCP 协议调外部服务”。如果这两者混在一起后果就是换一个流式输出方案业务代码要跟着改换一个记忆存储工具调用逻辑要跟着动。我踩过这个坑所以这次严格分层。具体分层是这样的领域层放 Agent 的核心行为定义包括记忆策略接口、工具调用编排、对话状态机应用层负责用例编排比如“处理一次用户请求”这个完整流程基础设施层实现具体的技术细节比如 SSE 推送、Redis 记忆存储、MCP 客户端。层与层之间通过接口依赖领域层不依赖任何具体技术。这样做的直接好处是我可以在测试环境用内存存记忆生产环境换成 Redis领域层代码一行不用改。流式输出从 SSE 换成 WebSocket也只是换一个基础设施实现。2.3 SSE 流式输出为什么不用 WebSocket流式输出这块我选了 SSE 而不是 WebSocket。很多人第一反应是 WebSocket 更强大双向通信为什么不用原因在于场景。Agent 的对话场景本质上是“客户端发一次请求服务端持续推流”这是典型的单向流。SSE 基于 HTTP天然支持断线重连浏览器端 EventSource 用起来也简单。WebSocket 虽然双向但连接管理、心跳、重连都要自己处理复杂度高出一截。当然 SSE 也有坑比如 idle timeout 导致连接断开报错信息经常是 “stream disconnected before completion: idle timeout waiting for sse”。这个后面排查章节会细讲。另外 SSE 是文本协议传输二进制数据要编码但 Agent 场景基本都是文本问题不大。2.4 MCP工具接入的标准化答案MCP 是我这次重点研究的一块。以前接工具每个工具都要写一套适配代码参数格式、鉴权方式、错误处理各不相同维护起来很痛苦。MCP 协议把工具的描述、调用、结果返回都标准化了Agent 只需要知道“有一个工具叫某某接受什么参数”具体怎么调由 MCP Server 负责。我实际接入了几个 MCP Server包括浏览器自动化相关的 Playwright MCP还有内部的一些数据查询服务。标准化之后新增一个工具的成本从半天降到半小时。MCP 的生态也在快速扩张蓝湖 MCP、Blender MCP 这些垂直领域的 Server 越来越多对 Agent 的能力扩展是实打实的利好。3. 记忆系统的核心设计与实操细节3.1 三层记忆模型短期、长期、工具缓存记忆是这个项目的核心也是我花时间最多的地方。我把记忆分成三层每层职责不同。第一层是短期会话记忆存的是当前会话的对话历史包括用户消息、Agent 回复、工具调用记录。这一层的特点是读写频繁、生命周期短会话结束就可以清理。我用 Redis 的 List 结构存每个会话一个 key设置合理的过期时间。第二层是长期用户记忆存的是跨会话需要保留的信息比如用户的偏好、历史关键事实、常用查询条件。这一层不能简单堆对话记录需要做信息抽取和压缩。我的做法是定期用大模型对会话记忆做摘要提取出值得长期保留的事实存到向量数据库里召回时用语义相似度匹配。第三层是工具调用结果缓存存的是工具调用的输入输出对。很多工具调用是幂等的比如查一个订单的状态短时间内重复查结果一样。缓存这一层能显著减少外部调用次数降低延迟和成本。三层记忆的读写策略不一样。短期记忆每次对话都读写长期记忆在会话结束时写入、在新会话开始时召回工具缓存按 key 查询。这个设计的关键是不要让三层混在一起否则召回时会引入噪音。3.2 记忆写入时机什么时候该记什么时候不该记记忆写入时机是个容易被忽略但极其重要的细节。我一开始的做法是每轮对话结束就把所有内容写进记忆结果发现记忆里全是废话召回时噪音很大。后来我改成事件驱动只有特定事件触发时才写入记忆。比如用户明确表达了偏好“我以后都用中文回复”或者完成了一个重要任务“订单已提交”或者工具返回了关键结果。普通寒暄、重复确认这些不写入长期记忆。具体实现上我在领域层定义了一个 MemoryEvent 的概念应用层在处理完一轮对话后判断是否产生了值得记忆的事件如果有就调用记忆服务的写入接口。判断逻辑可以用规则也可以用大模型做意图识别我目前是规则加轻量模型结合准确率够用。注意记忆写入一定要做去重。我遇到过同一个偏好被反复写入的情况召回时返回一堆重复内容浪费 token 还干扰判断。去重可以用内容哈希也可以用向量相似度。3.3 记忆召回策略怎么让 Agent 想起该想起的召回比写入更难。写入是“存什么”召回是“取什么”取错了比不取还糟糕。我的召回策略是混合的先用规则过滤掉明显不相关的记忆再用向量相似度做语义匹配最后按时间衰减和重要性加权排序。时间衰减很重要。三个月前用户说喜欢某个品牌不代表现在还喜欢。我给每条记忆加了一个时间戳和衰减因子召回时分数会随时间降低。重要性则根据记忆事件类型赋值比如“用户明确声明的偏好”重要性高于“一次性的查询条件”。召回数量也要控制。我实测下来每次召回 3 到 5 条记忆效果最好太多会稀释注意力太少可能漏掉关键信息。召回的記憶会拼接到系统提示里格式要清晰让模型知道这是历史记忆而不是当前对话。3.4 记忆持久化的工程细节持久化这块我踩过几个坑。第一个是序列化格式一开始用 Java 原生序列化后来发现跨版本兼容性差换成 JSON 之后好很多。第二个是并发写入同一个用户可能同时有多个会话写入长期记忆时要加锁或者用乐观并发控制否则会出现覆盖。Redis 的配置也有讲究。短期记忆用 List设置 maxLen 防止无限增长长期记忆的向量索引用专门的向量数据库不要硬塞进 Redis。工具缓存用 Hashkey 设计要包含工具名和参数哈希避免冲突。备份和恢复也要考虑。生产环境的记忆数据是有价值的我配置了定期快照并且做了跨可用区的冗余。这块虽然不复杂但上线前一定要验证恢复流程别等真出事了才发现备份不能用。4. SSE 流式输出的完整实现与避坑4.1 从请求到渲染SSE 链路的全貌SSE 这条链路我从头到尾捋一遍。前端发起一个 POST 请求带上用户消息和会话 ID。后端接收到请求后先做鉴权和参数校验然后进入 Agent 处理流程。Agent 处理过程中每生成一段文本就通过 SSE 推送给前端。前端用 EventSource 或者 fetch 的流式读取来接收实时渲染到界面上。关键点在于后端的推送机制。我用的是 Spring 的 SseEmitter每个请求创建一个 emitterAgent 生成内容时调用 emitter.send() 推送。要注意 emitter 的超时设置默认值往往不够我设成了 5 分钟并且在前端做了心跳保活。推送的数据格式我定义了几种事件类型message 事件推文本内容tool_call 事件推工具调用状态done 事件表示结束error 事件推错误信息。前端根据事件类型做不同处理这样用户体验更清晰比如工具调用时显示“正在查询...”而不是干等。4.2 实时渲染的前端处理前端这块我用 React 实现。核心逻辑是监听 SSE 事件把收到的文本片段追加到消息列表里。这里有个细节大模型返回的文本是逐 token 的如果每个 token 都触发一次 React 状态更新性能会很差。我的做法是用一个缓冲区每 50 毫秒批量更新一次状态视觉上依然是实时的但渲染压力小很多。另外要处理 abort 场景。用户可能在 Agent 还在生成时就取消请求这时候前端要能中断 SSE 连接后端也要能感知到并停止生成。我在前端用了 AbortController后端在 emitter 的 onCompletion 和 onTimeout 回调里做清理。这个细节不做的话用户取消后后端还在跑浪费资源。提示SSE 连接在浏览器里有并发限制同一个域名下最多 6 个。如果你的应用可能同时开多个会话要考虑用 HTTP/2 或者做连接复用。4.3 idle timeout 问题的排查与解决“stream disconnected before completion: idle timeout waiting for sse” 这个报错我遇到过好几次基本都是因为 Agent 处理时间过长中间没有推送任何数据导致连接被判定为空闲而断开。解决办法有两个方向。一是加心跳Agent 在处理工具调用等耗时操作时定期推送一个空的事件或者 ping 事件保持连接活跃。二是优化处理流程把耗时的操作拆成多个阶段每个阶段结束都推送一次状态更新。我两个都做了效果很稳。还有一个隐藏原因是反向代理的超时设置。Nginx 默认的 proxy_read_timeout 是 60 秒如果 Agent 处理超过这个时间连接会被代理断开。这个要在 Nginx 配置里调大或者配置 proxy_buffering off 让数据实时透传。4.4 断线重连与状态恢复SSE 本身支持自动重连浏览器在连接断开后会尝试重新连接。但问题是重连之后之前推送的内容怎么办如果不管用户会看到内容重复或者丢失。我的方案是给每个会话维护一个消息序列号。前端记录已接收的最大序列号重连时带上这个序列号后端从该序列号之后继续推送。这样即使断线用户也能无缝续上。实现上需要在后端缓存最近推送的内容缓存大小根据实际情况定我设的是最近 100 条。这个机制在移动端网络不稳定的场景下特别有用。我实测过在地铁里用断线重连几次用户基本无感知。5. MCP 工具接入的实操与经验5.1 MCP 协议的核心概念MCP 全称 Model Context Protocol核心思想是把工具的能力描述和调用方式标准化。一个 MCP Server 会暴露若干工具每个工具包括名称、描述、参数 schema。Agent 拿到这些信息后可以决定调用哪个工具、传什么参数。调用结果也按标准格式返回。这个协议的价值在于解耦。Agent 不需要知道工具背后的实现只需要知道工具的能力。工具提供方也不需要关心 Agent 怎么用只需要按协议暴露能力。双方通过标准协议通信新增工具的成本大幅降低。MCP 的传输方式有几种常见的是 stdio 和 HTTP。stdio 适合本地工具HTTP 适合远程服务。我目前两种都在用本地文件操作类工具走 stdio远程数据查询走 HTTP。5.2 接入 Playwright MCP 做浏览器自动化Playwright MCP 是我接入的第一个外部工具用来做网页内容抓取和自动化操作。接入过程比想象中简单启动 Playwright MCP ServerAgent 通过 MCP 客户端连接然后就可以调用它的工具了。实际用下来几个工具特别实用navigate 用来打开页面snapshot 用来获取页面结构click 和 type 用来交互。Agent 可以根据用户指令自动完成“打开某网站找到某个信息返回结果”这样的任务。踩过的坑主要是超时和页面加载。有些页面加载慢navigate 会超时需要设置合理的超时时间并做重试。另外页面结构变化会导致 snapshot 结果不稳定Agent 解析时要做容错。我的做法是在工具调用外面包一层重试逻辑失败后换一种方式再试。5.3 工具调用的错误处理与降级工具调用不可能永远成功错误处理必须做好。我把工具调用错误分成几类网络错误、参数错误、业务错误、超时。不同错误处理方式不同。网络错误和超时适合重试我配置了指数退避的重试策略最多重试三次。参数错误通常是 Agent 生成的参数不对这时候把错误信息返回给 Agent让它重新生成参数。业务错误比如“订单不存在”这种直接返回给用户不需要重试。降级策略也要有。如果某个工具连续失败Agent 应该能切换到备用方案或者明确告诉用户“当前无法完成该操作”。我见过一些 Agent 在工具失败后陷入死循环反复调用同一个失败的工具这是设计缺陷。5.4 MCP Server 的选型与管理MCP Server 越来越多选型要看几个维度稳定性、维护活跃度、文档完整度、安全性。我一般优先选官方或者大厂维护的社区项目要仔细看 issue 和最近提交时间。管理上我把 MCP Server 的配置集中管理包括地址、鉴权信息、超时设置、重试策略。新增 Server 只需要改配置不需要改代码。这样运维起来方便也便于做灰度发布。安全方面要注意MCP Server 能访问外部资源鉴权必须做好。我用的 token 鉴权token 定期轮换并且做了权限最小化每个 Server 只能访问它需要的资源。6. 常见问题排查与实战避坑记录6.1 记忆召回不准的排查思路记忆召回不准是最常见的问题表现是 Agent 答非所问或者忽略了明明存过的信息。排查我一般按这个顺序先看记忆有没有写进去再看召回时有没有被过滤掉最后看拼接格式有没有问题。写入问题通常是事件判断逻辑太严或太松。太严导致该记的没记太松导致记了一堆噪音。我一般会打印记忆写入日志观察一段时间后调整规则。召回问题多半是相似度阈值设得不对太高召回不到太低召回一堆不相关的。这个需要根据实际数据调没有万能值。拼接格式也容易被忽略。如果记忆和当前对话混在一起没有明确分隔模型可能分不清哪些是历史哪些是现在。我用的是带标签的格式比如memory.../memory效果比较清晰。6.2 SSE 连接异常的速查表现象可能原因排查方向解决方案连接立即断开鉴权失败或路径错误检查请求头和 URL修正鉴权信息中途断开无报错反向代理超时查看代理日志调大 timeout 或关闭 bufferingidle timeout 报错长时间无数据推送检查 Agent 处理耗时加心跳或拆分阶段内容重复重连后未去重检查序列号机制实现序列号续传内容丢失缓冲区溢出检查缓存大小扩大缓存或优化推送频率这张表是我实际遇到问题后整理的基本覆盖了 SSE 相关的常见故障。遇到问题时按表排查能省不少时间。6.3 工具调用超时与重试的平衡工具调用超时设置是个平衡活。设太短正常调用也被中断设太长用户等太久。我的经验是按工具类型分查询类工具 10 秒操作类工具 30 秒批量类工具 60 秒。超过这个时间基本可以判定有问题。重试也要有度。我见过重试十几次的配置结果是把下游服务打挂了。一般重试三次足够而且要用指数退避第一次等 1 秒第二次 2 秒第三次 4 秒。如果三次都失败基本可以判定不是偶发问题重试也没用。还有一个细节是幂等性。重试的前提是操作幂等如果工具调用有副作用比如下单重试可能导致重复操作。这种工具要么不支持重试要么在工具侧做幂等保证。6.4 生产环境部署的注意事项部署这块我踩过的坑不少。第一个是内存Agent 服务因为要缓存会话和记忆内存占用比普通服务高容器内存限制要留足。第二个是并发SSE 连接是长连接并发数上不去会导致新请求排队要合理配置线程池和连接数。日志和监控必须做好。Agent 的行为链路长出问题时没有日志根本没法排查。我记录了每个请求的完整链路请求进入、记忆召回、模型调用、工具调用、SSE 推送、请求结束。关键节点打点方便做性能分析。灰度发布也很重要。Agent 的行为受模型和提示词影响大新版本上线前一定要小流量验证观察记忆召回准确率、工具调用成功率、用户满意度这些指标确认没问题再全量。7. 我在这套系统上的一些个人体会这套系统跑了一段时间最大的感受是Agent 的难点不在模型在工程。模型能力再强如果记忆管理混乱、工具调用不稳、流式输出老断用户体验就是不行。反过来模型能力中等但工程做扎实了整体体验反而更好。DDD 分层这个决策我特别满意。后期我想换一个向量数据库做长期记忆只改了基础设施层的一个实现类领域层和应用层完全没动。如果当初没分层这次改动至少要动十几个文件。SSE 这块心跳机制是必须的别省这个事。我一开始觉得 Agent 处理很快不需要心跳结果遇到复杂任务处理超过一分钟连接就断了。加上心跳之后再没出现过这个问题。MCP 的生态还在快速演进我建议尽早接入但不要贪多。先把一两个核心工具接稳跑通整个链路再逐步扩展。工具多了之后Agent 的选择成本也会上升提示词里怎么描述工具、怎么引导选择都是要打磨的。最后分享一个小技巧记忆召回的结果可以加一个置信度分数低于阈值的直接丢弃不要硬塞给模型。我实测下来宁可不召回也不要召回错的前者模型还能靠常识回答后者会把模型带偏。这个阈值需要根据业务数据调没有通用值但方向是对的。