基于 Spring AI+MCP 协议实现大模型调用本地自定义工具:TaoToken 统一 Key 接入实战
1. 为什么要在 Spring AI 里用 MCP 调本地工具如果你正在用 Spring AI 做 AI 应用大概率会遇到一个很现实的问题大模型本身只会“聊天”它不知道你本地的库存表、订单接口、天气数据长什么样。想让模型真正干活就得把本地方法暴露成它能调用的工具。MCPModel Context Protocol就是干这个的——它定义了一套标准协议让大模型和本地自定义工具之间能双向通信客户端发现工具、传参数服务端执行本地业务逻辑再把结果回给模型生成最终回答。但真正落地时麻烦往往不在协议本身而在“Key 和鉴权”。我见过太多项目服务端一个 Key、客户端一个 Key、换个模型又要改配置多模型切换时 Key 分散在好几个 yml 里调试一次要翻半天。这篇就聚焦一条完整链路Spring AI 应用通过 MCP 协议调用本地自定义工具同时用 TaoToken 统一 Key 和 API 通道把多模型鉴权收敛成一份配置。适合谁适合已经写过 Spring Boot、想快速把本地业务方法接进大模型、又不想被 Key 管理拖后腿的开发者。核心检索词先摆出来Spring AI、MCP 协议、大模型调用本地自定义工具、TaoToken 统一 Key。整套案例基于 Spring Boot Spring AI MCP 异步服务端/客户端可直接落地复用。下面从原问题讲起再给 TaoToken 前置配置、可复制代码、验证请求和排错。2. TaoToken 统一 Key 前置准备与 Base URL 配置在动手写 MCP 代码之前先把“模型通道”这件事解决掉。传统做法是每个模型厂商一个 KeyDashScope 一个、OpenAI 一个客户端配置里散落一堆api-key。TaoToken 的思路是提供一个统一的 API 通道你只需要一个 Key就能通过兼容接口调用不同模型。对 Spring AI 来说这意味着客户端配置里只维护一份 Base URL 和一份 Key换模型时改 Model ID 就行。先拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 列表在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建完复制那串sk-开头的字符串后面配置要用。这里有个关键点TaoToken 的 API 入口是 https://taotoken.net/api 注意这个地址不带任何查询参数配置 Base URL 时就用它。Spring AI 的 OpenAI 兼容 starter 会把/v1/chat/completions拼在后面所以 Base URL 填到/api这一层即可。三件套先记牢后面每个环节都要对齐配置项值说明Base URLhttps://taotoken.net/api统一 API 通道入口API Key控制台创建的sk-开头字符串一份 Key 走通多模型Model ID如claude-sonnet-4-5、gpt-4o等按需切换不改 Key如果你用的是 Claude Code 这类编码工具TaoToken 也提供了对应的接入文档地址是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Base URL、Key、Model ID 的完整填写示例。对 Spring AI 项目来说我们主要用 OpenAI 兼容协议所以客户端依赖选spring-ai-openai-spring-boot-starter即可不需要为每个厂商单独引 starter。为什么要先做这一步因为 MCP 客户端本身要调用大模型来决定“是否调用工具、调用哪个工具”。如果模型通道没配好MCP 工具注册得再漂亮模型也收不到工具列表。把 TaoToken 作为统一出口后客户端配置里只有一份base-url和一份api-keyMCP 服务端则完全不需要关心模型 Key——它只负责暴露工具。职责一分开排错也清晰连不上模型查客户端工具调不到查服务端。3. 可复制的 MCP 服务端与客户端配置片段这一节直接给能粘贴的配置和代码。先明确一个高频踩坑点spring-ai-starter-mcp-server-webflux依赖绝对不能和spring-boot-starter-web共存。web 依赖会强制拉起 Tomcat而 MCP 的 WebFlux 服务需要 Netty 容器。两者共存时程序能启动但 MCP 服务端异常客户端连不上、工具调不到。所以服务端只引 webflux客户端可以正常引 web。3.1 服务端 pom 依赖dependencies !-- Spring Boot 核心启动器不引 web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency !-- MCP 服务端 WebFlux 异步依赖 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webflux/artifactId /dependency /dependencies3.2 服务端 application.ymlserver: port: 8014 servlet: encoding: enabled: true force: true charset: UTF-8 spring: application: name: SAA-14LocalMcpServer ai: mcp: server: type: async name: customer-define-mcp-server version: 1.0.03.3 自定义工具类天气查询用Tool注解标记方法description写清楚用途模型靠它识别工具能力。package com.atguigu.study.service; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Service; import java.util.Map; Service public class WeatherService { Tool(description 根据城市名称获取天气预报) public String getWeatherByCity(String city) { MapString, String weatherMap Map.of( 北京, 降雨频繁今天和后天雨势较强部分地区有暴雨并伴强对流天气, 上海, 多云15℃~27℃南风3级当前温度27℃, 深圳, 多云40天阴16天雨30天晴3天 ); return weatherMap.getOrDefault(city, 抱歉未查询到对应城市); } }3.4 工具注册配置类把工具类注册成ToolCallbackProvider对外暴露。package com.atguigu.study.config; import com.atguigu.study.service.WeatherService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class McpServerConfig { Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }3.5 客户端 pom 依赖客户端要引 web 提供接口测试能力模型通道用 OpenAI 兼容 starter 对接 TaoToken。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- OpenAI 兼容 starter对接 TaoToken 统一通道 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency !-- MCP 客户端依赖 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency /dependencies3.6 客户端 application.yml三件套齐全server: port: 8015 servlet: encoding: enabled: true force: true charset: UTF-8 spring: application: name: SAA-15LocalMcpClient ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-5 mcp: client: type: async request-timeout: 60s toolcallback: enabled: true sse: connections: mcp-server1: url: http://localhost:8014注意api-key用环境变量注入别硬编码进仓库。model这一项就是 Model ID换模型只改这里Base URL 和 Key 不动。3.7 ChatClient 集成 MCP 工具package com.atguigu.study.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class SaaLLMConfig { Bean public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) { return ChatClient.builder(chatModel) .defaultToolCallbacks(tools.getToolCallbacks()) .build(); } }3.8 测试控制器对比启用/关闭 MCPpackage com.atguigu.study.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; RestController public class McpClientController { Resource private ChatClient chatClient; Resource private ChatModel chatModel; GetMapping(/mcpclient/chat) public FluxString chat(RequestParam(name msg, defaultValue 北京) String msg) { System.out.println(【使用MCP工具调用】); return chatClient.prompt(msg).stream().content(); } GetMapping(/mcpclient/chat2) public FluxString chat2(RequestParam(name msg, defaultValue 北京) String msg) { System.out.println(【未使用MCP工具调用】); return chatModel.stream(msg); } }配置片段到这里就齐了。服务端只暴露工具、不碰模型 Key客户端一份 TaoToken 三件套 MCP 连接地址。启动顺序是先 8014 再 8015让客户端自动连上服务端加载工具。4. 验证请求与成功结果一次本地工具调用全链路配置写完最关键的是验证“模型真的调了本地工具”而不是自己编答案。启动顺序先起 MCP 服务端8014确认监听正常再起客户端8015。客户端启动日志里会打印发现工具的信息看到weatherTools或getWeatherByCity相关字样说明工具已加载。先测启用 MCP 的接口curl http://localhost:8015/mcpclient/chat?msg上海天气怎么样预期结果模型不会直接凭知识库回答而是先触发工具调用请求 8014 服务端的getWeatherByCity拿到我们自定义的“多云15℃~27℃南风3级”这段数据再组织成自然语言返回。返回内容里会包含我们写死在 Map 里的那段文本这就是“本地工具被真正调用”的铁证。再测未启用 MCP 的接口做对比curl http://localhost:8015/mcpclient/chat2?msg上海天气怎么样这个接口走的是原生ChatModel没有注入工具回调。模型只能用自身知识库回答返回的是泛泛的天气描述绝不会出现我们 Map 里那段自定义文本。两个接口一对比MCP 的价值就直观了一个能读本地数据一个只能靠模型记忆。如果你想更细地看调用过程可以在服务端getWeatherByCity里加一行日志System.out.println(MCP工具被调用入参 city city);再次请求/mcpclient/chat控制台出现这行日志说明链路完全打通客户端 → 模型决策 → MCP 协议 → 服务端工具执行 → 结果回传 → 模型生成回答。实测下来这个链路里最容易出问题的不是代码而是依赖冲突和配置地址下一节专门排。另外如果你想把模型换成别的比如验证不同模型对工具调用的支持可以直接在模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 里先手动试一下工具描述是否清晰再回到代码里改 Model ID。对长期跑编码和 Agent 任务的场景Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 里有更细的通道说明。5. 本篇常见错误排查401、连接失败与工具不触发排错这块我按真实报错来对照基本都是配置层的问题代码本身很少出错。报错一401 Unauthorized / invalid api key。这是 TaoToken Key 没配对。检查三处api-key是否用了环境变量且已 exportKey 是否是sk-开头且没多余空格Base URL 是否写成 https://taotoken.net/api 而不是带/v1的地址。Spring AI 的 OpenAI starter 会自己拼/v1/chat/completions你多写一层就 404 或 401。三件套对齐Base URL、Key、Model ID缺一不可。报错二local proxy failed / connection refused。客户端连不上 MCP 服务端。先确认 8014 是否真的起来了curl http://localhost:8014看有没有响应。再检查客户端sse.connections.mcp-server1.url是否写对端口。最常见的原因是服务端误引了spring-boot-starter-webTomcat 把端口占了WebFlux 的 MCP 服务没起来表面看进程在实际工具通道是死的。删掉 web 依赖重启即可。报错三reading choices 相关解析异常。通常是模型返回格式和客户端预期不一致或者 Model ID 写错导致通道返回了非预期内容。先确认model字段是 TaoToken 支持的模型名再确认 Base URL 没写错。如果换了模型后突然报这个八成是 Model ID 拼错。报错四模型不调用工具直接自己回答。检查Tool的description是否清晰模型靠描述判断要不要调。描述太模糊模型就忽略工具。再确认客户端是否真的注入了ToolCallbackProviderdefaultToolCallbacks(tools.getToolCallbacks())这行不能少。最后确认 MCP 客户端配置里toolcallback.enabledtrue。报错五工具参数匹配失败。工具方法参数名和类型要和大模型推断的一致。简单字符串参数最稳复杂对象容易匹配不上。如果一定要传对象把参数拆成多个基础类型字段。报错六OAuth / 鉴权相关提示。如果你在别的工具里见过 OAuth 报错本质是鉴权头没带对。Spring AI 这边走的是Authorization: Bearer key由 starter 自动加你只要保证api-key正确即可。别手动去拼鉴权头容易重复或格式错。排错顺序建议先确认模型通道通单独调一次模型对话再确认 MCP 服务端通curl 端口最后确认工具注册通看启动日志。三层分开查比一上来就翻代码快得多。6. 把统一 Key 接入沉淀成可复用工程习惯走到这里链路已经跑通了。最后说几个我踩过坑之后沉淀下来的习惯能让这套方案在真实项目里更稳。第一把 TaoToken 的三件套抽成环境变量或配置中心别写死在 yml。TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、TAOTOKEN_MODEL三个变量本地用.env线上用配置中心。换模型时只改TAOTOKEN_MODEL代码零改动。第二MCP 服务端和客户端分仓或分模块。服务端只依赖 webflux客户端才依赖 web。用 Maven 多模块把依赖边界卡死避免有人手滑把 web 引进服务端。这个坑我见过不止一次排查起来很费时间。第三工具描述当成接口文档来写。Tool(description ...)里的文字直接决定模型会不会调、调得对不对。把参数含义、返回格式、适用场景写清楚比事后调 prompt 有效得多。第四验证阶段保留/mcpclient/chat2这个无工具接口。它是你的对照组任何时候怀疑“模型是不是没调工具”跑一下对比就知道。等业务稳定了再决定要不要删。第五扩展工具时按业务域拆类。天气一个 Service、订单一个 Service、库存一个 Service每个类里用Tool标方法注册时用MethodToolCallbackProvider.builder().toolObjects(...)把多个对象一起传进去。这样工具多了也不会乱。这套结构跑顺之后往数据库查询、内部接口调用、文件处理上扩都很自然——本地方法加个Tool注册进去模型就能用。真正花时间的从来不是写工具而是把 Key 和鉴权收敛干净。统一通道 一份配置 清晰的三件套后面加多少工具都不慌。