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

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法