
1. SpringAi 调用 Tool 失败的典型现场模型配置与工具注册的错位SpringAi 在 1.0.0 版本之后把 Tool Calling 抽象成了ToolCallback理论上只要模型支持 function calling、工具注册进ChatClient调用就应该触发。但实际项目里最常见的现象是日志里模型正常返回了文本tool_calls字段却是空的或者干脆抛一个No tool call found之类的提示。你定义的Tool方法从头到尾没被执行过一次断点打上去毫无反应。这个问题的迷惑性在于它看起来像模型能力问题实际上大多数时候是模型配置的声明位置和工具注册链路对不上。SpringAi 的自动装配在 1.0.0 里做了比较大的调整ChatClient构建时会从ChatModelBean 里读取默认模型而application.yml里spring.ai.openai.chat.options.model这个配置项在某些版本组合下并不会被自动注入到ChatModel的默认 options 里。结果就是你请求里带的 model 是空的或者是一个默认占位值服务端拿到的模型不支持工具调用自然就不会返回tool_calls。我试过的一个典型场景是本地用 OpenAI 兼容协议接了一个支持 Tool 的模型ToolCallback也通过Bean注册了但ChatClient.prompt().user(...).call()返回的内容里完全没有工具调用痕迹。把请求日志打开发现 body 里model字段是gpt-3.5-turbo这种默认值而不是我在 yml 里写的那个支持工具的模型名。模型侧根本没收到工具定义或者收到了但因为模型本身不支持而忽略。所以排查的第一步不是怀疑工具代码而是确认请求真正发出去的 model 是什么。你可以通过打开 SpringAi 的 debug 日志或者用 TaoToken 的请求日志功能看到实际转发的 payload。这一步能直接区分是「模型侧没收到工具」还是「通道侧把工具字段丢了」。另一个高频坑是ToolCallback的注册方式。SpringAi 1.0.0 支持两种一种是Tool注解 ToolCallbackResolver自动扫描另一种是手动FunctionToolCallback.builder()构建后注册到ChatClient。如果你用的是注解方式但Tool所在类没有被 Spring 扫描到或者方法参数类型不是模型能理解的 JSON Schema 类型工具就不会出现在请求的tools数组里。这时候模型即使支持工具调用也无从触发。还有一种情况是依赖版本冲突。SpringAi 1.0.0 的spring-ai-openai-spring-boot-starter和spring-ai-core版本不一致时ToolCallback的接口签名可能对不上编译能过但运行时工具注册被静默跳过。建议用mvn dependency:tree确认所有 spring-ai 相关依赖版本统一。把这三个角度串起来看模型配置决定「模型支不支持工具」工具注册决定「工具定不定义得出来」请求链路决定「工具定义有没有被发出去」。任何一环断了表现都是「Tool 调用不到」。下面按这个顺序给出可复制的配置和验证动作。2. TaoToken 前置准备统一 Key 与 Base URL 的接入配置在排查模型配置之前先把请求通道固定下来。用 TaoToken 的好处是它兼容 OpenAI 协议Base URL 和 Key 统一管理切换模型只需要改一个 model 字符串不用动代码里的ChatModelBean。这样你在排查 Tool 问题时可以把「通道侧」的变量排除掉专注看模型和工具注册。TaoToken 的 API 地址是https://taotoken.net/api注意这个地址不带任何查询参数直接作为base-url使用。Key 在控制台的 API Keys 页面生成格式是sk-开头的一串字符。模型 ID 用你实际要调用的支持 Tool 的模型名比如claude-sonnet-4-20250514或者gpt-4o这类。这三个东西——Base URL、Key、Model ID——就是接入的三件套缺一不可。在 SpringAi 的application.yml里配置长这样spring: ai: openai: base-url: https://taotoken.net/api api-key: sk-你的TaoTokenKey chat: options: model: claude-sonnet-4-20250514 temperature: 0.7注意base-url后面不要加/v1SpringAi 的 OpenAI starter 会自动拼接/v1/chat/completions。如果你手动加了/v1请求路径会变成/v1/v1/chat/completions直接 404。这个坑在切换第三方兼容通道时特别常见。Key 的管理建议用环境变量注入不要硬编码在 yml 里spring: ai: openai: api-key: ${TAOTOKEN_API_KEY}然后在启动参数或 IDE 的运行配置里设置TAOTOKEN_API_KEYsk-xxx。这样本地和 CI 环境可以用不同的 Key也避免提交到仓库。如果你用的是ChatClient手动构建的方式配置类里可以这样写Configuration public class ChatConfig { Bean public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider toolCallbackProvider) { return ChatClient.builder(chatModel) .defaultToolCallbacks(toolCallbackProvider) .build(); } }这里ChatModel由 starter 自动装配它会读取 yml 里的base-url、api-key、model。ToolCallbackProvider是 SpringAi 提供的工具回调提供者会自动收集所有Tool注解的方法。如果你发现工具没注册上先检查这个 Bean 有没有被创建以及Tool所在类有没有加Component或Service。TaoToken 的控制台里可以创建多个 Key分别给不同项目用。排查 Tool 问题时建议单独建一个 Key只用于这个 SpringAi 项目这样在请求日志里能快速过滤出相关调用。模型对话页面也可以直接测试同一个模型在纯对话场景下能不能触发工具用来对比是代码问题还是模型问题。3. 可复制配置application.yml 与 ToolCallback 注册代码这一节给出完整的可复制片段包括application.yml、工具类、配置类和 Controller。你按这个结构搭一遍如果 Tool 还是调不到再进第 5 节的排障。先看application.yml的完整写法server: port: 8080 spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-20250514 temperature: 0.7 # 关键显式声明工具调用相关参数 tool-choice: auto # 如果你用的是 1.0.0 的 starter加上这个 retry: max-attempts: 1 logging: level: org.springframework.ai: DEBUG org.springframework.web.client: DEBUGtool-choice: auto这个参数在部分版本里需要显式声明否则模型可能不返回工具调用。logging.level打开 DEBUG 后你能在控制台看到实际发出的请求 body里面有没有tools数组一目了然。工具类用Tool注解Component public class WeatherTools { Tool(description 查询指定城市的天气返回温度和天气状况) public String getWeather(ToolParam(description 城市名称例如北京) String city) { // 模拟返回实际项目里调外部 API return city 今天晴温度 25 摄氏度; } }注意ToolParam的 description 要写清楚模型靠这个理解参数含义。参数类型用 String、int、boolean 这类基础类型复杂对象需要模型能生成对应 JSON Schema容易出问题。配置类里注册ToolCallbackProviderConfiguration public class ToolConfig { Bean public ToolCallbackProvider toolCallbackProvider(WeatherTools weatherTools) { return MethodToolCallbackProvider.builder() .toolObjects(weatherTools) .build(); } }如果你有多个工具类toolObjects可以传多个对象。SpringAi 会扫描每个对象里的Tool方法生成对应的ToolCallback。Controller 里调用RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }启动后访问http://localhost:8080/chat?message北京天气怎么样如果工具正常触发返回内容里会包含工具执行的结果比如「北京 今天晴温度 25 摄氏度」。如果返回的是模型自己编的天气说明工具没被调用。这里有个细节ChatClient的defaultToolCallbacks在构建时传入如果你在prompt()里又手动加了.tools()可能会覆盖默认的。建议统一在ChatClient构建时注册不要在每次请求里重复加。4. 验证请求一次完整的 Tool 调用与结果确认配置写完后用 curl 直接打 TaoToken 的接口先确认通道侧没问题。这一步能排除 SpringAi 的封装干扰curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的TaoTokenKey \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 北京天气怎么样}], tools: [{ type: function, function: { name: getWeather, description: 查询指定城市的天气, parameters: { type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } } }], tool_choice: auto }如果返回的 JSON 里choices[0].message.tool_calls有内容说明模型和通道都支持工具调用。如果tool_calls是空的或者返回了纯文本那就是模型侧的问题——换一个明确支持 function calling 的模型再试。通道确认后回到 SpringAi 应用。启动时观察日志搜索tools关键字看请求 body 里有没有工具定义。如果日志里tools数组是空的说明ToolCallbackProvider没生效。检查WeatherTools有没有加ComponentToolConfig有没有被扫描到以及MethodToolCallbackProvider的 import 是不是org.springframework.ai.tool.method.MethodToolCallbackProvider。调用/chat接口后看返回内容。如果工具执行了WeatherTools.getWeather方法里的断点会命中。如果没命中但模型返回了文本说明模型没选择调用工具。这时候把tool_choice改成required强制模型调用spring: ai: openai: chat: options: tool-choice: requiredrequired会强制模型必须调用至少一个工具。如果这样还是不行那就是工具定义没发出去回到上一步检查请求 body。还有一个验证技巧在ChatClient调用后打印ChatResponse的元数据ChatResponse response chatClient.prompt() .user(message) .call() .chatResponse(); System.out.println(response.getResult().getOutput().getToolCalls());如果getToolCalls()返回空列表但模型返回了文本说明模型没触发工具。如果返回了工具调用但方法没执行说明ToolCallback的解析有问题检查方法签名和参数类型。实测下来大部分「Tool 调不到」的问题都出在application.yml里 model 没声明对或者ToolCallbackProvider没注册成 Bean。把这两步确认后基本都能触发。5. 常见报错排查401、local proxy failed 与 reading choices这一节对照几个真实报错给出定位路径。401 UnauthorizedKey 不对或者没带上。检查spring.ai.openai.api-key是否读到了环境变量echo $TAOTOKEN_API_KEY确认值存在。如果 Key 里有空格或换行也会 401。TaoToken 的 Key 在控制台可以重新生成生成后立即复制不要手动输入。local proxy failed / Connection refusedBase URL 写错了或者本地网络到taotoken.net不通。先curl -I https://taotoken.net/api看能不能通。如果返回 404 是正常的说明域名解析和 TLS 没问题。SpringAi 里base-url不要带/v1也不要带末尾斜杠。reading choices 报错 / 返回体解析失败通常是通道返回的 JSON 结构和 SpringAi 期望的不一致。打开 DEBUG 日志看原始响应。如果响应里choices字段是空的或者message里没有content可能是模型返回了工具调用但 SpringAi 版本不兼容。升级spring-ai-openai-spring-boot-starter到 1.0.0 的最新 patch 版本。OAuth / token 相关报错如果你用的是需要 OAuth 的模型SpringAi 的 OpenAI starter 默认走 Bearer Token不兼容 OAuth 流程。这种情况建议用 TaoToken 的 Key 方式接入它统一用 Bearer Token不需要额外 OAuth 配置。Tool 定义被忽略检查Tool方法的参数有没有用ToolParam注解。没有注解的话SpringAi 可能生成不出 JSON Schema工具定义就是空的。另外方法返回值类型建议用 String 或简单对象复杂泛型容易序列化失败。模型不支持工具有些模型虽然能对话但不支持 function calling。换claude-sonnet-4-20250514或gpt-4o这类明确支持工具的模型。在 TaoToken 的模型对话页面可以直接测试同一个模型在纯对话下能不能返回tool_calls用来对比。版本冲突mvn dependency:tree | grep spring-ai看所有 spring-ai 依赖版本是否一致。如果spring-ai-core是 1.0.0 但spring-ai-openai是 0.8.0ToolCallback接口对不上工具注册会静默失败。统一用 1.0.0 的 BOM 管理版本。CC Switch / Cline MCP / Codex auth.json 场景如果你在 Claude Code 或 Cline 里配置 MCP 工具确保 Base URL、Key、Model ID 三件套都写全。Claude Code 的配置在~/.claude/settings.jsonCline 在 MCP 配置里Codex 在auth.json。任何一项缺失都会导致工具调用链路断掉。排障时建议按「通道 → 模型 → 工具注册 → 请求 body」的顺序每步都有明确的验证动作不要跳步。6. 统一 Key 后的模型切换与长期编码方案把模型配置从代码里挪到application.yml之后切换模型只需要改一个字符串。比如从claude-sonnet-4-20250514切到gpt-4o改 yml 里的model值重启应用即可。如果不想重启可以用 Spring Cloud Config 或者环境变量覆盖java -jar app.jar --spring.ai.openai.chat.options.modelgpt-4o这样同一个 Key 可以驱动多个模型TaoToken 侧按模型计费不用为每个模型单独申请 Key。对于需要频繁切换模型的场景比如白天用便宜模型跑批量任务晚上用强模型做复杂推理这种统一 Key 的方式省去了管理多个 Key 的麻烦。如果你在长期编码或 Agent 场景里用 SpringAi建议把ChatClient的构建和模型配置分离。模型配置放 ymlChatClient构建放配置类工具注册用ToolCallbackProvider自动收集。这样新增工具只需要加一个Tool方法不用改配置类。对于 Claude Code 这类终端工具TaoToken 的 Coding Plan 可以直接接入Base URL 填https://taotoken.net/apiKey 用控制台生成的Model ID 按需选。配置好后终端里的代码补全和工具调用走同一条通道排查问题时日志统一。模型对话页面可以用来快速验证某个模型在当前 Key 下能不能触发工具不用启动 Spring 应用。接入文档里有各语言的完整示例Java 部分和上面的配置一致。API Keys 页面可以管理多个 Key按项目或环境隔离。最后留一个实用技巧在application.yml里把spring.ai.openai.chat.options.model设成环境变量${TAOTOKEN_MODEL:claude-sonnet-4-20250514}这样本地开发用默认值CI 环境通过环境变量覆盖不用改代码。工具调用的排查路径和模型切换的灵活性本质上都来自「配置外置」这一个原则。