ARTICLE DETAIL

资讯详情

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

SpringAI 调用 MCP 服务的实现思路:从 FunctionCallback 到 ChatClient 的 SSE 链路拆解

SpringAI 调用 MCP 服务的实现思路:从 FunctionCallback 到 ChatClient 的 SSE 链路拆解 1. 从一次线上告警说起SpringAI 调用 MCP 服务到底卡在哪很多 Java 后端第一次把 MCP 工具接进 SpringAI 时都会遇到一个很迷惑的现象日志里明明看到tools/list拉回来一堆工具ChatClient也正常返回了文本但模型就是死活不调用工具或者调用了却报No FunctionCallback found for name。我试过在一个智能体项目里排查这类问题最后发现根因往往不在模型而在 FunctionCallback 的注册时机和 SSE 链路的连接状态上。先把概念对齐。MCPModel 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后在控制台生成 Key 即可。依赖这块pom.xml里至少要有 SpringAI 的 OpenAI starter 和 MCP 客户端库。版本要对齐SpringAI 1.0 之后 MCP 的支持才比较完整dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp-client-spring-boot-starter/artifactId version1.0.0/version /dependency拿到 Key 之后先别急着写业务代码用最简配置确认模型通道是通的。application.yml里把 base-url 指向 TaoTokenspring: 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_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite。MCP 客户端这边配置里要声明一个或多个 server 实例。SSE 类型给 urlStdio 类型给启动命令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 之后推荐用defaultToolsConfiguration public class ChatClientConfig { Bean public ChatClient chatClient(ChatModel chatModel, ListFunctionCallback mcpToolCallbacks) { return ChatClient.builder(chatModel) .defaultTools(mcpToolCallbacks.toArray(new FunctionCallback[0])) .build(); } }这里的ListFunctionCallback会被 Spring 自动注入所有 MCP 工具生成的回调。如果你只想启用部分工具可以在注入后按 name 过滤而不是全量塞进去——工具太多会撑大 prompt模型选择准确率反而下降。如果你需要手动注册一个本地函数做对照可以这样写Bean public FunctionCallback localEcho() { return FunctionCallback.builder() .function(local_echo, (MapString, 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. 验证一次工具调用往返请求、日志与成功结果配置写完跑一次完整往返。写一个最简单的 ControllerRestController 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()配合FluxString返回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_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。API Key 的管理在控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite建议给不同环境分配不同 Key方便按环境排查。如果你在做的是长期编码类智能体工具调用频次高、上下文长可以考虑 Coding Plan地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite它在长会话和 Agent 场景下的额度策略更适合持续调用。Claude Code 相关的接入配置在https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite需要填的三件套是 Base URL、API Key、Model ID缺一不可。最后留一个实用技巧在ChatClient外面包一层日志切面把每次工具调用的 name、入参、耗时、结果大小打出来。MCP 链路的故障大多发生在工具执行阶段而不是模型推理阶段有了这层日志下次再遇到No FunctionCallback或 SSE 超时你能在三十秒内定位到是注册问题还是连接问题。
返回列表