资讯动态

Spring AI多智能体架构实战:从编排到记忆隔离的生产落地指南

发布时间:2026/10/18 11:35:45 来源:尧图企业网站定制
1. 为什么单Agent不够用先聊需求再聊技术去年我接手一个内部知识服务类项目第一版是典型的超级Agent思路一个ChatClient挂上一堆工具方法System Prompt 里塞满各种角色要求。上线之后问题接二连三——会话一拉长上下文迅速膨胀每轮请求的 token 成本肉眼可见地上涨工具注册一多模型经常选错函数明明要查库存它先调了天气接口提示词里既想让它客气温和又想让它敢于下判断结果两边都没做好。后来我把系统重构为基于 Spring AI 的多智能体协作架构把不同职责拆成独立 Agent由一个编排层统一调度。这版上线后单轮请求的上下文体积下降了一半还多工具误调用比例也明显降低。这篇文章就把这次重构里我认为最值钱的几块写出来为什么拆、怎么拆、Spring AI 里每个基础设施怎么用以及真正上生产时容易翻车的记忆隔离和稳定性问题。适合已经跑通过 Spring AI 基础 Demo、想在真实业务里落地多 Agent 架构的开发者。1.1 超级Agent什么时候开始失控先说个结论多 Agent 不是银弹它引入的复杂度明显高于单 Agent。分布式系统领域的经典论断在这里同样成立——每多一个需要协同的节点就多一层协调成本和故障面。所以动工之前得先搞清楚单 Agent 到底哪里扛不住了。我总结出三个明确的失控信号上下文通胀单个 Agent 的任务边界越宽System Prompt 越长、工具越多、历史消息越复杂。而模型上下文是有限预算你每轮把全部家当塞进去回答质量和延迟都会一起变差。工具选择混乱注册 20 个函数后路由错误率上升得很快。模型不是不理解你的需求而是候选空间太大时它容易把所有看起来沾边的工具都试一遍浪费调用次数不说还经常返回一个看似合理但完全没执行到位的结果。提示词互相污染想让同一个 Agent 既能闲聊、又能算报表、还能写代码写到后面提示词越来越长行为越来越不听话。其实不是模型笨是人格分裂这种事一条 prompt 本来就很难写好。1.2 拆开之后Agent 到底是什么拆解的第一步是定义清楚Agent这个概念。在 Spring AI 里一个 Agent 不是什么玄学组件它就是四样东西的组合一个独立的ChatClient底层对应一个模型实例或同一模型的不同参数配置一段边界清晰的 System Prompt定义职责和说话方式一组专属工具Tool标注的方法只暴露它任务范围内需要的能力一段独立的记忆或上下文窗口不与其他 Agent 共享。把 Agent 做成 Spring 容器里的普通 Bean好处是天然可以注入、替换、打点、测试。编排层通过 Bean 的名称引用它们就像服务发现一样简单。后面我会详细展开这套基础设施先记住这个组合就够了。2. 协作模式选型不是所有场景都适合开会讨论很多人一提到多智能体第一反应是做多个 Agent 互相聊天、辩论、达成共识。这种 Peer-to-Peer 协商模式看起来热闹实际很贵一次协商要来回好几轮模型调用延迟和成本都是线性放大而且结果不可控。我自己的经验是能编排就不要协商能流水线就不要开会。2.1 四种常见协作模式对比我把实际工程里常见的多 Agent 协作方式整理成一张对照表方便大家选型模式结构优点缺点适用场景编排-执行Orchestrator-Worker一个协调者拆任务多个执行 Agent 并行/顺序干活控制点集中、任务可追踪、易扩展协调者可能成为瓶颈和单点任务可拆解为子任务子任务相对独立流水线PipelineAgent 按固定顺序接力前一个输出是后一个输入逻辑简单、链路清晰、易测试整体延迟等于各环节之和任一步失败全链失败流程固定意图识别→检索→生成→审核监督者Supervisor一个监督 Agent 负责决策要不要叫帮手、叫谁适合开放目标能灵活决定执行路径决策开销大路径不可完全预期用户需求开放性高无法预定义子任务协商Negotiation多个 Agent 来回讨论达成一致适合需要多方视角的高难度决策贵、慢、结果不稳定极少数决策类场景不建议默认采用2.2 我为什么选了编排-执行模式在知识服务这个项目里用户的请求大体可以拆成查数据、写结论、生成图表、组织语言几类子任务之间有部分依赖但更多是并行关系。比如一次对比最近两个季度各渠道业绩的请求完全可以拆成三个互不依赖的查询子任务并行执行最后再汇总。所以我的架构是一个编排协调者负责拆解意图、派发子任务、收集结果下面挂着数据查询 Agent、分析 Agent、文案生成 Agent、图表生成 Agent四个执行单元。只有当子任务之间确实存在前后依赖比如先检索再生成时我才会在编排器里显式串行。这里给刚上手的朋友一个忠告不要一上来就设计一堆角色互相商量。先用编排-执行把系统跑通让每个 Agent 职责单一、接口明确后面真遇到协商类需求再单独加一个专门做这件事的 Agent 也不迟。3. Spring AI 的 Agent 基础设施ChatClient、工具注册与 Advisor 扩展点Spring AI 最大的价值在于把 LLM 调用抽象成了 Spring 生态熟悉的方式Agent 的实现成本因此大幅降低。以当前 1.x 版本的 API 为例我从三个角度讲清楚怎么搭一个可复用的 Agent 基础设施。3.1 用 ChatClient 定义 Agent 的门面每个 Agent 本质就是一个配置好的ChatClient。我会定义一个方法专门生产带身份的 ClientService public class AgentFactory { Bean(analystAgent) public ChatClient analystAgent(ChatClient.Builder builder, MessageChatMemory chatMemory) { return builder .defaultSystem(你是一名资深数据分析师。 你的职责是根据给定的数据查询结果做结构化分析 输出结论时给出指标说明和异常提醒。 不得虚构数据也不得脱离查询结果自行编造。) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), new SimpleLoggerAdvisor()) .defaultFunctions(querySalesData, queryInventoryData) .build(); } }这里的defaultSystem是角色锚点defaultFunctions传的是 Spring 容器里被Tool标注的 Bean 方法名defaultAdvisors是扩展点记忆、日志、敏感词过滤都可以在这里挂。Agent 之间靠 Bean 名称区分执行完毕后配置里所有的组装细节都对上层透明。3.2 工具注册的边界控制工具函数不能一股脑全暴露给所有 Agent。我会在服务层通过Tool标注方法并在每个 Agent 的defaultFunctions里只声明它需要的Component public class DataQueryTools { Tool(name querySalesData, description 按日期范围与维度查询销售汇总数据返回 JSON 数组) public String querySalesData(String startDate, String endDate, String dimension) { // 内部走数据服务返回结构化的 JSON return dataService.query(startDate, endDate, dimension).toJson(); } Tool(name queryInventoryData, description 查询指定 SKU 的实时库存与在途数量) public String queryInventoryData(String skuId) { return inventoryService.currentStock(skuId).toJson(); } }注意工具描述一定要写清楚输入含义和返回结构模型对工具理解的好坏直接决定编排的成功率。我踩过的坑是描述写得太简略模型把日期格式传错、把 ID 传进名称后来我把每个参数的类型和示例都写进description误调用率立刻降下来了。3.3 Advisor把横切逻辑变成插拔式组件多 Agent 场景下日志、审计、限流、敏感信息脱敏这些事如果散落在每个 Agent 里后期会非常痛苦。Spring AI 的 Advisor 机制相当于给聊天链路加了过滤器我强烈建议至少挂两层SimpleLoggerAdvisor记录每次调用的 prompt、响应摘要和耗时排查问题时没有它寸步难行自定义安全 Advisor在某些 Agent 的输入输出上做脱敏和敏感词过滤这个后面说稳定性时会展开。4. 编排层实现路由决策、Agent 注册表与任务状态机有了 Agent 基础设施接下来是整个架构的心脏——编排层。它负责回答三个问题这个请求该给谁处理子任务拆成什么粒度状态怎么流转4.1 Agent 注册表让编排器知道每个 Agent 能干什么我维护一个AgentRegistry里面存每个 Agent 的元信息名称、职责描述、可用工具列表、适合处理的任务类型。这个注册表有两个用途一是给路由模型做候选集二是给监控面板展示拓扑。public record AgentSpec(String name, String desc, ListString tools, int priority) {} Component public class AgentRegistry { private final MapString, AgentSpec specs new LinkedHashMap(); public AgentRegistry() { specs.put(analystAgent, new AgentSpec(analystAgent, 数据分析与指标解读, List.of(querySalesData), 2)); specs.put(writerAgent, new AgentSpec(writerAgent, 报告文案撰写与润色, List.of(generateChart), 1)); } public ListAgentSpec candidates(String taskType) { /* ... */ } }有了注册表路由就不是硬编码 if-else而是把这组元信息喂给模型做选择。4.2 路由决策用一次轻量调用决定执行路径我实现了一个AgentRouter它本身也是一个ChatClient但职责单一接收用户请求和候选 Agent 描述返回一个 JSON 路由结果。Component public class AgentRouter { private final ChatClient routerClient; private final AgentRegistry registry; public Route decide(String userRequest) { String candidates JsonUtils.toJson(registry.candidates(all)); return routerClient.prompt() .system(你是任务路由员。根据用户请求从候选 Agent 中选择 1~3 个执行者输出 JSON 数组格式为 [{\agent\:\名称\,\task\:\子任务描述\}]。 子任务必须可以被单个 Agent 独立完成。) .user(userRequest) .user(候选列表 candidates) .call() .entity(Route.class); } }entity(Route.class)是 Spring AI 提供的结构化输出能力模型会按目标类型返回对象省去了手工解析 JSON 的步骤。路由调用本身尽量使用小模型或低温度参数追求的是准和快不需要发散。4.3 任务状态机与执行协调拆出来的子任务不能发了就不管。我给每个任务定义了状态机PENDING待派发→ ASSIGNED已分派→ RUNNING执行中→ DONE完成异常分支为NEED_HELP需要协助→ ESCALATED升级处理和FAILED失败。协调器的主循环大致如下public ExecutionResult execute(UserRequest request) { Route route router.decide(request); ListFutureSubResult futures route.tasks().stream() .map(task - executor.submit(() - runAgent(task))) .toList(); ListSubResult results futures.stream() .map(this::awaitWithTimeout) .toList(); if (results.stream().anyMatch(r - r.status() FAILED)) { return handlePartialFailure(results); } return aggregator.assemble(request, results); }这里最容易被忽视的是并行度控制。模型供应商对并发请求有限制我一开始直接把子任务全丢线程池结果一堆 429。后来改为信号量控制全局并发上限按供应商配额的一半配置同时在awaitWithTimeout里给每个子任务设置独立的超时时间避免一个慢任务拖死整个会话。5. 记忆与上下文隔离多 Agent 协同不串台的关键多 Agent 的记忆设计比单 Agent 复杂得多。单 Agent 只要管好一个会话窗口多 Agent 必须回答每个 Agent 能看到哪些历史共享还是隔离上下文超了怎么办5.1 串台事故是怎么发生的我第一版犯过一个典型错误所有 Agent 共用一个全局记忆对象。结果用户先问了帮我写个周报文案 Agent 记住了下一秒问查一下这个月销量数据 Agent 把周报的历史也塞进了上下文。模型被无关历史干扰回答质量直线下降还显得精神分裂。问题的本质是上下文是一种资源的携带者不能无差别共享。跨 Agent 共享的只能是结论摘要而不是原始对话历史。5.2 命名空间隔离方案我的做法是给记忆加上命名空间会话 ID Agent 名称作为隔离维度每个 Agent 只访问自己那份窗口。Component public class ScopedChatMemory { private final MapString, MessageWindow windows new ConcurrentHashMap(); public MessageWindow windowOf(String sessionId, String agentName) { return windows.computeIfAbsent( sessionId :: agentName, key - new MessageWindow(2000, 20)); } }在构建每个 Agent 的 Advisor 时注入从这个工厂取出的独立窗口。这样同一会话内三个 Agent 各自有账本谁也不会读到谁的原始记录。共享信息只通过编排器传递——它是唯一允许跨 Agent 传递数据的角色。5.3 上下文窗口的预算管理记忆不是越完整越好。我的预算策略是三层硬上限每个 Agent 的消息窗口最多保留最近 20 条关键交互超出按摘要替换策略压缩。摘要层窗口快满时额外用一个轻量调用把旧消息压缩成 200 字以内的摘要替换原始记录。重要信息锚定用户在请求里明确提到的 ID、日期、指标口径等关键实体编排器会单独抽出来放进每轮任务的事实卡里防止摘要压缩时丢信息。这套策略跑下来单轮调用的 token 消耗比共享记忆方案少了 40% 左右而且回答里该有的关键数字基本都能保留。6. 稳定性与成本控制限流、重试、兜底方案怎么落多 Agent 系统一上线上下游的瓶颈就全暴露了模型 API 限流、下游数据库慢查询、外部服务超时、编排器自身的异常……任何一环抖动都会顺着 Agent 链路放大。这一节是我认为高级版和 Demo 版最大的分水岭。6.1 针对模型 API 的限流与重试模型供应商限流是所有多 Agent 系统绕不过去的坎。我做了三层防护信号量限流控制进程内并发调用数避免超过配额令牌桶限流按用户维度限制每秒请求数和每日调用量防止个别用户把预算打穿重试策略429限流和 5xx服务端异常处理方式不同429 要等Retry-After5xx 可以做指数退避重试。Spring 生态里最简单的方式是给 Router 和 Agent 的调用路径加RetryableRetryable( retryFor RateLimitException.class, maxAttempts 3, backoff Backoff(delay 500, multiplier 2) ) public ChatResponse callWithRetry(ChatClient client, Prompt prompt) { return client.prompt(prompt).call(); }同时设置调用超时我一般把单次模型调用超时设在 30~60 秒超时直接走失败分支而不是无限挂起。6.2 兜底与降级策略就算重试也失败系统不能对着用户报 500。我的兜底链路分三级同模型不同供应商同一个 Agent 配置备用的模型供应商主供应商超时时自动切换静态规则兜底对纯查询类请求匹配到明确意图时走预置模板返回结果不经过 LLM人工升级确实无法处理时把请求连同任务上下文完整投递给人工处理队列。我在实践中发现第 2 级兜底的价值往往被低估。很多用户请求是高度重复的与其让模型每次重新推理不如在编排层前面加一层意图匹配缓存命中就直接返回。这个缓存命中的需求配合 Agent 注册表做规则判断完全够用。6.3 成本控制让预算可预测多 Agent 系统的成本 路由调用 各执行 Agent 调用 聚合调用 重试的额外调用。上线前我按每用户每日 100 次请求、每次平均 5 次 LLM 调用做了预算模型结果第一个月的账单比我预估的还高——原因是重试和子任务拆分的冗余调用被低估了。建议从一开始就在每个 Agent 的 Advisor 里记录 token 消耗输入/输出分别计数按会话、按 Agent、按用户三个维度汇总。没有这套数据后面谈成本优化都是拍脑袋。7. 可观测性与回归测试让多 Agent 系统敢上生产最后这部分是我被生产事故教会的东西。多 Agent 系统的排查难度远超普通 CRUD 应用——同一个请求可能经过四五次模型调用任何一次输出异常都会影响最终结果而且模型输出是概率性的同样的输入可能每次都不一样。7.1 全链路日志与 Trace 贯穿我给每个用户请求生成一个traceId从路由开始就把它塞进每个 Prompt 的附加信息里之后所有 Agent 调用、工具调用、重试事件都带这个 ID 打日志。这样排查问题时一条命令就能把一个请求经历的所有模型调用完整捞出来。Spring AI 对ChatClient内置了 Micrometer 观测能力模型调用的耗时、token 数、异常次数都是现成指标。把这些接到监控面板上我设置了三个关键告警单 Agent 调用失败率超过 5%路由决策平均耗时超过 3 秒单会话 token 消耗环比增长超过 30%。这三个指标能覆盖大部分静默劣化场景。7.2 回归测试把模型调用固定住模型是不可控的但我们的测试不能因此不可重复。我的做法是录制-回放在测试环境里用一个MockChatModel替代真实模型针对固定输入返回预先录制的响应。这样编排逻辑、状态机、异常分支都能稳定回归。对于路由这类核心决策我会额外准备一组金标测试比如 50 条典型用户请求每条标注期望路由到的 Agent 和期望拆出的子任务。每次改动编排逻辑先跑这组金标路由准确率掉一个点都不允许合并。7.3 用模型评估模型的质量闭环功能测试之外回答质量也要有闭环。我每周跑一次样本评估取最近一周的真实请求样本由部署在不同参数下的评审 Agent对系统输出打分维度包括完整性、准确性、格式符合度。这个分数与成本账单一并看既能发现提示词退化也能验证优化是否真的有效。最后说一个我个人的经验多 Agent 系统最难的不是写代码而是保持克制。每加一个 Agent都要问自己这个职责真的需要独立角色吗还是给现有 Agent 加一个工具就够了我见过太多项目拆了八个 Agent最后有四个永远没有流量。先从一个编排器加四五个执行 Agent 起步把记忆隔离、稳定性、可观测性这三件事做扎实等流量验证确实需要再扩展——这才是多 Agent 架构能长期跑下去的正确节奏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