news 2026/9/29 10:22:52

[AI工程] Spring AI 第十九篇:多 Agent 编排与 A2A——什么时候真该拆,工具箱太大用什么装,跨进程那层 Spring AI 到底给了什么?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[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 只装自己那 5~8 个工具,抽取质量立刻回来工具选错、指令被忽略,第十三篇的 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 有多大

一个中等复杂度的内部系统,工具集很容易到 20~40 个。每个工具的 input schema(DeepSeek、通义这些对 schema 长度同样敏感)按 200~400 token 估,光"告诉模型你能干什么"就要吃掉 6k~16k 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直接继承ToolCallingAdvisor:

publicclassToolSearchToolCallingAdvisorextendsToolCallingAdvisor{publicstaticBuilder<?>builder();publicvoidevictSession(StringsessionId);// 会话结束要主动清索引}// Builder 在 ToolCallingAdvisor.Builder 之上多出的方法TtoolIndex(ToolIndex)// 用哪个索引实现TmaxResults(Integer)TsessionIdKeyName(String)TevictionStrategy(ToolIndexEvictionStrategy)// Ttl / Lru / Composite / AlwaysEvictTsystemMessageSuffix(String)// 追加到 system:教模型怎么用这个工具TreferenceToolNameAccumulation(boolean)

索引层就三个方法(ToolIndex):

publicinterfaceToolIndex{voidindexTool(StringsessionId,ToolReferencereference);defaultvoidindexTools(StringsessionId,List<ToolReference>references);ToolSearchResponsesearch(ToolSearchRequestrequest);// sessionId/query/maxResults/categoryFiltervoidclearIndex(StringsessionId);}publicrecordToolReference(StringtoolName,DoublerelevanceScore,Stringsummary){}

运行时行为是:模型先调tool_search这个元工具(ToolSearchTool.toolSearchTool(query, maxResults, categoryFilter, ToolContext)),拿到一批工具名字,Advisor 再把这些名字对应的ToolCallback加进后续轮次。也就是"按需装配",而不是开局全量塞。

ChatClientagent=ChatClient.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();

两个必须注意的点:

  1. 它extends ToolCallingAdvisor,所以它就是链路上那个唯一的ToolAdvisor。不要再手动加ToolCallingAdvisor,否则撞上第十八篇 5.3 那条At most one ToolAdvisor is allowed in the advisor chain。
  2. 权限模型不会因为"搜索"而自动收紧。检索只是决定模型这轮"看得见哪些工具",索引里全量工具仍在你的进程里。第 4 节那层按角色过滤还是得做——否则一次成功的tool_search就能把危险工具捞进可见集。多 Agent 场景这条尤其重要:子 Agent 的工具集应当是父 Agent 权限的子集,不是并集。

3.3 拆 Agent 之后,工具箱怎么分

角色工具集规模索引选型备注
Coordinator3~8(分派、汇总、请求确认)不需要描述要极清晰,它选错一次整条链路偏
查询型 Worker10~20 个只读工具RegexToolIndex起步,量大换VectorToolIndex只读 → 可自由重试
写入型 Worker3~6 个,全部要确认不需要权限最小化,见第十八篇第 4 节
评审型(Evaluator-Optimizer)0~2不需要只吃上下文不吃工具,第十四篇模式 5

4. 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.1(2026-05-28)GitHub Releases API
Spring AI 支持没有官方 A2A 模块:spring-ai-bom:2.0.1169 个 artifact 无 a2a;2.0 与 2.1 文档搜a2a零命中;org:spring-projects下按 a2a 搜仓库 count 0Maven Central BOM + docs + GitHub search
Java 生态成熟度社区实现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 暴露给别人写的、异构的智能体(或者反过来要调别人的 Agent),A2A 的 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{privatefinalMap<String,WorkerAgent>workers;// 按能力名注册privatefinalExecutorServicepool=Executors.newVirtualThreadPerTaskExecutor();// JDK 21publicMap<String,WorkerResult>dispatch(StringtraceId,TaskPlanplan,Durationbudget){Instantdeadline=Instant.now().plus(budget);Map<String,CompletableFuture<WorkerResult>>futures=plan.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.failed,Coordinator 才能判断"这一步可跳过"还是"整单终止"。让异常穿透到最外层,等于放弃决策权。

5.2 补偿:把重试粒度定义在"步骤"上

模型调用的不确定性会让同一份输入产生不同输出,因此重试必须幂等——这比传统 RPC 的幂等要求更严:

步骤类型重试策略前提
只读(查询、抽取、分类)直接重试,最多 2 次,可换模型无副作用
写入(改签、下单)不重试:失败即挂起,人工或上游决定见第十八篇 5.2 的确认闸门
有外部副作用但可幂等带idempotencyKey重试Key 必须由上游生成并随任务传递,不能在执行侧生成
评审型(打分、反思)可重试,但要把上一轮结论一起给否则第二轮可能推翻第一轮,链路振荡

idempotencyKey我建议就是(traceId, stepName, attempt=0)的哈希——attempt 固定为 0,因为"重跑同一步"在语义上必须复用同一个键。

5.3 链路上下文:Runnable里的 ThreadLocal 会丢

ChatMemory靠.advisors(a -> a.param(CONVERSATION_ID, id))显式传,这点在并行 Worker 下没问题。丢的是另一些东西:

// 错误:SecurityContext / MDC 都是 ThreadLocal,supplyAsync 之后是空的CompletableFuture.supplyAsync(()->assistant.answer(q),pool);// 正确:显式捕获后传参,别指望上下文自动穿越线程边界varctx=SecurityContextHolder.getContext();varmdc=MDC.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(本地 span,child of traceId) ├──> Worker B(跨进程:W3C traceparent header) └──> MCP Server(跨进程:MCP 请求里自己带 sessionId)
层怎么串记什么
模型调用Spring AI 的 Micrometer observation(第十三篇)每个 Worker 的 usage / 延迟 / 模型名
工具执行ToolCallingObservationDocumentation.TOOL_CALLTOOL_DEFINITION_NAME(低基数,安全);参数与结果只在include-content=true时才进 span——多 Agent 下别打开,第十八篇 6.3
跨进程W3Ctraceparent自动透传;MCP 侧自己带目标 Agent 名 + 任务 ID
决策自建agent_decision表traceId、agent、candidateTools、chosenTool、argumentsHash、decision、tokens

sessionId与traceId不是一回事:一次用户会话(sessionId)可能包含多条链路(traceId),而ToolSearchToolCallingAdvisor的索引是按 sessionId 组织的——会话结束一定要evictSession(...),否则长期跑下来索引会积累一堆用不到的工具引用(我用的 Ttl 策略就是防这个,但别只靠 TTL)。

成本归因也变得必须:多 Agent 的 Token 消耗是"乘法"(N 个 Worker × M 轮),单看总账只会得出"变贵了",看不到是谁花的。第十三篇那套按用户 / 功能维度归因,在这里再加一维agentName就够用。


7. 自检清单

#检查项不过关的表征
1拆分能对应到三条判据之一(写进设计文档)“为了看起来像多 Agent”
2每个 Worker 的工具集 ≤ 8,或已接tool-search光 schema 就吃掉半窗,模型忽略 system
3子 Agent 工具集是父权限的子集一次 tool_search 把危险工具捞进可见集
4ChatClient链路上只有一个ToolAdvisorAt most one ToolAdvisor is allowed异常
5超时预算自入口下发单跳合理,端到端 3 分钟
6写操作步骤不自动重试重复改签、重复出款
7idempotencyKey由上游生成并跨跳传递重跑生成新键,下游重复执行
8上下文穿越线程边界是显式传参Worker 里Authentication为 null
9traceId/sessionId/agentName三者在 trace 与决策表里都齐出事只能猜是哪个 Agent 干的
10evictSession在会话结束被调用长跑后工具索引虚胖,检索结果变差
11跨进程协议选型明确(MCP 做能力 / 自研做任务)文档里写"A2A",代码里是 HTTP POST

最后总结

  • 拆 Agent 只有三条正当理由:上下文预算、权限隔离、独立伸缩。都不沾就别拆——"多 Agent"在多数内部系统里是个成本项,不是能力项。
  • 拓扑上我默认选中心化编排,其次是契约清晰的流水线;点对点除非真有动态协作,否则收敛性和可观测都太难。
  • 工具箱规模化的正解是 2.0.1 新增的tool-search全家桶(ToolSearchToolCallingAdvisor+Regex / Lucene / VectorToolIndex+ TTL/LRU 驱逐):让模型自己去搜工具,而不是开局把所有 schema 塞进 prompt。1.1.2 没有这套,需要自研。
  • A2A 在 Spring AI 2.0.1 里没有官方支持(BOM 无相关 artifact、文档零命中),协议本身已进 Linux Foundation(Google 贡献,规范最新 v1.0.1),Java 侧社区实现还很薄。当前阶段能力复用交给 MCP 就够,A2A 留到真要跨组织互操作时自研适配层。
  • 稳定性靠 Spring 本体的三件事:预算自入口下发、失败变成结果而不是异常、幂等键由上游生成并跨跳传递。
  • 对于后端 / 架构开发者,我个人更关注决策表:agentName / candidateTools / chosenTool / decision / tokens。多 Agent 系统里唯一能回答"当时为什么这么干"的,就是这张表,框架不会替你写它。

参考资料 & 致谢

[1] Spring AI Reference(2.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 里怎么写

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 10:22:48

Unity手游iOS深度链接全流程:从URL Scheme到C#参数投递

Unity 手游 iOS Deep Link 唤醒全流程&#xff1a;从 URL Scheme / Universal Links 到 C# 层参数投递做手游运营或者用户增长的同学应该都有同感&#xff1a;一条短信、一个分享卡片、一个广告点击&#xff0c;用户点下去之后能不能直接落到游戏里的指定界面&#xff0c;直接决…

作者头像 李华
网站建设 2026/9/29 10:19:02

AI漫剧助手:面向漫剧短剧创作者的一站式提示词管理工具

当前AI漫剧、竖屏短剧赛道越来越火热&#xff0c;但很多创作者会耗费大量时间编写、调试提示词&#xff0c;反复处理人物崩脸、画面风格不统一、分镜设计繁琐等问题。AI漫剧助手&#xff0c;是专为漫剧创作者打造的提示词素材管理工具&#xff0c;集成全套漫剧创作资源&#xf…

作者头像 李华
网站建设 2026/9/29 10:17:50

2026年免费AI智能体实测:OpenCode+Ollama本地跑通TaoToken统一Key配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 10:16:49

AlexNet网络结构逐层拆解与PyTorch实战

前几天有个刚入门的朋友问我&#xff0c;都这个年代了&#xff0c;YOLO系列已经迭代到v11&#xff0c;Transformer在各种任务上横扫榜单&#xff0c;再回头啃一个2012年的AlexNet网络结构&#xff0c;是不是有点浪费时间&#xff1f;我当时没直接回答&#xff0c;而是让他先说说…

作者头像 李华
网站建设 2026/9/29 10:16:33

DC/DC恒压输出控制原理与环路补偿:24V转5V/5A实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 10:13:48

计算机组成原理课设CPU设计全攻略:从指令集到答辩

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华