ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java开发实践MCP:用Spring Boot把Function Call接进LLM工作流

2026/10/7 19:54:31 拓冰建站 浏览量
Java开发实践MCP:用Spring Boot把Function Call接进LLM工作流 1. Java 后端接 MCP 的真实痛点Function Call 写三遍模型换一家全白干如果你是一名 Java 后端最近大概率被两件事同时夹击一边是产品经理催着「给系统加个 AI 助手能查订单、能算报价」另一边是你打开文档发现 Function Call 的定义方式每家模型都不一样。OpenAI 一套 JSON SchemaClaude 一套 tool_use 结构国产模型又是另一套字段名。你辛辛苦苦在 Spring Boot 里写好的工具方法换一个模型供应商就得重写一遍适配层测试用例全部推倒重来。MCPModel Context Protocol模型上下文协议要解决的就是这个事。你可以把它理解成「工具调用的 USB-C 标准」以前每个模型厂商自己定接口现在大家约定一套协议你的 Java 服务只要按 MCP 规范暴露工具任何支持 MCP 的客户端都能直接调用。对 Java 开发者来说最直接的收益是——工具逻辑只写一次模型侧通过统一通道接入换模型不用动业务代码。这篇文章面向的是有 Spring Boot 基础、想把 Function Call 真正接进 LLM 工作流的后端开发者。我会带你从零搭一个 MCP Server把计算器工具注册进去再用 MCP Client 打通调用链最后通过 TaoToken 统一 Key 和 API 通道完成模型侧接入与验证。全程可复制命令和配置都给你踩过的坑我也会标出来。核心检索词先明确Java MCP 服务端开发、Spring Boot Function Call 接入 LLM、MCP Client 本地联调。这三个词贯穿全文你跟着做就能跑通。2. TaoToken 前置准备统一 Key 与 API 通道别让模型接入拖后腿在写代码之前先把模型侧的通道理顺。很多 Java 开发者卡在第一步不是不会写 Spring Boot而是不知道该用哪家的 API、Key 怎么管、base-url 填什么。我的建议是工具逻辑归工具逻辑模型接入归模型接入两者解耦。TaoToken 在这里扮演的就是「统一通道」的角色——你拿一个 Key配一个 base-url后面换模型只改 model 字段不用动 MCP 那套代码。先说清楚它是什么TaoToken 提供统一的 API 通道兼容 OpenAI 风格的接口协议。对 Spring Boot 项目来说这意味着你可以直接用spring-ai-starter-model-openai这个 starter把base-url指向 TaoToken 的 API 地址api-key填你申请的 Key就能调用背后的模型。适合谁适合不想在多个模型厂商之间反复注册、反复改配置的后端团队尤其是做原型验证和内部工具的阶段。具体操作路径第一步访问官网了解通道能力地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 进去后先看文档里支持的模型列表和接口格式。第二步进控制台创建 API Key。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建后把 Key 复制出来注意别提交到 Git后面我们用环境变量注入。第三步如果你只是想先验证模型通不通可以用模型对话页面直接试https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。输入一句话看有没有正常返回确认 Key 有效。第四步API Key 管理页可以随时查看和轮换https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。这里有个关键点TaoToken 的 API 地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数配置到base-url里就用这个。很多同学把带参数的链接填进去结果请求 404就是这里出的问题。注意Key 只创建一次就够不要每个环境建一个。用 Spring 的 profile 或者环境变量区分 dev/prodKey 本身复用。如果你后面要做长期编码或者 Agent 类项目可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。不过本文的重点是 MCP 联调先用按量调用验证链路即可。前置准备做完你手里应该有三样东西一个可用的 API Key、base-url 是https://taotoken.net/api、以及确认模型能正常返回。接下来进入编码环节。3. 可复制配置Spring Boot 搭 MCP Server把计算器工具注册进去这一节是全文技术核心篇幅会重一些。我们分两步先写 MCP Server暴露工具再写 MCP Client调用工具。环境要求 JDK 17、Spring Boot 3.3 及以上这是 Spring AI MCP 的硬性门槛低于这个版本会缺类。3.1 MCP Server 依赖与工具定义新建一个 Spring Boot 项目pom.xml里加核心依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp/artifactId /dependency如果你用的是 Spring AI 的 BOM 管理版本记得在dependencyManagement里引入对应的 BOM否则版本号对不上会报NoClassDefFoundError。然后写工具类。这里定义一个计算器工具用Tool注解暴露给 LLMimport org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; public class ToolService { Tool(name calculator, description 简单的计算器工具支持加减乘除) public String calculator( ToolParam(description 第一个数字 a) Double a, ToolParam(description 第二个数字 b) Double b, ToolParam(description 计算操作符支持 - * /) String operation) { double result switch (operation) { case - a b; case - - a - b; case * - a * b; case / - a / b; default - throw new IllegalArgumentException(未知操作: operation); }; return String.format(计算结果: %s %s %s %s, a, operation, b, result); } }注意ToolParam里的 description 不是写给人看的是写给模型看的。描述越清楚模型选工具、填参数越准。我试过把 description 写成「参数1」「参数2」模型经常把 a 和 b 填反改成「第一个数字」「第二个数字」之后就正常了。3.2 注册工具并打包在启动类或者配置类里注册ToolCallbackProviderimport 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 McpConfig { Bean public ToolCallbackProvider calculatorTools(ToolService toolService) { return MethodToolCallbackProvider.builder() .toolObjects(toolService) .build(); } }打包命令mvn clean package -DskipTests打包完成后你会得到一个 jar比如target/spring-mcp-server.jar。这个 jar 后面要用 stdio 模式启动所以启动参数里要关掉 Web 容器和 banner否则控制台输出会污染 stdio 通道导致 MCP 握手失败。3.3 MCP Client 依赖与 application.yaml再建一个项目作为 Client依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency /dependenciesapplication.yaml配置这里就是 TaoToken 统一通道发挥作用的地方spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: deepseek-chat mcp: client: enabled: true name: demo-client version: 1.0.0 type: sync request-timeout: 20s stdio: servers-configuration: classpath:/mcp-server-config.json logging: level: io.modelcontextprotocol: debug三件套对齐一下Base URL 是https://taotoken.net/apiKey 用环境变量TAOTOKEN_API_KEY注入Model ID 这里填deepseek-chat你可以按文档换成其他支持的模型。这三个字段是接入的核心缺一个都跑不通。3.4 mcp-server-config.json 配置在src/main/resources下建mcp-server-config.json{ mcpServers: { demo: { command: java, args: [ -Dspring.ai.mcp.server.stdiotrue, -Dspring.main.banner-modeoff, -Dspring.main.web-application-typenone, -Dserver.port7002, -Dlogging.pattern.console, -jar, /你的路径/spring-mcp-server/target/spring-mcp-server.jar ], env: {} } } }路径要换成你本机打包出来的绝对路径。Windows 用户注意command可能要写成cmdargs前面加/c这是 stdio 启动方式在 Windows 上的差异很容易踩坑。3.5 启动类与调用链import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import io.modelcontextprotocol.client.McpSyncClient; import java.util.List; import java.util.Scanner; SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } Bean public CommandLineRunner predefinedQuestions( ChatClient.Builder chatClientBuilder, ListMcpSyncClient mcpSyncClients) { return args - { var chatClient chatClientBuilder .defaultSystem( 你是个人小助手。 如果用户问计算问题请使用 demo 相关工具回答 如果问题与计算无关提醒用户问题不在处理范围内。 ) .defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients)) .build(); for (McpSyncClient client : mcpSyncClients) { System.out.println(已连接 MCP Server: client.getServerInfo().name()); } try (Scanner scanner new Scanner(System.in)) { while (true) { System.out.print(\n用户: ); String input scanner.nextLine(); System.out.println(\nASSISTANT: chatClient.prompt(input).call().content()); } } }; } }到这里配置部分就完整了。你可以看到MCP Server 负责暴露工具MCP Client 负责连接 Server 并把工具注册给 ChatClient模型侧通过 TaoToken 统一通道调用。三层解耦换模型只改 yaml 里的 model 字段。4. 验证请求本地联调跑通计算器调用链配置写完接下来是验证。这一步别跳过很多问题都是启动阶段暴露的。先启动 MCP Server 的 jar确认它能独立跑起来java -Dspring.ai.mcp.server.stdiotrue \ -Dspring.main.banner-modeoff \ -Dspring.main.web-application-typenone \ -Dlogging.pattern.console \ -jar target/spring-mcp-server.jar如果控制台没有任何输出别慌这是正常的——stdio 模式下我们把日志关掉了就是为了不污染通道。你可以临时把-Dlogging.pattern.console去掉看到启动日志后再加回来。然后启动 MCP Client 项目。启动日志里应该能看到类似这样的输出已连接 MCP Server: demo这说明 Client 成功通过 stdio 连上了 Server并且拿到了 Server 的工具列表。如果这一步没打印出来说明mcp-server-config.json里的路径不对或者 jar 启动失败。接着在控制台输入测试问题用户: 帮我算一下 128 乘以 47 等于多少预期返回ASSISTANT: 128 乘以 47 等于 6016。如果模型返回的是「我无法计算」或者直接给了一个错误答案说明工具没有被正确注册。这时候打开 debug 日志看io.modelcontextprotocol包下有没有工具列表的打印。正常情况下你会看到calculator这个工具被注册进去了。再测一个边界情况用户: 今天天气怎么样预期返回ASSISTANT: 问题不在处理范围内。我目前可以帮你做计算相关的操作。这个测试是为了验证 system prompt 里的能力边界有没有生效。如果模型硬答天气说明 system prompt 没起作用检查defaultSystem有没有正确设置。验证模型侧通道是否走通还有一个简单办法把base-url临时改错看请求是否报错。如果报 401 或者连接失败说明确实在走 TaoToken 通道如果还能正常返回说明你本地有缓存或者配置没生效。验证完记得改回来。实测下来整个链路跑通后从输入问题到返回结果大概 2-4 秒取决于模型响应速度。工具调用本身的开销很小主要耗时在模型推理。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把我踩过的坑和社区里高频报错整理出来对照着排查能省不少时间。报错一401 Unauthorized这是最常见的。原因通常是 Key 没注入或者注入错了。检查application.yaml里的api-key: ${TAOTOKEN_API_KEY}确认环境变量真的设置了。在 IDEA 里跑的话Run Configuration 的 Environment variables 里要手动加。命令行跑的话export TAOTOKEN_API_KEY你的Key之后再启动。还有一种情况是 Key 复制时带了空格肉眼看不出来建议重新复制一次。报错二local proxy failed / Connection refused这个报错通常出现在 MCP Client 连不上 Server 的时候。检查mcp-server-config.json里的 jar 路径是不是绝对路径Windows 下路径分隔符要不要转义。另外确认 Server 的 jar 能独立启动如果 Server 本身启动就报错Client 自然连不上。端口冲突也会导致这个问题-Dserver.port7002换一个没被占用的端口试试。报错三reading choices / choices is null这个报错说明请求发出去了但返回体里没有choices字段。常见原因是base-url填错了比如填成了带 UTM 参数的链接或者填成了官网首页。正确地址是https://taotoken.net/api注意结尾没有斜杠。还有一种可能是 model 字段填了一个不支持的模型名返回体结构不一样。对照文档确认模型 ID。报错四OAuth / authentication failed如果你在配置里误开了某些需要 OAuth 的选项会报这个。MCP Client 的 stdio 模式不需要 OAuth检查application.yaml里有没有多余的认证配置。另外 Spring AI 某些版本对spring.ai.mcp.client.type的取值敏感sync和async要写对。报错五NoClassDefFoundError: org/springframework/ai/mcp/...这是版本不匹配。Spring AI MCP 的版本要和 Spring Boot 3.3 对齐BOM 没引入或者版本号写错都会报这个。检查pom.xml里的 BOM 配置确保spring-ai-mcp和spring-ai-starter-mcp-client版本一致。报错六工具被调用但参数为空模型选了工具但参数没填进去。检查ToolParam的 description 是否清晰参数类型是否用了包装类型Double而不是double基本类型在某些版本下会导致参数解析失败。注意排查顺序建议从外到内——先确认 Key 和 base-url 对再确认 MCP Server 能独立启动最后看工具注册日志。大部分问题出在前两步。如果你用的是 Claude Code 或者 Cline 这类客户端接 MCP配置格式和上面类似但字段名可能不同。Cline 的 MCP 配置里transportType要写stdioClaude Code 的配置在 settings 里。核心三件套不变Base URL、Key、Model ID。6. 继续往下走把 MCP 接进真实业务与统一通道收尾跑通计算器只是起点。真实业务里你会把订单查询、库存校验、报价计算这些方法都用Tool暴露出去让 LLM 通过 MCP 调用。这时候有几个实践建议。第一工具粒度别太细。一个「查询订单」工具比「查订单号」「查订单状态」「查订单金额」三个工具更好用模型选择成本低参数也少。第二工具返回值要结构化别返回一大段自然语言模型解析起来费劲。第三给工具加超时和降级LLM 调用工具失败时要有兜底不能让整个对话卡死。模型侧的统一通道建议就用 TaoToken 这一套。Base URL 固定https://taotoken.net/apiKey 走环境变量Model ID 按需切换。这样你的 MCP Server 代码完全不用感知模型变化换模型只改一行配置。需要验证新模型时去模型对话页面快速试一下https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到字段问题先查文档。如果你后面要做长期编码助手或者 Agent 工作流Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 适合需要稳定调用额度的场景。最后说一个我踩过的坑MCP Server 打包成 jar 后如果依赖里有 Web 容器一定要在启动参数里关掉web-application-typenone否则 stdio 通道会被 HTTP 服务的日志干扰表现为 Client 连上了但工具列表为空。这个坑排查了我一个下午希望你别再踩。代码写到这里链路已经完整。你可以把计算器换成任何业务方法MCP 那层不用动。这就是协议标准化的价值——工具归工具模型归模型中间用统一通道连起来。