构建企业级AI工具链:基于SpringAI与MCP协议的天气服务深度实践
最近在重构公司的一个智能客服项目时,我遇到了一个典型的技术挑战:多个AI应用都需要调用相同的天气查询功能,但每个团队都在重复实现类似的工具代码。这种重复不仅浪费开发资源,更让后续的维护和升级变得异常困难。经过一番技术选型,我最终选择了SpringAI结合MCP协议来构建统一的工具服务,这个方案不仅解决了当前的痛点,更为未来的AI工具生态打下了坚实基础。
如果你也在为AI应用的工具复用问题而烦恼,或者正在寻找一种标准化的方式来连接大模型与业务系统,那么这篇文章将为你提供一套完整的解决方案。我将从一个真实的天气查询场景出发,带你深入理解MCP协议的核心价值,并手把手教你如何用SpringAI构建生产级的MCP服务端和客户端。
1. MCP协议:AI时代的“USB接口”
1.1 什么是MCP协议?
MCP(Model Context Protocol)模型上下文协议,可以理解为AI领域的“USB接口”。就像USB接口让不同设备能够即插即用一样,MCP协议为大模型提供了一个标准化的方式来连接外部工具和数据源。
想象一下这样的场景:你的AI助手需要查询天气、搜索商品、分析数据,传统做法是为每个功能单独开发接口,每个大模型都需要适配不同的调用方式。而MCP协议的出现,让这一切变得简单统一。
MCP的核心架构包含三个关键组件:
- MCP客户端:通常是AI应用本身,负责发起工具调用请求
- MCP服务端:连接具体的数据源和工具,执行实际操作
- 传输层:处理基于JSON-RPC 2.0的标准化消息通信
1.2 为什么选择MCP而非传统Function Call?
在我最初的技术选型中,我对比了多种方案。传统的Function Call虽然简单直接,但在企业级应用中存在明显短板:
// 传统Function Call的问题:每个模型都需要单独适配
public class TraditionalFunctionCall {
// OpenAI格式
public OpenAIToolCall callOpenAI() {
return new OpenAIToolCall("get_weather", "北京");
}
// Claude格式
public ClaudeToolCall callClaude() {
return new ClaudeToolCall("weather_query", "北京");
}
// 其他模型又有各自的格式...
}
MCP协议解决了这些痛点:
| 对比维度 | 传统Function Call | MCP协议 |
|---|---|---|
| 协议统一性 | 各模型厂商自定义格式 | 标准化JSON-RPC 2.0 |
| 开发成本 | 每个模型都需要适配 | 一次开发,多模型复用 |
| 工具链支持 | 有限 | 丰富的工具生态 |
| 企业级特性 | 基础 | 支持认证、监控、负载均衡 |
1.3 SpringAI与MCP的完美结合
SpringAI框架为MCP协议提供了原生的Spring Boot支持,这让Java开发者能够以熟悉的方式构建AI工具链。我最欣赏的几个特性包括:
- 声明式开发:使用
@Tool注解即可将方法暴露为MCP工具 - 自动序列化:无需手动处理JSON转换,SpringAI自动处理协议层通信
- 企业级集成:完美融入Spring生态,支持配置管理、依赖注入、监控等
提示:SpringAI 1.0.0-M7版本对MCP的支持已经相当成熟,建议在生产环境中使用该版本或更高版本。
2. 构建生产级MCP服务端
2.1 项目初始化与依赖配置
让我们从一个实际的天气查询服务开始。首先创建Maven项目,配置关键依赖:
<!-- pom.xml -->
<properties>
<java.version>17</java.version>
<spring-boot.version>3.4.4</spring-boot.version>
<spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- MCP服务端核心依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 工具类库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<!-- 开发工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这里我选择了spring-ai-starter-mcp-server-webmvc作为服务端启动器,它支持SSE(Server-Sent Events)传输模式,适合需要长连接的场景。如果你的场景更偏向于本地进程通信,也可以选择spring-ai-starter-mcp-server(Stdio模式)。
2.2 服务端配置详解
在application.yml中,我们需要配置MCP服务端的基本信息:
# application.yml
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: weather-mcp-server
ai:
mcp:
server:
# 服务标识,客户端连接时需要指定
name: weather-service
version: 1.0.0
# 同步/异步模式选择
type: ASYNC
# SSE端点路径
sse-message-endpoint: /mcp/messages
# 工具变更通知
tool-change-notification: true
# 资源变更通知
resource-change-notification: true
配置项说明:
- name:服务名称,在客户端连接时作为标识
- type:ASYNC模式适合I/O密集型操作,如网络请求;SYNC模式适合CPU密集型计算
- sse-message-endpoint:SSE通信的端点路径,客户端通过此路径建立连接
2.3 实现健壮的天气查询工具
在实际项目中,我遇到过很多天气API调用的问题:网络超时、API限流、数据格式变化等。下面是一个经过生产验证的天气服务实现:
@Service
@Slf4j
public class WeatherService {
private final WebClient webClient;
private final Cache<String, WeatherData> weatherCache;
// 城市编码映射表
private static final Map<String, String> CITY_CODES = Map.of(
"北京", "110100",
"上海", "310100",
"广州", "440100",
"深圳", "440300",
"成都", "510100"
);
public WeatherService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://restapi.amap.com/v3")
.defaultHeader("Content-Type", "application/json")
.build();
// 使用Caffeine构建本地缓存,减少API调用
this.weatherCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
@Tool(
name = "get_weather_forecast",
description = "获取指定城市的天气预报信息,包括温度、湿度、风速等"
)
public WeatherResponse getWeatherForecast(
@ToolParameter(
description = "城市名称,如:北京、上海、广州",
required = true
) String city
) {
// 1. 参数校验
if (StringUtils.isBlank(city)) {
throw new IllegalArgumentException("城市名称不能为空");
}
// 2. 检查缓存
WeatherData cachedData = weatherCache.getIfPresent(city);
if (cachedData != null) {
log.info("从缓存获取{}的天气数据", city);
return buildResponse(cachedData);
}
// 3. 获取城市编码
String cityCode = CITY_CODES.get(city);
if (cityCode == null) {
throw new IllegalArgumentException("暂不支持该城市:" + city);
}
try {
// 4. 调用第三方API
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/weather/weatherInfo")
.queryParam("city", cityCode)
.queryParam("key", "${amap.api.key}")
.queryParam("extensions", "all")
.build())
.retrieve()
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
clientResponse -> Mono.error(new RuntimeException("API调用失败: " + clientResponse.statusCode())))
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(5))
.block();
// 5. 解析响应
WeatherData weatherData = parseWeatherResponse(response);
// 6. 更新缓存
weatherCache.put(city, weatherData);
return buildResponse(weatherData);
} catch (TimeoutException e) {
log.error("天气API调用超时: {}", city, e);
return WeatherResponse.error("请求超时,请稍后重试");
} catch (Exception e) {
log.error("获取天气信息失败: {}", city, e);
return WeatherResponse.error("获取天气信息失败: " + e.getMessage());
}
}
private WeatherData parseWeatherResponse(String response) {
// 实际项目中应该使用JSON解析库
// 这里简化为模拟数据
return new WeatherData(
"晴",
25.5,
18.0,
30.0,
"东南风",
3,
65,
LocalDateTime.now()
);
}
private WeatherResponse buildResponse(WeatherData data) {
return WeatherResponse.success(data);
}
// 数据类定义
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class WeatherData {
private String condition; // 天气状况
private Double temperature; // 温度
private Double tempMin; // 最低温度
private Double tempMax; // 最高温度
private String windDirection; // 风向
private Integer windLevel; // 风力等级
private Integer humidity; // 湿度
private LocalDateTime updateTime; // 更新时间
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class WeatherResponse {
private boolean success;
private String message;
private WeatherData data;
public static WeatherResponse success(WeatherData data) {
return new WeatherResponse(true, "成功", data);
}
public static WeatherResponse error(String message) {
return new WeatherResponse(false, message, null);
}
}
}
这个实现有几个关键点值得注意:
- 参数校验:对输入进行严格校验,避免无效调用
- 缓存机制:使用Caffeine减少API调用次数
- 超时控制:设置合理的超时时间,避免长时间阻塞
- 异常处理:对不同类型的异常进行差异化处理
- 结构化响应:返回标准化的响应格式
2.4 工具注册与暴露
要让MCP客户端能够发现和调用我们的工具,需要将其注册到Spring容器中:
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider toolCallbackProvider(
WeatherService weatherService,
// 可以注入更多工具服务
ProductService productService,
LocationService locationService
) {
return MethodToolCallbackProvider.builder()
.toolObjects(
weatherService,
productService,
locationService
)
.build();
}
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
2.5 服务端启动与验证
创建启动类并运行服务:
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public CommandLineRunner checkTools(ToolCallbackProvider provider) {
return args -> {
log.info("MCP服务已启动,可用工具列表:");
provider.getTools().forEach(tool -> {
log.info("工具名称: {}, 描述: {}",
tool.getName(),
tool.getDescription());
});
};
}
}
启动后访问http://localhost:8080/api/mcp/messages,如果看到SSE连接建立,说明服务端配置成功。
3. 构建智能MCP客户端
3.1 客户端项目配置
创建独立的客户端项目,配置依赖:
<!-- 客户端pom.xml -->
<dependencies>
<!-- MCP客户端 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- 大模型集成(以智谱AI为例) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 响应式支持(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>


1598

被折叠的 条评论
为什么被折叠?



