用SpringAI开发天气查询MCP服务:从百度地图API到LLM调用的完整链路

构建企业级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);
        }
    }
}

这个实现有几个关键点值得注意:

  1. 参数校验:对输入进行严格校验,避免无效调用
  2. 缓存机制:使用Caffeine减少API调用次数
  3. 超时控制:设置合理的超时时间,避免长时间阻塞
  4. 异常处理:对不同类型的异常进行差异化处理
  5. 结构化响应:返回标准化的响应格式

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>

3.2 客户端配置

内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值