尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手搓生产级记忆型AI Agent:AgentScope+DDD+SSE+MCP全链路实战

从零手搓生产级记忆型AI Agent:AgentScope+DDD+SSE+MCP全链路实战 1. 为什么我要从零手搓一个记忆型 AI Agent先说结论市面上能跑通 Demo 的 Agent 框架一抓一大把但真正能扛住生产流量、还能记住上下文、断线能续、工具能热插拔的少之又少。我这次拿 AgentScope 做底座从零搭了一个带长期记忆的生产级 AI Agent踩的坑比想象中多收获也比预期大。这篇文章不讲虚的就把整个设计思路、核心实现、参数取舍、排查过程全部摊开讲适合已经写过基础 Agent Demo、想往生产环境推一步的开发者也适合正在选型 Agent 框架的技术负责人。AgentScope 这个框架我关注挺久了它的定位很清晰——面向多智能体协作的开发框架消息传递、角色编排、工具调用这些基础能力都做得比较扎实。但能跑和生产级之间隔着一条鸿沟记忆怎么持久化、流式输出怎么保证不中断、工具协议怎么标准化、领域模型怎么拆才不乱这些才是真正决定一个 Agent 能不能上线的关键。我这次的项目目标很明确就是要做一个记忆型 Agent——它能记住用户之前说过什么、做过什么决策、偏好是什么而不是每次对话都从零开始。为什么强调记忆型因为绝大多数 Agent 的痛点就在这。你问它上次那个方案改得怎么样了它一脸茫然你让它按我之前的风格来它根本不知道你之前的风格是什么。这种体验在 Demo 阶段无所谓一旦放到真实业务里用户立刻就会觉得这玩意儿不智能。所以记忆能力不是锦上添花而是生产级 Agent 的入场券。整个项目我用了几个关键技术栈AgentScope作为 Agent 编排框架DDD领域驱动设计来拆分业务边界SSEServer-Sent Events做流式输出MCPModel Context Protocol做工具协议标准化。这几个词看着独立实际上是一条完整的链路——DDD 决定代码怎么组织AgentScope 决定 Agent 怎么跑SSE 决定前端怎么实时收到结果MCP 决定 Agent 怎么调用外部工具。下面我逐个拆开讲把每个环节的为什么和怎么做都说透。2. 整体架构设计与技术选型拆解2.1 为什么用 DDD 而不是简单的分层架构很多人搭 Agent 项目上来就是 controller-service-dao 三层走天下Demo 阶段确实快但一旦 Agent 数量超过三个、工具超过十个、记忆类型超过两种代码就会迅速腐化。我这次坚持用 DDD核心原因是Agent 系统的复杂度不在技术层而在领域层。一个记忆型 Agent 的领域概念其实很多会话Session、记忆Memory、工具Tool、角色Role、任务Task、上下文窗口Context Window。这些概念之间有明确的边界和交互规则如果用传统的贫血模型所有逻辑都会堆到 Service 里最后变成一个几千行的上帝类。DDD 的价值就是把这些概念显式建模成聚合根、实体、值对象让业务规则内聚在领域层。具体怎么拆我把整个系统分成四个限界上下文会话上下文管理 Session 生命周期、消息历史、上下文窗口裁剪记忆上下文管理长期记忆的写入、检索、衰减、合并工具上下文管理 MCP 工具的注册、发现、调用、超时编排上下文管理 Agent 的角色定义、任务分发、多 Agent 协作每个上下文独立演进通过领域事件通信。比如会话上下文产生一条消息已写入事件记忆上下文订阅后决定是否要抽取长期记忆。这种解耦在生产环境里非常关键因为记忆抽取是个重操作不能阻塞主对话流程。2.2 AgentScope 在架构中的定位AgentScope 我把它放在编排上下文里作为 Agent 运行时的引擎。它负责的是给定一个角色定义和一批工具怎么驱动大模型完成一轮推理。我不把业务逻辑写进 AgentScope 的 Agent 里而是让它保持纯粹——只做推理和工具调用业务规则全部在领域层。这样做的好处是AgentScope 的版本升级不会影响我的业务代码。我见过太多项目把业务逻辑和框架 API 深度耦合框架一升级整个项目就得重写。保持框架的可替换性是生产级系统的基本素养。AgentScope 的几个能力我特别看重一是消息传递机制它支持结构化的消息对象不是简单的字符串拼接二是工具调用它有一套标准的工具描述格式三是多 Agent 协作支持 Agent 之间的消息路由。这三点正好对应我需要的编排能力。2.3 SSE 而不是 WebSocket 的取舍流式输出这块我在 SSE 和 WebSocket 之间纠结了很久。最后选 SSE理由有三条第一Agent 的输出是单向流。用户发一条消息Agent 流式返回结果这个场景本质上是服务器推送不需要双向通信。WebSocket 的双向能力在这里是浪费。第二SSE 基于 HTTP运维成本低。不需要额外的协议升级、不需要处理心跳、不需要考虑代理兼容性。生产环境里任何额外的协议复杂度都是故障源。第三SSE 天然支持断线重连。浏览器端的 EventSource 会自动重连配合 Last-Event-ID 还能做断点续传。这一点对长对话场景特别重要用户网络抖动一下不能整个对话就断了。当然 SSE 也有坑最大的坑就是空闲超时。我实测下来很多反向代理默认 60 秒没数据就断连接而 Agent 思考时间可能超过这个值。解决办法是定期发送心跳注释行: heartbeat\n\n保持连接活跃。这个细节后面会详细讲。2.4 MCP 作为工具协议的价值MCP 这个东西简单说就是给大模型调用外部工具定了一套标准协议。在没有 MCP 之前每个框架都有自己的工具描述格式你写一个工具要适配 N 个框架。有了 MCP工具实现一次所有支持 MCP 的客户端都能用。我这次把所有外部能力都封装成 MCP Server包括数据库查询、文件操作、HTTP 请求。Agent 侧只需要一个 MCP Client就能动态发现和调用这些工具。这种设计让工具的热插拔成为可能——新增一个工具不需要改 Agent 代码只需要注册一个新的 MCP Server。MCP 的通信方式支持 stdio 和 SSE 两种我选的是 SSE因为工具服务可能部署在独立进程甚至独立机器上stdio 只能本地通信。SSE 模式下MCP Server 暴露一个 HTTP 端点Client 通过 SSE 接收工具列表和调用结果。3. 记忆系统的核心设计与实操要点3.1 记忆分层短期、长期、工作记忆记忆不是一个大池子必须分层。我设计了三层记忆结构短期记忆就是当前会话的消息历史存在内存里随会话结束而销毁。它的作用是维持对话连贯性让 Agent 知道刚才聊了什么。短期记忆的关键参数是上下文窗口大小我设的是最近 20 轮对话超过就裁剪。为什么是 20 轮因为实测下来大部分任务的有效上下文不超过 15 轮20 轮留了点余量再多了纯属浪费 token。长期记忆是跨会话持久化的存在数据库里。它记录的是用户的偏好、重要事实、历史决策。比如用户偏好简洁的回答风格、用户上次选择了方案 B这类信息。长期记忆的写入不是每轮都做而是通过一个记忆抽取器异步处理判断当前对话里有没有值得长期保留的信息。工作记忆是任务级的临时存储比如 Agent 执行一个多步任务时中间结果放这里。任务结束就清空。工作记忆的价值在于避免中间结果污染长期记忆同时让 Agent 在多步推理时能引用前面的结果。三层的生命周期和存储介质都不一样用表格对比一下更清楚记忆类型生命周期存储介质典型容量写入时机短期记忆会话级内存20 轮对话每轮对话长期记忆永久数据库无上限异步抽取工作记忆任务级内存/Redis任务相关任务执行中3.2 记忆抽取的触发时机与策略记忆抽取是整个系统里最容易出问题的地方。抽取太频繁浪费算力还容易写入噪音抽取太少长期记忆就形同虚设。我试过三种策略第一种是每轮都抽取结果发现大量无意义记忆被写入比如用户说了你好这种。而且每轮都调用一次抽取模型成本翻倍。第二种是定时抽取比如每 10 轮抽一次。问题是可能错过关键信息而且定时任务和对话流程解耦后上下文可能已经丢失。第三种是我最终采用的事件触发 阈值判断。具体来说当一轮对话满足以下任一条件时才触发抽取对话轮数达到 5 的倍数、用户显式表达了偏好通过关键词识别、Agent 完成了某个任务节点。这样既不会太频繁也不会漏掉关键信息。抽取本身用一个小模型来做不需要用主模型。我用的是一个 7B 级别的模型专门做信息抽取把对话内容压缩成结构化的记忆条目。这样成本可控速度也快。3.3 记忆检索向量检索 关键词混合长期记忆检索我一开始只用向量检索后来发现纯向量检索有两个问题一是对精确匹配不敏感比如用户问我上次说的那个项目叫什么向量检索可能召回一堆语义相近但不相关的记忆二是冷启动问题记忆库小的时候向量检索效果很差。所以我改成了混合检索向量检索负责语义召回关键词检索BM25负责精确召回两路结果用 RRFReciprocal Rank Fusion融合。实测下来混合检索的召回准确率比纯向量高了大概 30%。检索的 top-k 我设的是 5也就是每次最多召回 5 条记忆注入上下文。为什么是 5因为记忆注入会占用上下文窗口太多会挤压对话空间。5 条是个平衡点实测覆盖了大部分场景。注意记忆检索一定要加时间衰减因子。三个月前的记忆和昨天的记忆权重不应该一样。我用的是指数衰减半衰期设 30 天。这样旧记忆会自然淡化除非被反复召回。3.4 记忆冲突的处理记忆冲突是个隐蔽的坑。比如用户先说我喜欢 Python后来说我现在主要用 Java这两条记忆是冲突的。如果不处理Agent 可能会给出矛盾的回复。我的处理策略是新记忆覆盖旧记忆 保留历史版本。当抽取器发现新记忆和旧记忆冲突时把旧记忆标记为已过期新记忆设为当前有效。检索时只召回有效记忆但历史版本保留在库里用于审计和回溯。判断冲突的方法是用一个轻量的语义相似度模型如果两条记忆的相似度超过 0.85 但内容矛盾通过一个小的分类模型判断就触发冲突处理。这个阈值 0.85 是调出来的太低会误判太高会漏判。4. SSE 流式输出的生产级实现4.1 SSE 连接的生命周期管理SSE 看着简单就是一个 HTTP 长连接但生产环境里的坑一点不少。我先把连接的生命周期理清楚连接建立时服务端要设置正确的响应头Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive。这三个头缺一不可少一个就可能导致浏览器不识别或者代理缓存。连接保持时要定期发送心跳。我设的心跳间隔是 15 秒发送一个注释行: heartbeat\n\n。为什么是 15 秒因为大部分反向代理的空闲超时是 60 秒15 秒的心跳留了足够的安全边际。心跳太频繁浪费带宽太稀疏又起不到保活作用。连接关闭时要清理服务端的资源比如取消正在进行的 Agent 推理、释放会话锁。这一步很容易被忽略结果就是连接断了但后台任务还在跑资源泄漏。4.2 断线重连与断点续传SSE 的断线重连是浏览器自动做的但断点续传需要服务端配合。原理是这样的服务端每条消息都带一个id字段浏览器重连时会带上Last-Event-ID头服务端根据这个 ID 找到断点把之后的消息补发。我的实现是给每条 SSE 消息分配一个单调递增的序号服务端维护一个消息缓冲区最近 100 条重连时根据 Last-Event-ID 从缓冲区里补发。超过 100 条的情况就返回一个需要重新开始的事件让前端重新发起请求。这里有个细节消息缓冲区要按会话隔离不能全局共享。否则多用户并发时会串消息。我用的是MapsessionId, CircularBuffer的结构每个会话一个环形缓冲区。4.3 空闲超时的排查与解决stream disconnected before completion: idle timeout waiting for SSE这个报错我遇到过好几次每次原因都不一样。整理一下排查思路第一次是反向代理的超时配置太短默认 60 秒。解决办法是调大代理的proxy_read_timeout同时加上心跳。第二次是 Agent 推理时间太长中间没有任何输出。解决办法是在 Agent 推理过程中插入思考中的事件保持连接有数据流动。第三次是服务端的写超时设置问题底层 socket 写阻塞了。解决办法是给写操作加超时超时就主动关闭连接让客户端重连。排查这类问题的通用方法是在链路的每一层都打日志从客户端到代理到服务端看数据流在哪一层断掉。我一般会先用 curl 直接连服务端排除客户端问题再用 curl 连代理排除代理问题。逐层缩小范围。4.4 前端消费 SSE 的正确姿势前端用 EventSource 消费 SSE有几个坑要注意一是EventSource 不支持自定义请求头所以认证信息只能放在 URL 参数或者 Cookie 里。我选的是 Cookie相对安全一些。二是EventSource 会自动重连但重连时不会带请求体所以如果初始请求有参数重连时会丢失。解决办法是把参数放在 URL 里或者用 fetch ReadableStream 自己实现 SSE 消费。三是要处理error事件不能只监听message。连接断开、服务端返回错误都会触发 error 事件前端要据此更新 UI 状态。我最终用的是 fetch ReadableStream 的方案虽然代码多一点但可控性强能自定义请求头、能精确控制重连逻辑。5. MCP 工具集成与动态调用5.1 MCP Server 的注册与发现MCP 的核心价值是工具的动态发现。Agent 启动时通过 MCP Client 连接各个 MCP Server拉取工具列表构建工具注册表。这个过程是异步的不阻塞 Agent 启动。工具注册表的结构大概是这样的每个工具有一个唯一的名称、一段描述、一个参数 schema。Agent 在推理时把这些工具描述注入到 prompt 里模型决定调用哪个工具、传什么参数。我踩过一个坑工具描述的质量直接决定调用准确率。一开始我写的工具描述很简略比如查询数据库结果模型经常传错参数。后来我把描述写详细包括用途、参数含义、返回值格式、使用示例调用准确率明显提升。工具描述本质上是在给模型写文档文档写得好模型才用得好。5.2 工具调用的超时与重试生产环境里工具调用失败是常态。网络抖动、下游服务超时、参数错误各种情况都可能发生。我的处理策略是超时控制每个工具调用设一个超时默认 30 秒特殊工具可以单独配置。超时后立即返回错误不让 Agent 无限等待。重试策略只对幂等操作重试非幂等操作不重试。重试次数最多 2 次采用指数退避。为什么是 2 次因为 3 次以上重试的收益递减而且会放大下游压力。降级处理工具调用失败后Agent 要能优雅降级。比如数据库查询失败Agent 可以回复暂时无法查询请稍后再试而不是直接报错崩溃。5.3 工具权限与安全边界工具是 Agent 的手脚权限控制必须严格。我的做法是最小权限原则 白名单机制。每个 MCP Server 注册时要声明它需要哪些权限。Agent 调用工具前先检查当前会话是否有对应权限。比如文件操作工具只允许访问特定目录数据库工具只允许执行只读查询。白名单机制是指只有显式注册的工具才能被调用未注册的工具即使 MCP Server 暴露了也不可用。这样防止了工具被意外调用。提示工具的参数一定要做校验不能直接透传给下游。我见过因为参数没校验导致 SQL 注入的案例Agent 场景下这个风险更高因为参数是模型生成的不可控。5.4 工具调用结果的格式化工具返回的结果不能直接塞给模型要做格式化。原因有两个一是原始结果可能很长直接塞会爆上下文二是原始结果可能是结构化的模型理解起来费劲。我的做法是结果摘要 结构化提取。对于长结果先用一个小模型做摘要只保留关键信息对于结构化结果提取关键字段用自然语言重新组织。这样既节省 token又提升模型理解准确率。6. 常见问题与排查技巧实录6.1 记忆检索召回不准怎么办这是最常见的问题。排查思路分三步第一步检查记忆写入质量。如果写入的记忆本身就是噪音检索再准也没用。可以抽样看几条记忆判断抽取器是否工作正常。第二步检查检索参数。top-k 是不是太小相似度阈值是不是太高时间衰减因子是不是太激进这些参数都要根据实际数据调。第三步检查 embedding 模型。不同的 embedding 模型对中文的支持差异很大选一个适合中文的模型很关键。我试过几个模型最后选的是一个在中文语义相似度任务上表现较好的。6.2 SSE 消息乱序或丢失SSE 本身是基于 TCP 的理论上不会乱序。如果出现乱序大概率是服务端多线程写入没有加锁。解决办法是给每个会话的 SSE 写入加一个锁保证同一会话的消息串行发送。消息丢失通常是缓冲区溢出导致的。如果客户端消费速度慢于服务端生产速度缓冲区满了就会丢消息。解决办法是加背压机制服务端发现缓冲区快满时暂停生产或者降级。6.3 Agent 陷入死循环Agent 死循环通常发生在工具调用环节。比如 Agent 调用工具失败重试又失败又重试无限循环。解决办法是设置最大迭代次数我设的是 10 次。超过就强制终止返回一个兜底回复。另一个原因是工具返回的结果让 Agent 误判。比如工具返回操作成功但实际失败了Agent 就会继续下一步。解决办法是工具返回结果要明确成功和失败要区分清楚。6.4 上下文窗口爆掉上下文窗口爆掉的表现是模型报错或者回复质量骤降。根本原因是注入的内容太多短期记忆 长期记忆 工具描述 系统提示加起来超了。解决办法是动态裁剪。我实现了一个上下文管理器实时计算当前上下文的 token 数超过阈值就按优先级裁剪。优先级从高到低是系统提示 当前用户消息 最近几轮对话 长期记忆 工具描述。裁剪时从低优先级开始。6.5 常见问题速查表问题现象可能原因排查方法解决方案记忆召回不准写入质量差/参数不当抽样检查记忆/调参优化抽取器/调 top-kSSE 消息乱序多线程写入无锁检查写入代码加会话级锁SSE 空闲超时代理超时/无心跳逐层排查加心跳/调代理超时Agent 死循环无迭代上限看调用日志设最大迭代次数上下文爆掉注入内容过多算 token 数动态裁剪工具调用失败超时/参数错看工具日志超时重试/参数校验7. 我踩过的坑和几条实在建议第一个坑是过早优化。我一开始就想着做完美的记忆系统结果花了两周在记忆抽取上主流程还没跑通。后来调整策略先把主流程跑通记忆系统用最简单的版本再逐步迭代。生产级系统是演进出来的不是设计出来的。第二个坑是忽视可观测性。Agent 系统的不确定性很高没有完善的日志和监控出了问题根本不知道从哪查。我后来加了全链路追踪每个环节都打点排查效率提升了好几倍。建议一开始就把日志和监控做进去别等出问题再补。第三个坑是工具描述写得太随意。前面提过工具描述的质量直接决定调用准确率。我现在的做法是每个工具描述都要经过测试用几个典型场景验证模型能不能正确调用。几条实在建议记忆系统的参数一定要根据实际数据调别照搬网上的配置SSE 的心跳间隔要小于代理超时的一半MCP 工具一定要做权限控制别图省事全放开上下文管理要动态别用固定窗口。最后分享一个小技巧Agent 的回复质量很大程度上取决于系统提示的质量。我花了不少时间打磨系统提示把角色定义、行为准则、输出格式都写清楚效果比调模型参数明显得多。系统提示是 Agent 的人格值得认真对待。这个项目后续还可以扩展的方向不少比如多 Agent 协作、记忆的主动遗忘机制、工具的自适应选择。但核心思路是不变的领域建模要清晰框架要保持可替换流式输出要稳工具协议要标准。把这几点做好Agent 就能从 Demo 走向生产。
返回列表