StackTraces
Spring AI已验证

Spring AI Tool Calling 不生效的三种静默失效

@debug_master12 天前更新阅读 6 min00

Spring AI 的 Tool Calling 失效,最坑的是它往往“静默”:编译不报错、启动不报错,只是模型死活不调你的工具。三个常见根因:defaultOptions 用了不支持工具调用的 DefaultChatOptions;.tools(...) 的重载把 FunctionToolCallback 吞了;工具执行阶段新建 options 实例导致 callback 丢失。先用“请求里有没有 tools 字段”这一条把范围切开,再对号入座。

Issue & Env(案发现场与环境)

  • 业务场景:Spring Boot 应用用 ChatClient + @Tool 声明了工具方法(查天气、查订单之类),期望模型在合适时机自动调用,实际模型一律不调,直接用自己的知识回答。
  • 环境:Spring AI 1.1.x(按 1.1.8),Spring Boot 3.5.x(按 3.5.15),JDK 17。ChatClient 通过 defaultOptions 装配请求选项。
  • 一个反直觉的地方:这类故障大多是静默失效。代码能编译、应用能启动、日志里没有 ERROR,只有模型的行为不对劲。三个根因里只有“执行阶段丢 callback”这一种会显式抛异常。

StackTrace(堆栈日志)

坑 1(defaultOptions 类型不对)和坑 2(.tools() 重载吞参数)都不抛异常,可观察到的输出只有正常 INFO 日志,没有任何堆栈:

(无异常,日志停在正常 INFO,模型直接给出不含工具调用的回答)

坑 3(执行阶段 callback 丢失)会显式抛错,报错原文:

java.lang.IllegalStateException: No ToolCallback found for tool name: get_weather

完整堆栈从框架找回调一路回到你的业务调用:

java.lang.IllegalStateException: No ToolCallback found for tool name: get_weather
    at org.springframework.ai.model.tool.DefaultToolCallingManager.executeToolCalls(DefaultToolCallingManager.java:166)
    at org.springframework.ai.chat.model.AbstractToolCallSupport.callWithToolSupport(AbstractToolCallSupport.java:245)
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$InternalChatClientRequest.call(DefaultChatClient.java:686)
    at com.example.demo.assistant.WeatherAssistant.askWeather(WeatherAssistant.java:38)
    at com.example.demo.assistant.WeatherController.ask(WeatherController.java:25)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)

判断技巧:工具没被调用时,先看发给模型的请求里有没有 tools 字段。开 DEBUG 日志或临时打印请求体即可。没有 tools,问题在工具注册或 options 类型(坑 1、坑 2);有 tools、模型也发起了调用却抛 No ToolCallback found,问题在回调查找阶段(坑 3)。

Reproduce(复现代码)

最小复现只需要一个 ChatClient Bean,把 ChatOptions.builder() 的结果塞进 defaultOptions:

@Configuration
class ChatClientConfig {

    @Bean
    ChatClient chatClient(ChatModel chatModel) {
        return ChatClient.builder(chatModel)
                .defaultOptions(ChatOptions.builder()   // ❌ 生成 DefaultChatOptions
                        .model("gpt-4o-mini")
                        .temperature(0.7)
                        .build())
                .build();
    }
}

配一个 @Tool 方法:

@Component
class WeatherTools {

    @Tool(description = "查询指定城市的天气")
    String getWeather(String city) {
        return city + ":晴,26 摄氏度";
    }
}

调用侧:

String answer = chatClient.prompt()
        .user("北京今天天气怎么样?")
        .call()
        .content();

预期模型调用 get_weather,实际它直接编一段回答;换成坑 3 的写法,则变成调用时抛 IllegalStateException。

Root Cause(根因分析)

坑 1:defaultOptions 用了 DefaultChatOptions

