先说个我自己的体感这两年做 Java 后端最明显的变化就是 AI 从“能用”到“用得起”的中间层开始出现。早期我习惯把大需求塞进一次模型调用里提示词越堆越长效果反而越来越差。后来接触 Spring AI才把多智能体协作系统逐渐落地。这篇文章不是入门手册而是我在生产环境里踩过一轮坑之后的经验总结。我会把架构设计、核心实现、常见问题一次讲清楚适合已经跑通单模型 ChatClient、准备让多个智能体协作完成复杂任务的团队参考。1. 为什么需要多智能体协作系统而不是单一大模型调用1.1 单体智能体的边界在哪里你可能遇到过这种场景业务方提了一个需求说“让 AI 帮我理解用户问题、查订单、写回复、判断要不要升级人工”于是你把所有要求写进一个 system prompt让模型一次性完成。刚开始 demo 很惊艳等到真实流量进来就露馅了。问题出在三个地方。第一是职责冲突同一个模型既要语气轻柔地安抚客户又要严格判断风险等级这两种指令在 prompt 里互相干扰。第二是上下文膨胀为了让模型完成多种任务你需要在上下文中塞入大量背景规则真正有用的对话历史反而被挤到边缘模型注意力分配不均结果就是不相关的内容也会被编造出来。第三是排查困难一次调用里的逻辑越复杂出问题后越难定位到底是提示词不合适还是返回结果格式异常整个调试过程非常痛苦。打个比方单体智能体就像一个什么都会一点的小卖部店员既要收银、又要理货、还要处理售后顾客一多就手忙脚乱。多智能体协作系统则更像一家连锁超市每个岗位上的人都只管自己最擅长的事店长负责分派任务员工各司其职最后汇总结果。这个类比放到系统设计上就是把“一个大 prompt 包打天下”拆成“多个专职智能体各管一段”。1.2 多智能体协作系统的三种基础模式我做过以及观察过的多智能体系统核心都逃不开三种协作模式。第一种是主管-工人模式也叫 Orchestrator-Worker。一个主管智能体接收原始需求拆解成子任务分发给若干工人智能体执行最后主管汇总结果。这种模式最直观、最容易实现适合“需求明确、子任务互相独立”的场景比如生成一篇包含数据分析、图表解读、结论建议的周报。第二种是流水线模式也叫 Pipeline。多个智能体按顺序执行前一个输出是后一个输入。这种模式适合处理有严格先后顺序的任务比如先做敏感信息脱敏再做情感分析最后生成回复文案。流水线模式的优点是每段逻辑都很简单缺点是只要中间有一个环节出错后面全崩。第三种是共享黑板模式也叫 Blackboard。多个智能体共享一份可读写的状态数据谁对当前数据感兴趣谁就去处理处理完再挂回黑板。这种模式适合开放性问题比如多个智能体共同推理一个复杂病例各自贡献解读最后综合判断。但它的实现复杂度最高数据一致性和并发控制都是坑。三种模式不是互斥的我在实际项目里经常混合使用主管-工人负责主线调度流水线负责单个子任务内部的处理黑板模式只在需要多智能体共同沉淀结论时使用。关键是想清楚每个智能体之间的数据依赖关系依赖关系确定了模式自然就选定了。1.3 高级版多智能体到底“高级”在哪里抛开概念不说多智能体协作系统相对单模型调用真正的收益体现在四个地方。第一提示词可以瘦身。每个智能体只需要关注自己的职责system prompt 能控制在 200 字以内模型不需要在一大堆互相矛盾的指令里猜优先级输出稳定性明显提升。第二系统具备扩展点。想增加一个“催单检测”智能体不需要改已有智能体只需要往主管的分派规则里加一条路由逻辑代码层面就是新增一个 Bean。第三可观测性变强。每个智能体单独打印日志、单独统计耗时和 token 消耗出问题时能直接定位到具体环节而不是面对一次黑盒调用无从下手。第四可以针对性优化。哪个智能体效果差就优化哪个的 prompt哪个智能体调用贵就给它换更便宜的模型这种细粒度控制在单体模型调用里很难做到。2. 基于 Spring AI 的总体架构设计2.1 核心角色划分协调者、执行者、监督者我搭建的第一个可用的多智能体系统结构上只有三层协调者、执行者、监督者。协调者智能体只做两件事第一是拆解用户请求第二是调度执行者。它不直接处理业务细节也不直接调用下游数据接口它的 prompt 里只需要写清楚“如何根据用户意图拆解任务以及每个任务应该交给哪个执行者”。执行者智能体是真正干活的比如查库存、写邮件、生成图表。每个执行者是一个独立的 ChatClient 实例拥有自己的 system prompt 和工具集。执行者之间不直接通信所有交互都通过协调者转发这样能避免智能体之间的 prompt 互相污染。监督者智能体负责质量控制和兜底。它不参与每一次调用而是定期检查执行者的输出是否符合规范如果发现结果异常会把任务重新打回给执行者或者直接降级走人工流程。监督者本质上也是一个智能体只是它的输入不是用户消息而是其他智能体的输出。三者之间的关系可以想象成项目经理、开发工程师和测试工程师。项目经理拆需求开发工程师写代码测试工程师卡质量。这个三层结构适合绝大多数业务场景而且很好维护。public interface Agent { String getAgentName(); String execute(String task, ChatMemory memory); }所有智能体实现这个接口协调者里维护一个MapString, Agent实现路由后续新增智能体只需要往 Spring 容器里注册即可。2.2 协作机制选型同步编排还是事件驱动多智能体系统最麻烦的问题不是怎么调模型而是多个智能体之间怎么通信。我见过两种主流做法。第一种是同步编排。协调者依次调用每个执行者拿到结果后再进入下一步。这种方式的优点是逻辑直观、链路清晰缺点是整体耗时会累加如果某个智能体卡住整个请求就卡住。适合对响应时间要求不高的内部后台任务。第二种是事件驱动。智能体通过消息队列或者本地事件总线异步通信协调者发出一个任务后就返回执行者完成后再把结果发回来。这种方式的优点是响应快、并发度高缺点是系统复杂度显著增加排错难度也上升。我的建议是如果你的系统还处在早期验证阶段优先用同步编排。等业务量证明某些步骤确实需要并行或者异步后再改造千万不要一上来就上事件驱动。脱离实际的架构设计都是给自己挖坑。Spring AI 的异步能力本身已经够用比如用CompletableFuture并行调用多个独立的执行者大部分场景根本不需要引入消息队列。2.3 为什么在 Java 后端里选 Spring AI可能有人会问现在 Python 生态的智能体框架那么多为什么非要用 Spring AI我的理由很直接团队是 Java 背景现有服务全是 Spring Boot如果我为一个智能体系统另起一个 Python 服务维护成本至少要翻一倍。Spring AI 最大的价值是把模型调用抽象成了和数据库操作一样自然的东西你不需要了解每个模型厂商的 API 差异只要面向ChatClient编程即可。Spring AI 的几个优点我非常看重。首先是ChatClient的流式接口设计得很顺手prompt、message、advisors、tools 都能链式组合。其次是工具调用Function Calling的 Java 集成做得够好用Tool注解就能把现有 service 方法暴露给模型。最后是 Advisor 机制类似于 Spring MVC 里的拦截器可以在模型调用前后插入通用逻辑比如日志记录、敏感词过滤、质量检查。这些能力组合起来搭建多智能体系统相当顺手。3. 高级版核心实现细节与实操3.1 工程结构与依赖配置我用的是标准 Spring Boot 多模块工程总共分四个模块agent-core、agent-executor、agent-orchestrator、agent-api。核心模块放公共定义比如 Agent 接口、消息对象、工具注册表执行者模块放各个业务智能体编排模块放协调者和路由逻辑API 模块对外暴露 HTTP 接口。依赖方面Spring AI 的起步依赖非常清晰。如果你接入的是兼容 OpenAI 协议的模型服务只需要引入dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency如果你需要结构化输出、记忆管理和内置 Advisor建议把spring-ai-autoconfigure相关的模块也加上。实际工程里我的凭据配置放在application.yml中通过环境变量注入模型 base-url、API key、默认模型名称分开管理。spring: ai: openai: base-url: ${AI_BASE_URL:https://api.openai.com} api-key: ${AI_API_KEY:} chat: options: model: ${AI_MODEL:gpt-4o} temperature: 0.2我这里刻意把 temperature 调低到 0.2后面会细讲原因。3.2 用 ChatClient 搭建第一个专职智能体Smart 初始化 ChatClient 的时候有几个细节需要特别注意。不要每个请求都 new 一个 ChatClient而是通过构建器模式初始化一个带有默认配置的 Bean再注入到具体智能体里。Configuration public class ChatClientConfig { Bean public ChatClient supervisorChatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem(你是一个任务协调者负责把用户请求拆解为子任务...) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory())) .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); } }这里有几个容易踩的坑。defaultSystem只会在调用链里没有任何体系消息时生效如果后续代码塞入了 system message默认值会被覆盖。SimpleLoggerAdvisor会打印完整请求和响应日志量很大生产环境要控制级别。defaultAdvisors的顺序很重要先注册的先执行我通常把记忆 Advisor 放前面Logger 放最后避免把记忆历史也打印到日志里。写一个最小可用的执行者智能体Service public class OrderAgent implements Agent { private final ChatClient chatClient; public OrderAgent(Qualifier(executorChatClient) ChatClient chatClient) { this.chatClient chatClient; } Override public String getAgentName() { return order-agent; } Override public String execute(String task, ChatMemory memory) { return chatClient.prompt() .system(systemSpec - systemSpec.text(你是订单助理只能根据工具查询结果回答不要猜测订单状态。)) .user(task) .call() .content(); } }这里的重点在于我要求模型“只能根据工具查询结果回答”这是减少幻觉的核心手段。如果允许模型基于训练数据编造订单状态一定会出乱子。3.3 智能体工具注册让模型真正“动手”多智能体系统里工具调用是执行者智能体的生命线。模型本身没有业务数据只有通过工具才能查询数据库、调用下游接口、写入结果。Spring AI 支持用Tool注解直接注册工具方法。Component public class OrderTools { Tool(name queryOrderStatus, description 根据订单号查询订单当前状态返回结构化状态信息) public String queryOrderStatus(String orderId) { OrderStatus status orderService.getStatus(orderId); return String.format(订单号%s, 状态%s, 更新时间%s, orderId, status.getCode(), status.getUpdateTime()); } }工具注册之后模型会在需要查询订单时主动发起函数调用Spring AI 负责把模型生成的参数 JSON 反序列化成 Java 方法入参再调用业务方法最后把返回值传回给模型。整个过程对业务方基本透明。但工具调用有一个非常经典的坑模型生成的参数不总是符合预期。比如订单号应该是一个 16 位字符串模型可能传一个带空格的“1234567890123456 ”下游接口直接报错。我的处理方式是在工具方法入口做防御性校验和清洗宁可工具返回“参数不合法”的提示也不要把异常抛给模型再让它瞎猜。另外一个经验是工具返回内容不能太长。模型会把你返回的内容当成上下文继续处理如果你返回一张几百行的数据库表token 消耗会爆炸而且模型会迷失在细节里。工具方法里尽量聚合信息只返回关键字段和必要的判断依据。3.4 多智能体编排任务分解与结果聚合协调者智能体的核心逻辑可以拆成三步意图识别、任务分派、结果汇聚。意图识别这一步我试过两种做法。低成本的方案是让协调者模型直接输出 JSON 数组列出子任务列表和对应的执行者名称高可靠的方案是给协调者配置一个工具routeToAgent(task, agentName)让模型自己决定调哪个工具。第二种方案更稳因为工具调用的参数格式由模型生成你可以加校验逻辑。Service public class SupervisorAgent { Tool(name routeToAgent, description 将子任务路由给指定执行者) public String routeToAgent(String task, String agentName) { Agent executor agentRegistry.get(agentName); return executor.execute(task, currentMemory); } public String dispatch(String userRequest) { return chatClient.prompt() .user(userRequest) .call() .content(); } }任务分派完成后结果汇聚也很讲究。不要只是简单地把多个执行者的返回文本拼在一起那是懒人做法。更好的做法是给汇聚阶段单独写一个 prompt要求模型基于多个结果生成最终答案并标明每部分信息来源。这样做的好处是最终回复有据可查用户追问时能定位到具体智能体。并行执行方面如果多个子任务之间没有依赖务必使用异步并行而不是串行调用。我常用CompletableFuture把多个执行者调用包起来再配合allOf等待全部完成。实测下来四个独立智能体并行比串行节省约 50% 以上的响应时间。3.5 记忆系统与上下文管理多智能体系统最容易被低估的部分是记忆。如果你只是把用户问题传给每个智能体对话一多模型就会失忆。Spring AI 提供了ChatMemory与MessageChatMemoryAdvisor作用是把历史消息追加到每次调用前。我在系统中把记忆分成了两层。第一层是会话级短时记忆保存当前对话窗口的消息列表用InMemoryChatMemory就能实现适合单机部署。第二层是业务级长期记忆比如用户偏好、订单编号、历史问题标签需要持久化到数据库或缓存服务中在编排智能体时先查询再注入到 prompt。跨智能体传记忆时要特别注意不要把所有智能体的历史消息堆在同一个ChatMemory实例里否则协调者会看到执行者内部的工具调用记录导致上下文变得混乱。更好的方案是按智能体维度隔离 memory key比如用userId agentName作为命名空间。配置记忆 Advisor 的代码很简单var memory new InMemoryChatMemory(); var advisor new MessageChatMemoryAdvisor(memory, session- userId, 20);其中20表示保留最近 20 条消息。轮次太大会冲淡重点太小又会失忆我通常先用 20 跑业务再根据评测结果调整。3.6 执行保障重试、超时、降级多智能体系统跑在生产环境最怕的不是模型回答错而是模型调用超时拖垮整个链路。模型服务偶尔会抖动这是客观事实所以要提前做好三件事。第一是超时控制。我给每个执行者的模型调用都设置了连接超时和读取超时通常连接超时 5 秒、读取超时 30 秒。超时之后不能直接抛异常而是返回一个“暂时无法处理”的降级响应让用户感知到系统已经努力过。public String executeWithTimeout(String task, int timeoutSeconds) { try { return chatClient.prompt().user(task).call().content(); } catch (Exception e) { return 系统暂时繁忙请稍后再试。你的请求已记录人工客服稍后跟进。; } }第二是重试策略。不是所有异常都适合重试网络超时、限流这种瞬时异常可以重试但业务逻辑错误、prompt 格式错误重试也没用。我用Retryable注解控制重试次数和退避时间最多重试两次避免雪上加霜。第三是熔断降级。如果某个执行者智能体连续调用失败超过阈值我会在路由注册表里直接把任务改派给兜底智能体兜底智能体不需要调用模型直接走人工处理流程。这个开关帮我避免了很多线上事故。4. 常见问题与排查实录4.1 token 超限怎么办多智能体系统上下文消耗比单体模型调用更快因为每次路由、每次任务分派都会产生新的消息记录。我遇到比较频繁的就是长对话后报 context length exceeded。解决思路有三个层级。第一层是控制消息数量用 memory advisor 的轮次窗口截断。第二层是摘要替换当消息超过一定数量时调用一个专门的摘要智能体把早期对话浓缩成一段摘要替代原文塞入上下文。第三层是业务级精简尽量不在上下文里放重复的大段工具返回工具结果按需查询即可。顺手提醒一句如果系统并发量高token 超限还会带来成本问题。我习惯在日志里记录每个智能体每次调用的 token 消耗找一个最小满足需求的模型组合能省不少钱。4.2 模型输出不稳定怎么办多智能体系统对外表现是否稳定很大程度取决于模型输出格式是否稳定。如果你让模型自由发挥结果肯定五花八门。我的做法是尽量使用结构化输出强制模型返回 JSON Schema 定义好的格式。Spring AI 的StructuredOutputConverter可以把模型输出映射成自定义 Java 对象解析失败会自动重试一次。public record TaskPlan(ListString steps, ListString assignedAgents) {} TaskPlan plan chatClient.prompt() .user(userRequest) .call() .entity(TaskPlan.class);用了结构化输出后解析异常少了非常多。但这不代表万事大吉模型返回的内容仍然可能在语义上跑偏所以我觉得监督者智能体不可省。监督者不参与每一次调用而是抽查执行者输出按校验规则打分不合格就重新执行。4.3 多个智能体互相“打架”怎么解多智能体系统里有一个很隐蔽的问题多个执行者智能体可能会因为共享状态而互相覆盖数据。比如客服流程里A 智能体把用户订单标记为“已处理”B 智能体不知道再次查询时发现状态不对就发起重复操作。解决这个问题我在共享数据上加了两个约束。第一是权限隔离每个执行者只能通过自己的工具访问数据不能跨智能体直接改别人负责的数据域。第二是状态版本号更新共享数据时带上 version 字段并发更新时乐观锁会拒绝过期版本。这些约束看起来像传统后端设计但它们恰恰是多智能体系统落地时最容易被忽略的工程细节。4.4 线上运行的性能与成本控制最开始我把所有执行者都配置成同一个模型后来发现这是最浪费的做法。简单任务用大模型既要付额外成本又增加了响应延迟。我调整之后把执行者按任务复杂度分了两个级别关键推理和汇总用强模型简单查询和格式转换用轻量模型。另外同一个用户、同一个会话内如果有重复工具调用我会加一层结果缓存。比如用户反复查询同一订单状态在会话有效期内直接从缓存取不进模型调用链。这些优化合在一起整体成本下降约三到四成。最后再分享一个我从项目里沉淀下来的经验多智能体协作系统的落地顺序应该是先跑通单智能体再拆职责再加编排最后才上监督和降级。如果一上来就照着教科书搭一套三维架构大概率会因为排查困难和调试成本太高而中途放弃。每个团队都应该根据自己的团队结构和业务复杂度选择合适版本先让系统跑起来再逐步增强。
阅读完成 · 觉得有帮助?