1. 为什么 Java 侧 MCP 工具调用总在“最后一公里”翻车
如果你正在用 Spring AI 做智能体,大概率遇到过这种场景:ChatClient 能正常对话,模型也能返回一段看起来像工具调用的 JSON,但真正落到业务方法上——比如保存一篇文章、查一次订单、写一条记录——就是没执行。日志里没有异常,返回值也“像那么回事”,可数据库里空空如也。这不是模型的问题,而是 Java 侧的配置链路没打通。
Spring AI 的 MCP(Model Context Protocol)支持,本质上是把外部工具以标准协议暴露给模型,再由 ChatClient 在对话过程中决定是否调用。它适合谁?适合已经用 Spring Boot 搭好后端、想让 Java 服务具备“工具调用能力”的开发者,尤其是需要把内部 API 包装成模型可调用工具的场景。这篇内容聚焦一件事:用 ChatClient 接入 MCP,跑通一次真实的工具调用,并确认链路生效。我会给出配置骨架、可复制代码,以及验证请求的完整动作。实测下来,最容易出问题的不是模型,而是 ToolCallbackProvider 的注册和 MCP Server 的暴露方式。
2. TaoToken 前置:先把模型入口和 Key 准备好
在写 Java 代码之前,得先有一个能稳定调用的模型入口。Spring AI 本身不绑定具体模型服务,你需要配置一个兼容 OpenAI 协议的 endpoint 和 API Key。我这边用的是 TaoToken 的模型对话入口,它兼容标准 Chat 接口,Spring AI 的 OpenAiChatModel 可以直接对接。
操作路径很直接:先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 了解整体能力,然后进入控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。创建后复制那串 sk- 开头的 Key,后面配置里要用。
注意:API Key 只显示一次,建议创建后立刻存到环境变量或配置中心,不要硬编码进 Git 仓库。
模型对话的接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面写清了 base_url 和兼容参数。API 基础地址是 https://taotoken.net/api ,注意这个地址不带 UTM 参数,配置时直接用。如果你后面要做长期编码或 Agent 任务,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,它更适合高频调用场景。
3. 可复制配置:ChatClient + MCP 的骨架
这一节是核心。Spring AI 接入 MCP 需要三块:MCP Client 配置、ToolCallbackProvider 注册、ChatClient 构建。我按 Maven 依赖、application.yml、Java 配置类、工具定义四步拆开,你可以直接抄。
3.1 Maven 依赖
Spring AI 的版本迭代较快,建议用 1.0.0-M6 及以上。核心依赖是 spring-ai-openai-spring-boot-starter 和 spring-ai-mcp-client-spring-boot-starter。
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency>如果你用的是 Gradle,把 groupId 和 artifactId 对应替换即可。注意 MCP 相关 starter 在 M6 之后才比较稳定,早期版本 API 差异较大。
3.2 application.yml 配置
这里要配两段:模型入口和 MCP Server 连接方式。模型部分对接 TaoToken 的兼容接口。
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.2 mcp: client: enabled: true name: csdn-mcp-client version: 1.0.0 type: SYNC request-timeout: 30stype: SYNC表示同步调用,适合大多数工具调用场景。如果你的工具执行时间长,可以改成 ASYNC,但要注意线程模型。request-timeout建议设 30 秒以上,MCP 握手和工具发现需要时间。
3.3 MCP Client 配置类
Spring AI 的 MCP Client 支持 stdio 和 SSE 两种传输方式。本地工具用 stdio,远程工具用 SSE。下面这个配置类注册一个基于 stdio 的 MCP Client,并把它暴露的 ToolCallback 注入到 ChatClient。
@Configuration public class McpClientConfig { @Bean public McpSyncClient mcpSyncClient() { ServerParameters params = ServerParameters.builder("node") .args("mcp-server.js") .build(); McpClientTransport transport = new StdioClientTransport(params); McpSyncClient client = McpClient.sync(transport) .requestTimeout(Duration.ofSeconds(30)) .build(); client.initialize(); return client; } @Bean public ToolCallbackProvider toolCallbackProvider(McpSyncClient mcpSyncClient) { return SyncMcpToolCallbackProvider.builder() .mcpClients(List.of(mcpSyncClient)) .build(); } }关键点在SyncMcpToolCallbackProvider,它负责把 MCP Server 暴露的工具转换成 Spring AI 能识别的 ToolCallback。如果这一步没注册,ChatClient 就不知道有哪些工具可用,模型也不会触发调用。
3.4 工具定义与 ChatClient 构建
假设 MCP Server 暴露了一个saveArticle工具,参数是 title 和 content。ChatClient 构建时把 ToolCallbackProvider 传进去。
@Bean public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { return builder .defaultToolCallbacks(toolCallbackProvider) .defaultSystem("你是一个可以调用工具的助手,需要保存文章时调用 saveArticle。") .build(); }defaultToolCallbacks是 M6 之后的写法,早期版本用defaultFunctions。如果你编译报错,先确认版本。系统提示词里明确告诉模型“需要保存文章时调用 saveArticle”,能显著提高工具触发率。
4. 验证请求:一次真实的工具调用
配置写完,怎么确认链路真的通了?我建议分两步:先验证工具发现,再验证工具执行。
4.1 验证工具是否被发现
写一个 CommandLineRunner,启动时打印当前可用的工具列表。
@Component public class ToolListRunner implements CommandLineRunner { private final ToolCallbackProvider provider; public ToolListRunner(ToolCallbackProvider provider) { this.provider = provider; } @Override public void run(String... args) { ToolCallback[] callbacks = provider.getToolCallbacks(); System.out.println("发现工具数量: " + callbacks.length); for (ToolCallback cb : callbacks) { System.out.println("工具名: " + cb.getToolDefinition().name()); } } }启动应用,如果控制台打印出saveArticle,说明 MCP Server 连接成功、工具发现正常。如果数量为 0,问题在 MCP Client 初始化或 Server 启动参数上,先排查这一层。
4.2 发起一次工具调用请求
工具被发现后,用 ChatClient 发一条会触发工具调用的消息。
@RestController public class ArticleController { private final ChatClient chatClient; public ArticleController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/test-save") public String testSave() { String prompt = "请调用 saveArticle 工具,保存一篇标题为《MCP 测试》的文章,内容为:链路验证成功。"; return chatClient.prompt() .user(prompt) .call() .content(); } }访问/test-save,观察三件事:第一,返回内容里是否提到工具调用;第二,MCP Server 侧日志是否收到 saveArticle 请求;第三,目标存储(比如 CSDN 后台)是否出现这篇文章。三者都满足,链路才算真正打通。
提示:如果模型返回了工具调用意图但没执行,检查
defaultToolCallbacks是否生效,以及 MCP Server 的工具名是否和提示词里写的一致。大小写敏感。
5. 本篇常见错排查
工具调用链路涉及模型、Spring AI、MCP Client、MCP Server 四层,任何一层出问题都会表现为“没反应”。下面是我踩过的几个坑,按排查顺序列出来。
错误一:工具数量为 0。最常见原因是 MCP Server 进程没起来,或者 stdio 的 command 路径不对。先手动执行node mcp-server.js,确认能启动并响应 initialize 请求。如果 Server 正常,检查 Spring AI 的 MCP Client 是否调用了initialize(),漏掉这一步工具列表就是空的。
错误二:模型不触发工具调用。模型返回纯文本,没有 tool_calls 字段。原因通常是系统提示词不够明确,或者模型本身对工具调用支持弱。换一个工具调用能力强的模型,同时在提示词里直接写出工具名和参数格式。另外确认defaultToolCallbacks真的传进去了,可以用第 4.1 节的 Runner 验证。
错误三:工具被调用但参数为空。模型生成了 tool_calls,但 arguments 是空对象。这通常是工具的参数 schema 定义不清晰。MCP Server 侧的工具定义要写清 required 字段和类型,Spring AI 会把 schema 转给模型。schema 越明确,模型填参越准。
错误四:调用超时。MCP 的 request-timeout 默认值偏短,工具执行慢就会中断。把request-timeout调到 30s 以上,同时检查 MCP Server 是否有阻塞操作。如果是远程 SSE 方式,还要排查网络链路。
错误五:返回 401 或模型不可用。这是模型入口配置问题,和 MCP 无关。检查base-url是否为 https://taotoken.net/api ,API Key 是否有效。可以先用模型对话入口单独测一次普通对话,确认模型层没问题,再排查 MCP 层。
排查时建议按“模型层 → MCP Client 层 → MCP Server 层 → 工具执行层”的顺序,每层单独验证,不要一上来就怀疑模型。
6. 把链路固定下来:接入文档与后续动作
链路跑通一次之后,建议把配置固化成可复用的模板。API Key 走环境变量,MCP Server 的启动参数走配置中心,工具列表在启动时打印一次作为健康检查。这样下次换环境或换工具,排查成本会低很多。
如果你在接入过程中遇到 Key 或模型入口的问题,直接看接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有针对 Spring AI 兼容配置的说明。需要新建或轮换 Key,去 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。想先单独验证模型对话是否正常,用模型对话入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你的场景是长期编码或 Agent 高频调用,Coding Plan 会更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后说一个实用技巧:在 MCP Server 侧给每个工具加一行入参日志,打印收到的 arguments。这样当模型填参不准时,你能立刻看到是模型的问题还是工具 schema 的问题。这个日志在排查阶段比任何断点都好用。