最近接了个活儿,客户问我能不能把公司现有的订单系统、库存服务接到大模型上,让AI帮忙查单、改地址、算运费。第一反应是这事儿肯定能干,但真动手才发现麻烦不少:模型怎么知道系统里有哪些接口?接口参数怎么安全暴露?对话中的工具调用怎么编排?各个服务各写一套协议的话,后面维护就是灾难。
后来我把方案落到了SpringAI MCP Server上,整条链路算是彻底理顺了。这个项目说白了就是用 Spring Boot 那套熟悉到不能再熟悉的方式,把 AI 应用本身也变成一个标准的服务来开发、部署和治理——MCP Server在这里就是 AI 应用里的“统一接入层”,让模型能通过标准协议发现工具、调用工具、拿结果再生成回答。整个过程就像写 Controller 一样简单。这篇文章我想把项目的前因后果、核心设计、实操步骤和踩过的坑都摊开聊一聊。
1. 项目整体设计思路:为什么 AI 服务需要“Spring Boot 化”
1.1 智能接手现有业务,难在哪里
很多团队第一次接触 AI 应用开发,最容易犯的错就是盯着“模型能生成多惊艳的内容”看,把大模型当成了一个孤立的黑盒。但真实业务里,AI 的价值从来不在“会聊天”,而在“能办事”——用户问“帮我查下 JD10086 的物流进度”,背后是查询系统、物流系统、甚至短信通知系统的联动,模型本身并不知道这些系统的存在。
要让模型“知道”并且“用得上”这些系统,通常有三条路:
- 把接口文档喂给模型,让它在回答里“建议调用”,再由前端手动触发,体验割裂。
- 用 Function Calling(函数调用),把每个业务接口注册成函数,让模型自动选择并生成参数,再由代码执行。
- 用 MCP(Model Context Protocol,模型上下文协议)把工具、数据源全部标准化,模型侧通过协议发现并调用,彻底告别“每个接口都要定制”的野蛮生长。
第一种方案基本是演示级的,真上生产会累死人。第二种是目前的主流,但各家实现五花八门,切换模型就要改代码。第三种就是本项目选型的核心——MCP Server把工具调用做成了标准协议,上游模型和下游服务完全解耦。而Spring Boot的加入,则把这一套流程收拢到 Java 生态内,依赖注入、配置管理、监控、部署,全都可以复用团队已有的基建。
1.2 用 Spring Boot 那套思维管理 AI 服务
我见过很多团队把 AI 服务单独拎出来,用 Python 写了个小脚本,丢在一台服务器上跑,接口文档全靠嘴传,参数变更满地飞。时间一长,这个“AI 服务”就成了整个架构里最不可控的一块。项目SpringAI MCP Server想解决的就是这个问题:把 AI 应用也当作一个标准的 Spring Boot 工程来管理。
具体做到三层标准化。第一层是工程标准化:Maven/Gradle 管理依赖,统一的目录结构,application.yml放配置,和团队里其他微服务一模一样。第二层是接口标准化:通过@Tool注解暴露方法,方法即工具,参数自动完成 Schema 转换,不再需要手写 JSON Schema。第三层是治理标准化:项目启动后自动注册到服务发现,日志走统一采集,指标上报监控系统,出了问题能查、能追、能告警。
说白了,SpringAI MCP Server不是发明了一套新东西,而是把过去几年 Spring Boot 生态沉淀下来的最佳实践,平移到 AI 应用这个新领域。对于一个已经用 Spring Boot 跑了多年业务的团队来说,接入成本几乎为零。这也是我最终选择这个方向的最重要的原因——团队不需要为新方案单独养一支特种部队。
1.3 谁适合直接上手这套方案
不是所有项目都需要 MCP Server。如果你是做个人玩具、简单问答机器人,模型直接调 API 就够了,引入 MCP 反而多一层开销。但如果你遇到下面几种情况,这套方案基本就是标准答案:
- 公司已有多个业务系统(订单、CRM、支付、库存),希望 AI 助手能直接调动这些内部能力完成闭环操作。
- 团队以 Java 为主,不想为了一个 AI 功能引入一套全新的技术栈。
- 需要把 AI 能力开放给多个前端(Web、小程序、IM 机器人),希望在服务端统一管理工具权限和调用日志。
- 正在规划 Agent(智能体)产品,希望后续能不断叠加新工具而不改核心逻辑。
这套方案适合的,是那些把 AI 当正经基础设施来做的团队,而不是只想跑通一个 Demo 的临时需求。
2. MCP 协议与 Spring AI 框架的核心机制拆解
2.1 一次 MCP 调用,到底发生了什么
MCP Server本质上是一个协议端点,模型侧的客户端通过 JSON-RPC 消息与它通信。整个调用流程可以拆成五个环节:
- 客户端发送
initialize请求,完成握手,确认协议版本和服务能力。 - 客户端调用
tools/list,拿到当前服务暴露的全部工具清单,包括名称、描述、参数 Schema。 - 模型根据用户问题,挑选合适的工具,并按 Schema 生成参数。
- 客户端发送
tools/call,携工具名和参数,服务端执行具体方法并返回结构化结果。 - 模型把工具返回的结果组织成自然语言,回复给用户。
这个设计最大的价值在于:MCP Server侧的开发者完全不需要关心上游是 OpenAI 还是 Qwen 还是 Claude,只要实现标准协议,任何兼容 MCP 的客户端都能直接接入。反过来,模型侧也不需要关心下游服务是 Java、Python 还是 Node,只要对方开口说 MCP,就能对话。
Spring AI 项目对这一协议做了完整的 Java 化封装。开发者不需要碰 JSON-RPC,不需要手写 Schema,只需要写一个普通方法,加个注解,Spring 容器会自动完成这一切。这就是“像 Spring Boot 一样简单”最直观的体现。
2.2 Spring AI 在项目中扮演的三个角色
第一个角色是模型接入层。Spring AI 提供了统一的模型客户端抽象,切换模型厂商只需要改配置,业务代码不用动。我在项目里用的就是spring-ai-starter-model-openai这套 starter,但把它替换成 Qwen、Ollama 或其他实现也是同样的套路。
第二个角色是 MCP Server 的运行时。spring-ai-starter-mcp-server会自动创建一个标准的 MCP 端点,把标注了@Tool的 Bean 方法收集起来,启动时注册到工具注册表。当客户端来拉取工具清单时,框架会动态生成每个方法的 JSON Schema,参数名、类型、描述、必填项,全部从方法签名和注解中推导。
第三个角色是 Agent(智能体)的编排层。光有工具还不够,模型得知道“什么时候该用哪个工具”。Spring AI 的ChatClient提供了自动工具调用能力:模型在生成回答时,如果判断需要外部数据,会在内部发起一次工具调用,拿到结果后继续生成,整个过程对用户无感。这种“模型决策—工具执行—结果回填—再生成”的循环,就是 Agent 最核心的运行机制。
2.3 项目里工具的真正边界:不是所有方法都适合暴露
@Tool注解非常方便,但方便背后藏着风险。团队里有人随手给内网管理接口加了注解,AI 就能通过对话调用后台权限操作——这不是危言耸听。我在设计项目时给自己定了三条铁律:
- 工具必须面向“AI 场景”单独设计,而不是直接复用现有业务 Controller 里的方法。AI 可以一次调用拆解出多个参数、甚至对参数做归并,API 设计思路和面向前端的接口完全不同。
- 工具方法必须做幂等控制。模型在生成参数时可能出错、超时重试时可能重复提交,写操作一定要有幂等设计,否则一次对话可能触发两笔订单创建。
- 工具返回值绝不能原样透传数据库实体。要把实体转成精简的 DTO,只暴露必要字段,避免内部数据结构泄漏到提示词上下文里。
这不是框架层面的限制,而是架构设计层面的约束。你要清楚,一旦把工具暴露给了 MCP,就等于把能力开放给了所有能连上这个端点的客户端。权限、限流、审计,一样都不能少。
3. 实操:从零搭建一个 SpringAI MCP Server
3.1 项目骨架与依赖配置(以 Spring Boot 3.3 为例)
先看最基础的工程配置。我这里以 Java 17 和 Spring Boot 3.3.x 为例,Maven 构建,因为这是目前兼容性最稳的组合。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> </dependencies>几个细节值得多说一句。spring-ai-starter-model-openai是模型接入层,如果你用的是其他模型,比如通义千问,就换成spring-ai-starter-model-qwen,接口风格一致。spring-ai-starter-mcp-server负责把当前 Spring Boot 应用变成一个 MCP Server 端。spring-boot-starter-web是必需的,因为当前版本默认走 Streamable HTTP 传输方式,应用需要对外提供 HTTP 端点。
3.2 配置文件的几个关键项解读
配置集中在application.yml里,我用的生产级配置是这样:
spring: application: name: ai-order-assistant ai: model: openai: base-url: https://your-model-endpoint.example api-key: ${AI_API_KEY} chat: options: model: gpt-4o-mini mcp: server: enabled: true name: order-assistant-mcp version: 1.0.0 streamable-http: true transport: http main: allow-bean-definition-overriding: true server: port: 8080spring.ai.mcp.server.enabled是整个 MCP Server 的开关,必须设为true。name和version会出现在握手响应里,客户端用它识别服务身份,建议用有业务含义的名字。streamable-http表示启用 HTTP 传输,这是目前跨网络最方便的 MCP 传输方式,适合服务和客户端部署在不同机器上的场景。
补充一个容易踩的坑:spring.main.allow-bean-definition-overriding=true这行,如果项目里同时引入多个 Spring AI 相关 starter,可能会出现同名 Bean 冲突,提前把这个开启能省掉很多排查时间。如果没遇到冲突,这行不写也没问题。
3.3 第一个 MCP 工具:写法和普通 Service 没有区别
工具定义的核心就是一个注解。我项目里最常用的例子是查订单:
@Service public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService = orderService; } @Tool(description = "根据订单号查询订单当前状态,返回订单状态、物流进度、预计送达时间") public OrderStatusDTO queryOrderStatus(String orderId) { return orderService.fetchOrderStatus(orderId); } @Tool(description = "为用户订单修改收货地址,只有订单处于待发货状态时才允许修改") public OperationResult changeOrderAddress( @ToolParam(description = "订单号") String orderId, @ToolParam(description = "新的完整收货地址") String newAddress) { return orderService.updateAddress(orderId, newAddress); } }注意@Tool注解的description一定要写清楚,这个文本会被直接塞进模型请求里,成为模型判断是否调用该工具的核心依据。描述写得模糊,模型就会在多个工具间犹豫。参数上的@ToolParam同理,描述越精确,模型生成的参数就越可靠。
方法的可见性必须是public,否则框架扫描不到。返回值类型建议用自定义 DTO,而不是直接返回Map,因为 JSON Schema 需要根据类型推导字段结构,DTO 的表达力远比 Map 强。
3.4 手动验证 MCP Server 是否可用
代码写完不要直接去调模型,先用工具客户端手动验证。我平时会用MCP Inspector这类调试工具,或者直接用命令行发送 JSON-RPC 请求。
先确认应用启动无异常,然后请求工具列表:
curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'能返回工具清单,说明服务端已经正常注册。接下来测试真实的工具调用:
curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"queryOrderStatus","arguments":{"orderId":"JD10086"}}}'如果这一步返回了正确的业务结果,说明从协议层到业务逻辑层整条链路是通的。这时候再接入模型客户端,排查就从“全链路盲猜”变成了“分段确认”,哪一段出问题一目了然。
3.5 把 Agent(智能体)的角色放进链路中
工具就绪之后,真正体现“智能”的是 Agent 的编排能力。Spring AI 的ChatClient提供了开箱即用的自动工具调用,代码如下:
@Service public class AssistantService { private final ChatClient chatClient; public AssistantService(ChatClient.Builder builder) { this.chatClient = builder .defaultSystem("你是一个订单助手。查询订单时使用查单工具,修改地址前必须确认用户身份。") .build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }当用户问“JD10086 到哪了”,模型会在生成过程中内部触发queryOrderStatus工具调用,把返回结果组织成一句自然语言回答。整个过程链路是:用户问题进来,模型判断需要查单,调用工具,拿到状态,整理成回复。多工具场景下,模型还会自主决定调用顺序——比如先查订单再算运费,这在传统 if-else 编程里几乎是写不动的逻辑,但在 Agent 范式里只是“选择工具—逐步执行”的自然结果。
4. 工具选型与关键参数设置的进阶细节
4.1 为什么优先选择 Streamable HTTP 而不是 Stdio
MCP 协议支持多种传输方式,最常见的两种是 Stdio(标准输入输出)和 HTTP。Spring AI 分别对应spring.ai.mcp.server.transport=stdio和streamable-http。
Stdio 模式下,MCP Server 作为客户端进程的子进程启动,双方通过标准输入输出通信。这种方式配置简单、开销低,但强依赖本机部署:客户端和 Server 必须在同一台机器、同一个进程树里。这对单体本地应用没问题,但对分布式部署完全不可行。
我在项目里选的是 Streamable HTTP。Spring Boot 应用本身就内嵌了 Tomcat,直接对外提供/mcp端点,客户端通过 HTTP 调用来发现和调用工具。这样 MCP Server 可以独立部署成微服务,也可以和现有业务模块部署在一起,远程连接、防火墙规则、负载均衡全部沿用现有基础设施。省心不是一点点。
4.2 模型参数与工具调用成功率的联动调优
工具调用不是“写完就能百分百命中”的。模型需要根据用户的话,理解意图、匹配工具、生成参数,任何一个环节偏差都会导致调用失败或返回错误结果。我梳理几个实际影响最大的参数:
- temperature:这类任务建议调低,设置在 0.1~0.3。生成参数需要确定性,温度过高会让工具名的选择都出现随机性。
- max tokens:不要设太短,工具结果回填后还要生成自然语言回复,太短的上下文经常把回答截断。
- system prompt:建议明确说明“必须调用哪个工具来获取什么信息”,模型在没有工具信息时往往会根据自己的“记忆”胡编状态。
举个例子,我在项目里一开始的 system 提示词只写了“你是订单助手”,结果模型对“查一下快递”这种口语经常不调工具,直接回答“您的快递正在运输中”——完全在编。后来改成“查询任何订单状态前,必须先调用 queryOrderStatus 工具”,情况立刻改善。模型不是不会用工具,是你要清楚告诉它什么时候必须用。
4.3 工具数量与上下文开销的平衡
每暴露一个工具,工具的完整 Schema 就会出现在每次请求的上下文中。工具越多,模型需要“阅读”的 token 就越多,响应延迟和成本都会上升。我见过一个团队把 100 多个方法全部打上@Tool,结果一次普通聊天请求就烧掉几万 token。
项目里的解法是对工具做分组。Spring AI 支持为不同客户端配置不同的工具集合:高频查询类工具放一组,管理操作类工具放另一组,模型端按场景绑定。核心原则是“按需暴露,够用就好”,而不是“全家桶式全量注册”。如果确实有几百个工具,建议拆成多个 MCP Server,而不是强行塞进一个端点。
4.4 权限控制不能靠模型自觉
模型在生成工具参数时只认文本,不认权限。你把“删除用户”工具暴露给 MCP,理论上任何人都能通过对话触发删除。所以项目里必须做服务端权限校验。
我的做法是在工具方法内部校验调用者身份。MCP 请求头里可以携带自定义凭证,服务端在@Tool方法执行前先解析凭证,再判断该调用者是否有权限执行此操作。另一个思路是把操作类工具和查询类工具分开部署到不同的 MCP Server,前端只把查询类端点暴露给用户,管理类端点放在内网。权限边界在架构层面划定,永远比在代码里补救要可靠。
5. 实操过程中踩过的典型问题与排查实录
5.1 应用启动正常,但 tools/list 拉不到工具
现象:MCP Server 启动没报错,但客户端请求tools/list返回空数组。
排查步骤:
- 先确认工具类是否被 Spring 容器扫描到。如果
@Service所在包不在主类的组件扫描路径下,这个 Bean 根本不会被创建。 - 确认方法是否加了
@Tool,并且是public方法。我一度漏掉public修饰符,方法被框架静默跳过,没有任何报错。 - 确认 MCP Server 是否已启用,检查
spring.ai.mcp.server.enabled是否为true。这个配置默认值在不同版本里有差异,建议显式设置。 - 最后看版本兼容性。Spring AI 1.0.0 与 Spring Boot 3.4 以下版本搭配时比较稳,如果用了 Boot 4.x 的预览版,MCP 相关 starter 可能失效。
这套排查流程基本覆盖了 90% 的“工具失踪”问题。
5.2 工具调用报错:参数序列化失败
现象:在tools/call阶段,客户端返回参数格式错误或服务端反序列化异常。
这个问题的根因通常是参数类型过于复杂。模型侧的 Schema 是根据 Java 类型推导的,如果方法参数是自定义对象,Schema 就会包含嵌套结构,模型生成的 JSON 很容易嵌套错位。我吃过最大的亏是直接传了一个OrderQuery对象,里面套了List<Filter>,模型生成的 JSON 缺了层级,反序列化直接炸掉。
最稳妥的做法是尽量使用基本类型和简单字符串参数。参数一多,就把多个入参打包成一个扁平的 DTO,字段尽量是字符串、整数、布尔这种基本类型。复杂嵌套对象的表达能力不适合在工具调用里硬扛,那是给内部 API 用的,不是给模型用的。
5.3 调用超时:工具执行时间与客户端超时设置不匹配
现象:工具本身执行只要 2 秒,但 MCP 客户端在 1.5 秒就主动断开了连接。
工具调用是在模型生成过程中的一次内部往返:模型生成参数,客户端发请求,服务端执行,结果返回,模型继续生成。任何一环超时,整个对话就会中断。我在项目里遇到过数据库慢查询把工具调用拖到 10 秒以上的情况,当时客户端 3 秒超时,导致用户发一句话,模型就报错。
解决思路分三层。服务端:确保工具方法本身高效,数据库查询加索引,外部 HTTP 调用设置合理的超时上限。客户端:调大 MCP 调用的连接超时和读取超时。架构层:对确实耗时的操作(比如批量导出)改成异步任务 + 轮询结果,而不是让模型干等。
5.4 工具名冲突导致不可预期的调用
现象:多个@Service里都有queryOrderStatus同名方法,模型调用时随机命中一个,结果有时返回 A 系统的数据,有时返回 B 系统的数据。
Spring AI 的工具注册机制要求工具名全局唯一。不唯一时框架默认会用类名限定,但如果你没有重写@Tool的 name 属性,多个重名方法就会产生歧义。排查时先列出所有注册工具名,检查格式是否带类名前缀,然后在@Tool注解里显式指定 name,例如@Tool(name = "oms_queryOrderStatus", description = "...")。工具名要当成 API 来治理,不要随意起名。
5.5 内网跨系统部署,MCP 请求被网关拦截
现象:本地测试一切正常,部署到测试环境后其他系统访问 MCP 端点直接 404。
大部分团队的内部服务都走网关,网关默认只放行注册过的路径。MCP 端点默认路径是/mcp,如果网关路由没有匹配这条路径,请求就到不了应用。我曾因为网关把/mcp当成了静态资源路径直接拦掉,排查了很久。解法很直接:在网关配置里把 MCP 端点路径显式加入白名单,或者把 MCP Server 暴露在一个独立的端口上,不要让网关掺和。
6. Spring AI MCP Server 的扩展思路与落地建议
6.1 从单工具到多 Agent 协作
单个 MCP Server 暴露一组工具,是 AI 服务的第一阶段。再往前走一步,你会遇到更复杂的场景:用户问“我的订单怎么还没到,帮我催一下”,这涉及查询订单、判断状态、查询物流、发送通知四个动作。
Spring AI 本身支持 Agent 编排,可以把不同领域能力拆成多个独立 Agent,每个 Agent 再绑定不同的 MCP Server。订单 Agent 只管查单,售后 Agent 只管退款,用户发给统一入口,由一个调度 Agent 决定转给谁。这就像把一个大服务拆成了多个微服务,每个服务各自维护、独立扩展。MCP 在这里扮演的是 Agent 之间的“协议总线”,而不是绑死在某个实现里。
6.2 模型可观测性:日志才是最后的防线
AI 应用的黑盒属性比普通后端服务强得多,用户说了一句话,模型内部可能走了三个工具、做了两次推理。一旦结果不对,排查难度远高于普通接口报错。
项目里我坚持做三件事。第一件,给每次对话分配一个 traceId,模型请求、工具调用、结果返回的相关日志全部串起来。第二件,记录每次工具调用的入参和出参,这是判断“模型选错工具”还是“工具本身报错”的唯一依据。第三件,接入监控看板,统计工具调用成功率、平均响应时长、模型 token 消耗。这些指标比模型生成的文字质量更值得每天盯。
6.3 从“能用”到“好用”的落地节奏
如果团队正准备尝试这套方案,我的建议是不要一上来就搞大而全的 Agent 平台。先从一个小而具体的场景切入,比如“AI 订单查询助手”,只暴露两三个工具,跑通一条完整链路,让业务方看到价值。第二步再叠加权限、日志、监控这些治理能力,把服务的“生产属性”补齐。第三步才去考虑多 Agent、多 MCP Server 的组合编排。
我实际感受最深的,是技术选型从来不是越复杂越好。MCP Server 解决了协议标准化的问题,Spring Boot 解决了工程化的问题,但最终决定项目成败的,是工具边界划得是否清楚、提示词设计得是否准确、权限控制是否扎实。这些工作看起来不起眼,却决定了 AI 服务能不能真正活在生产环境里。
提示:项目中使用到的完整示例代码和工程配置,建议搭一个最小可运行仓库跑通后再逐步扩展。工具描述、系统提示词这些文本,一定要在真实场景里反复迭代,不要相信第一版写出来就是最优的。