当前位置:首页 > 技术 > 正文内容

基于Spring AI与MCP协议构建企业级气象查询工具链实战

访客 技术 2026年9月24日 14

重构AI工具链:Spring AI与MCP协议的深度融合

在企业级AI应用落地过程中,工具复用一直是一个核心痛点。不同的业务线在集成大模型时,往往需要重复开发相似的业务接口适配层,例如天气查询、地理编码等。这种碎片化的开发模式不仅降低了研发效率,也提高了后续维护的成本。采用Spring AI结合MCP(Model Context Protocol)协议构建统一的工具服务层,能够有效解决多模型适配与工具共享的难题,为AI应用提供标准化的外部能力接入方案。

1. MCP协议:大模型外部工具的标准接入层

1.1 协议核心定义

MCP(模型上下文协议)定义了大模型与外部数据源、工具之间交互的通用规范。类似于通用接口在硬件体系中的作用,MCP使得大模型无需关注底层工具的实现细节与调用差异,只需遵循统一的JSON-RPC 2.0标准即可完成能力调用。

MCP架构由以下三个核心组件构成:

  • Host(宿主应用):发起交互请求的AI应用程序主体。
  • Client(客户端):与宿主应用同生命周期,负责与服务端维持1:1的连接会话。
  • Server(服务端):提供具体工具和数据访问能力的独立服务进程。

1.2 MCP与原生Function Call的差异

原生Function Call高度耦合于特定大模型厂商的私有协议,而MCP提供了模型无关的抽象层。

// 原生Function Call的适配困境:不同厂商协议差异导致代码膨胀
public class LegacyModelIntegration {
    // 适配OpenAI协议
    public Object queryGptWeather(String location) {
        return new GptFunctionInvocation("weather_fetch", location);
    }
    // 适配Claude协议
    public Object queryClaudeWeather(String location) {
        return new ClaudeFunctionInvocation("climate_info", location);
    }
}

两者的核心维度对比如下:

评估维度原生Function CallMCP协议
协议标准厂商私有,互不兼容基于JSON-RPC 2.0的开放标准
跨模型复用需针对不同模型单独适配一次发布,多模型通用接入
生态扩展性受限于单一平台支持分布式工具网格与生态共享
企业级治理基础能力原生支持鉴权、流量控制与可观测性

1.3 Spring AI对MCP的工程化支持

Spring AI框架将MCP协议无缝融入Spring Boot生态,为Java开发者提供了极具效率的工程化实践路径:

  • 注解驱动:通过@Tool注解声明式导出方法,零样板代码。
  • 自动协商:内置协议握手与消息序列化逻辑,开发者无需处理底层通信细节。
  • 生态融合:天然支持Spring的依赖注入、配置管理及WebFlux/WebMVC异步非阻塞模型。

建议:Spring AI 1.0.0-M7及以上版本对MCP Server的WebMVC适配已趋于稳定,推荐作为生产环境基线。

2. 生产级MCP Server工程实践

2.1 依赖构建与版本管理

以气象数据查询服务为例,初始化Maven工程并引入核心依赖:

<!-- pom.xml -->
<properties>
    <java.release>17</java.release>
    <spring-boot.revision>3.4.4</spring-boot.revision>
    <spring-ai.revision>1.0.0-M7</spring-ai.revision>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.revision}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- MCP Server WebMvc支持 (SSE模式) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 缓存与工具库 -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
</dependencies>

此处采用spring-ai-starter-mcp-server-webmvc构建基于SSE(Server-Sent Events)长连接的分布式服务端,适用于微服务架构下的跨网络调用。若仅为本地单进程集成,可替换为Stdio模式的starter。

2.2 核心通信参数配置

通过YAML配置文件定义MCP服务端元数据与传输层行为:

server:
  port: 9090
  servlet:
    context-path: /mcp-hub

spring:
  application:
    name: climate-mcp-provider
  ai:
    mcp:
      server:
        name: climate-info-provider
        version: 2.0.0
        type: ASYNC
        sse-message-endpoint: /events
        tool-change-notification: true
        resource-change-notification: true

关键参数解析:

  • type: ASYNC:启用异步响应模式,适配I/O密集型的外部API网关调用。
  • sse-message-endpoint:SSE消息端点,MCP Client将通过此路径完成双向消息协商。

2.3 高可用气象查询工具实现

面对外部API的不确定性(限流、网络抖动),工具实现需内置缓存降级与超时熔断机制:

@Service
@Slf4j
public class ClimateDataService {

    private final WebClient httpClient;
    private final Cache<String, ClimateRecord> localForecastCache;

    // 区域编码映射
    private static final Map<String, String> MUNICIPALITY_DICT = Map.of(
        "Beijing", "110100",
        "Shanghai", "310100",
        "Guangzhou", "440100"
    );

    public ClimateDataService(WebClient.Builder clientBuilder) {
        this.httpClient = clientBuilder
            .baseUrl("https://restapi.amap.com/v3")
            .defaultHeader("Content-Type", "application/json")
            .build();

        this.localForecastCache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(500)
            .build();
    }

