工具调用进阶
本教程共 45 篇 · 第 25 篇 · 更新于 2026-08-16 · 约 10 分钟阅读
本节目标:掌握多工具的注册与组织、结果处理、错误处理三大进阶能力。学完你能设计带多个工具的聊天应用,并避开工具调用最常见的坑。
25.1 多工具注册的三种姿势
第 24 章只注册了一个工具类。真实应用里工具往往有十几个,怎么组织?
方式一:一个类多个 @Tool 方法。 功能相关的放一起,比如日期时间工具、计算器工具。第 24 章的 DateTimeTools 就是这种。
方式二:多个工具类实例一起传。 tools() 接受可变参数,可以一次传多个对象。Spring AI 官方 showcase 项目里有个钱包计算例子,就是两个工具类一起注册:
@RestController
@RequestMapping("/wallet")
public class WalletController {
private final StockTools stockTools;
private final WalletTools walletTools;
// 构造器注入两个工具类
@GetMapping("/with-tools")
String calculateWalletValueWithTools() {
PromptTemplate pt = new PromptTemplate("""
What's the current value in dollars of my wallet based on the latest stock daily prices?
""");
return this.chatClient.prompt(pt.create())
.tools(stockTools, walletTools)
.call()
.content();
}
}
StockTools 提供股票价格工具,WalletTools 提供持仓数量工具。模型要算钱包总价值,得先调钱包工具拿持仓,再逐个调股票工具查价格。两个工具类互不依赖,靠模型串联。
方式三:注册成 Spring Bean。 工具类本身是 Bean 的话(比如 @Component),可以直接注入使用。用 FunctionToolCallback 构建的 ToolCallback Bean 更灵活,见 25.5。
Warning工具名在整个请求里必须唯一。两个类里出现同名工具,执行时无法区分,会直接报错。取名时加上业务前缀,比如 walletGetShares、stockGetPrice。
25.2 多步调用与并行调用
一次对话里模型可能连续调用多个工具,这是常态,不是异常。
串行多步:前一个工具的结果是后一个工具的输入。闹钟例子就是:先 getCurrentDateTime 拿当前时间,算出闹钟时刻,再 setAlarm。模型每完成一轮调用,框架就把结果发回去,模型再决定下一步。
并行调用:模型一次响应里请求多个工具。问”阿姆斯特丹和巴黎天气如何”,模型可能同时请求两次 currentWeather。框架会依次执行并全部回传。日志里会看到两个工具调用记录。
并行调用和 returnDirect 有个配合规则,见 25.3。
25.3 结果回传与 returnDirect
默认行为:工具结果作为 ToolResponseMessage 发回模型,模型基于结果生成最终回答。这适合绝大多数场景。
但有些场景你不想让模型碰结果。比如 RAG 检索工具,检索结果直接返回给用户更省事,回传模型只是多一轮无意义的加工。再比如某个工具代表终态,调用完代理的推理循环就该结束。
这时用 returnDirect。声明式写法:
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
编程式写法通过 ToolMetadata:
ToolMetadata toolMetadata = ToolMetadata.builder()
.returnDirect(true)
.build();
returnDirect 为 true 时,ToolCallingAdvisor 会跳出工具循环,直接把结果返回给调用方,模型不再参与。
Note如果一次请求里同时有多个工具调用,必须所有工具的 returnDirect 都是 true,结果才会直接返回。只要有一个是 false,全部结果都会发回模型。
25.4 ToolContext:给工具传上下文
第 24 章介绍了 ToolContext 的基础用法。进阶场景里它很有用:多租户的 tenantId、当前用户 ID、请求追踪号,这些都不该由模型生成,也不该发给模型。
String response = chatClient.prompt("Tell me more about the customer with ID 42")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme", "userId", "u-1001"))
.call()
.content();
工具里取用:
@Tool(description = "Retrieve customer information")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
String tenantId = toolContext.getContext().get("tenantId");
return customerRepository.findByTenantAndId(tenantId, id);
}
如果默认选项和运行时选项都设置了 toolContext,两者会合并,运行时优先。这让你可以在 Builder 里放通用上下文,请求时再补个别字段。
25.5 FunctionToolCallback:函数式工具
方法式工具适合业务逻辑分散在类里的情况。如果逻辑本身就是个函数,用 FunctionToolCallback 更直接。它支持 Function、Supplier、Consumer、BiFunction 四种函数式接口。
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
public WeatherResponse apply(WeatherRequest request) {
return new WeatherResponse(30.0, Unit.C);
}
}
public enum Unit { C, F }
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}
构建工具:
ToolCallback toolCallback = FunctionToolCallback
.builder("currentWeather", new WeatherService())
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
函数式工具的参数和返回值必须是 VOID 或 POJO,类型必须是 public。它不能处理基本类型和集合,这些正好是方法式工具擅长的。两类工具各有边界,按需选择。
25.6 ToolCallback Bean:把工具交还给 Spring
把工具定义成 ToolCallback Bean,是官方推荐的组织方式。工具逻辑与聊天代码彻底分离,还能享受依赖注入:
@Configuration(proxyBeanMethods = false)
class WeatherTools {
private final WeatherService weatherService;
WeatherTools(WeatherService weatherService) {
this.weatherService = weatherService;
}
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
}
使用时注入并传给 tools():
@Autowired
ToolCallback currentWeather;
String response = ChatClient.create(chatModel)
.prompt("What's the weather like in Copenhagen?")
.tools(currentWeather)
.call()
.content();
所有 ToolCallback 类型的 Bean 会自动被 StaticToolCallbackResolver 收集。想按名称动态解析工具,可以传工具名给 tools():
ChatClient.create(chatModel)
.prompt("What's the weather like in Copenhagen?")
.tools("currentWeather")
.call()
.content();
tools() 会自动把字符串名字解析成对应工具。工具名就是 Bean 名,注意保持稳定,别随手改。
25.7 错误处理
工具执行抛异常时,Spring AI 会把它包装成 ToolExecutionException。默认情况下,RuntimeException 的错误消息会被转成文本发回模型,让模型自己解释错误;受检异常和 Error(如 IOException、OutOfMemoryError)则直接抛出给调用方。
这个行为由 ToolExecutionExceptionProcessor 控制,Starter 自动配置了 DefaultToolExecutionExceptionProcessor。想改成”出错就抛”,用一个配置项:
spring.ai.tools.throw-exception-on-error=true
或者自定义 Bean:
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
return new DefaultToolExecutionExceptionProcessor(true);
}
两种模式各有适用场景。错误回传模型适合对话场景:模型可以说”抱歉,查询失败了,请稍后再试”,体验自然。直接抛出适合后台任务:调用方需要明确知道失败,才能决定重试还是告警。
Tip如果你自己实现 ToolCallback,执行出错时记得抛 ToolExecutionException,而不是裸抛 RuntimeException。这样异常才能被统一处理器接管。
25.8 典型坑清单
把常见的坑集中列出来,写代码时对照检查:
描述不详细。 模型判断”何时调用、怎么调用”全靠 description。描述含糊,模型就不调或乱调。描述要写清触发条件和参数格式。
必填参数标错。 参数没有可靠来源却标成 required,模型只能编值,幻觉就来了。没有值就标 optional。
工具名冲突。 同名工具在同一请求里必然出错。命名加业务前缀。
默认工具滥用。 defaultTools() 是全局共享的,敏感工具误配成默认,等于把操作权限送给所有会话。能按请求传的就别用默认。
直接调用 ChatModel。 ChatModel 不会自动执行工具调用,响应里的工具调用只是声明。想要自动执行必须走 ChatClient,或者自己写循环手动执行(user-controlled 模式,见 25.9)。
忽视调试日志。 工具调用的主要操作都记录在 DEBUG 级别。排查”模型怎么不调用工具”时,把 org.springframework.ai 包的日志级别调到 DEBUG,调用过程一目了然:
logging.level.org.springframework.ai=DEBUG
25.9 手动控制执行循环
框架自动执行适合大多数场景。但你要做流式进度上报、自定义可观测性、迭代间加条件判断时,就得自己接管循环。
思路:关闭自动执行的 ToolCallingAdvisor,检查响应里的工具调用,用 ToolCallingManager 手动执行,把结果拼回历史再调模型,循环直到没有工具调用。核心代码就一个 while 循环:
ChatModel chatModel = ...;
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new CustomerTools()))
.build();
Prompt prompt = new Prompt("Tell me more about the customer with ID 42", chatOptions);
ChatResponse chatResponse = chatModel.call(prompt);
while (chatResponse.hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);
prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);
chatResponse = chatModel.call(prompt);
}
System.out.println(chatResponse.getResult().getOutput().getText());
手动模式把每一轮对话都暴露给你,代价是你要自己维护历史消息。框架自动模式把这些都封装了,能用自动就别手写。
25.10 小结
进阶能力围绕三件事:多工具的组织(类、实例、Bean 三种姿势)、结果的去向(回传模型或直接返回)、错误的分发(回传模型或抛出)。工具多了之后,命名规范、描述质量和必填标注决定了调用准确率,这是比 API 用法更值得花时间的部分。
下一章讲从 1.x 的 FunctionCallback 迁移到 2.0 的 ToolCallback。