刚开始用 Spring AI 的时候,我以为 Tool Calling 这件事很简单:写个 @Tool,再把工具挂到 ChatClient 上,模型自然就会调。

结果真上手以后,最常见的场面是这样的:

  • 代码能跑
  • 模型也正常返回
  • 但是工具就是不调用
  • 你明明觉得这一步“应该”触发工具,它偏偏开始一本正经地胡说八道

这个坑很适合作为 Spring AI 入门后的第一篇排障文,因为它不是单一原因造成的。很多时候,不是 Spring AI 坏了,而是你以为“已经把工具接上了”,实际上只完成了一半。

这篇我不讲大而全的原理,只讲最容易卡人的 5 个排查点。你可以把它当成一份速查表。

先看一个最小例子

Spring AI 官方文档里给了一个很经典的例子:问“明天星期几”,模型本身不知道今天是哪天,所以它需要先拿到当前时间,再继续回答。

一个最小工具可以这么写:

import java.time.LocalDateTime;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now()
                .atZone(LocaleContextHolder.getTimeZone().toZoneId())
                .toString();
    }
}

调用侧类似这样:

String response = ChatClient.create(chatModel)
        .prompt("What day is tomorrow?")
        .tools(new DateTimeTools())
        .call()
        .content();

按官方文档的说明,这类问题需要实时信息,模型本身答不出来。只有把工具定义带进这次请求,它才有机会请求工具,再利用工具结果生成最终回答。

如果你现在的代码结构和这个例子差不多,但工具还是不触发,通常就往下看这 5 个点。

排查点 1:你选的模型根本不支持 Tool Calling

这是第一个该查的,而且很多人反而最后才查。

Spring AI 官方文档里有一张 Chat Models Comparison 表,里面单独列了 Tools/Function Calling 这一列。意思很明确:不是所有接进 Spring AI 的模型都支持工具调用。

也就是说,下面这件事要先确认:

  • Spring AI 支持这个模型接入
  • 这个模型本身也支持 Tools/Function Calling

如果模型不支持,你把工具注册得再漂亮也没用。模型压根不会返回 tool call 请求。

我自己的建议是,排查 Tool Calling 问题时先别怀疑 Spring 代码,先看模型能力表。这个动作能省掉你后面一大截无效调试。

排查点 2:工具没有真正挂到“这一次请求”上

这个坑特别像“看起来已经配了,实际上没生效”。

Spring AI 的文档里有一个很容易被忽略的点:如果你同时配置了默认工具和运行时工具,那么运行时工具会完全覆盖默认工具,不是合并。

比如你这么写:

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultTools(new CommonTools())
        .build();

String response = chatClient.prompt("帮我查一下今天日期")
        .tools(new OrderTools())
        .call()
        .content();

很多人会下意识以为这次请求同时拥有 CommonToolsOrderTools

其实不是。按官方文档的说明,这里的运行时工具会把默认工具整个顶掉。结果就是:

  • 你以为日期工具还在
  • 实际上这次请求只拿到了 OrderTools
  • 模型自然不会调用日期工具

如果你现在的项目里既用了 defaultTools() / defaultToolCallbacks(),又在某些请求上单独用了 .tools(),这里一定要查。

这是我见过最像“死活不触发”的假象之一。

排查点 3:工具描述太烂,模型根本不知道什么时候该用它

这个问题不算框架 bug,但非常常见。

Spring AI 官方文档在 @Tool 那一节写得很直白:description 对模型理解工具用途非常关键。如果你不给,默认就会退回到方法名;如果描述写得太差,模型可能不会在该用的时候用,或者用错。

很多入门代码都是这种写法:

@Tool
String query() {
    ...
}

从 Java 角度看没问题,从模型角度看问题很大。query 到底查什么?什么时候查?返回什么?模型不知道。

更靠谱一点的写法应该是把“用途”和“触发条件”写进去:

@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
    ...
}

如果你的工具还有参数,最好把参数语义也说清楚。Spring AI 的文档还提到,方法和函数工具最终都会生成输入 schema。schema 和描述都太模糊时,模型就更容易放弃调用。

这里我补一个工程判断:很多人说“模型怎么这么笨,明明该调工具却不调”,本质上不是模型笨,而是你给它的工具定义信息不够用。这一点从官方文档对 descriptioninput schema 的强调里是能推出来的。

排查点 4:其实已经触发了,只是你把自动执行关掉了

这也是一个很容易误判的点。

Spring AI 默认会帮你做完工具执行闭环:

  1. 模型返回 tool call 请求
  2. Spring AI 执行工具
  3. 把工具结果再喂回模型
  4. 模型生成最终回答

按官方文档的说法,这个过程默认是透明的,由 ToolCallingManager 驱动。

但文档也明确写了,工具是否会自动执行,取决于 ToolCallingChatOptions.internalToolExecutionEnabled。默认值是 true

如果你把它显式改成了 false,那就不是“框架自动帮你调用工具”了,而是“模型把工具请求还给你,你自己继续驱动后续循环”。

像这样:

ChatOptions chatOptions = ToolCallingChatOptions.builder()
        .toolCallbacks(ToolCallbacks.from(new DateTimeTools()))
        .internalToolExecutionEnabled(false)
        .build();

