☰
SpringAI 调用 MCP 服务的实现思路:从 FunctionCallback 到 ChatClient 的 SSE 链路拆解
2026/10/8 12:24:42 网站建设 项目流程

1. 从一次线上告警说起:SpringAI 调用 MCP 服务到底卡在哪

很多 Java 后端第一次把 MCP 工具接进 SpringAI 时,都会遇到一个很迷惑的现象:日志里明明看到tools/list拉回来一堆工具,ChatClient也正常返回了文本,但模型就是死活不调用工具,或者调用了却报No FunctionCallback found for name。我试过在一个智能体项目里排查这类问题,最后发现根因往往不在模型,而在 FunctionCallback 的注册时机和 SSE 链路的连接状态上。

先把概念对齐。MCP(Model Context Protocol)本质是一套让模型发现并调用外部能力的协议,它把「工具」和「资源」用标准 JSON Schema 描述出来。SpringAI 这边负责对话编排,核心抽象是ChatClient和FunctionCallback。两者之间需要一个适配层:把 MCP 服务器暴露的远程工具,动态包装成 SpringAI 能识别的FunctionCallback,再交给ChatClient在对话中按需触发。这条链路里,SSE 承担的是客户端与 MCP 服务器之间的长连接传输,工具调用的请求和结果都从这条流上走。

所以「SpringAI 调用 MCP 服务」这件事,拆开就是三段:连接与握手、工具发现与注册、对话中的函数回调执行。适合谁看?适合已经会用 Spring Boot 写 REST 接口、现在想把本地或远程 MCP 工具接进大模型对话的 Java 后端。下面我按可复制的顺序,把配置、代码、验证和排错一次讲透。

2. 前置准备:TaoToken 接入与 MCP 客户端依赖

在写FunctionCallback之前,得先让ChatClient有一个能用的模型端点。我用 TaoToken 做统一入口,它的 API 地址是https://taotoken.net/api,兼容 OpenAI 风格的调用方式,SpringAI 的 OpenAI starter 可以直接指过去。官网在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,注册后在控制台生成 Key 即可。

依赖这块,pom.xml里至少要有 SpringAI 的 OpenAI starter 和 MCP 客户端库。版本要对齐,SpringAI 1.0 之后 MCP 的支持才比较完整:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>

拿到 Key 之后,先别急着写业务代码,用最简配置确认模型通道是通的。application.yml里把 base-url 指向 TaoToken:

spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: gpt-4o-mini

这里有个容易踩的点:base-url不要带/v1后缀,SpringAI 的 OpenAI 客户端会自己拼路径,多写一层会 404。Key 建议走环境变量,别硬编码进仓库。模型 ID 用gpt-4o-mini这类通用名即可,具体可用列表可以在模型对话页确认,地址是https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite。

MCP 客户端这边,配置里要声明一个或多个 server 实例。SSE 类型给 url,Stdio 类型给启动命令:

spring: ai: mcp: clients: weather-server: type: sse url: http://localhost:8000/sse file-server: type: stdio command: "node /path/to/my-file-server.js"

启动时,MCP 客户端会自动完成 initialize 握手,然后发tools/list和resources/list。这一步的日志观察点是Initialized MCP client和Discovered N tools,如果只看到连接成功却没有工具数量,说明握手后的 list 请求失败了,先查服务器端有没有正确实现tools/list。

3. 可复制配置:ChatClient 与 FunctionCallback 的注册片段

核心问题来了:MCP 工具怎么变成FunctionCallback并注入ChatClient。SpringAI 的 MCP starter 在启动时会为每个发现的工具动态生成FunctionCallback实例,这些实例会作为 Bean 暴露出来。你要做的是把它们收集起来,交给ChatClient。

先看ChatClient的构建。注意defaultFunctions和defaultTools在不同版本里名字有差异,1.0 之后推荐用defaultTools:

@Configuration public class ChatClientConfig { @Bean public ChatClient chatClient(ChatModel chatModel, List<FunctionCallback> mcpToolCallbacks) { return ChatClient.builder(chatModel) .defaultTools(mcpToolCallbacks.toArray(new FunctionCallback[0])) .build(); } }

这里的List<FunctionCallback>会被 Spring 自动注入所有 MCP 工具生成的回调。如果你只想启用部分工具,可以在注入后按 name 过滤,而不是全量塞进去——工具太多会撑大 prompt,模型选择准确率反而下降。

如果你需要手动注册一个本地函数做对照,可以这样写:

@Bean public FunctionCallback localEcho() { return FunctionCallback.builder() .function("local_echo", (Map<String, Object> args) -> "echo: " + args.get("text")) .description("本地回声测试函数") .inputType(Map.class) .build(); }

