![[AI工程] Spring AI 第十九篇:多 Agent 编排与 A2A——什么时候真该拆,工具箱太大用什么装,跨进程那层 Spring AI 到底给了什么?](http://pic.xiahunao.cn/yaotu/[AI工程] Spring AI 第十九篇:多 Agent 编排与 A2A——什么时候真该拆,工具箱太大用什么装,跨进程那层 Spring AI 到底给了什么?)
第十四篇写完五种编排模式之后我在自己的 Demo 上试了再拆一个 Agent 出来调度 Agent 负责分派专业 Agent 负责查、算、写。结果单测跑通了链路一拉长就三件事同时冒出来——上下文预算炸了一个 Agent 挂 30 个工具光 schema 就吃掉大半窗口、并行任务的超时和取消没法统一、失败之后不知道从哪一步重跑。更现实的一个问题是协议社区里多 Agent 协作讲得最多的是 A2A我按惯例去找 Spring AI 的 A2A 支持结果是——2.0.1 的 BOM 里 169 个 artifact 没有一个和 A2A 有关文档里搜a2a零命中。它管的是 MCP能力跨进程复用不管 Agent 之间怎么说话。这一篇就按这个事实往前推拆 Agent 的判据是什么、工具箱规模化 Spring AI 在 2.0.1 给了什么新部件tool-search全家桶1.1.2 完全没有、跨进程那一层该用 MCP 还是等 A2A、以及 Java 侧并行、补偿、链路追踪具体怎么写。1. 什么时候才真的需要拆三个判据满足一个再动手一个都不满足就别拆。第十四篇 已经把 Chain / Parallelization / Routing / Orchestrator-Workers / Evaluator-Optimizer 五种模式各给了一段能跑的 Java。那一层全在同一个进程、同一个ChatClient里本质是多轮模型调用怎么组织跟多 Agent没什么关系。要不要真的拆开我的判据只有三条判据含义拆开的收益不拆的代价上下文预算一个 Agent 能挂的工具 检索片段把窗口占掉一半以上模型开始看不见系统提示每个子 Agent 只装自己那 58 个工具抽取质量立刻回来工具选错、指令被忽略第十三篇的 trace 里能看到但改不动权限隔离不同能力对应不同信任级别只读 vs 能写 vs 能出款子 Agent 是不同服务账号 / 不同进程越权在 OS 层就被挡住只能靠第八篇、第十八篇那套应用层过滤一处漏了就全漏独立伸缩与发布某一步是重计算或第三方慢调用需要单独扩缩 / 单独回滚拆成服务后按自己节奏部署一次慢调用把主流程的线程池拖住反过来这三条一条都不沾只是想看起来更智能那多 Agent 只会带来三个新问题链路线性叠加延迟、错误在多跳之间放大、没有一份完整上下文可供归因。笔者的实际做法先用单 Agent 动态工具集撑到撑不住第 3 节有具体部件撑不住再拆进程。因为拆进程之后所有原本一次函数调用的事都要变成网络调用 超时策略 补偿逻辑。Q1拆成多个 ChatClient 实例算不算多 Agent不算。多ChatClientbean 只是配置隔离不同模型、不同 Advisor 链、不同系统提示它们仍然共享线程池、共享进程、共享故障域。真正多 Agent的分界线是进程边界跨了进程才有独立的部署、独立的权限和必须显式传递的上下文。2. 跨进程的三种拓扑成本完全不一样中心化编排 Coordinator -- [Worker A] [Worker B] [Worker C] 上下文由 Coordinator 汇总 │ 一份决策多份执行失败重试最容易做 │ 代价Coordinator 是瓶颈且它必须理解所有子任务 流水线 A -- B -- C 每跳只处理自己那段 │ 上下文最小、最好测单跳可换实现 │ 代价早期丢的信息后面拿不回来需要显式契约结构化 DTO 点对点 A -- B -- C共享黑板 / 消息总线 灵活 代价没有中心决策收敛性靠 prompt 保证最难调试拓扑上下文传递失败处理可观测我的适用判断中心化Coordinator 持有全量分派时截断一处重试幂等键集中在它手里一个 traceId 串全场默认选它业务型任务几乎都够用流水线跳与跳之间是结构化 DTO第七篇的 entity 输出每跳独立重试 补偿每跳一个 span天然好查步骤固定、可预先定义契约时用点对点无共享靠消息内容分布式事务级的问题留给你需要先建 correlationId 体系除非真有动态协作需求否则别碰一个具体的反例我做过解析 → 校验 → 生成回复三段流水线把第一段做成独立 Agent 后它在解析阶段丢掉了用户的口语化时间表达校验段就再也判不出日期歧义。流水线要求每一跳的输出是完备的而模型输出天然不完备——所以流水线拓扑里跳与跳之间应该传结构化 DTO 原始输入留档而不是传上一跳的理解。3. 工具箱规模化tool-search是 2.0.1 的新答案这是本篇最实用的一节。多 Agent 场景里第一个撞墙的通常是工具太多。3.1 问题定量30 个工具的 schema 有多大一个中等复杂度的内部系统工具集很容易到 2040 个。每个工具的 input schemaDeepSeek、通义这些对 schema 长度同样敏感按 200400 token 估光告诉模型你能干什么就要吃掉 6k16k token。后果不是费用是选择质量工具越多、描述越相似模型选错率越高第八篇痛点 1。1.x 时代只能自己写工具目录 检索 动态挂载。2.0.1 直接把这套做成了模块——我在 BOM diff 里确认这六个 artifact 在 1.1.2 中完全不存在spring-ai-tool-search-tool 核心ToolIndex / ToolSearchTool / 索引实现 spring-ai-tool-search-advisor ToolSearchToolCallingAdvisor spring-ai-tool-search-tool-advisor 桥接 spring-ai-tool-search-tool-lucene LuceneToolIndex spring-ai-tool-search-tool-vectorstore VectorToolIndex spring-ai-starter-tool-search-advisor starter另有RegexToolIndex在 tool 模块的index/regex包里——正则版最适合起步和排查。3.2 机制让模型自己去搜工具ToolSearchToolCallingAdvisor直接继承ToolCallingAdvisorpublicclassToolSearchToolCallingAdvisorextendsToolCallingAdvisor{publicstaticBuilder?builder();publicvoidevictSession(StringsessionId);// 会话结束要主动清索引}// Builder 在 ToolCallingAdvisor.Builder 之上多出的方法TtoolIndex(ToolIndex)// 用哪个索引实现TmaxResults(Integer)TsessionIdKeyName(String)TevictionStrategy(ToolIndexEvictionStrategy)// Ttl / Lru / Composite / AlwaysEvictTsystemMessageSuffix(String)// 追加到 system教模型怎么用这个工具TreferenceToolNameAccumulation(boolean)索引层就三个方法ToolIndexpublicinterfaceToolIndex{voidindexTool(StringsessionId,ToolReferencereference);defaultvoidindexTools(StringsessionId,ListToolReferencereferences);ToolSearchResponsesearch(ToolSearchRequestrequest);// sessionId/query/maxResults/categoryFiltervoidclearIndex(StringsessionId);}publicrecordToolReference(StringtoolName,DoublerelevanceScore,Stringsummary){}运行时行为是模型先调tool_search这个元工具ToolSearchTool.toolSearchTool(query, maxResults, categoryFilter, ToolContext)拿到一批工具名字Advisor 再把这些名字对应的ToolCallback加进后续轮次。也就是按需装配而不是开局全量塞。ChatClientagentChatClient.builder(chatModel).defaultAdvisors(ToolSearchToolCallingAdvisor.builder().toolCallingManager(this.toolCallingManager)// 仍是你自己的 manager含 limits.toolIndex(newRegexToolIndex())// 起步用正则版够用且零依赖.maxResults(5).evictionStrategy(newTtlEvictionStrategy(Duration.ofMinutes(10))).build()).defaultTools(ToolCallbacks.from(this.hugeToolbox))// 全量注册进索引不进 prompt.build();两个必须注意的点它extends ToolCallingAdvisor所以它就是链路上那个唯一的ToolAdvisor。不要再手动加ToolCallingAdvisor否则撞上第十八篇 5.3 那条At most one ToolAdvisor is allowed in the advisor chain。权限模型不会因为搜索而自动收紧。检索只是决定模型这轮看得见哪些工具索引里全量工具仍在你的进程里。第 4 节那层按角色过滤还是得做——否则一次成功的tool_search就能把危险工具捞进可见集。多 Agent 场景这条尤其重要子 Agent 的工具集应当是父 Agent 权限的子集不是并集。3.3 拆 Agent 之后工具箱怎么分角色工具集规模索引选型备注Coordinator38分派、汇总、请求确认不需要描述要极清晰它选错一次整条链路偏查询型 Worker1020 个只读工具RegexToolIndex起步量大换VectorToolIndex只读 → 可自由重试写入型 Worker36 个全部要确认不需要权限最小化见第十八篇第 4 节评审型Evaluator-Optimizer02不需要只吃上下文不吃工具第十四篇模式 54. A2A现状说清楚别再照着过时说法写A2A 是最容易被写成框架已支持的一块我把它按核到的事实摆一遍。事项核到的事实依据治理归属已不是Google 的协议Linux Foundation 官发新闻稿宣布托管 Agent2Agent Protocol 项目表述为“an open source project under the Linux Foundation, contributed by Google”linuxfoundation.org 新闻页规范版本官方规范仓库a2aproject/A2A最新 release 为v1.0.12026-05-28GitHub Releases APISpring AI 支持没有官方 A2A 模块spring-ai-bom:2.0.1169 个 artifact 无 a2a2.0 与 2.1 文档搜a2a零命中org:spring-projects下按 a2a 搜仓库 count 0Maven Central BOM docs GitHub searchJava 生态成熟度社区实现a2a-java的integrations目录目前只有microprofile-config一项没有 Spring 集成GitHub contents API所以结论很直白Spring 侧要用 A2A只能自己写适配层或者接第三方 SDK 再手工桥接到ChatClient。这就带来一个判断题现在到底需不需要 A2A我的答案是暂时不需要因为 A2A 要解决的问题MCP 已经解决了一大半而两者层次不同MCP第九篇A2A抽象对象能力tool / resource / promptAgent有身份、有任务生命周期、会主动推进通信语义请求-响应工具调用任务提交 / 流式状态更新 / 产物artifact/ 取消发现机制工具列表Agent Card声明能力、端点、认证方式适合把接口和能力标准化复用跨组织、跨框架的 Agent 互操作Spring AI 2.0.1官方支持starter 注解 SDK 2.0.0无绝大多数内部多 Agent 系统的真实需求是能力复用 鉴权边界那是 MCP 的地盘。只有当你要把 Agent 暴露给别人写的、异构的智能体或者反过来要调别人的 AgentA2A 的 Agent Card 与任务生命周期才真正派上用场。现在就能落 你的 Java Agent -- MCP client -- 别人的 MCP Server能力 需要 A2A 时 你的 Java Agent -- A2A client -- 别人的 Agent任务 状态 产物 └── 适配层自己写Spring AI 不给你 ──┘真要接的话我会把 A2A 的 Agent Card 映射成自己的AgentRegistry服务发现走 Nacos / Eureka任务生命周期映射到 Spring 的状态机 CompletableFuture认证沿用 Spring Security 的 token 透传——本质上就是自己实现一遍 A2A 的子集好处是链路里少一个不受控的黑盒。5. Java 侧的并行、超时与补偿这一节全是 Spring 本体没有任何 AI 魔法但多 Agent 系统的稳定性 90% 由它决定。5.1 并行分派超时必须逐跳可控ServicepublicclassFanOutCoordinator{privatefinalMapString,WorkerAgentworkers;// 按能力名注册privatefinalExecutorServicepoolExecutors.newVirtualThreadPerTaskExecutor();// JDK 21publicMapString,WorkerResultdispatch(StringtraceId,TaskPlanplan,Durationbudget){InstantdeadlineInstant.now().plus(budget);MapString,CompletableFutureWorkerResultfuturesplan.subTasks().stream().collect(toMap(SubTask::name,t-CompletableFuture.supplyAsync(()-this.workers.get(t.name()).run(traceId,t),this.pool).orTimeout(Duration.between(Instant.now(),deadline).toMillis(),MILLISECONDS).exceptionally(ex-WorkerResult.failed(t.name(),rootCauseOf(ex)))));CompletableFuture.allOf(futures.values().toArray(CompletableFuture[]::new)).join();returnfutures.entrySet().stream().collect(toMap(Entry::getKey,e-e.getValue().join()));}}三个决定虚拟线程JDK 21适合大量阻塞式模型调用这种 IO 密集场景但如果 Worker 内部走 WebFlux就别再套一层supplyAsync直接组合Flux。混用只会让排查看到两套线程模型。预算从入口传下来budget参数而不是每个 Worker 各设各的 30 秒。多跳链路里每跳都合理的超时加起来一定超过用户耐心。失败要变成结果不是异常。.exceptionally(...)把失败收集成WorkerResult.failedCoordinator 才能判断这一步可跳过还是整单终止。让异常穿透到最外层等于放弃决策权。5.2 补偿把重试粒度定义在步骤上模型调用的不确定性会让同一份输入产生不同输出因此重试必须幂等——这比传统 RPC 的幂等要求更严步骤类型重试策略前提只读查询、抽取、分类直接重试最多 2 次可换模型无副作用写入改签、下单不重试失败即挂起人工或上游决定见第十八篇 5.2 的确认闸门有外部副作用但可幂等带idempotencyKey重试Key 必须由上游生成并随任务传递不能在执行侧生成评审型打分、反思可重试但要把上一轮结论一起给否则第二轮可能推翻第一轮链路振荡idempotencyKey我建议就是(traceId, stepName, attempt0)的哈希——attempt 固定为 0因为重跑同一步在语义上必须复用同一个键。5.3 链路上下文Runnable里的 ThreadLocal 会丢ChatMemory靠.advisors(a - a.param(CONVERSATION_ID, id))显式传这点在并行 Worker 下没问题。丢的是另一些东西// 错误SecurityContext / MDC 都是 ThreadLocalsupplyAsync 之后是空的CompletableFuture.supplyAsync(()-assistant.answer(q),pool);// 正确显式捕获后传参别指望上下文自动穿越线程边界varctxSecurityContextHolder.getContext();varmdcMDC.getCopyOfContextMap();CompletableFuture.supplyAsync(()-{SecurityContextHolder.setContext(ctx);if(mdc!null)MDC.setContextMap(mdc);try{returnassistant.answer(q);}finally{SecurityContextHolder.clearContext();MDC.clear();}},pool);这是 Spring 老问题DelegatingSecurityContextExecutorService也能做。但在多 Agent 场景里我更倾向于显式传参谁把 userId 带进了这个 Worker看调用点就知道不靠框架隐式搬运。第十八篇 3.1 的ToolContext同理。6. 一条多 Agent 链路怎么追没有这一步前面所有设计都只是看起来合理。关键就一件事traceId与sessionId必须跨进程传且不能只靠 HTTP header。用户请求 ──traceId── Coordinator ──┬── Worker A本地 spanchild of traceId ├── Worker B跨进程W3C traceparent header └── MCP Server跨进程MCP 请求里自己带 sessionId层怎么串记什么模型调用Spring AI 的 Micrometer observation第十三篇每个 Worker 的 usage / 延迟 / 模型名工具执行ToolCallingObservationDocumentation.TOOL_CALLTOOL_DEFINITION_NAME低基数安全参数与结果只在include-contenttrue时才进 span——多 Agent 下别打开第十八篇 6.3跨进程W3Ctraceparent自动透传MCP 侧自己带目标 Agent 名 任务 ID决策自建agent_decision表traceId、agent、candidateTools、chosenTool、argumentsHash、decision、tokenssessionId与traceId不是一回事一次用户会话sessionId可能包含多条链路traceId而ToolSearchToolCallingAdvisor的索引是按 sessionId 组织的——会话结束一定要evictSession(...)否则长期跑下来索引会积累一堆用不到的工具引用我用的 Ttl 策略就是防这个但别只靠 TTL。成本归因也变得必须多 Agent 的 Token 消耗是乘法N 个 Worker × M 轮单看总账只会得出变贵了看不到是谁花的。第十三篇那套按用户 / 功能维度归因在这里再加一维agentName就够用。7. 自检清单#检查项不过关的表征1拆分能对应到三条判据之一写进设计文档“为了看起来像多 Agent”2每个 Worker 的工具集 ≤ 8或已接tool-search光 schema 就吃掉半窗模型忽略 system3子 Agent 工具集是父权限的子集一次 tool_search 把危险工具捞进可见集4ChatClient链路上只有一个ToolAdvisorAt most one ToolAdvisor is allowed异常5超时预算自入口下发单跳合理端到端 3 分钟6写操作步骤不自动重试重复改签、重复出款7idempotencyKey由上游生成并跨跳传递重跑生成新键下游重复执行8上下文穿越线程边界是显式传参Worker 里Authentication为 null9traceId/sessionId/agentName三者在 trace 与决策表里都齐出事只能猜是哪个 Agent 干的10evictSession在会话结束被调用长跑后工具索引虚胖检索结果变差11跨进程协议选型明确MCP 做能力 / 自研做任务文档里写A2A代码里是 HTTP POST最后总结拆 Agent 只有三条正当理由上下文预算、权限隔离、独立伸缩。都不沾就别拆——多 Agent在多数内部系统里是个成本项不是能力项。拓扑上我默认选中心化编排其次是契约清晰的流水线点对点除非真有动态协作否则收敛性和可观测都太难。工具箱规模化的正解是 2.0.1 新增的tool-search全家桶ToolSearchToolCallingAdvisorRegex / Lucene / VectorToolIndex TTL/LRU 驱逐让模型自己去搜工具而不是开局把所有 schema 塞进 prompt。1.1.2 没有这套需要自研。A2A 在 Spring AI 2.0.1 里没有官方支持BOM 无相关 artifact、文档零命中协议本身已进 Linux FoundationGoogle 贡献规范最新 v1.0.1Java 侧社区实现还很薄。当前阶段能力复用交给 MCP 就够A2A 留到真要跨组织互操作时自研适配层。稳定性靠 Spring 本体的三件事预算自入口下发、失败变成结果而不是异常、幂等键由上游生成并跨跳传递。对于后端 / 架构开发者我个人更关注决策表agentName / candidateTools / chosenTool / decision / tokens。多 Agent 系统里唯一能回答当时为什么这么干的就是这张表框架不会替你写它。参考资料 致谢[1] Spring AI Reference2.0.1[2] Spring AI - MCP Overview[3] Spring AI - Tools[4] Linux Foundation 托管 A2A 项目的官方新闻稿[5] A2A Protocol 规范[6] a2aproject/A2A - GitHub[7] spring-projects/spring-ai - GitHub[8] Spring AI 第八篇Tools 七大痛点[9] Spring AI 第九篇MCP 实现、原理与鉴权[10] Spring AI 第十三篇可观测性与 Token 归因[11] Spring AI 第十四篇Agent 五种模式在 2.0 里怎么写