1. 从一次“反向请求”说起:MCP Sampling Client 到底解决什么问题
如果你已经用过 Spring AI 的 MCP 工具调用,会发现一个固定套路:客户端把工具清单交给模型,模型决定调用哪个工具,客户端执行工具再把结果回传。整个链路里,LLM 是“大脑”,MCP 服务器是“手脚”。
但 Sampling 把这个方向反过来了。MCP 服务器在运行过程中,可以主动向客户端发起一个“帮我生成一段文本”的请求,客户端再去调用真正的 LLM,把结果回传给服务器。也就是说,服务器不再只是被动执行工具,它也能借用客户端的模型能力。
这个能力在实际项目里非常有用。比如你写了一个 MCP 天气服务器,它拿到原始气象数据后,想直接生成一段人类可读的天气描述甚至一首诗,但服务器本身不持有任何模型 Key。这时候 Sampling 就派上用场了:服务器发出 sampling 请求,客户端负责路由到具体 LLM,生成结果后返回。
Spring AI MCP Sampling Client 就是这套机制的客户端实现。它适合谁?适合正在用 Spring Boot 3.x 构建 AI 应用、已经接触过 MCP 工具调用、想进一步理解“服务端反向请求 LLM”这条链路的开发者。本文会给出可复制的application.yml、Sampling 回调实现、TaoToken 统一 Key 配置,并用一次端到端调用验证整条链路是否打通。
我试过把这套流程跑通,中间踩过几个坑,后面会逐个拆开讲。先明确一点:Sampling 的核心不是“多模型路由”本身,而是“服务器请求 → 客户端回调 → LLM 生成 → 结果回传”这个闭环。理解了闭环,配置就只是填空题。
2. TaoToken 前置准备:统一 Key 与 API 通道配置
在写 Sampling 回调之前,得先解决“客户端拿什么去调 LLM”的问题。传统做法是给 OpenAI 配一个 Key、给 Anthropic 配一个 Key,环境变量一堆,切换模型还要改配置。这里我们用 TaoToken 做统一入口,一个 Key 走通多个模型通道。
TaoToken 的定位是统一的模型 API 通道,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。它的 API 基址是 https://taotoken.net/api ,注意这个地址后面不加任何查询参数,直接作为 Base URL 使用。
你需要先拿到一个 API Key。登录后进入控制台,在 API Keys 页面创建一个新 Key,复制出来备用。这个 Key 就是后面application.yml里要填的值。控制台入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
拿到 Key 之后,先别急着写 Java 代码,用一条 curl 验证通道是否可用。这一步很关键,因为后面 Sampling 回调如果报 401,你至少能确定是 Key 问题还是代码问题。
curl https://taotoken.net/api/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的Key" \ -d '{ "model": "gpt-4o-mini", "messages": [ {"role": "user", "content": "用一句话说明什么是 MCP Sampling"} ] }'如果返回里能看到choices数组和正常的content,说明通道没问题。如果返回 401,检查 Key 是否复制完整、有没有多余空格。如果返回model not found,说明模型 ID 写错了,换成通道支持的模型名再试。
这里有个细节:TaoToken 的 Base URL 是https://taotoken.net/api,而 OpenAI 兼容接口的完整路径是/v1/chat/completions。所以在 Spring AI 配置里,base-url填https://taotoken.net/api,Spring AI 会自动拼接/v1/chat/completions。不要手动把/v1写进 base-url,否则会变成/api/v1/v1/...。
另外,如果你打算同时验证多个模型,可以在控制台里确认哪些模型 ID 可用。常见的有gpt-4o-mini、claude-3-5-sonnet这类。模型 ID 要和后面 Sampling 回调里的modelHint对应上,否则路由会找不到对应的 ChatClient。
3. 可复制配置:application.yml 与 Sampling 回调实现
这一节是全文的核心,所有配置都可以直接复制。先看pom.xml的依赖,Spring Boot 版本用 3.4.5,Spring AI 用 1.1.0 的 BOM。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.5</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> </dependencies>这里只保留 OpenAI 一个 starter,因为 TaoToken 走的是 OpenAI 兼容协议,一个 starter 就能覆盖多个模型。如果你确实要接 Anthropic 原生协议,再加spring-ai-starter-model-anthropic,但本文用统一通道,不需要。
接下来是application.yml。注意我用的是 YAML 而不是 properties,因为嵌套结构更清晰。
spring: application: name: mcp-sampling-client main: web-application-type: none ai: chat: client: enabled: false openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7 mcp: client: toolcallback: enabled: false sse: connections: weather-server: url: http://localhost:8080 logging: level: io.modelcontextprotocol.client: WARN io.modelcontextprotocol.spec: WARN几个关键点。第一,spring.ai.chat.client.enabled=false必须关掉,因为我们要手动管理多个 ChatClient,自动配置会干扰。第二,base-url填https://taotoken.net/api,api-key用环境变量TAOTOKEN_API_KEY注入,不要把 Key 硬编码进文件。第三,toolcallback.enabled=false是因为本文聚焦 Sampling,不混入工具回调逻辑。第四,SSE 连接指向本地 8080 的 MCP 服务器,这个服务器后面要单独启动。
然后是 Sampling 回调的实现。核心是注册一个McpSyncClientCustomizer,在sampling方法里处理服务器发来的请求。
@Bean McpSyncClientCustomizer samplingCustomizer(Map<String, ChatClient> chatClients) { return (name, spec) -> { spec.sampling(llmRequest -> { var userPrompt = ((McpSchema.TextContent) llmRequest.messages().get(0).content()).text(); String modelHint = llmRequest.modelPreferences().hints().get(0).name(); ChatClient hintedChatClient = chatClients.entrySet().stream() .filter(e -> e.getKey().contains(modelHint)) .findFirst() .orElseThrow(() -> new IllegalStateException("no chat client for hint: " + modelHint)) .getValue(); String response = hintedChatClient.prompt() .system(llmRequest.systemPrompt()) .user(userPrompt) .call() .content(); return McpSchema.CreateMessageResult.builder() .content(new McpSchema.TextContent(response)) .build(); }); }; }这段代码的逻辑是:从请求里取出用户提示词和模型偏好提示,根据提示找到对应的 ChatClient,调用模型生成结果,再包装成CreateMessageResult返回。modelHint是服务器指定的,比如服务器说“这次用 gpt-4o-mini”,客户端就路由到对应的客户端。
ChatClient 的注册用下面这个 Bean:
@Bean public Map<String, ChatClient> chatClients(List<ChatModel> chatModels) { return chatModels.stream().collect(Collectors.toMap( model -> model.getClass().getSimpleName().toLowerCase(), model -> ChatClient.builder(model).build() )); }这里 key 是模型类名的小写形式,比如openaichatmodel。所以modelHint里如果写openai,contains判断就能命中。如果你有多个模型,key 会各自不同,路由就靠这个匹配。
主类里再加一个CommandLineRunner做端到端触发:
@Bean public CommandLineRunner run(OpenAiChatModel chatModel, List<McpSyncClient> mcpClients) { return args -> { var toolProvider = new SyncMcpToolCallbackProvider(mcpClients); ChatClient chatClient = ChatClient.builder(chatModel) .defaultToolCallbacks(toolProvider) .build(); String question = "What is the weather in Amsterdam right now?"; System.out.println("> USER: " + question); System.out.println("> ASSISTANT: " + chatClient.prompt(question).call().content()); }; }注意这里defaultToolCallbacks把 MCP 工具挂上了,所以模型在需要时会调用天气工具,工具执行过程中服务器再发起 Sampling 请求,形成完整闭环。
4. 验证请求:端到端跑通 Sampling 链路
配置写完,接下来验证。分三步:启动 MCP 服务器、设置环境变量、运行客户端。
第一步,启动 MCP 天气服务器。假设你已经有一个基于 Spring Boot 的 MCP 服务器项目,在它目录下执行:
./mvnw clean install -DskipTests java -jar target/mcp-weather-server-0.0.1-SNAPSHOT.jar服务器启动后会监听 8080,提供 SSE 端点。你可以在浏览器或 curl 里访问http://localhost:8080/sse确认它活着。
第二步,设置环境变量。把 TaoToken 的 Key 注入进去:
export TAOTOKEN_API_KEY=sk-你的Key如果你在 Windows 上用 PowerShell,换成$env:TAOTOKEN_API_KEY="sk-你的Key"。
第三步,运行客户端:
./mvnw clean install java -jar target/mcp-sampling-client-0.0.1-SNAPSHOT.jar预期输出会先打印用户问题,然后打印助手回答。回答里应该包含天气数据,以及服务器通过 Sampling 生成的描述文本。如果你在服务器端加了日志,还能看到 Sampling 请求的进出记录。
判断链路是否真的通了,看三个信号。第一,客户端日志里出现MCP LOGGING开头的行,说明 SSE 连接建立成功。第二,服务器端日志里出现 sampling 请求记录,说明服务器确实发起了反向请求。第三,客户端返回的文本里包含模型生成的内容,而不是空字符串或报错。
如果只看到用户问题、没有助手回答,大概率是 Sampling 回调没注册上,或者modelHint匹配失败抛了异常。这时候把日志级别调到 DEBUG,看io.modelcontextprotocol包下的输出。
验证模型通道是否正常,可以单独打开模型对话页面发一条消息,确认 Key 和模型 ID 都对。模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。如果那边能正常返回,说明通道没问题,问题就出在 Spring AI 配置或代码上。
5. 常见报错排查:401、local proxy failed 与 choices 为空
这一节列几个真实会遇到的报错,以及对应的排查路径。
报错一:401 Unauthorized。这是最常见的。原因通常是 Key 没注入、Key 复制不完整、或者base-url写错。先检查环境变量TAOTOKEN_API_KEY是否真的存在,用echo $TAOTOKEN_API_KEY确认。然后检查application.yml里api-key的占位符拼写是否和变量名一致。最后确认base-url是https://taotoken.net/api,没有多余斜杠或/v1后缀。
报错二:local proxy failed 或 connection refused。这个通常出现在 MCP 服务器没启动、或者 SSE 地址写错的时候。检查spring.ai.mcp.client.sse.connections.weather-server.url是否指向正确的http://localhost:8080。如果服务器换了端口,这里要同步改。另外确认服务器确实暴露了 SSE 端点,有些服务器默认只开 stdio,不开 SSE。
报错三:reading choices 时返回空或 NPE。这说明请求发出去了,但响应体里没有choices字段。常见原因是模型 ID 写错,通道返回了错误信息而不是正常补全结果。把spring.ai.openai.chat.options.model换成通道确认支持的模型 ID,比如gpt-4o-mini。如果还是不行,用第 2 节的 curl 命令单独测一次,对比返回结构。
报错四:OAuth 相关错误。如果你在配置里误加了 OAuth 相关参数,或者用了需要 OAuth 的端点,会看到这类报错。TaoToken 的 API 通道用 Bearer Token 即可,不需要 OAuth 流程。检查配置里有没有多余的client-id、client-secret字段,删掉即可。
报错五:Sampling 回调抛 IllegalStateException。错误信息是no chat client for hint: xxx。这说明服务器发来的modelHint和客户端注册的 ChatClient key 对不上。检查chatClients的 key 生成逻辑,确认modelHint里包含的字符串能匹配到某个 key。比如 key 是openaichatmodel,hint 写openai就能命中;如果 hint 写gpt,就匹配不上。
排查时有个通用技巧:把logging.level.io.modelcontextprotocol.client和logging.level.io.modelcontextprotocol.spec都设成 DEBUG,能看到完整的请求和响应报文。另外,Spring AI 的OpenAiChatModel在启动时会打印实际使用的 base-url,确认它是不是https://taotoken.net/api。
如果你在 Coding Plan 或 Agent 场景里长期跑这套链路,建议把 Key 管理、模型路由、重试逻辑都收敛到配置层,不要散落在代码里。Coding Plan 入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合需要稳定通道和统一计费的场景。
6. 继续往下走:接入文档与下一步动作
整条链路跑通后,你会发现 Sampling 的本质是“把模型调用能力从服务器侧转移到客户端侧”。服务器不需要持有 Key,只需要声明“我要生成一段文本,偏好某个模型”,客户端负责落地。这种解耦在多租户、多模型、多环境的项目里特别有价值。
下一步可以做的几件事。第一,把modelHint的路由逻辑做得更细,比如按任务类型、成本、延迟来选择模型,而不是简单字符串匹配。第二,给 Sampling 回调加缓存,相同提示词直接返回缓存结果,减少重复调用。第三,加降级逻辑,某个模型通道不可用时自动切到备用模型。
如果你要接更多模型,或者想把 Key 管理、用量统计、模型切换都统一起来,可以看接入文档,里面有完整的 Base URL、鉴权方式和模型列表说明。文档入口在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。API Keys 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
最后提醒一个实操细节:Spring AI 的版本迭代比较快,McpSyncClientCustomizer的包路径和sampling方法签名在不同版本里可能有差异。如果你用的不是 1.1.0,先确认对应版本的 API 签名,再套用本文代码。跑通一次之后,把配置和代码固化下来,后面换模型只需要改model字段和modelHint,不用动主逻辑。