Spring AI 2.0 组合式工具调用
上个月 Spring AI 2.0.0 GA 正式发布,翻 release notes 时一条改动引起了注意——工具调用(Tool Calling)的实现方式被整个重写了。
Spring AI 1.x 里,工具调用循环嵌在各个 ChatModel 实现里。OpenAI 写一套,Ollama 写另一套,MCP 工具再走一套。每接入一个新模型,工具调用逻辑就得重复实现一次。2.0 把工具调用循环从 ChatModel 中抽出来,变成了统一的 ToolCallingAdvisor 组件。
1.x 的工具调用痛点
1.x 时在 ChatClient 注册几个 ToolCallback,底层处理是这样的:ChatClient 把工具定义塞进请求发给模型,模型返回 tool_calls,AbstractChatModel 的实现类里写一个循环解析 tool_calls、执行工具、把结果追加到消息列表、再调模型。每个模型 provider 各写各的,OpenAI 一套、Ollama 一套。想在每次工具调用前后加自定义逻辑——比如记录日志、校验参数——找不到统一的切入点。
ToolCallingAdvisor 架构详解
2.0 把整个工具调用循环变成一个 Advisors。ChatClient 启动调用时经过 Advisor 链,ToolCallingAdvisor 就是链上的一个环节。
// JDK 21, Spring AI 2.0.0
ChatClientchatClient=ChatClient.builder(chatModel)
.defaultAdvisors(newToolCallingAdvisor(toolCallbacks))
.build();
Stringresult=chatClient.prompt()
.user("查一下最近三个月销售数据,按区域汇总")
.call()
.content();
核心逻辑是递归 advisor——每次模型响应里有 tool_calls,就执行工具、追加结果、再次调模型,直到模型返回无待处理 tool_calls 的响应。支持 blocking(.call())和 streaming(.stream())两种模式。
看一个带两个工具的完整示例:
// JDK 21, Spring AI 2.0.0
varweatherTool=newToolCallbackBuilder()
.name("getWeather").description("获取天气")
.inputType(WeatherRequest.class)
.toolFunction((WeatherRequestreq)->
newWeatherResponse(req.city(),"晴天",28))
.build();
varsalesTool=newToolCallbackBuilder()
.name("querySales").description("查询销售数据")
.inputType(SalesQuery.class)
.toolFunction((SalesQueryq)->
newSalesReport("Q3",1_250_000L))
.build();
ChatClientclient=ChatClient.builder(chatModel)
.defaultAdvisors(newToolCallingAdvisor(weatherTool,salesTool))
.build();
Stringresult=client.prompt()
.user("北京今天天气?再查一下今年Q3销售")
.call()
.content();
record WeatherRequest(Stringcity){}
record SalesReport(Stringperiod,longamount){}
两个工具注册在一个 ToolCallingAdvisor 里,模型自动选工具、填参数,advisor 循环处理。
渐进式工具披露和扩展点
1.x 时注册 50 个工具,每次请求把 50 个工具定义塞进 prompt,token 消耗很大。2.0 的 ToolSearchToolCallingAdvisor 做了渐进式披露——第一次只发少量核心工具,模型按需查询补充。官方数据说 token 消耗降低 34%-64%。
spring:
ai:
chat:
client:
tool-search-advisor:
enabled:true
ToolCallingAdvisor 留了四个扩展点:doInitializeLoop(循环开始前)、doBeforeCall(每次调模型前)、doAfterCall(模型返回后)、doFinalizeLoop(循环结束后)。最常用的是 doAfterCall——配合 AugmentedToolCallbackProvider 拦截模型参数,注入 innerThought 结构化思维链,让模型在调用工具前输出推理过程。
MCP 集成和升级影响
2.0 新增三个注解:@McpTool、@McpResource、@McpPrompt,MCP 工具定义做成注解驱动的。默认传输从 SSE 改成 Streamable HTTP,连接复用更好。
@McpTool
publicclass DatabaseTools{
@McpTool(name="queryDatabase")
publicStringquery(@McpToolParam("sql")Stringsql){
returnjdbcTemplate.queryForList(sql).toString();
}
}
从 1.x 升级要留意几个 breaking changes。Builder.options() 删了,配置改走 builder 方法。toolNames() 也没了,工具注册统一声明 ToolCallback 的 @Bean。
个人判断
这个设计方向是对的。工具调用循环从 ChatModel 抽出放进 Advisor 链,把"模型调用"和"工具编排"两个关注点解耦了。以后新增模型 provider 只需要关心怎么跟 API 通信,工具循环逻辑不用重复实现。
不过也有代价。Advisor 链是线性处理的,ToolCallingAdvisor 作为递归 advisor 嵌在链中,调试链路比以前复杂。以前打断点看 ChatModel 源码就能追踪,现在分布在多层递归里,出问题要多花点时间理链路。
渐进式工具披露虽省 token,但多了一次模型调用来判断"是否需要更多工具"。工具总数不到 10 个时,全量发反而延迟更低。建议按实际注册的工具数量决定是否开启。
总结
Spring AI 2.0 的工具调用重写是一次扎实的架构升级。ToolCallingAdvisor 把分散在 ChatModel 实现里的工具循环统一起来,配合渐进式披露和扩展点,既减少了代码重复,又给了足够的定制空间。如果你在用 1.x,升级主要改下工具注册方式,核心代码改动不大。还在观望的话,直接上 2.0 吧。
夜雨聆风