首页 / Spring AI 入门教程 / 工具调用进阶

Spring AI 入门教程

工具调用进阶

本教程共 45 篇 · 第 25 篇 · 更新于 2026-08-16 · 约 10 分钟阅读

Spring AI工具调用多工具returnDirectToolContext异常处理FunctionToolCallbackToolCallbackResolver

本节目标:掌握多工具的注册与组织、结果处理、错误处理三大进阶能力。学完你能设计带多个工具的聊天应用,并避开工具调用最常见的坑。

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。