    @Tool(name = "query_local_climate", description = "Retrieve detailed meteorological data including temp, humidity and wind for a specific region")
    public ClimatePayload fetchCurrentClimate(
        @ToolParameter(description = "Target city name, e.g., Beijing", required = true) String cityName
    ) {
        if (StringUtils.isBlank(cityName)) {
            throw new IllegalArgumentException("City name must not be empty");
        }

        ClimateRecord cachedRecord = localForecastCache.getIfPresent(cityName);
        if (cachedRecord != null) {
            log.debug("Cache hit for city: {}", cityName);
            return ClimatePayload.success(cachedRecord);
        }

        String regionCode = MUNICIPALITY_DICT.get(cityName);
        if (regionCode == null) {
            throw new IllegalArgumentException("Unsupported region: " + cityName);
        }

        try {
            String rawResponse = httpClient.get()
                .uri(builder -> builder
                    .path("/weather/weatherInfo")
                    .queryParam("city", regionCode)
                    .queryParam("key", "${amap.secret-key}")
                    .queryParam("extensions", "all")
                    .build())
                .retrieve()
                .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
                    resp -> Mono.error(new ServiceException("API invocation failed: " + resp.statusCode())))
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5))
                .block();

            ClimateRecord record = deserializePayload(rawResponse);
            localForecastCache.put(cityName, record);
            return ClimatePayload.success(record);

        } catch (TimeoutException ex) {
            log.error("Meteorological API timeout for city: {}", cityName, ex);
            return ClimatePayload.fail("Request timeout, please retry later");
        } catch (Exception ex) {
            log.error("Failed to fetch climate data for city: {}", cityName, ex);
            return ClimatePayload.fail("Data acquisition failed: " + ex.getMessage());
        }
    }

    private ClimateRecord deserializePayload(String json) {
        // 实际生产中需进行严格的JSON反序列化与字段映射
        return new ClimateRecord("Sunny", 26.0, 18.5, 31.0, "South", 2, 55, LocalDateTime.now());
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ClimateRecord {
        private String status;
        private Double currentTemp;
        private Double minTemp;
        private Double maxTemp;
        private String windDir;
        private Integer windScale;
        private Integer humidityPct;
        private LocalDateTime lastUpdate;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ClimatePayload {
        private boolean success;
        private String message;
        private ClimateRecord data;

        public static ClimatePayload success(ClimateRecord data) { return new ClimatePayload(true, "OK", data); }
        public static ClimatePayload fail(String msg) { return new ClimatePayload(false, msg, null); }
    }
}

该实现具备以下工程化特征:

  • 前置校验:阻断无效请求向下游传递。
  • 本地短时缓存:利用Caffeine降低对第三方API的调用频次,提升响应稳定性。
  • 非阻塞超时:通过WebClient的timeout控制避免线程长时间挂起。
  • 异常隔离:将网络异常转化为业务友好的结构化降级响应。

2.4 工具注册与发布

将带有@Tool注解的Service实例注入Spring AI的ToolCallbackProvider,即可完成MCP工具的自动发布:

@Configuration
public class ToolExportConfiguration {

    @Bean
    public ToolCallbackProvider mcpToolRegistrar(ClimateDataService climateDataService) {
        return MethodToolCallbackProvider.builder()
            .toolObjects(climateDataService)
            .build();
    }

    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

2.5 启动与连通性验证

@SpringBootApplication
public class WeatherMcpServerApp {

    public static void main(String[] args) {
        SpringApplication.run(WeatherMcpServerApp.class, args);
    }

    @Bean
    public CommandLineRunner logRegisteredTools(ToolCallbackProvider provider) {
        return args -> {
            log.info("MCP Server initialized. Exposed tools:");
            provider.getTools().forEach(tool -> log.info("- Tool ID: {}, Desc: {}", tool.getName(), tool.getDescription()));
        };
    }
}

服务启动后,监听http://localhost:9090/mcp-hub/events端点,若客户端能成功建立SSE连接并完成初始化握手,则表明Server端部署就绪。

3. AI应用端MCP Client集成

3.1 客户端依赖体系

在业务应用侧引入MCP Client Starter及对应的大模型驱动依赖:

<dependencies>
    <!-- MCP Client -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-client</artifactId>
    </dependency>

    <!-- 大语言模型驱动 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
        <version>1.0.0-M6</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.2 客户端连接配置

spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            climate-service:
              url: http://localhost:9090/mcp-hub/events
    zhipuai:
      api-key: ${ZHIPU_API_KEY}
      chat:
        options:
          model: glm-4

通过上述配置,Spring AI将在应用启动时自动与目标MCP Server建立SSE连接,拉取远端工具列表,并将其转换为标准Function Call格式注入至大模型的Prompt上下文中,从而实现业务系统与大模型工具能力的无缝打通。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。