Spring AI+MCP+SSE:让大模型真正调用业务工具

发布时间:2026/10/10 3:22:30
Spring AI+MCP+SSE:让大模型真正调用业务工具
1. 这个组合到底解决什么问题先说结论Spring AI MCP SSE 这套东西解决的是让大模型不再只会聊天而是能真的调用你的业务工具。最近我在做一个智能审核助手需求很朴素用户提交一条工单AI 要能查数据库里的审批状态、调 CRM 接口拿客户信息、把审核结论写回业务系统。一开始我用的是最原始的方式——手写 Function Calling也就是在调用大模型的时候把每个函数的 JSON Schema 手工声明一遍模型返回参数后自己解析、分发、执行。头两三个函数还好一旦工具数量超过五个代码就开始失控了。每个工具要写两遍一遍是 JSON Schema 定义一遍是 Java 方法本身。多个项目之间还不能复用换个客户端全得重来。MCPModel Context Protocol把这个问题彻底解掉了。它由 Anthropic 提出并开源核心思路是给大模型调用外部工具定一个统一协议。工具提供方把能力暴露成一个标准端点消费方通过协议自动发现工具列表、完成调用、拿到结构化结果。打个比方没有 MCP 之前每个 AI 应用像一台需要专属充电线的老手机有了 MCP大家统一走 USB-C设备随便换线不用换。那 SSE 又在这套体系里扮演什么角色SSEServer-Sent Events是一种基于 HTTP 的服务端推送技术。MCP 协议规定了消息的语义但消息怎么传输需要落地方案。目前官方支持三种传输方式stdio本地进程间通信、HTTPSSE旧版、Streamable HTTP新版也兼容 SSE。在一个 Spring Boot 服务作为 MCP 服务端向外提供能力的场景里SSE 是最自然的选型客户端通过普通 HTTP 发送请求服务端把事件流持续推给客户端。因为走的是标准 HTTP天然穿透各种网关、代理和负载均衡运维层面上几乎零成本。我实际跑下来最直观的感受是接入 MCP 之后给 AI 加工具变成了一件可以标准化、甚至可以外包给其他团队的事情。你不需要关心对方的工具是怎么实现的只要他给你一个 MCP 端点你就能把能力接进来。这篇文章我就用自己踩过坑的真实案例把 Spring AI 构建 MCP-SSE 服务的完整链路讲透包括代码、配置、排错。适合正在做 Spring AI 项目、想把 AI 接入业务系统的同学参考无论你是打算自己暴露 MCP 服务还是想消费别人的 MCP 服务这套经验都通用。2. 没有 MCP 的时候接入工具集有多痛苦2.1 手写 Function Calling 的痛点先展开说说我前面提到的原始方式。假设你要让 AI 调用一个查询工单状态的接口在 OpenAI 风格的 Function Calling 里你得写这样的定义{ name: query_order_status, description: 根据工单ID查询当前审批状态, parameters: { type: object, properties: { orderId: { type: string, description: 工单ID } }, required: [orderId] } }然后你还得写对应的 Java 方法再把模型返回的参数解析出来手动映射到方法入参。这个流程有两个致命问题第一Schema 定义和 Java 方法代码分离改一处忘另一处是常态第二每次接入新的业务系统都要重复这一套手工对接动作。工具从三个变成十个的时候光维护这些 Schema 就让人头大。MCP 把这些全部标准化了。你用注解写一个方法框架自动生成工具定义工具列表由服务端动态下发客户端不用写死调用参数和返回结果用统一的 JSON 结构封装天然支持复杂嵌套。这不是某个框架的语法糖而是协议层面的统一。2.2 MCP 的核心模型客户端、服务端、工具MCP 的架构只有三个角色MCP Host运行大模型的宿主程序在 Spring AI 场景下就是你的业务应用。MCP Client负责与远程 MCP Server 建立连接、发现工具、发起调用。MCP Server暴露一组工具能力可以是本地进程也可以是远程 HTTP 服务。在 Spring AI 里这三者的边界可以灵活组合。你的应用可以扮演客户端去消费远程 MCP 服务比如连一个 Playwright MCP 做浏览器自动化你的应用也可以扮演服务端把自己的 REST 接口、数据库操作、内部服务包装成 MCP 工具供外部 AI 应用调用。这里有一个容易被绕晕的点同一个应用可以同时既是客户端又是服务端。我的智能审核项目就是这么干的审核服务本身把查状态提交意见暴露成 MCP 服务端同时它又作为客户端连接了一个外部地图服务地址校验把两个外部工具也注入给大模型。Spring AI 对这两种角色都有完整支持后面我会分别演示。2.3 SSE 和其他传输方式的取舍MCP 官方传输方案里除了 SSE 之外还有一个 stdio 和一个新版 Streamable HTTP。我用过之后说说适应场景。stdio 走的是本地进程管道适合AI 应用和 MCP 服务在同一个机器上的场景。比如你在本地跑一个 Claude Desktop通过 stdio 启动一个 Node.js 脚本作为 MCP 工具。优点是启动零配置缺点是没法跨网络显然不适合作为业务系统向外部提供能力的方式。Streamable HTTP 是 2025 年官方主推的新协议它统一了普通 HTTP 响应和 SSE 流式响应一次 POST 请求可以同步拿结果也可以建立长连接持续收流。Spring AI 的spring-ai-mcp-server-webmvc和spring-ai-mcp-server-webflux都支持。那为什么很多老项目还在用 SSE 方式两个原因一是兼容性现网已经有很多基于旧版 HTTPSSE 协议实现的 MCP Server比如 2024 年发布的各类 MCP 服务端客户端为了兼容必须保留 SSE 支持二是在需要服务端主动下发消息的场景下SSE 从设计上就更直观——服务端保持一个长连接有事件就推客户端不需要轮询。实际上新版 Streamable HTTP 协议内部消息流依然是按 SSE 格式编码的只是把端点和握手流程做了简化。所以不管新老协议懂 SSE都是绕不开的基本功。从部署层面讲SSE 走的是普通 HTTP 80/443 端口不依赖 WebSocket 那种自定义握手协议也不需要在 Nginx 里单独配置 Upgrade 头。云厂商的负载均衡、CDN 对这种长连接都有成熟的超时和缓冲配置方案。我公司内网网关默认就是放行的这也是我最初选 SSE 方式的重要原因。3. Spring AI 对 MCP 的支持到底怎么用3.1 版本选择和依赖引入先提醒一个很多人容易踩的坑Spring AI 的 API 在 1.0.0 GA 版本前后变动很大网上搜到的很多教程拿 0.8.x 的老代码硬套 1.0 的新项目编译直接报错。我的项目基于 Spring Boot 3.4 Spring AI 1.0.0以下内容都以这个版本为准。MCP 相关的核心依赖有三个!-- 客户端模式让你的应用作为 MCP Client 连接外部服务 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency !-- 服务端模式让你的应用作为 MCP Server 对外提供工具 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId /dependency !-- 服务端 SSE 需要基于 WebFlux -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp-server-webflux/artifactId /dependency注意MCP Client 模式下Spring AI 底层用 WebClient 连接远程服务所以项目中至少要有spring-boot-starter-webflux。如果你的应用本身是 WebMVC 的比如你用的是spring-boot-starter-web两个 Web 容器会冲突实测下来 WebFlux 可以正常嵌入但你要确保只用一个主容器避免端口冲突。3.2 客户端模式连接远程 MCP Server 并自动注入工具这是最常用的场景。在application.yml里声明你要连的 MCP Server 列表spring: ai: mcp: client: # 连接的 MCP Server 配置 connections: - id: order-tool type: sse url: http://localhost:8081/mcp/sse - id: map-tool type: sse url: http://localhost:8082/mcp/sseSpring AI 启动时会自动为每个 connection 创建一个McpClient并通过ToolCallbackProvider把所有工具注入到你的ChatClient里。代码里你什么都不用管直接让模型去调用Service public class AuditAssistant { private final ChatClient chatClient; public AuditAssistant(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是工单审核助手请根据工具返回的数据给出审批结论) .build(); } public String audit(String orderId) { return chatClient.prompt() .user(请查询工单 orderId 的审批状态并给出审核意见) .call() .content(); } }你没有写任何一行如何调用工具的代码工具定义是 MCP Server 通过协议动态下发的模型看到可用工具后会自行决定何时调用哪个工具。这就是 MCP 的价值配置即接入。3.3 系统提示词怎么配置这阵子在社群里被问得特别多的是springai 系统提示词怎么配置。其实和 MCP 没直接关系但既然要完整跑通智能审核场景一并说了。上面的代码里已经演示了defaultSystem()用它是为了在应用启动时就把系统提示词绑定到所有会话。如果你想按每次请求动态调整就在prompt()链上追加chatClient.prompt() .system(当前处理的是高优先级工单请严格按合规要求审核) .user(请查询工单 orderId) .call() .content();系统提示词决定了模型怎么使用工具。我这里踩过一个坑默认系统提示词里没有强调工具结果可能为空请如实告知用户结果模型拿到空查询结果后经常自行脑补一条审核结论。后来我在系统提示词里明确加了一句当工具返回数据为空时必须说明无数据禁止编造效果立竿见影。4. 实操把 Spring Boot 服务暴露成 MCP-SSE 服务器4.1 场景设计内置一个智能审批服务这部分来个能直接抄作业的完整示例。假设你有一个老系统里面已经写了查工单状态和提交审批意见两个 REST 接口GET /api/order/{id}返回工单信息POST /api/order/{id}/approve提交审批现在业务方要求让 AI 助手能直接查和能直接审批。最粗暴的做法是教 AI 用 HTTP 工具去请求这些接口但这样会带来两个问题一是你得把接口的 URL、认证方式、参数格式全部塞进提示词维护成本极高二是没有权限控制一说模型如果被诱导可能会调用不恰当的接口。MCP Server 模式下你可以创建一个专门的审核服务层把允许 AI 执行的操作以Tool注解方法暴露出去。方法内部可以做白名单校验、审计日志、权限判断然后再调底层 REST。这样AI 能做什么完全由你控制而不是由接口暴露面控制。4.2 服务端代码实现先加服务端依赖见 3.1然后在 Spring Boot 主类上开启 MCP 服务端功能SpringBootApplication EnableAutoConfiguration public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } }Spring AI 的自动配置会自动探测所有标注了Tool的 Bean并注册为 MCP 工具。核心代码就是一个普通 Beanimport org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; Component public class OrderTools { // 模拟的工单存储 private final MapString, String orderStore new ConcurrentHashMap(); Tool(description 根据工单ID查询当前审批状态返回结果为JSON字符串) public String queryOrderStatus( ToolParam(description 工单ID) String orderId) { String status orderStore.getOrDefault(orderId, NOT_FOUND); return {\orderId\:\ orderId \,\status\:\ status \}; } Tool(description 提交工单审批意见approve为true表示通过false表示驳回) public String submitApproval( ToolParam(description 工单ID) String orderId, ToolParam(description 是否通过) boolean approve, ToolParam(description 审批意见) String comment) { orderStore.put(orderId, approve ? APPROVED : REJECTED); return {\orderId\:\ orderId \,\result\:\SUCCESS\}; } }Tool注解的description会自动转换成 MCP 工具描述传给大模型所以描述一定要写清楚这会直接影响模型的选择准确性。ToolParam同理。然后只需要在application.yml里声明服务端模式spring: ai: mcp: server: # 开启服务端自动配置 enabled: true # 工具扫描包路径默认会扫描整个应用上下文 tool-callback-imports: com.example.mcpserver.tools启动后访问http://localhost:8080/mcp/sse你会发现这是一个一直挂着、不断输出注释行的 SSE 端点。再用任何 MCP 客户端比如 Spring AI 的 MCP Client或者 Claude Desktop配上这个地址就能自动列出queryOrderStatus和submitApproval两个工具。4.3 把普通 REST 接口快速转为 MCP 接口结合热搜词java rest接口快速转为mcp 接口多说一点。很多业务系统已经是 REST 风格你完全不需要推倒重来只需在原 Service 或 Controller 外层包一个 Tool 方法即可。比如现有的订单查询接口长这样RestController public class OrderController { GetMapping(/api/order/{id}) public OrderDetail getOrder(PathVariable String id) { return orderService.getDetail(id); } }你想让 AI 也能查不用让 AI 直接调 HTTP新建一个适配 BeanComponent public class OrderMcpAdapter { private final OrderService orderService; public OrderMcpAdapter(OrderService orderService) { this.orderService orderService; } Tool(description 查询订单详情包含客户名称、金额、状态。返回JSON字符串。) public String getOrderInfo(ToolParam(description 订单ID) String id) { OrderDetail detail orderService.getDetail(id); // 序列化为 JSON 字符串返回 return JsonUtils.toJson(detail); } }要点是工具方法内部复用原 Service 层代码不做任何 HTTP 转发。这样你就获得了两个东西一是 AI 可调用能力二是你可以在方法里加参数校验、加权限判断、加审计日志这些都不影响原来 REST 接口的正常工作。同一个业务方法一条链路给人类用一条链路给 AI 用底层逻辑完全一致。4.4 Spring Boot REST 服务快速转 MCP热搜词里还有一句java rest接口快速转为mcp 接口基于我上面的代码再补充一种场景如果你手头的接口实现是 Controller 方法不想拆到 Service 层那就直接从 Controller 里提取入参在 Tool 方法里调用同一个 Controller 方法即可。本质上是一样的思路。但有一点提醒MCP 工具方法的入参要尽量扁平。如果原接口需要一个复杂的请求体对象你最好在 Tool 方法里拆成多个标量参数比如把OrderQueryRequest拆成orderId、customerName、startTime、endTime。大模型生成 JSON 时对于扁平的标量参数准确率明显更高嵌套对象容易漏字段或写错层级。这是我在实际测试中反复验证过的经验。5. 实操Spring AI 作为 MCP 客户端通过 SSE 接入外部服务5.1 接入一个外部 MCP Server 的全过程服务端写好了得有个客户端来连。还是用 Spring AI 的自动配置方式最省事。假设你要接一个外部的地图服务MCP Server它的地址是http://map-server:8080/sse只需要配置即可spring: ai: mcp: client: connections: - id: map type: sse url: http://map-server:8080/sse如果你有多个 MCP Server就多配几个连接项。Spring AI 会为每个连接建一个客户端实例并把所有工具合并到一起。然后你正常使用 ChatClient模型会自动发现需要调用哪个连接上的哪个工具。这里有个我在生产环境实测过的关键点工具总量多了以后模型会挑花眼甚至频繁调用错误的工具。我的建议是控制单个客户端的工具数量最好不要超过 15 个。工具一多模型在选哪个工具上的出错率会明显上升。如果你的业务工具确实多就按领域拆成多个 MCP Server再在客户端按场景选择性地连接或者利用系统提示词引导模型优先使用某个连接下的工具。5.2 动态工具发现不用重启也能感知工具变更MCP 的一个隐藏好处是工具是运行时发现的。Spring AI 客户端启动时会向服务端发起tools/list请求拿到当前全部工具列表。这意味着你在服务端新增一个 Tool 方法后客户端只需要重启或者重新初始化 ChatClient就能感知不需要改任何代码。如果服务端是动态注册工具的Spring AI 还支持定时刷新工具列表。这种插拔式体验对于多团队协作的项目特别友好AI 团队只需要跟服务端团队约定好协议不需要提交代码依赖。5.3 客户端工具与本地工具能否共存可以。Spring AI 允许你在同一个 ChatClient 里混合本地 Tool 方法和MCP 服务端下发的工具。本地工具用ToolCallbackProvider传入MCP 工具自动注入两者会合并为一份工具列表交给模型。我自己的项目就是这么干的本地定义了一个获取当前登录人的 Tool 方法远程接入了查订单查地图的 MCP 工具模型用起来没有任何障碍。但要注意命名冲突。如果本地工具有一个叫getOrderInfo远程 MCP Server 也暴露一个同名方法运行时不会直接报错但模型可能会选错。目前 Spring AI 对重名工具的优先级没有做明确定义所以最好的做法是起名时加上统一前缀比如本地工具统一叫local_*外部工具用各自服务的名字缩写从源头避免冲突。5.4 没有 MCP 能不能开发 Agent结合热搜词没有mcp可以开发agent吗这里一并回答完全可以。MCP 解决的是工具接入标准化问题不是 Agent 的必要条件。你完全可以自己在代码里写好ToolCallback列表然后手动传给 ChatClient效果一样。Spring AI 里定义一个工具回调非常直接ToolCallback tool new MethodToolCallback.builder() .toolDefinition(ToolDefinition.builder(queryStatus, 查询状态).build()) .toolMethod(this) .build();但问题是如果工具一多、团队一多、系统一多没有 MCP 这套统一协议你每个连接都要写一遍工具定义、参数解析、错误处理。MCP 的价值不在能不能而在维护成本。小项目、三五个工具手写完全够用一旦你要接入地图 浏览器自动化 禅道工单 数据库这类多系统能力MCP 的收益会指数级上升。6. 常见问题与排查技巧实录6.1 SSE 连接不稳定消息中断、两边不同步症状客户端连上 SSE 端点后偶尔收不到工具调用的结果通知或者服务端日志正常返回但客户端超时了。排查思路先确认你用的是哪版协议。旧版 HTTPSSE 的流程是客户端先 POST/mcp/init拿到endpoint字段再从 SSE 流里接收message事件而新版 Streamable HTTP 简化了这个握手。如果你在同一端口上混用了两个协议版本服务端和客户端很容易各自按自己的理解对接结果消息丢失。实际处理经验升级一个连接就全部按新版协议对接不要混。如果因为历史原因必须连旧版那就确保客户端也显式指定旧版协议。Spring AI 目前默认能兼容新老协议但我实测下来新版 Streamable HTTP 在代理穿透和心跳保活上更稳所以新项目直接走新版。6.2 Nginx 缓冲导致的SSE 事件收不到症状本地直连服务端一切正常通过 Nginx 转发后SSE 流里的内容迟迟不出现或者要等几秒才一次性刷出来。原因Nginx 默认会对上游响应做缓冲导致事件被攒在内存里。SSE 是需要实时推送的缓冲直接破坏了流式效果。解决办法是在 Nginx 配置里关闭响应缓冲proxy_buffering off; proxy_cache off; proxy_set_header Connection ; proxy_http_version 1.1;再设置合理的超时时间proxy_read_timeout 3600s; proxy_send_timeout 3600s;如果你的网关是云厂商 SLB 或 API 网关同样要检查是否支持 SSE。大部分云网关默认支持 HTTP 长连接但有的会限制空闲超时需要在网关侧把空闲超时调大。6.3 工具返回的数据格式不对模型开始胡编症状工具方法明明返回了数据但模型的回答里出现了数据里没有的内容。这个坑我踩得很深。MCP 协议要求工具返回结果分为content和structuredContent两部分新版协议里更强调结构化返回。如果你的工具方法返回一个普通的 StringSpring AI 会把它包装成纯文本内容。对文本内容模型倾向于自由发挥尤其当结果是个 JSON 字符串时模型经常自动提取某个字段再补充一些推测。解决办法工具方法返回结构化数据不要返回序列化好的字符串。Spring AI 支持直接返回 Java 对象框架会自动封装成结构化内容Tool(description 查询订单详情) public OrderDetail getOrderInfo(String id) { return orderService.getDetail(id); }返回 Java 对象后模型能直接基于字段做推理编造内容的概率大幅降低。这是我强烈建议的一个改进点所有工具方法的返回值能返回对象就不要返回 String除非你明确知道字符串的语义。6.4 工具调用的权限和审计工具一旦暴露给 AI就意味着任意调用方只要连上你的 MCP 端点都能触发这些操作。我从第一天就把这块当安全红线对待。推荐至少做三层防护第一层MCP Server 接入鉴权。在服务端配置 token客户端连接时通过 Header 透传。Spring AI 支持在连接配置里自定义 headers。第二层方法级别校验。Tool 方法开头检查调用方身份、检查参数合法性。特别是审批、回写类的操作必须校验当前会话是否有权限不能只靠模型自觉。第三层完整审计日志。每个工具被哪次会话调用、入参是什么、结果是什么全部落库。这不仅是合规要求也是排查模型乱调用的关键依据。我的生产项目里所有写操作工具都加了二次确认逻辑——模型第一次提交时只做校验和记录真正执行需要业务人员点击确认。虽然多了人工环节但在财务、审批这类场景里这是根本底线。6.5 偶发超时模型等待工具结果的时间不够症状模型调用了一个耗时的工具比如生成一个 30 秒的报表但模型侧在 10 秒左右就中断了。这是 SSE 和 LLM 调用链路叠加带来的问题。ChatClient 调用模型后要等模型返回的工具调用请求然后执行工具再把结果通过上下文回传给模型。整个过程在代码里的一行call()中完成但时间线上可能跨越几十秒。如果 LLM 提供方的超时时间设置得太短工具执行时间一长就会导致整体失败。解决思路给 ChatClient 增加足够的响应超时并把工具执行时间控制住。我一般会让工具方法内部先做限时控制超过 5 秒就返回一个仍在处理中的中间状态然后下次会话再查最终结果。这比让 LLM 一直等一个慢工具要稳妥得多。7. 写在最后的经验把这套东西从零搭起来我花了两周中间踩了无数坑。最深的体会是MCP 真正解决的不是技术问题而是协作问题。以前每个 AI 项目接入外部系统都是一对一写代码现在通过 MCP 标准协议工具提供方只需要发布一个端点所有 AI 应用都能即插即用。SSE 作为传输层虽然没有 WebSocket 那么时髦但胜在简单可靠配合 Spring AI 的自动配置改造现有 Spring Boot 服务的成本低到惊人。如果你也要搞类似的项目我给三条建议。第一先从一个只读工具开始就是查询类、不产生副作用的方法跑通全链路后再逐步增加写操作风险可控。第二工具描述一定要写清楚这是模型准确调用工具的最重要因素比任何参数调优都有效。第三把权限和审计前置设计好不要等功能上线后再补AI 能调用的工具就是攻击面这个意识必须从第一天就有。有多余精力的话可以再去看看 Streamable HTTP 与 SSE 的区别以及 MCP 官方如何规划二者合流。但无论如何先把服务端暴露一个工具客户端自动发现并调用这条链路跑通你就已经掌控了这套体系里最核心的部分。