ChatOptions.builder() 生成的是 DefaultChatOptions,它不支持 Tool Calling。工具回调即便被包进去,构建请求时也会被丢弃(spring-ai issue #3392)。

// ❌ 工具调用失效
.defaultOptions(ChatOptions.builder()
        .model(model)
        .temperature(0.7)
        .build())

// ✅ 用支持工具调用的选项
.defaultOptions(ToolCallingChatOptions.builder()
        .model(model)
        .temperature(0.7)
        .build())

这是 Spring AI 1.x 的已知问题,需要换 ToolCallingChatOptions(或模型特化的 options,如 DeepSeekChatOptions)来搭 defaultOptions。

坑 2:.tools(Object…) 重载吞掉 FunctionToolCallback

ChatClient 的 .tools(...) 有多个重载。传 FunctionToolCallback 实例时,编译器可能匹配到 tools(Object... toolObjects),它内部走 ToolCallbacks.from(toolObjects),而后者只接受 Method tool,FunctionToolCallback 被静默忽略,且没有编译错误(spring-ai issue #2495)。

// ❌ 可能命中 tools(Object...),FunctionToolCallback 不生效
FunctionToolCallback cb1 = ...;
FunctionToolCallback cb2 = ...;
client.prompt().tools(cb1, cb2).call();

// ✅ 声明成 ToolCallback 类型,或逐个注册
ToolCallback cb1 = ...;
ToolCallback cb2 = ...;
client.prompt().tools(cb1, cb2).call();
// 或者
client.prompt().tools(cb1).tools(cb2).call();

坑 3:中途换了 options 实例,callback 丢失

工具回调只在 .call() 阶段被写进 ChatClient 初始的那个 options 对象。若在自定义 tool 执行逻辑里为模型换了一个全新的 options 实例,新实例里没有 callback,框架转成 ToolCallingOptions 取回调时就是空的,于是抛 No ToolCallback found(spring-ai issue #4734)。判定方法:看 options 是不是又新建了一份。

The Fix(解决方案)

1. 用 ToolCallingChatOptions 配 defaultOptions

需要 Tool Calling 就别用裸 ChatOptions.builder():

@Bean
ChatClient chatClient(ChatModel chatModel) {
    return ChatClient.builder(chatModel)
            .defaultOptions(ToolCallingChatOptions.builder()
                    .model("gpt-4o-mini")
                    .temperature(0.7)
                    .build())
            .build();
}

2. 工具注册用 ToolCallback 类型,避免重载歧义

ToolCallback weatherCallback = FunctionToolCallback.builder("get_weather", weatherFn)
        .description("查询指定城市的天气")
        .inputType(WeatherRequest.class)
        .build();

String answer = chatClient.prompt()
        .user("北京今天天气怎么样?")
        .tools(weatherCallback)
        .call()
        .content();

声明变量时用 ToolCallback 而不是 FunctionToolCallback,或者把 .tools(...) 拆成逐个调用。

3. 别在 tool 执行链路里新建 options

复用最初构建 ChatClient 时的那个 options 对象;需要改动就 .copy() 复制它,回调仍在:

var base = ToolCallingChatOptions.builder()
        .model("gpt-4o-mini")
        .build();

// ✅ 复制而非新建,回调随 options 一起带过去
var adjusted = base.copy();

如果沿用旧的裸 ChatOptions 装配思路,先对照 ChatModel 与 ChatClient 的依赖配置 检查 options 类型。

小结

  • Tool Calling 失效是典型的静默失效,先查请求里有没有 tools 字段,用它切开“注册/选项”与“回调查找”两段。
  • 三个高发坑:DefaultChatOptions 不支持工具、.tools(Object...) 重载吞 FunctionToolCallback、执行阶段新建 options 丢回调。
  • 记一句:要函数调用,用 ToolCallingChatOptions,注册时声明成 ToolCallback 类型,不要中途换 options。
  • 三种根因里只有第三种会显式抛 No ToolCallback found,前两种只能靠观察上游请求来定位。
  • MCP 场景下工具不生效的排查另见 Spring AI MCP stdio 日志污染 stdout。

References(参考来源)

最后更新于 2026-09-29

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法