Google ADK Java 1.14.0 把官方 Spring Boot 客服样例做成了「ADK FunctionTool + Spring AI @Tool」同跑:SpringAiToolset 把 @Tool(returnDirect = true) 映射成 ADK 的 skipSummarization=true,工具结果可以直接回给用户,不必再进一轮模型总结。本地用 Spring Boot 4.0.2、google-adk-spring-ai 1.14.0 和 Spring AI 2.0.1 起了一个无 Web 进程,直接调用工具层;代码已编译验证,未实际调用外部模型 API。
1.14.0 的 GitHub Release 发布时间是 2026-11-02 23:21:36(Asia/Shanghai)。Maven Central 上 com.google.adk:google-adk 与 google-adk-spring-ai 的 1.14.0 均可下载,metadata lastUpdated 换算为 2026-11-03 00:33:03(Asia/Shanghai)。Release Features 里和本文相关的三条是:Spring Boot chat backed by ADK、streams replies and mixes ADK and Spring AI tools、bidirectional conversion between ADK and Spring AI。对应提交分别是 bca0591(2026-10-29 19:36:36 Asia/Shanghai)、7391fa8(2026-10-29 23:35:38 Asia/Shanghai)、9949265(2026-10-27 21:00:07 Asia/Shanghai)。同版本还带了工作流暂停/恢复等能力,本文只核工具混接与 returnDirect 映射,不展开那些路径。
ADK Java 1.13 工作流图那篇停在图调度;1.14.0 的 examples/spring_chat_advanced 则面向 Spring Boot 开发者:订单服务用 ADK FunctionTool,物流工具用 Spring AI @Tool,再由 SpringAiToolset 交给同一个 LlmAgent。
样例怎么拼工具
官方高级样例的配置大致是这样:
ImmutableList<Object> tools =
ImmutableList.of(
FunctionTool.create(orderService, "getOrderStatus"),
FunctionTool.create(orderService, "getDeliveryEstimate"),
new SpringAiToolset(
MethodToolCallbackProvider.builder().toolObjects(shippingTools).build()));
return LlmAgent.builder()
.name("order_support_agent")
.model(model)
.instruction("... {" + ShippingTools.LAST_TRACKED_ORDER + "?}")
.tools(tools)
.build();
ShippingTools.getTracking 会从 Spring AI 的 ToolContext 里取出 ToolConverter.ADK_TOOL_CONTEXT_KEY(常量值 adk_tool_context),再写入 ADK session state。官方注释写明:带 returnDirect 的工具结果会直接给用户,同轮其它工具仍会执行,但不再走模型回复。SpringAiTool 的实现里,当 callback metadata 的 returnDirect() 为 true 时,会调用 toolContext.actions().setSkipSummarization(true)。
有一点要小心:如果 Spring 给 bean 做了代理,FunctionTool.create(bean, "methodName") 可能读不到具体类上的 @Schema。官方 README 建议改传 OrderService.class 上的 Method。本地验证里用的是 FunctionTool.create(orderService, OrderService.class.getMethod(...)),调用才正常返回。
本地验证
环境:JDK 21.0.12.1、Maven 3.9.9、Spring Boot 4.0.2、ADK 1.14.0、Spring AI 2.0.1。应用是 web-application-type=none,注入一个从不调用的 stub ChatModel,只在 CommandLineRunner 里跑工具。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.2</version>
</parent>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-spring-ai</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
<!-- managed by spring-ai-bom 2.0.1 -->
</dependency>
mvn -q -DskipTests package
mvn -q spring-boot:run
启动后的关键输出如下(原文摘录):
=== ADK Java 1.14.0 SpringAiToolset + FunctionTool offline verify ===
adk.version=1.14.0 spring-ai.version=2.0.1 spring-boot=4.0.2
no remote model API call
--- tool mix ---
adk.FunctionTool names=[getOrderStatus, getDeliveryEstimate]
SpringAiToolset names=[talkToHuman, getTracking]
FunctionTool declaration name=getOrderStatus desc=Looks up the status and item of an order.
FunctionTool declaration name=getDeliveryEstimate desc=Returns the delivery estimate of an order.
SpringAiTool declaration name=talkToHuman desc=Hands the conversation to a human support agent when the user asks for one.
SpringAiTool declaration name=getTracking desc=Returns the carrier and tracking number of a shipped order.
--- getTracking via SpringAiTool ---
result={orderId=1001, tracking=DHL JD014600001001}
ADK_TOOL_CONTEXT_KEY=adk_tool_context
session.state.lastTrackedOrder=1001
skipSummarization.after.getTracking=Optional.empty
--- talkToHuman returnDirect ---
result={result=A human support agent will join this chat within 5 minutes.}
skipSummarization.after.talkToHuman=Optional[true]
--- FunctionTool getOrderStatus ---
result={orderId=1002, status=processing, item=USB-C hub}
=== DONE ===
returnDirect 落成 skipSummarization
同一次进程里,getTracking(没有 returnDirect)跑完后 skipSummarization 仍是 Optional.empty;talkToHuman(returnDirect = true)跑完后变成 Optional[true]。工具返回值被包成 {result=A human support agent will join this chat within 5 minutes.}。这是源码层的映射结果,与模型对话日志无关。
@Tool(
description = "Hands the conversation to a human support agent when the user asks for one.",
returnDirect = true)
String talkToHuman() {
return "A human support agent will join this chat within 5 minutes.";
}
Spring AI 工具也能改 ADK session state
getTracking("1001") 返回 tracking=DHL JD014600001001,同时把 lastTrackedOrder=1001 写进 ADK ToolContext.state()。官方高级样例的 agent instruction 用 {lastTrackedOrder?} 读这个键,所以物流工具改的状态可以直接进下一轮提示词。键名常量是 ToolConverter.ADK_TOOL_CONTEXT_KEY = "adk_tool_context"。
if (toolContext.getContext().get(ToolConverter.ADK_TOOL_CONTEXT_KEY)
instanceof com.google.adk.tools.ToolContext adkToolContext) {
adkToolContext.state().put(LAST_TRACKED_ORDER, orderId);
}
以下是笔者对官方说明的归纳,与 Release 原文区分开:已经有 Spring AI @Tool 资产的团队,可以用 SpringAiToolset 挂进 ADK agent,而不必先把工具全部改写成 FunctionTool;需要「工具结果直接出给用户」时,继续用 Spring AI 的 returnDirect,ADK 侧会落到 skipSummarization。
一起交流
分享你的思考,让讨论更进一步。