Spring AI Alibaba 之五:我把五个子智能体拼成客服流水线,才看懂内置 flow 编排和 A2A 远程调用
前面四篇我把图引擎和 ReactAgent 拆完了。这一篇讲 agent-framework 里真正拿来搭业务的两种积木:内置 flow 编排,以及 A2A 远程调别人家 Agent。我做客服总机时,把分流、并行查知识、条件复核、循环重试、再远程抛给另一个服务的 Agent,全用这两套拼了起来,过程中详读源码。
先放一张 A2A 远程调用的时序,后面再讲 flow 怎么把本地五个智能体串成图。

A2A:把一个 Agent 当远程节点用
A2A 的核心想法很朴素:我这边有个图,某个节点不该本地算,要打到另一台机器上跑着的 Agent。框架给的入口是 A2aRemoteAgent,它继承 BaseAgent(a2a/A2aRemoteAgent.java:34)。但它不像 ReactAgent 那样自己建 ReAct 循环,schedule 直接抛 UnsupportedOperationException(a2a/A2aRemoteAgent.java:80),因为它本来就不是给自己跑计划的,只是挂到图里当一个子图节点。
它的 initGraph(a2a/A2aRemoteAgent.java:60)只做一件事:建一个 StateGraph,加一个名为 A2aNode 的节点,START 连 A2aNode,A2aNode 连 END。A2aNode 的真实动作在 A2aNodeActionWithConfig(a2a/A2aNodeActionWithConfig.java:60),一个 NodeActionWithConfig。
真正发请求的是 apply(a2a/A2aNodeActionWithConfig.java:98)。它分两路:开了 streaming 就走 createStreamingGenerator 拿到 Flux 一路吐;没开就 buildSendMessageRequest 组装请求,sendMessageToServer 发出去,autoDetectAndParseResponse 再 extractResponseText 抽文本,最后返回 Map。
那么走不走流,谁定的?A2aRemoteAgent.build() 里有一行很关键:没有显式关 streaming 时,直接读对端 AgentCard 的能力位,this.streaming = agentCard.capabilities().streaming()(a2a/A2aRemoteAgent.java:228)。也就是说,能不能流,取决于对方声不声明支持流,而不是我本地拍脑袋。
两条 JSON-RPC 方法也对应两套构造。非流式走 message/send(a2a/A2aNodeActionWithConfig.java:671,method:"message/send" 配 jsonrpc:"2.0" 在 :670);流式走 message/stream(a2a/A2aNodeActionWithConfig.java:714,method:"message/stream")。两者的请求体都在 buildSendMessageRequest(:644)和 buildSendStreamingMessageRequest(:687)里拼,区别是流式那一份走 SSE 长连接。

AgentCard:怎么知道对面是谁、在哪
A2A 不是硬编码 URL 调接口,而是靠 AgentCard 这个"名片"。你可以直接给 agentCard,构造器会把它包成 AgentCardWrapper(a2a/A2aRemoteAgent.java:174);也可以给一个 agentCardProvider,让框架自己去拉。拉卡的实现在 RemoteAgentCardProvider,它做懒加载,而且支持 extended card,拉扩展信息时会带 Bearer dummy-token-for-extended-card 这种占位鉴权头。
这里有个坑我踩过:shareState 默认值不一样。A2aRemoteAgent 默认 shareState=true(a2a/A2aRemoteAgent.java:141),意思是把本地的 messages 状态直接喂给远程;但 A2aNodeActionWithConfig 的 7 参构造里 shareState 默认是 false(:88)。一旦 false,节点会另开一个带 subgraph_ 前缀的新 threadId(a2a/A2aNodeActionWithConfig.java:116 的 getSubGraphRunnableConfig),远程那边看到的是一条独立会话,本地上下文根本传不过去。我做总机时一度以为远程 Agent 没拿到历史,其实就是这个默认值撞上了。
flow 内置编排:五种模式加一个没实现的枚举
讲完远程,讲本地怎么把多个智能体拼起来。入口是 FlowGraphBuilder,它按类型从注册表拿策略、校验、buildGraph;配置 FlowGraphConfig收着 rootAgent、subAgents、conditionalAgents、hooks。
策略注册表 FlowGraphBuildingStrategyRegistry 是单例,真注册内置策略的 registerDefaultStrategies只认五个:SEQUENTIAL、ROUTING、PARALLEL、CONDITIONAL、LOOP。但枚举 FlowAgentEnum 里明明有第六个 SUPERVISOR,却没任何策略类实现、注册表也查不到;createStrategy 遇没注册的 type 直接抛 IllegalArgumentException。所以写 SUPERVISOR 想用监督式编排,运行期必炸。五种策略长什么样,看下图。


