拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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 把 MCP(Model 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 和 starter:

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>2.0.0-M4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> </dependencies>

注意spring-ai-starter-mcp-server这个 starter,M4 之后 MCP Server 能力已经并入核心,不需要再单独引老版本的spring-ai-mcp。

2.2 为什么用 TaoToken 做统一出口

Spring AI 默认的 OpenAI starter 会把请求打到官方地址,但实际项目里你往往需要:

  • 一个 Key 覆盖多个模型,不用为每个供应商单独申请;
  • Base URL 可配置,本地、测试、生产用不同通道;
  • 模型 ID 通过配置切换,不改代码。

TaoToken 提供的就是这样一个兼容 OpenAI 协议的 API 通道。你拿到一把 Key,配好 Base URL,Spring AI 的 OpenAI starter 就能直接指向它。模型侧通过model参数指定,比如对话模型、代码模型各用各的 ID,但出口是同一个。

需要提前准备的东西:

  1. 一个 TaoToken 账号,在控制台创建 API Key;
  2. 记下 Base URL:https://taotoken.net/api;
  3. 确认你要用的模型 ID(在模型列表里能看到)。

控制台入口在这里,创建 Key 的时候建议按环境分,比如spring-ai-dev、spring-ai-prod,方便后面排查是谁在调:

API Key 管理:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=springai_mcp_java

如果你还没决定用哪个模型,可以先在模型对话页面试几条 prompt,确认返回格式和延迟符合预期,再写进配置:

模型对话体验:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=springai_mcp_java

2.3 项目结构建议

我习惯把 AI 相关的东西单独放一个包,避免和业务代码混在一起:

com.example.demo ├── ai │ ├── config // ChatClient、MCP 配置 │ ├── tool // MCP 工具声明 │ └── resource // MCP Apps 的 UI 资源 ├── service // 业务 Service,被工具调用 └── DemoApplication.java

这样后面加工具、改模型,改动范围可控。MCP 工具本质上就是 Spring Bean,@Tool标注的方法会被框架扫描并注册到 MCP Server 上,模型在对话中决定要不要调用。

3. 可复制配置:application.yml 与 MCP 工具声明

这一节是全文的核心,所有片段都可以直接复制到你的项目里改。

3.1 application.yml 里的 Base URL 与 Key

Spring AI 的 OpenAI starter 读取spring.ai.openai前缀。把base-url指向 TaoToken 的 API 地址,api-key用环境变量注入,避免硬编码进 Git:

spring: 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结尾不要带/v1,Spring 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 返回 UI

MCP 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 charset="utf-8"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <canvas id="orderChart"></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 工具调用

工具调用的验证要稍微绕一点,因为模型是否调用工具取决于它的判断。最稳的办法是给一个明确会触发工具的 prompt:

curl "http://localhost:8080/ai/ping?q=帮我查一下订单 ORD-20240501-001 现在什么状态"

预期结果是模型返回类似“订单 ORD-20240501-001 当前状态为 SHIPPED,更新时间……”的内容。这背后发生的事是:

  1. Spring AI 把queryOrderStatus的工具描述发给模型;
  2. 模型判断需要调用工具,返回 tool_call;
  3. Spring AI 执行你的 Java 方法,拿到OrderStatus;
  4. 把结果回传给模型,模型生成自然语言回复。

如果你想确认工具真的被调用了,在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。

排查顺序:

  1. 检查 JVM 启动参数里有没有-Dhttp.proxyHost之类的设置;
  2. 检查系统环境变量HTTP_PROXY、HTTPS_PROXY;
  3. 用 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 Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=springai_mcp_java

接入文档里有各语言客户端的完整示例,Java 侧如果遇到 starter 版本兼容问题,可以对照文档里的依赖矩阵排查:

接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=springai_mcp_java

最后给一个实用建议:把application.yml里的模型 ID 全部做成环境变量,本地用便宜的模型调试,生产用能力更强的模型。这样团队里新人拉下代码,配好 Key 就能跑,不用问任何人“这个模型 ID 填什么”。链路跑通之后,剩下的就是往工具里加业务逻辑了。

返回列表