这个时候,如果你只是简单 chatModel.call(prompt) 一次,然后就盯着结果看,很容易得出一个错误结论:

“Tool Calling 没触发。”

实际上更可能是:

  • 模型已经请求了工具
  • 只是你没继续执行 tool loop

官方文档对这点给了完整示例:当你选择 user-controlled tool execution 时,需要自己检查 chatResponse.hasToolCalls(),再调用 ToolCallingManager.executeToolCalls(...),然后把新的会话历史继续送回模型。

所以这类问题更准确的描述不是“没触发”,而是“没自动执行完”。

排查点 5:你的工具签名本身就不适合 Spring AI 解析

这一点在一开始不一定最显眼,但很值得早点检查。

Spring AI 官方文档列了方法工具和函数工具的限制:

  • 方法工具不支持 Optional
  • 不支持 CompletableFutureFuture
  • 不支持 MonoFlux
  • 函数工具还不支持 primitive 和集合类型作为输入输出

如果你平时写 Spring 业务代码已经习惯了响应式类型、异步类型或者 Optional,很容易顺手把这些类型塞进工具方法。

比如下面这种写法,我就不建议直接当工具暴露:

@Tool(description = "Query user info")
Mono<UserInfo> queryUserInfo(Long userId) {
    ...
}

你可能觉得这是正常 Spring 风格,但从 Spring AI 的工具定义和 schema 生成视角看,这种签名并不在推荐范围内。

更稳妥的做法是先把工具边界收窄,返回一个简单、可序列化、语义明确的对象或字符串。别一上来就把业务层最复杂的响应类型扔给工具层。

为什么这个问题特别难排

因为 Spring AI 默认把工具调用过程包起来了。

官方文档明确提到,框架默认模式下,工具执行过程中那些内部消息不会直接暴露给你。你最后常常只能看到一个“模型给了你答案”,却看不到中间有没有请求工具、请求了什么、哪一步断掉了。

这也是为什么很多人会有一种很强的错觉:代码没报错,所以工具应该没问题。

其实不是。很多 Tool Calling 问题根本不报错,它只是静悄悄地退回普通聊天回答。

如果你想把这条链路看清楚,官方文档给的建议是走 ToolCallAdvisor 这条路。它的一个直接好处就是可观察性更好,advisor 链可以看到每次工具调用迭代。

对排障来说,这个信息很值钱。

我会怎么排这个问题

如果是我自己接手一个“工具就是不触发”的项目,我会按这个顺序查:

  1. 先看模型是否支持 Tools/Function Calling
  2. 再确认这次请求到底挂了哪些工具
  3. 再看工具描述和参数 schema 是否像人话
  4. 再确认是不是把 internalToolExecutionEnabled 关掉了
  5. 最后再检查工具方法签名是不是超出了 Spring AI 支持范围

这个顺序的好处是,前面几步成本最低,而且命中率很高。

最后给一份速查清单

  • 你的模型支持 Tools/Function Calling 吗?
  • 你的工具是挂在默认配置里,还是挂在这次请求里?
  • 你是不是以为默认工具和运行时工具会自动合并?
  • @Tool(description = "...") 写得是否足够具体?
  • 参数 schema 是否能让模型看懂?
  • 你有没有把 internalToolExecutionEnabled(false) 打开?
  • 如果关闭了自动执行,你有没有自己继续跑 tool loop?
  • 工具方法有没有使用 OptionalMonoFluxCompletableFuture 之类的类型?
  • 你现在看到的是“没触发”,还是“触发了但你没观察到”?

把这 9 个点过一遍,大多数 Spring AI Tool Calling 的入门坑都能缩小到很小范围。

结尾

Tool Calling 这件事最烦的地方,不是它难,而是它太像“已经接好了”。很多时候你离跑通只差一个细节,但如果没有排查顺序,就会一直在错误方向上怀疑框架、怀疑模型、怀疑网络,最后把时间耗光。

下一篇我准备继续写另一个高频坑:Java Agent 上下文总是丢?我排查了 Spring AI 会话记忆的 5 个误区。这两个问题经常连在一起出现,前一个没理顺,后一个也很难看明白。

参考资料

  • Spring AI Tool Calling Reference: https://docs.spring.io/spring-ai/reference/api/tools.html
  • Spring AI Chat Models Comparison: https://docs.spring.io/spring-ai/reference/2.0/api/chat/comparison.html
  • Spring AI GitHub Repo: https://github.com/spring-projects/spring-ai

说明

文中的以下结论直接来自 Spring AI 官方文档:

  • 不是所有模型都支持 Tools/Function Calling
  • 运行时工具会覆盖默认工具,而不是和默认工具合并
  • description 对模型正确使用工具非常关键
  • internalToolExecutionEnabled 默认是 true,关闭后需要自己执行 tool loop
  • 方法工具和函数工具存在明确的类型限制

文中的这些表述属于我的工程归纳,不是官方原句:

  • “看起来没触发,实际上是没自动执行完”
  • “工具描述太烂,模型不知道什么时候该用它”
  • “很多 Tool Calling 问题根本不报错,只会静悄悄退回普通回答”
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