模板方法:钩子是怎么被统一种进图的
五种策略核心结构不同,但建图骨架同一套:AbstractFlowGraphBuildingStrategy.buildGraph 是 final 模板方法(:53 / :104),各策略只 override buildCoreGraph(:129)填核心,钩子按 @HookPositions 由基类统一织进图。六步骨架与钩子出身,看下图。

Loop 策略:钩子为什么要内联进循环体
LoopGraphBuildingStrategy最典型。它的 buildCoreGraph先取出 loopStrategy、取出唯一一个 subAgent,然后排三条关键节点:根智能体透明节点、loopInitNode、loopDispatchNode,再嵌入 subAgent 的子图。循环体里它在 subAgent 前后手动接了 connectBeforeModelHooks和 connectAfterModelHooks,循环尾接 afterSubAgent → loopDispatch,最后 loopDispatch 用条件边按 loopFlagKey 返回 continue 或 break。
注意它把 connectBeforeModelHooks 和 connectAfterModelHooks 直接 override 成空方法。为什么?因为基类模板方法在 :134、:137 还会再调一次这两个 connect,如果 Loop 不在循环体内自己接、又让基类在外面接一遍,钩子就被接到循环外面、只跑一次,而不是每轮一次。Loop 选择把钩子逻辑内联进 buildCoreGraph,再用空实现把基类的那两次调用"短路"掉。connectBeforeAgentHooks 则保留一次,在循环前连到根节点。
循环本身有三种形态,由 LoopStrategy 接口加 LoopMode 工厂决定:按次数 count、按数组 array、按条件 condition。不管是哪种,迭代靠 loopInit 初始化、loopDispatch 派发,用 LOOP_FLAG 这个键记录是继续还是退出。

我踩过的三个坑
第一,A2A 的 shareState 默认值和远程节点不一致,本地上下文传不到远程,排查了半天才发现是 A2aNodeActionWithConfig 7 参构造里把 shareState 默认设成 false,而 agent 层 Builder 默认 true,两层默认值打架。
第二,想用 SUPERVISOR 监督式编排,结果注册表里压根没注册,运行期 createStrategy 直接抛异常。枚举里有,不代表能用,这是命名误导。
第三,Loop 的钩子如果不内联、交给基类模板方法在循环外连,每个 subAgent 轮次都不会再过钩子,拦截和计 token 全失效。看懂 LoopGraphBuildingStrategy 把 connect 方法 override 成空才明白这套短路设计。
结尾
你有没有遇到过:明明框架文档列了六种 flow 模式,代码一跑却只认五种?这一篇的 SUPERVISOR 就是这种"枚举在、实现不在"的坑。下一篇我讲上下文工程与 HITL,会把 HumanInTheLoopHook 怎么在 AFTER_MODEL 把工具调用挂起等人审、 SummarizationHook 怎么在 BEFORE_MODEL 压缩上下文、以及 SubAgentInterceptor 怎么用 task 工具把子智能体隔离出去,一次性讲透。如果你也在用 SAA 搭多智能体,欢迎在评论区说说你卡在哪个节点,我下一篇挑典型的回。