MCP 工具和本地函数在ChatClient眼里没有区别,都是FunctionCallback。区别在于 MCP 工具的执行体内部会通过 SSE 向远程服务器发tools/call,而本地函数直接执行 Java 逻辑。

关于 SSE 连接参数,如果服务器在弱网环境,建议在 MCP 客户端配置里加上超时和重连。部分版本支持:

spring: ai: mcp: clients: weather-server: type: sse url: http://localhost:8000/sse request-timeout: 30s

超时太短会导致工具调用还没返回就断开,日志里表现为SSE connection closed before response。这个值要大于你 MCP 工具的最长执行时间。

4. 验证一次工具调用往返:请求、日志与成功结果

配置写完,跑一次完整往返。写一个最简单的 Controller:

@RestController public class McpController { private final ChatClient chatClient; public McpController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/ask") public String ask(@RequestParam String question) { return chatClient.prompt() .user(question) .call() .content(); } }

启动应用,观察启动日志。正常顺序是:MCP 客户端连接 SSE → 发送 initialize → 收到 initialized → 发送 tools/list → 打印发现工具数量。如果这一步工具数量为 0,后面模型一定不会调用。

然后发请求:

curl "http://localhost:8080/ask?question=北京今天天气怎么样"

一次成功的工具调用往返,日志里应该能看到这几个关键点。第一,模型返回的响应里带有tool_calls,说明模型决定调用工具。第二,SpringAI 打印Executing function: get_weather,参数是模型生成的 JSON。第三,MCP 客户端向 SSE 流写入tools/call请求。第四,服务器返回结果,日志出现Function execution result。第五,模型拿到结果后生成最终自然语言回复。

如果用的是流式接口,把.call()换成.stream(),配合Flux<String>返回,SSE 链路上的 token 会逐个推给前端。注意流式模式下工具调用的中间态也会出现在流里,前端要能识别tool_calls事件并做展示,否则用户会看到一段空白等待。

验证成功的标志是:/ask返回的文本里包含了真实天气数据,而不是模型编造的。你可以故意把 MCP 服务器停掉再发一次请求,如果返回的是「工具调用失败」而不是编造答案,说明链路是真实走通的。

5. 常见报错排查:401、local proxy failed 与 reading choices

排错这块我按真实遇到的报错对照讲。

401 Unauthorized基本是 Key 问题。先确认TAOTOKEN_API_KEY环境变量真的注入了,再确认base-url没写错。如果 Key 是对的还 401,检查是不是把 Key 写进了spring.ai.mcp而不是spring.ai.openai下面——这两个配置段容易混。

local proxy failed或Connection refused出现在 MCP 客户端连接阶段,说明 SSE 地址不通。先curl http://localhost:8000/sse看服务器是否在监听。如果是 Stdio 类型报这个错,通常是command路径不对或 node 不在 PATH 里,用绝对路径。

Error reading choices一般出现在模型响应解析阶段,常见原因是 base-url 多写了/v1,或者模型 ID 在当前账号下不可用。换成gpt-4o-mini这类通用模型先验证通道。

No FunctionCallback found for name: get_weather说明模型调用了工具,但注册表里没有这个名字。检查tools/list返回的工具名和模型生成的名字是否一致,MCP 工具名有时带前缀,模型可能只取了后半段。可以在ChatClient构建时打印所有已注册的 callback name 做对照。

OAuth相关报错如果出现在 MCP 服务器侧,说明该服务器要求鉴权。SSE 类型可以在 url 里带 token,或在 header 里配置。这部分要看具体 MCP 服务器的文档,别硬猜。

还有一个隐蔽的坑:ChatClient是单例 Bean,但FunctionCallback列表如果在运行时有变化(比如 MCP 服务器动态增减工具),单例不会自动刷新。需要重新构建ChatClient或改用每次请求传入 tools 的方式。

6. 把链路跑稳之后:接入文档与长期编码方案

链路跑通只是第一步。真正上生产,你会关心工具调用的可观测性、超时重试、以及多 MCP 服务器的统一管理。这些在接入文档里有更细的说明,地址是https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite。API Key 的管理在控制台https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,建议给不同环境分配不同 Key,方便按环境排查。

如果你在做的是长期编码类智能体,工具调用频次高、上下文长,可以考虑 Coding Plan,地址是https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite,它在长会话和 Agent 场景下的额度策略更适合持续调用。Claude Code 相关的接入配置在https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite,需要填的三件套是 Base URL、API Key、Model ID,缺一不可。

最后留一个实用技巧:在ChatClient外面包一层日志切面,把每次工具调用的 name、入参、耗时、结果大小打出来。MCP 链路的故障大多发生在工具执行阶段,而不是模型推理阶段,有了这层日志,下次再遇到No FunctionCallback或 SSE 超时,你能在三十秒内定位到是注册问题还是连接问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询