Google ADK Java 1.12.0 于 2026 年 10 月 14 日发布,加了一套标为实验性的工作流图类型,每个 Agent 现在也都是一个工作流节点。图可以搭,单个节点也能单独跑,但整张图还不能跑:Workflow.runNode 在 1.12.0 里返回的是一个以 UnsupportedOperationException 结束的 Flowable。同一版还给 MCP 加了一个开关,可以禁止 Agent 配置文件里声明本地 stdio MCP 服务器。
1.12.0 加了哪些工作流类型
这部分对照了 v1.12.0 发布说明 和 v1.12.0 tag 下 com.google.adk.workflow 包的源码。发布说明里 Workflow、Node、BaseNode、Edge、Start、JoinNode 都写明是 experimental,源码里的新类型也都标了 @Experimental。
Node 是工作流里的一个工作单元。接口里只有 name() 和 description() 是稳定的,其余方法都是实验性的。核心方法是 runNode(Context context, Object nodeInput),返回 RxJava 的 Flowable<?>。按源码注释,发出的 Event 原样往下传,发出的其他值就是节点输出,每次运行最多一个。BaseNode 是它的抽象实现,自定义节点继承它就行。
Edge 是一个 record,字段是 from、to 和 routes。routes 为空就是无条件边,前一个节点完成就触发。不为空时,只有节点发出的路由和边上的某个路由匹配,这条边才会走。
Route 是一个 sealed interface,有四种取值。Route.Tag 是字符串,最常用;Route.Num 是整数;Route.Flag 是布尔,适合二选一;Route.Default.INSTANCE 是兜底边,其他路由都没匹配上时才走。序列化后它们都是裸的标量,Default 写成字符串 __DEFAULT__。
Start.INSTANCE 是图的入口,名字固定为 __START__。JoinNode 负责汇合,它的 requiresAllPredecessors() 返回 true,等所有前驱都完成后,把它们的输出按节点名放进一个 Map 往下传。
Workflow 本身也继承 BaseNode,所以一个工作流可以嵌进另一个工作流。Builder 上除了 name 和 edges,还有 maxConcurrency、rerunOnResume、waitForOutput、config,以及三个 schema 设置。maxConcurrency 不能小于 1,不设就是不限并发。工作流恢复时默认从头重跑,这一点和普通节点相反。
另一个关键改动在 dbc973b:BaseAgent 现在继承 BaseNode。LlmAgent 这类现有 Agent 不用改就能直接当节点放进图里。BaseAgent.runNode 的实现是调用自己的 runAsync,节点输入不会被用到。配套的还有新的 Context 类型,现在是 CallbackContext 和 ToolContext 的共同父类;Event 加了 output、nodeInfo 字段,EventActions 加了 route 字段。
整图执行的入口还没有接上。Workflow.runNode 的源码如下:
@Override
public Flowable<?> runNode(Context context, @Nullable Object nodeInput) {
return Flowable.error(
new UnsupportedOperationException("Running a workflow is not supported yet."));
}
负责调度的 Graph 类是包内可见的 final class,外部拿不到,包里也只有 Workflow 引用它。它已经实现了路由匹配:普通路由命中就触发;一个都没命中时,所有 Default 边一起触发;有路由边但全部没命中时,打一条 WARN 日志,这条分支就此结束。ADK Python 那边的工作流已经在迭代,前一天(10 月 13 日)发布的 ADK Python 2.12.0 给工作流节点加了执行前人工确认 require_confirmation。Java 这次先把类型和事件字段对齐。
在 Spring Boot 里搭一张图
项目用 Spring Boot 4.0.5 和 JDK 21,依赖只有一个:
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>1.12.0</version>
</dependency>
先写一个不调模型的节点。它继承 BaseNode,拿输入去一个本地 Map 里查值:
public class LookupNode extends BaseNode {
private final Map<String, String> data;
public LookupNode(String name, Map<String, String> data) {
super(name, "local lookup: " + name);
this.data = data;
}
@Override
public Flowable<?> runNode(Context context, @Nullable Object nodeInput) {
String value = data.get(String.valueOf(nodeInput));
return value == null ? Flowable.empty() : Flowable.just(value);
}
}
然后在 CommandLineRunner 里拼一个客服流程。入口同时扇出到查订单和查用户两个节点,结果在 merge 汇合。汇合后按路由走:发出 auto 就交给 LlmAgent 回复,否则走兜底边转人工。
LookupNode order = new LookupNode("fetch_order", Map.of("A-1001", "shipped"));
LookupNode user = new LookupNode("fetch_user", Map.of("A-1001", "alice"));
JoinNode join = JoinNode.builder().name("merge").build();
LlmAgent replier = LlmAgent.builder()
.name("replier")
.model("gemini-2.5-flash")
.instruction("根据订单和用户信息回复客户")
.build();
LookupNode human = new LookupNode("human_review", Map.of());
Workflow wf = Workflow.builder()
.name("support_flow")
.maxConcurrency(2)
.edges(
new Edge(Start.INSTANCE, order),
new Edge(Start.INSTANCE, user),
new Edge(order, join),
new Edge(user, join),
new Edge(join, replier, new Route.Tag("auto")),
new Edge(join, human, Route.Default.INSTANCE))
.build();
LlmAgent 在这里只是构造出来当节点用,整个程序没有调用任何模型 API。
接着单独调用两个节点的 runNode,再调用整个工作流的 runNode。节点自己的 runNode 不依赖调度器,可以直接调用。这次实测中 Context 传的是 null,LookupNode 和 JoinNode 的 runNode 都正常返回了结果。
order.runNode(null, "A-1001").blockingFirst();
Map<String, Object> joined = new LinkedHashMap<>();
joined.put("fetch_order", "shipped");
joined.put("fetch_user", "alice");
join.runNode(null, joined).blockingFirst();
wf.runNode(null, "A-1001").blockingFirst(); // 1.12.0 会抛异常
最后用 Jackson 把四种路由序列化再读回来,看看序列化格式。
MCP stdio 开关
28810cd 的提交说明讲了来龙去脉。McpToolset.fromConfig() 会从 Agent 配置里读 stdioServerParams 或 stdioConnectionParams,里面是一条命令和参数。MCP 的 stdio 传输会把它当本地进程启动,而且发生在第一轮解析工具的时候,比联系模型还早。Agent 配置又经常是别人写的,比如共享的模板、示例、注册表条目。
1.12.0 加了开关,打开后 fromConfig() 会拒绝这两种 stdio 参数。默认值是允许,现有配置照常工作。打开有两种办法:代码里调 McpToolset.setAllowConfigStdioServers(false),或者启动时加 -Dadk.mcp.allowConfigStdioServers=false。远程传输 sseServerParams 和直接用 Java 构造的 McpToolset 都不受影响。
BaseTool.ToolArgsConfig args = new BaseTool.ToolArgsConfig()
.put("stdioServerParams", Map.of("command", "npx", "args", List.of("-y", "some-mcp-server")));
BaseTool.ToolConfig config = new BaseTool.ToolConfig("McpToolset", args);
McpToolset.setAllowConfigStdioServers(false);
McpToolset.fromConfig(config, "agent.yaml");
运行结果
mvn -B package -DskipTests
java -jar target/adk-workflow-demo-0.0.1.jar
构建输出 BUILD SUCCESS。日志级别调到了 WARN,下面是程序打印的全部内容:
[wf] support_flow maxConcurrency=2
[wf] __START__ -> fetch_order
[wf] __START__ -> fetch_user
[wf] fetch_order -> merge
[wf] fetch_user -> merge
[wf] merge -> replier routes=[Tag[value=auto]]
[wf] merge -> human_review routes=[INSTANCE]
[wf] LlmAgent is a Node: true, rerunOnResume=false
[wf] JoinNode requiresAllPredecessors=true
[node] fetch_order(A-1001) -> shipped
[node] merge(...) -> {fetch_order=shipped, fetch_user=alice}
[wf] runNode -> UnsupportedOperationException: Running a workflow is not supported yet.
[route] json=["auto",2,true,"__DEFAULT__"]
[route] back=[Tag[value=auto], Num[value=2], Flag[value=true], INSTANCE]
[mcp] ConfigurationException: Refusing to start a local MCP server declared in an agent config. stdioServerParams and stdioConnectionParams launch the config-supplied command as a local process while tools are resolved, before the model is contacted. This rejection is disabled by default; it is active because McpToolset.setAllowConfigStdioServers(false) was called or -Dadk.mcp.allowConfigStdioServers=false was set. Remote transports (sseServerParams) are unaffected.
前几行是图的结构。入口在边里显示为 __START__,兜底边的路由打印成 INSTANCE,因为 Route.Default 是个枚举。LlmAgent 直接赋给了 Node 类型的变量,它的 rerunOnResume 是默认的 false。
单个节点能正常出结果。merge 把两个前驱的输出合成了一个按节点名索引的 Map。整张图一跑就是那条 UnsupportedOperationException,和源码一致。
路由的 JSON 是 ["auto",2,true,"__DEFAULT__"],读回来类型都对得上。Route 上的 @JsonCreator 只接受字符串、整数和布尔,其他类型会抛 IllegalArgumentException。
最后一行是 stdio 开关生效后的报错。异常类型是 ConfigurationException,消息里写明了拒绝原因和两种开启方式。
其他改动
上下文缓存是这版另一块比较大的内容。新增了 GeminiContextCacheManager,ContextCacheConfig 加了 cacheIntervals 和 createHttpOptions,对齐 ADK Python。修复里有一条说的是之前设了 ContextCacheConfig 也不会真的创建 Gemini 缓存(#1530),1.12.0 修好了。
还加了可注入的调度器,用于 ADK 内部的扇出;时间戳和 id 也改成可以注入时间和 UUID 提供者。
Spring AI 集成修了两处。SpringAIAutoConfiguration 加了 @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE),排到 Spring AI 模型自动配置之后,条件判断才能看到模型 Bean;四个 Bean 方法从 @ConditionalOnBean 换成了 @ConditionalOnSingleCandidate,同时引了多个模型 starter 的应用会退让,不再在启动时报错选不出模型,标了 @Primary 的模型仍然会被用上(d342a95)。另一处是工具 schema 转换时保留 enum 和 items(#1609)。
A2A 方面,远端 A2A 消息里有转换不了的 part 时,现在会跳过它,不再让整轮对话失败。MCP 工具的输入 schema 现在也会正确放进 chat completions 请求里。
一起交流
分享你的思考,让讨论更进一步。