1. 从一次“模型不调工具”的排查说起
SpringAI 的 ChatClient 工具回调机制,简单说就是让大模型在对话过程中主动调用你写的 Java 方法:模型判断需要查订单、退票、查天气时,不再只吐文字,而是生成一个tool_call请求,框架解析后执行对应方法,再把结果塞回上下文让模型继续回答。它适合已经在用 Spring Boot 做后端、想让 LLM 真正“动手干活”的开发者,尤其是订单、工单、风控这类需要真实业务动作的场景。
我试过在本地把@Tool注解、ToolCallback、FunctionToolCallback三种方式都跑了一遍,最容易卡住的不是工具本身,而是模型通道的 Key 和 Base URL 配置——工具描述写得再清楚,请求发不出去或者模型能力不匹配,回调链路根本触发不了。这篇就聚焦工程落地:用一份可复制的settings.json骨架接入 TaoToken 统一 Key/API 通道,然后在 Spring Boot 里完成工具回调注册与触发链路验证,预期跑通一次带工具调用的对话请求。
TaoToken 在这里的角色是统一模型入口:一个 Key 走通对话与工具调用,省去在多个模型供应商之间来回切换配置。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,注意 API 路径不带 UTM 参数,配置时别把推广参数拼进去。
2. TaoToken 前置:Key 与通道准备
2.1 为什么工具回调对通道有要求
工具回调不是纯文本补全,它要求模型返回结构化的tool_calls字段。如果通道背后的模型不支持 function calling,或者网关把tools参数吞掉了,你会看到模型正常回文字、但永远不触发工具。所以第一步不是写 Java,而是确认通道支持工具调用。
TaoToken 的模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,可以先在里面选一个明确支持 function calling 的模型,比如常见的 GPT 系列或 Claude 系列。选模型时留意页面标注的能力项,工具调用通常和“函数调用/工具”能力绑定。
2.2 创建统一 Key
进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,在 API Keys 页面新建一个 Key,建议按项目命名,比如springai-tool-demo。Key 只在创建时完整显示一次,复制后先存到本地环境变量或配置中心,别直接硬编码进 Git。
API Keys 直达页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
2.3 settings.json 骨架
SpringAI 本身用application.yml配置模型,但很多团队会把模型通道参数抽到独立的settings.json里统一管理,方便多环境切换。下面这份骨架可以直接复制,把apiKey换成你自己的:
{ "springai": { "provider": "openai-compatible", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoTokenKey", "chat": { "model": "gpt-4o-mini", "temperature": 0.2, "toolCalling": { "enabled": true, "maxIterations": 5 } } } }几个参数说明:baseUrl固定为https://taotoken.net/api,不要带任何查询参数;toolCalling.enabled打开工具调用;maxIterations控制工具回调的最大轮次,防止模型陷入“调用—返回—再调用”的死循环,一般 3 到 5 足够。
注意:
apiKey不要提交到代码仓库。生产环境建议用环境变量TAOTOKEN_API_KEY注入,settings.json里只留占位符。
3. 可复制配置:Spring Boot 接入与工具注册
3.1 依赖与读取 settings.json
在pom.xml里引入 SpringAI 的 OpenAI starter(版本按你项目实际选,这里以 1.0 系列为例):
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency>然后写一个配置类,把settings.json读进来并构建ChatClient。核心是把baseUrl和apiKey传给OpenAiApi:
@Configuration public class ChatClientConfig { @Bean public OpenAiApi openAiApi() throws IOException { ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree( new ClassPathResource("settings.json").getInputStream()); JsonNode cfg = root.path("springai"); return OpenAiApi.builder() .baseUrl(cfg.path("baseUrl").asText()) .apiKey(cfg.path("apiKey").asText()) .build(); } @Bean public ChatClient chatClient(OpenAiApi openAiApi) { return ChatClient.builder(new OpenAiChatModel(openAiApi)) .defaultSystem("你是一个订单助手,需要退票时调用工具。") .build(); } }这里baseUrl指向 TaoToken 的 API 地址,OpenAiApi会拼出/v1/chat/completions这类路径,所以配置里只写到/api即可。
3.2 用 @Tool 注解定义工具
先定义一个最简单的退票工具,用@Tool和@ToolParam声明语义:
@Slf4j @Service public class TicketTool { @Tool(description = "当用户明确要求退票时调用,参数为姓名和订单号") public String cancel( @ToolParam(description = "用户姓名") String name, @ToolParam(description = "订单号") String orderId) { log.info("执行退票工具: {} - {}", name, orderId); // 这里替换成真实业务调用 return "退票成功,订单号 " + orderId; } }description要写清楚“什么时候调用”,而不是只写“退票”。模型靠这段文字判断是否触发,写得越贴近用户口语,命中率越高。
3.3 注册到 ChatClient 并触发
在服务里把工具注册进去,注意.tools(ticketTool)这一步:
@Service public class ToolCallService { private final ChatClient chatClient; private final TicketTool ticketTool; public ToolCallService(ChatClient chatClient, TicketTool ticketTool) { this.chatClient = chatClient; this.ticketTool = ticketTool; } public String chat(String message) { return chatClient.prompt() .user(message) .tools(ticketTool) .call() .content(); } }写个 Controller 或测试方法调用chat("帮我退票,姓名张三,订单号 A1001"),如果链路通了,日志里会先打印“执行退票工具”,然后模型基于工具返回值生成最终回复。
4. 验证请求:跑通一次带工具调用的对话
4.1 用 curl 先验证通道
在写 Java 之前,先用 curl 确认 TaoToken 通道支持工具调用,能省掉大量排查时间:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "帮我退票,姓名张三,订单号A1001"}], "tools": [{ "type": "function", "function": { "name": "cancel", "description": "当用户明确要求退票时调用", "parameters": { "type": "object", "properties": { "name": {"type": "string", "description": "用户姓名"}, "orderId": {"type": "string", "description": "订单号"} }, "required": ["name", "orderId"] } } }] }'如果返回的choices[0].message里带tool_calls字段,说明通道和模型都支持工具调用,可以放心往下走。如果只返回普通文本,先换一个支持 function calling 的模型再试。
4.2 观察 Spring 侧日志
启动 Spring Boot 后调用接口,重点看两类日志:一是TicketTool里的log.info,确认方法真的被执行;二是 SpringAI 的调试日志,能看到tool_call的 name 和 arguments。可以在application.yml里打开:
logging: level: org.springframework.ai: DEBUG成功时你会看到类似Tool execution result: 退票成功,订单号 A1001的日志,随后模型输出最终答复。到这一步,工具回调链路就算跑通了。
5. 本篇常见错排查
5.1 模型不触发工具调用
最常见的原因是description太笼统。把“退票”改成“当用户明确要求退票时调用”,并在系统提示里补一句“涉及退票必须调用工具”,命中率会明显提升。另外确认settings.json里toolCalling.enabled是true,且请求确实带上了tools参数。
5.2 参数反序列化失败
如果日志报Cannot deserialize或参数为 null,检查 JSON Schema 里的type和 Java 类型是否一致。比如订单号在 Schema 里写成integer、Java 里却是String,就会失败。统一用String接收订单号这类标识符,避免前导零丢失。
5.3 工具执行后模型无响应
工具方法返回null或抛异常被吞掉,模型拿不到结果就会卡住。给工具方法加全局 try-catch,返回值强制非空:
try { // 业务逻辑 return "退票成功"; } catch (Exception e) { log.error("工具执行失败", e); return "退票失败: " + e.getMessage(); }5.4 401 或 404 报错
401 通常是 Key 无效或没带Bearer前缀;404 多半是baseUrl写错,比如多写了/v1或带了 UTM 参数。记住 API 地址就是https://taotoken.net/api,路径拼接交给 SDK。
5.5 回调轮次过多
模型反复调用同一个工具,通常是工具返回值没有给出明确结论。让返回值包含“成功/失败”和关键信息,模型才能判断下一步。同时把maxIterations设成 5 以内兜底。
6. 继续往下走
工具回调跑通后,下一步通常是把工具集从单个 Bean 扩展成注册中心,或者接入更复杂的编码/Agent 场景。如果你要长期跑编码类任务、需要稳定的额度和多模型切换,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合持续性的开发工作流。
接入过程中遇到通道或 Key 的问题,直接查接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有针对 OpenAI 兼容协议的参数说明。想先验证模型本身是否支持工具调用,去模型对话页手动发一条带工具的请求最快:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Key 管理仍在 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后留一个实用习惯:把settings.json里的模型名、maxIterations、工具description当成三个独立变量分别调优,别一次改一堆,否则出问题很难定位是通道、模型还是工具描述导致的。