Spring AI 2.0 + MCP Apps 实战:Java 开发者用 TaoToken 打通多模型调用链路

📅 发布时间:2026/10/1 20:02:22
Spring AI 2.0 + MCP Apps 实战:Java 开发者用 TaoToken 打通多模型调用链路
1. 从“能调通”到“能管住”Java 后端的多模型调用困局如果你正在用 Spring Boot 写业务系统最近又被要求“加个 AI 能力”大概率会经历这么一段心路先拿 Spring AI 的 ChatClient 调通一个模型跑起来挺爽接着产品说“再支持一下另一个模型做对比”于是你复制一份配置再后来测试环境、预发环境、生产环境各一套 Key模型供应商还换了两家——配置文件里的api-key开始满天飞谁也不敢删谁也不知道哪个还在用。这就是 Java 团队做 AI 集成时最真实的痛点调用链路能跑通但多模型 Key 和 Base URL 管不住。Spring AI 2.0 把 MCPModel Context Protocol能力合并进核心之后Java 侧终于可以像写普通 Service 一样声明工具、暴露资源甚至让模型在对话里直接调用你后端的业务方法。但工具一多、模型一多问题就从“怎么写注解”变成了“怎么统一出口”。我试过在一个 Spring Boot 3.3 的项目里同时接三家模型结果光是application.yml就写了四套spring.ai.openai.*配置切换模型要改代码、重启服务测试同学想验证一个 MCP 工具调用还得找我要 Key。后来把出口统一到 TaoToken 的 API 通道上Base URL 和 Key 收敛成一份模型 ID 通过配置项切换才算把这条链路理顺。这篇就按“本地跑通端到端”的目标来写从依赖版本、application.yml配置到 MCP 工具声明、一次真实的工具调用验证再到几个我踩过的报错。适合已经在用 Spring Boot、想给项目加 AI 能力、又不想把 Key 管理搞成一团乱麻的 Java 后端。2. 前置准备Spring AI 2.0 与 TaoToken 通道怎么接先说清楚这一节要解决什么让你的 Spring Boot 项目有一个统一的模型出口后面不管加多少个模型、多少个 MCP 工具都走同一个 Base URL 和同一把 Key。2.1 版本对齐别在 M 版本上翻车Spring AI 2.0 目前处于里程碑阶段MCP 注解和 MCP Apps 的 metadata 支持是在 2.0.0-M3 之后才逐步稳定的。如果你用 M2 或更早McpServer、ToolMetadata这些注解会直接编译不过。建议直接对齐到 2.0.0-M4 及以上。Maven 里需要引入 Spring AI 的 BOM 和 starterdependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version2.0.0-M4/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId /dependency /dependencies注意spring-ai-starter-mcp-server这个 starterM4 之后 MCP Server 能力已经并入核心不需要再单独引老版本的spring-ai-mcp。2.2 为什么用 TaoToken 做统一出口Spring AI 默认的 OpenAI starter 会把请求打到官方地址但实际项目里你往往需要一个 Key 覆盖多个模型不用为每个供应商单独申请Base URL 可配置本地、测试、生产用不同通道模型 ID 通过配置切换不改代码。TaoToken 提供的就是这样一个兼容 OpenAI 协议的 API 通道。你拿到一把 Key配好 Base URLSpring AI 的 OpenAI starter 就能直接指向它。模型侧通过model参数指定比如对话模型、代码模型各用各的 ID但出口是同一个。需要提前准备的东西一个 TaoToken 账号在控制台创建 API Key记下 Base URLhttps://taotoken.net/api确认你要用的模型 ID在模型列表里能看到。控制台入口在这里创建 Key 的时候建议按环境分比如spring-ai-dev、spring-ai-prod方便后面排查是谁在调API Key 管理https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentspringai_mcp_java如果你还没决定用哪个模型可以先在模型对话页面试几条 prompt确认返回格式和延迟符合预期再写进配置模型对话体验https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentspringai_mcp_java2.3 项目结构建议我习惯把 AI 相关的东西单独放一个包避免和业务代码混在一起com.example.demo ├── ai │ ├── config // ChatClient、MCP 配置 │ ├── tool // MCP 工具声明 │ └── resource // MCP Apps 的 UI 资源 ├── service // 业务 Service被工具调用 └── DemoApplication.java这样后面加工具、改模型改动范围可控。MCP 工具本质上就是 Spring BeanTool标注的方法会被框架扫描并注册到 MCP Server 上模型在对话中决定要不要调用。3. 可复制配置application.yml 与 MCP 工具声明这一节是全文的核心所有片段都可以直接复制到你的项目里改。3.1 application.yml 里的 Base URL 与 KeySpring AI 的 OpenAI starter 读取spring.ai.openai前缀。把base-url指向 TaoToken 的 API 地址api-key用环境变量注入避免硬编码进 Gitspring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: ${TAOTOKEN_CHAT_MODEL:gpt-4o-mini} temperature: 0.7 embedding: options: model: ${TAOTOKEN_EMBED_MODEL:text-embedding-3-small} mcp: server: name: demo-mcp-server version: 1.0.0 protocol: STREAMABLE几个关键点base-url结尾不要带/v1Spring AI 的 OpenAI 客户端会自己拼路径。如果你写成https://taotoken.net/api/v1请求会变成/api/v1/v1/chat/completions直接 404。api-key用${TAOTOKEN_API_KEY}占位本地开发在 IDE 的 Run Configuration 里配环境变量生产环境用 K8s Secret 或配置中心注入。这样 Key 不会进代码仓库。model也做成可覆盖的默认给一个便宜的对话模型需要换模型时改环境变量即可不用动 yml。mcp.server.protocol用STREAMABLE这是 MCP 的流式传输协议Spring AI 2.0 里对它的支持比较完整。如果你用老版本的 SSE部分客户端会连不上。3.2 声明一个 MCP 工具下面这个例子模拟“查询订单状态”的工具业务 Service 先写好Service public class OrderService { public OrderStatus queryStatus(String orderId) { // 真实项目里查数据库 return new OrderStatus(orderId, SHIPPED, LocalDateTime.now()); } }然后声明 MCP 工具。注意McpServer标在类上Tool标在方法上description会作为工具描述发给模型写清楚一点模型才知道什么时候调Component McpServer public class OrderMcpTools { private final OrderService orderService; public OrderMcpTools(OrderService orderService) { this.orderService orderService; } Tool(name queryOrderStatus, description 根据订单号查询订单当前状态返回状态码和更新时间) public OrderStatus queryOrderStatus( ToolParam(description 订单号例如 ORD-20240501-001) String orderId) { return orderService.queryStatus(orderId); } }ToolParam的 description 同样重要模型靠它理解参数含义。参数名和类型也要清晰别用String a这种。3.3 配置 ChatClient 并注册工具Spring AI 2.0 里 ChatClient 通过 Builder 构建工具可以全局注册也可以在单次调用时挂载Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder, OrderMcpTools orderMcpTools) { return builder .defaultSystem(你是一个电商后台助手可以查询订单状态。) .defaultTools(orderMcpTools) .build(); } }defaultTools会把工具注册到每次对话里模型根据用户问题决定是否调用。如果你工具很多建议按场景拆分多个 ChatClient避免一次塞太多工具导致模型选择困难。3.4 如果要用 MCP Apps 返回 UIMCP Apps 允许工具返回交互式界面。在 Spring AI 2.0 里通过ToolMetadata指定 UI 资源Tool(name showOrderDashboard, description 展示订单看板) ToolMetadata(ui UiResource(uri ui://order-dashboard)) public String showOrderDashboard() { return dashboard-opened; } Resource(uri ui://order-dashboard) public String orderDashboardHtml() { return !DOCTYPE html html head meta charsetutf-8 script srchttps://cdn.jsdelivr.net/npm/chart.js/script /head body canvas idorderChart/canvas script // 通过 MCP 调用工具拿数据后渲染 /script /body /html ; }资源 URI 必须用ui://协议写成http://或文件路径都不会被识别。如果 UI 里要加载外部 CDN记得配 CSP否则 sandboxed iframe 会拦掉ToolMetadata( ui UiResource( uri ui://order-dashboard, csp Csp( scriptSrc {self, https://cdn.jsdelivr.net}, styleSrc {self, unsafe-inline} ) ) )CSP 这块是踩坑重灾区后面第 5 节会展开。4. 验证请求跑通一次端到端 MCP 工具调用配置写完怎么确认真的通了分两步先验证模型通道再验证 MCP 工具调用。4.1 先用一个 REST 接口验证模型通道写一个最简单的 Controller确认 Base URL 和 Key 没问题RestController RequestMapping(/ai) public class AiController { private final ChatClient chatClient; public AiController(ChatClient chatClient) { this.chatClient chatClient; } GetMapping(/ping) public String ping(RequestParam String q) { return chatClient.prompt() .user(q) .call() .content(); } }启动项目用 curl 打一下curl http://localhost:8080/ai/ping?q你好用一句话介绍你自己如果返回一段正常的模型回复说明base-url、api-key、model三件套都对。如果报 401先检查环境变量有没有注入成功如果报连接超时检查base-url是不是写成了带/v1的地址。4.2 验证 MCP 工具调用工具调用的验证要稍微绕一点因为模型是否调用工具取决于它的判断。最稳的办法是给一个明确会触发工具的 promptcurl http://localhost:8080/ai/ping?q帮我查一下订单 ORD-20240501-001 现在什么状态预期结果是模型返回类似“订单 ORD-20240501-001 当前状态为 SHIPPED更新时间……”的内容。这背后发生的事是Spring AI 把queryOrderStatus的工具描述发给模型模型判断需要调用工具返回 tool_callSpring AI 执行你的 Java 方法拿到OrderStatus把结果回传给模型模型生成自然语言回复。如果你想确认工具真的被调用了在OrderService.queryStatus里打一行日志public OrderStatus queryStatus(String orderId) { log.info(MCP tool invoked, orderId{}, orderId); return new OrderStatus(orderId, SHIPPED, LocalDateTime.now()); }看到日志输出就说明端到端链路通了。4.3 用 MCP Inspector 单独验证 Server如果你想把 MCP Server 和模型解耦开验证可以用 MCP Inspector 这类客户端工具直连你的 Server。Spring AI 2.0 的 MCP Server 默认暴露 streamable 端点启动后日志里会打印监听地址。用 Inspector 连上后能看到注册的工具列表直接调用queryOrderStatus不经过模型。这一步的好处是当工具调用失败时你能快速判断是模型没选对工具还是工具本身执行报错。4.4 切换模型验证统一出口前面说过TaoToken 的价值在于一个出口覆盖多个模型。验证方式很简单改环境变量重启export TAOTOKEN_CHAT_MODEL另一个模型ID再打一次/ai/ping如果返回正常说明模型切换不需要改任何代码和 Base URL。这就是统一出口的意义——Key 和地址收敛模型 ID 变成配置项。5. 本篇常见报错排查401、local proxy failed 与 choices 解析这一节按真实报错来都是我或身边同事遇到过的。5.1 401 Unauthorized最常见的 401 有两种原因。一是环境变量没生效${TAOTOKEN_API_KEY}解析成了空字符串请求头里Authorization: Bearer后面是空的。排查方法是在启动日志里打印一下配置注意别把完整 Key 打出来PostConstruct public void checkConfig() { log.info(base-url configured: {}, baseUrl); log.info(api-key present: {}, apiKey ! null !apiKey.isBlank()); }二是 Key 本身失效或被删。去控制台确认 Key 状态必要时重新生成一把。5.2 local proxy failed 或连接被拒这个报错通常出现在你本地配了某些网络工具或者公司网络有出口限制。Spring AI 的 HTTP 客户端会走 JVM 的代理设置如果代理配置不对就会报local proxy failed或Connection refused。排查顺序检查 JVM 启动参数里有没有-Dhttp.proxyHost之类的设置检查系统环境变量HTTP_PROXY、HTTPS_PROXY用 curl 直接打https://taotoken.net/api看能不能通。如果 curl 能通但 Java 不通基本就是 JVM 代理配置的问题清掉相关参数即可。5.3 解析 choices 失败 / reading choices 报错完整报错类似Error while extracting response for type ... reading choices。这通常意味着返回的 JSON 结构和 Spring AI 期望的不一致。可能原因base-url写错请求打到了某个返回 HTML 的地址解析自然失败模型 ID 不存在通道返回了错误结构请求路径被重复拼接比如/api/v1/v1/chat/completions。排查方法打开 Spring AI 的 debug 日志看实际请求的 URL 和返回体logging: level: org.springframework.ai: DEBUG org.springframework.web.client: DEBUG看到实际 URL 后对照base-url配置基本一眼能定位。5.4 MCP 工具没被调用模型回复了文字但没有触发工具。先确认工具描述是否清晰description太模糊模型不会选。其次确认defaultTools有没有真的注册上可以在启动日志里看 MCP Server 注册的工具列表。还有一个容易忽略的点如果你用的是流式调用.stream()部分版本对工具调用的支持不完整先用.call()验证。5.5 OAuth 相关报错如果你在 MCP Server 上开了 OAuth 保护客户端连接时会报 OAuth 相关错误。本地验证阶段建议先关掉鉴权把链路跑通再加。Spring AI 2.0 的 MCP Server 配置里可以显式关闭mcp: server: auth: enabled: false生产环境再按需开启并配好 token 校验。5.6 三件套对照表出现任何连接类问题先对照这张表检查配置项正确值常见错误Base URLhttps://taotoken.net/api多写/v1、写成首页地址API Key控制台生成的 Key环境变量注入硬编码、Key 失效、变量名拼错Model ID模型列表里的准确 ID拼写错误、用了不存在的模型Base URL、Key、Model ID 这三件套对齐90% 的连接问题都能解决。6. 把链路收进配置长期编码与 Agent 场景的下一步本地跑通只是第一步。真正上项目之后你会遇到更多场景多个环境共用一套代码、团队里每个人都要本地调试、CI 里要跑集成测试。这时候统一出口的价值会更明显——Base URL 和 Key 收敛成环境变量模型 ID 按环境覆盖代码里不出现任何供应商相关的硬编码。如果你的项目开始往 Agent 方向走比如让模型连续调用多个工具完成一个任务建议把 Coding Plan 这类长期方案纳入考虑按用量规划比临时申请 Key 更可控Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentspringai_mcp_java接入文档里有各语言客户端的完整示例Java 侧如果遇到 starter 版本兼容问题可以对照文档里的依赖矩阵排查接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentspringai_mcp_java最后给一个实用建议把application.yml里的模型 ID 全部做成环境变量本地用便宜的模型调试生产用能力更强的模型。这样团队里新人拉下代码配好 Key 就能跑不用问任何人“这个模型 ID 填什么”。链路跑通之后剩下的就是往工具里加业务逻辑了。