Spring AI Alibaba ReactAgent:Java多智能体开发范式重构

Spring AI Alibaba ReactAgent:Java多智能体开发范式重构 1. 项目概述为什么一个“5天→5小时”的压缩比值得专门写篇长文Spring AI Alibaba 这个词最近在 Java 技术圈里出现的频率已经快赶上当年 Spring Boot 刚出来时大家刷屏“终于不用写 XML 配置了”的盛况。但和当年不同的是这次不是“简化配置”而是直接把 MultiAgent 构建这件事从“需要搭骨架、连神经、调心跳、写容错”的系统工程变成了“定义角色、写提示词、配模型、跑起来”的流水线作业。标题里那个“5天压到5小时”不是夸张修辞是我上周给客户做 PoC 时的真实记录——原来用纯 Spring Boot 自研调度框架 手搓 Agent 协作逻辑光是让三个 Agent规划员、执行员、校验员能稳定交换上下文、不丢状态、不串会话就花了整整三天半而换上 Spring AI Alibaba 的 ReactAgent 模式后核心流程代码从 870 行降到 213 行本地启动调试耗时从平均 42 分钟缩短到 6 分钟以内真正意义上实现了“上午提需求下午交可运行 demo”。这个转变背后不是某个 API 更好用了而是整个开发范式被重写了。ReactAgent 不是又一个“调大模型的工具类”它是把 LLM 的推理过程、状态管理、工具调用、错误恢复这四层能力全部封装进 Spring 的生命周期和事件总线里。你写的不再是“我让模型思考三步”而是“我声明一个 Agent它应该具备什么能力、能调用哪些服务、失败时走哪条降级路径”。这种声明式编程才是 Java 工程师真正熟悉的语言。所以这篇文章不会讲“Spring AI Alibaba 是什么”而是直接带你拆开它的引擎盖看清楚 ReactAgent 是怎么把 MultiAgent 的复杂度一层层吃进 Spring 容器里的——包括它如何用AgentState替代手动维护的 MapString, Object 上下文如何用ToolExecutor统一调度数据库查询、HTTP 调用、文件读写这些异构操作以及最关键的它怎么用AgentLoop的有限状态机机制把原本需要人工编排的“思考→行动→观察→反思”循环变成一个可配置、可监控、可打断的标准 Bean。如果你正在被以下问题困扰面试官问“Java 做 MultiAgent 有哪些坑”你只能答出“状态同步难”“工具调用散乱”“错误处理没统一入口”或者你手头有个智能客服项目想加个“自动归因知识库检索工单生成”三级协作流但评估下来开发周期要两周又或者你刚学完 LangChain发现 Java 版本文档少、示例碎、升级踩坑多……那这篇就是为你写的。它不教你怎么配 Maven 依赖而是告诉你当spring-ai-alibaba-starter启动时容器里到底多了哪些关键 Bean它们之间怎么握手以及当你在Agent注解里写maxIterations 7时底层发生了多少次线程切换、状态快照和异常包装。2. 核心设计思路ReactAgent 不是“调模型”而是“建协议”2.1 传统 MultiAgent 开发的三大反模式在 Spring AI Alibaba 出现之前Java 里实现 MultiAgent 的主流做法基本逃不出这三种反模式。我拿上周客户那个“航空客服工单自动归因”项目为例还原一下当时踩过的坑第一种状态裸奔型典型表现是用ConcurrentHashMapString, Object存所有 Agent 共享数据比如context.put(flightNumber, CA123)、context.put(lastQueryResult, ListFlightInfo)。问题在于类型完全丢失后续每个 Agent 都得if (context.get(lastQueryResult) instanceof List)做强转没有生命周期管理一个 Agent 修改了flightNumber另一个 Agent 可能还在用旧值调试时根本不知道哪个线程在什么时候改了哪个 key。我们当时为此加了 137 行日志埋点才定位到是“校验员 Agent”在重试时覆盖了“规划员”刚存的航班时刻表。第二种工具调用碎片化型每个 Agent 自己写RestTemplate调航班查询接口、自己写JdbcTemplate查知识库、自己写FileUtils读附件。结果是12 个 Agent 共用同一套数据库连接池但每个都用自己的事务传播行为导致偶尔出现“查到未提交数据”HTTP 调用超时时间五花八门有的设 3 秒有的设 30 秒熔断策略根本没法统一最致命的是当知识库接口返回 503 时“执行员”直接抛RuntimeException而“规划员”根本收不到通知还在继续生成下一步指令。第三种循环控制硬编码型用while (iterations MAX_LOOP !isDone)包裹整个思考链里面混着模型调用、工具执行、结果解析。问题在于每次迭代都要手动序列化/反序列化整个上下文GC 压力大实测 JVM 老年代每分钟 GC 2.3 次“是否结束”的判断逻辑散落在各处有的看response.contains(已完成)有的看toolResult null没有统一出口一旦某次迭代卡死比如航班接口假死整个 while 循环就 hang 住必须 kill 进程重启。这三种反模式本质都是把本该由框架解决的横切关注点状态管理、资源调度、流程控制扔给了业务代码去缝合。而 ReactAgent 的设计哲学就是把这些缝合线全部替换成 Spring 的标准协议。2.2 ReactAgent 的四层协议化封装Spring AI Alibaba 的 ReactAgent其实是用 Spring 的四个核心能力重新定义了 MultiAgent 的交互契约第一层状态契约 ——AgentState接口它不是一个 Map而是一个带泛型、可继承、有版本号的接口。你定义自己的状态类时必须实现AgentState比如public class AirlineTicketState implements AgentState { private String flightNumber; private ListFlightInfo queryResult; private int retryCount 0; private LocalDateTime lastModified; // 框架自动注入时间戳 }关键点在于所有字段默认为private强制你通过 getter/setter 访问框架可在 setter 中插入审计日志框架会在每次 Agent 执行前后自动对AirlineTicketState做深拷贝彻底杜绝状态污染lastModified字段由框架在setState()时自动填充你无需关心线程安全。第二层工具契约 ——Tool接口与ToolExecutor所有外部调用必须封装成Tool实现类比如航班查询工具Component public class FlightQueryTool implements Tool { Override public String getName() { return query_flight_info; } Override public String getDescription() { return 根据航班号查询实时航班信息返回起飞时间、到达时间、状态; } Override public ToolResult invoke(ToolExecutionRequest request) { // 这里写你的 RestTemplate 调用逻辑 return ToolResult.success(flightInfoJson); } }框架会自动将所有Component标记的Tool注入ToolExecutor并在 Agent 执行时按需调用。好处是所有工具调用统一走ToolExecutor.invoke()超时、重试、熔断策略在此集中配置ToolResult封装了成功/失败状态、原始响应、错误码Agent 无需再解析 HTTP 状态码当你新增一个“发送短信通知”工具时只需写一个新Tool类Agent 代码一行不用改。第三层流程契约 ——AgentLoop有限状态机ReactAgent 不用while循环而是用状态机驱动。它的核心状态只有四个INITIAL加载初始状态准备第一次模型调用THINKING调用 LLM 生成下一步动作可能是调工具也可能是直接返回结果ACTING执行Tool并将结果写回状态DONE满足终止条件如maxIterations达到或模型明确说“任务完成”。状态流转由AgentLoop控制你只需配置maxIterations7和terminationCondition比如state.getRetryCount() 3框架自动处理所有状态跳转、异常捕获、重试计数。第四层模型契约 ——ChatClient与PromptTemplateReactAgent 不直接操作AlibabaCloudChatClient而是通过ChatClientBean 与PromptTemplate解耦。你定义提示词模板spring: ai: alibaba: chat: prompt-template: | 你是一个航空客服专家请根据以下信息完成任务 - 当前航班号{flightNumber} - 最近一次查询结果{queryResult} - 已重试次数{retryCount} 请严格按 JSON 格式输出{action: query_flight_info | return_result, reason: ...}框架在每次THINKING状态时自动将AgentState字段注入模板生成最终 prompt。这样换模型从 Qwen1.5 切到 Qwen2只需改配置提示词逻辑完全复用。这四层协议把原本需要业务代码手工编织的网变成了 Spring 容器里自动装配的管道。你写的Agent类本质上只是在声明“这条管道里流什么数据、接什么工具、走什么状态”而不是去拧每一个阀门。3. 核心实现细节从零搭建一个可运行的 ReactAgent 流程3.1 环境准备与依赖精简指南别急着mvn clean install先确认你的项目基础是否干净。Spring AI Alibaba 对环境有隐性要求很多“启动报错”其实和它无关而是环境冲突导致的。我整理了一份最小可行依赖清单基于 Spring Boot 3.2.0 Java 17dependencies !-- Spring Boot Web 是必须的ReactAgent 依赖 WebMvcConfigurer -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI Alibaba 核心注意版本 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-alibaba-spring-boot-starter/artifactId version0.8.1/version !-- 这是目前最稳的版本0.9.0-rc 有状态快照 bug -- /dependency !-- 如果要用数据库工具JDBC Starter 必须显式引入 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- 日志增强方便追踪 Agent 执行链 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency /dependencies关键避坑点不要引入spring-ai-langchain4j-spring-boot-starter它和 Alibaba Starter 有ChatClientBean 冲突会导致NoUniqueBeanDefinitionExceptionspring-boot-starter-webflux可以不加ReactAgent 默认走 Servlet 容器加 WebFlux 反而会触发不必要的 Netty 线程模型切换Lombok 可以用但必须关掉Data的toString()AgentState的toString()会被框架频繁调用自动生成的toString()会递归打印整个上下文极易 OOM。正确写法是Getter Setter NoArgsConstructor手动写toString()只打关键字段。配置文件application.yml的核心项spring: ai: alibaba: # 必须配置 endpoint否则会连公网测试地址 endpoint: https://dashscope.aliyuncs.com/api/v1 # access-key 从阿里云 DashScope 控制台获取不要写在代码里 access-key: ${DASHSCOPE_ACCESS_KEY:your-default-key} # 模型选 Qwen2-72b-Instruct它对多 step 推理支持最好 model-name: qwen2-72b-instruct # 关键关闭 streamingReactAgent 的状态机依赖完整响应 streaming: false # 提示词模板放这里比硬编码在 Java 里更易维护 chat: prompt-template: | 你是一个严谨的航空客服助手请严格按以下步骤执行 1. 若 {flightNumber} 为空立即返回 {action: return_result, reason: 航班号缺失} 2. 若 {retryCount} 3返回 {action: return_result, reason: 已重试3次放弃查询} 3. 否则调用 query_flight_info 工具查询航班信息 输出必须是合法 JSON无任何额外字符。 # 工具调用超时统一设为 8 秒比默认 30 秒更合理 tool: timeout: 8000 max-retries: 2提示DASHSCOPE_ACCESS_KEY必须用环境变量注入不要写死在配置文件里。本地开发可以用export DASHSCOPE_ACCESS_KEYsk-xxx生产环境用 K8s Secret 挂载。3.2 定义你的第一个 Agent从Agent注解开始ReactAgent 的入口是一个被Agent注解标记的类。这不是一个普通的 Spring Bean而是一个被框架特殊处理的“Agent 定义”。我们来写一个最简版的航班查询 AgentComponent Agent( id airline-ticket-agent, // 必须唯一用于日志和监控 name 航空票务专家, description 负责查询航班实时信息并生成工单, maxIterations 7, // 状态机最大循环次数 terminationCondition #{state.retryCount 3 || state.queryResult ! null} // SpEL 表达式 ) public class AirlineTicketAgent { // 注入你定义的状态类框架会自动创建实例 private final AirlineTicketState initialState; // 构造器注入确保状态不可变 public AirlineTicketAgent(AirlineTicketState initialState) { this.initialState initialState; } // 这是 Agent 的“大脑”框架会在 THINKING 状态调用它 Thinking public ChatResponse think(ChatRequest request, AgentState state) { // request 是框架组装好的 promptstate 是当前状态 // 你只需要返回模型调用结果框架负责解析 JSON 并决定下一步 return chatClient.call(request); } // 这是 Agent 的“手脚”框架会在 ACTING 状态调用它 Acting public void act(ToolExecutionRequest request, AgentState state) { // request 包含工具名和参数state 是当前状态 // 框架会自动找到对应的 Tool 并执行 toolExecutor.invoke(request); } // 这是 Agent 的“终点”框架会在 DONE 状态调用它 Done public AgentResult done(AgentState state) { // state 此时已是最终状态你可以提取结果 return AgentResult.success( 航班 state.getFlightNumber() 查询完成状态 state.getQueryResult().get(0).getStatus() ); } }逐行解释关键点Agent(id airline-ticket-agent)这个 id 会出现在所有日志里比如Agent [airline-ticket-agent] entering THINKING state排查问题时靠它定位terminationCondition #{state.retryCount 3 || state.queryResult ! null}这是 SpEL 表达式框架每次状态流转前都会计算它。注意state是AirlineTicketState的实例字段名必须和类里定义的一致Thinking方法它接收ChatRequest而不是原始字符串。ChatRequest里已经包含了注入了状态字段的 prompt你不需要手动拼接Acting方法参数ToolExecutionRequest里封装了工具名如query_flight_info和参数如{flightNumber: CA123}框架会自动匹配Tool并执行Done方法返回AgentResult它会被框架包装成标准响应体前端可以直接消费。3.3 编写 Tool把所有外部调用变成可插拔的“积木”Tool 是 ReactAgent 的“肌肉”它把所有脏活累活封装起来。我们写一个航班查询 ToolComponent public class FlightQueryTool implements Tool { private final RestTemplate restTemplate; public FlightQueryTool(RestTemplate restTemplate) { this.restTemplate restTemplate; } Override public String getName() { return query_flight_info; // 必须和提示词里写的 action 名一致 } Override public String getDescription() { return 根据航班号查询实时航班信息返回起飞时间、到达时间、状态; } Override public ToolResult invoke(ToolExecutionRequest request) { try { // 1. 从 request 中提取参数 String flightNumber request.getArguments().get(flightNumber).toString(); // 2. 构造请求体这里模拟调用内部航班服务 String url https://internal-api.airline.com/flights/ flightNumber; HttpHeaders headers new HttpHeaders(); headers.set(X-Auth-Token, internal-service-token); HttpEntityString entity new HttpEntity(headers); // 3. 执行 HTTP 调用框架的 tool.timeout 会生效 ResponseEntityString response restTemplate.exchange( url, HttpMethod.GET, entity, String.class ); // 4. 成功时返回 ToolResult.success框架会自动把结果塞回 state return ToolResult.success(response.getBody()); } catch (HttpClientErrorException e) { // 5. 业务异常返回 ToolResult.error框架会增加 retryCount 并重试 return ToolResult.error(航班服务返回 e.getStatusCode() 错误); } catch (Exception e) { // 6. 系统异常同样走 error 流程 return ToolResult.error(调用航班服务失败: e.getMessage()); } } }为什么 Tool 必须这么写getName()返回的字符串必须和你在prompt-template里写的{action: query_flight_info}完全一致大小写、下划线都不能错ToolResult.success()的参数会被框架自动解析成 JSON并尝试映射到AirlineTicketState的对应字段比如如果返回{status: 延误}框架会尝试调用state.setStatus(延误)ToolResult.error()不会直接抛异常而是触发AgentLoop的重试逻辑state.retryCount会自动 1然后回到THINKING状态让模型决定是重试还是换方案。3.4 状态类AgentState的实战写法AirlineTicketState看似简单但它的写法直接影响整个流程的健壮性。这是经过三次重构后的最终版public class AirlineTicketState implements AgentState { // 1. 所有字段必须 private且提供 getter/setter private String flightNumber; private ListFlightInfo queryResult; private int retryCount 0; private String lastAction; // 记录上一步 action用于 debug private LocalDateTime lastModified; // 框架自动注入不要自己 new // 2. 必须有无参构造器框架用反射创建实例 public AirlineTicketState() {} // 3. 提供 builder 模式构造器方便测试 public static AirlineTicketStateBuilder builder() { return new AirlineTicketStateBuilder(); } // 4. getter/setter注意 setter 里可以加业务逻辑 public void setFlightNumber(String flightNumber) { if (StringUtils.isBlank(flightNumber)) { throw new IllegalArgumentException(航班号不能为空); } this.flightNumber flightNumber.trim().toUpperCase(); } public void setQueryResult(ListFlightInfo queryResult) { // 框架会传 null这里做防御 this.queryResult Optional.ofNullable(queryResult) .map(ArrayList::new) // 深拷贝避免外部修改影响 state .orElse(new ArrayList()); } public void setRetryCount(int retryCount) { this.retryCount Math.min(retryCount, 5); // 限制最大重试次数 } // 5. 重写 toString()只打关键字段防止日志爆炸 Override public String toString() { return AirlineTicketState{ flightNumber flightNumber \ , retryCount retryCount , queryResultSize (queryResult ! null ? queryResult.size() : 0) }; } // 6. 可选添加业务方法让 Agent 代码更简洁 public boolean isQueryCompleted() { return CollectionUtils.isNotEmpty(queryResult); } }关键经验setQueryResult()里做了ArrayList::new深拷贝这是为了防止外部代码修改List导致状态不一致。ReactAgent 的状态快照是浅拷贝不加这层防护queryResult.add(new FlightInfo())就会污染快照setRetryCount()里加了Math.min(retryCount, 5)这是兜底逻辑。即使terminationCondition写错了也不会让重试次数无限增长toString()只打queryResultSize而不是整个列表线上环境日志量能减少 90%。我们曾因为toString()打全量 JSON导致 ELK 日志集群磁盘爆满。3.5 启动与调试如何让第一个 Agent 跑起来写完上面所有代码启动类就一句话SpringBootApplication public class AirlineAgentApplication { public static void main(String[] args) { SpringApplication.run(AirlineAgentApplication.class, args); } }启动后访问http://localhost:8080/actuator/health看到{status:UP}就说明容器起来了。但 ReactAgent 不是 Web 服务它需要你主动触发。我们写一个简单的 Controller 来调用它RestController RequestMapping(/api/agent) public class AgentController { private final AgentRunner agentRunner; public AgentController(AgentRunner agentRunner) { this.agentRunner agentRunner; } PostMapping(/ticket) public ResponseEntityAgentResult runTicketAgent(RequestBody TicketRequest request) { try { // 1. 构建初始状态 AirlineTicketState initialState AirlineTicketState.builder() .flightNumber(request.getFlightNumber()) .build(); // 2. 调用 AgentRunner传入 Agent ID 和初始状态 AgentResult result agentRunner.run( airline-ticket-agent, // 和 Agent.id 一致 initialState ); return ResponseEntity.ok(result); } catch (Exception e) { // 3. 捕获 Agent 执行异常比如状态校验失败 return ResponseEntity.status(400) .body(AgentResult.error(Agent 执行失败: e.getMessage())); } } } // 请求体 public class TicketRequest { private String flightNumber; // getter/setter }调试技巧在Thinking方法里加断点看request.getMessages()里是不是你期望的 prompt在Acting方法里加断点看request.getArguments()里参数是否正确查看日志搜索Agent [airline-ticket-agent]你会看到完整的状态流转日志INFO Agent [airline-ticket-agent] entering INITIAL state INFO Agent [airline-ticket-agent] entering THINKING state INFO Agent [airline-ticket-agent] entering ACTING state, actionquery_flight_info INFO Agent [airline-ticket-agent] entering THINKING state INFO Agent [airline-ticket-agent] entering DONE state4. 实战问题排查那些文档里不会写的“血泪教训”4.1 常见问题速查表问题现象根本原因解决方案验证方式启动报错No qualifying bean of type ChatClientspring-ai-alibaba-spring-boot-starter未正确引入或版本与 Spring Boot 不兼容检查mvn dependency:tree | grep spring-ai确认spring-ai-alibaba-spring-boot-starter在依赖树顶层且版本为0.8.1curl http://localhost:8080/actuator/beans | grep chatClient应返回chatClientBeanAgent 执行卡在THINKING状态日志停在entering THINKING state模型 API 调用超时但streaming: false配置未生效检查application.yml中spring.ai.alibaba.chat.streaming是否为false并确认没有其他配置源如bootstrap.yml覆盖它在Thinking方法里加日志看是否进入方法体ToolResult.success()返回的数据没有自动映射到AgentState字段ToolResult的 JSON 结构和AgentState字段名不匹配或字段类型不兼容用ObjectMapper手动解析ToolResult的content对比字段名和类型。例如ToolResult返回{flight_status: 延误}但AgentState字段是private String status;则需改名为flightStatus或加JsonProperty(flight_status)在Acting方法里打印request.getArguments()和toolResult.getContent()terminationCondition不生效Agent 一直循环到maxIterationsSpEL 表达式语法错误或AgentState字段为null导致 NPE将表达式简化为#{state ! null}确认能生效再逐步加字段。检查AgentState构造器是否正确初始化了所有字段在Done方法里加日志看是否被调用多个 Agent 并发执行时状态互相污染AgentState类里用了静态变量或Tool类里用了共享的RestTemplate实例变量AgentState必须是无状态的 POJORestTemplate必须用Autowired注入不要自己new用 JMeter 并发 100 个请求检查每个请求返回的flightNumber是否和输入一致4.2 三个真实踩过的坑及修复过程坑一maxIterations设为 1Agent 却执行了 3 次现象配置maxIterations 1但日志显示THINKING → ACTING → THINKING → DONE共两次THINKING。排查翻看AgentLoop源码发现maxIterations控制的是“模型调用次数”而ACTING状态不计入。第一次THINKING模型返回{action: query_flight_info}进入ACTINGACTING完成后框架自动进入下一次THINKING让模型判断是否完成。所以maxIterations 1的实际含义是“最多让模型思考一次”但整个流程可能包含多次ACTING。修复把maxIterations改为3并在terminationCondition里明确写#{state.isQueryCompleted()}让状态驱动而不是靠迭代次数硬控。坑二ToolResult.error()后Agent 直接DONE没有重试现象航班服务返回 503ToolResult.error()被调用但日志显示entering DONE stateretryCount没变。排查ToolResult.error()的 Javadoc 写着“Indicates the tool execution failed and should be retried.” 但前提是AgentState的retryCount字段存在且可写。我们最初的状态类里retryCount是final int retryCount 0;setRetryCount()方法被注释掉了。修复去掉final补全setRetryCount()并确保terminationCondition里用了state.retryCount。框架在error时会自动调用setRetryCount(state.getRetryCount() 1)。坑三本地测试 OKK8s 环境下 Agent 启动失败报ClassNotFoundException: com.fasterxml.jackson.databind.JsonNode现象本地mvn spring-boot:run正常但打包成 jar 部署到 K8s启动时报 Jackson 类找不到。排查spring-ai-alibaba-spring-boot-starter的pom.xml里jackson-databind是optionaltrueMaven 不会传递依赖。而我们的pom.xml里没显式引入 Jackson。修复在项目pom.xml里显式添加dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency并确认mvn dependency:tree里jackson-databind的版本是2.15.2和 Spring Boot 3.2.0 一致。4.3 性能调优让 5 小时的流程真正稳定在 5 小时内ReactAgent 的性能瓶颈90% 不在模型调用而在状态序列化和日志。我们做了三项关键优化让 P95 响应时间从 12.4 秒降到 3.8 秒第一项禁用AgentState的toString()日志默认情况下AgentLoop在每次状态流转时会调用state.toString()并打 INFO 日志。而我们的AirlineTicketState里queryResult是一个包含 50 个FlightInfo的列表toString()会生成 2MB 的日志。优化在application.yml里加logging: level: org.springframework.ai.agent: WARN # 把 AgentLoop 日志降到 WARN同时在AirlineTicketState.toString()里只打关键字段如前所述。第二项状态快照深度控制ReactAgent 默认对AgentState做深拷贝但如果state里有InputStream或Socket这种不能序列化的字段就会失败。我们曾在一个Tool里不小心把FileInputStream塞进了state。优化在Agent注解里加stateSnapshotStrategy StateSnapshotStrategy.SHALLOW告诉框架只拷贝对象引用不深拷贝。前提是你的AgentState里所有字段都是不可变的String,Integer,LocalDateTime或者你自己保证不修改。第三项工具调用线程池隔离所有Tool默认在TaskExecutor里执行而 Spring Boot 的默认线程池是SimpleAsyncTaskExecutor它为每个任务新建线程高并发下容易 OOM。优化在application.yml里配置专用线程池spring: task: execution: pool: max-size: 20 core-size: 5 queue-capacity: 100并在Tool类上加Async让工具调用走这个池子。5. 进阶扩展从单 Agent 到 MultiAgent 协同网络5.1 多 Agent 协作的三种模式ReactAgent 本身是单 Agent 框架但通过组合可以构建出复杂的 MultiAgent 网络。我们实践过三种模式按复杂度排序模式一链式调用Chain最简单A Agent 的输出作为 B Agent 的输入。比如PlanningAgent输入用户问题输出结构化任务列表[查询航班, 检查知识库, 生成工单]ExecutionAgent接收任务列表逐个执行把结果汇总SummarizationAgent接收汇总结果生成自然语言回复。实现要点用AgentRunner.run()的返回值AgentResult提取result.getOutput()作为下一个 Agent 的initialState。代码像流水线一样直白。模式二广播-聚合Broadcast-Aggregate一个主 Agent 同时触发多个子 Agent并行执行最后聚合结果。比如主 Agent 发送航班号CA123FlightQueryAgent查实时状态KnowledgeBaseAgent查历史投诉记录PolicyAgent查退改签规则主 Agent 等待三个子 Agent 都