SpringAI+MCPClient实战-MiniMax模型打造智能AIAgent

文章目录

    • 一、前言
    • [二、MCP Client 核心概念](#二、MCP Client 核心概念)
      • [2.1 MCP Client 是什么?](#2.1 MCP Client 是什么?)
      • [2.2 核心术语解释](#2.2 核心术语解释)
      • [2.3 MCP Client 架构全景](#2.3 MCP Client 架构全景)
      • [2.4 MCP Client 传输方式对比](#2.4 MCP Client 传输方式对比)
    • 三、完整实战项目
      • [3.1 项目功能清单](#3.1 项目功能清单)
      • [3.2 项目结构](#3.2 项目结构)
      • [3.3 依赖配置](#3.3 依赖配置)
      • [3.4 配置文件(application.yml)](#3.4 配置文件(application.yml))
      • [3.5 启动类](#3.5 启动类)
      • [3.6 本地工具实现](#3.6 本地工具实现)
      • [3.7 工具调用日志拦截器](#3.7 工具调用日志拦截器)
      • [3.8 MCP Client 请求定制器配置](#3.8 MCP Client 请求定制器配置)
      • [3.9 ChatClient 配置](#3.9 ChatClient 配置)
      • [3.10 动态工具加载服务](#3.10 动态工具加载服务)
      • [3.11 聊天服务](#3.11 聊天服务)
      • [3.12 控制器](#3.12 控制器)
    • 四、一次工具调用的完整链路分析
      • [4.1 完整调用流程图](#4.1 完整调用流程图)
      • [4.2 调用链路详细说明](#4.2 调用链路详细说明)
    • 五、端到端测试
      • [5.1 启动服务](#5.1 启动服务)
      • [5.2 测试用例](#5.2 测试用例)
      • [5.3 预期效果](#5.3 预期效果)
      • [5.4 调试日志示例](#5.4 调试日志示例)
    • 六、部署方案
      • [6.1 Docker 部署](#6.1 Docker 部署)
      • [6.2 Kubernetes 部署](#6.2 Kubernetes 部署)
    • 七、总结

一、前言

各位好,上一篇我们聊了 MCP Server 端的架构设计和实战代码,今天来聊聊 MCP Client 端。在实际项目中,MCP Client 才是真正面向用户的 AI 应用核心,它负责连接各种 MCP Server,自动发现并调用工具,让大模型具备"动手"的能力。

本文与上一篇的关系

  • 上一篇:MCP Server 端,提供天气查询、订单查询、审批通知等工具服务(端口 8080)
  • 这一篇:MCP Client 端,连接 Server 端的工具,让 MiniMax 大模型能够调用这些工具

为什么选 MiniMax 作为示例模型?因为国内大模型里,MiniMax 的 API 设计非常规范,而且 Spring AI 1.1.5 版本已经原生支持 MiniMax,配合 MCP 协议,简直是天作之合。

本文特色:一个 Demo 项目搞定核心功能!我们将构建一个轻量的 MCP Client 项目,包含:

  • 连接多个 MCP Server(天气、订单、文件系统等)
  • 本地工具与 MCP 工具混合使用
  • 动态工具加载(基于角色配置)
  • 同步和流式对话
  • 对话历史管理
  • 完整的端到端测试

废话不多说,直接上干货!

二、MCP Client 核心概念

2.1 MCP Client 是什么?

MCP Client 是 AI 应用与 MCP Server 之间的桥梁。它负责:

  • 协议协商:与 Server 协商 MCP 协议版本和能力
  • 工具发现:自动获取 Server 暴露的工具列表和 JSON Schema
  • 工具调用:将 LLM 的工具调用请求转发给 Server
  • 结果回传:将 Server 的执行结果返回给 LLM

2.2 核心术语解释

术语 解释 类比
MCP (Model Context Protocol) 模型上下文协议,AI 应用与工具服务之间的标准通信协议 就像 USB 协议,让不同设备可以即插即用
MCP Client 连接 MCP Server 的客户端,负责工具发现和调用 就像浏览器,连接各种网站服务
MCP Server 提供工具服务的后端,暴露工具给 Client 调用 就像网站服务器,提供各种 API
ToolCallback Spring AI 中的工具回调接口,统一本地工具和 MCP 工具 就像统一的插头接口,不管什么电器都能插
ChatClient Spring AI 的对话客户端,构建 Prompt 并调用模型 就像智能助手的大脑,理解用户意图并执行
ChatModel 大模型接口(MiniMax、OpenAI 等),负责与 LLM 通信 就像 AI 的思考引擎
Streamable HTTP MCP 的传输协议,基于 HTTP 的流式通信 就像 WebSocket,支持实时双向通信
JSON-RPC MCP 的通信协议格式,基于 JSON 的远程过程调用 就像 REST API,但使用 JSON 格式

2.3 MCP Client 架构全景

2.4 MCP Client 传输方式对比

传输方式 适用场景 优势 劣势
Streamable HTTP 远程服务、微服务 断线重连、会话恢复、统一端点 需要 HTTP 基础设施
SSE 远程服务(旧版) 实现简单 断线无法恢复,逐步弃用
STDIO 本地进程 简单安全,无需网络 仅支持本地,无法跨网络

三、完整实战项目

3.1 项目功能清单

我们将构建一个轻量的 MCP Client Demo 项目,包含以下功能:

功能 说明 实现方式
多 MCP Server 连接 同时连接天气、订单、文件等多个 Server spring.ai.mcp.client.connections 配置
本地工具 项目内的 Java 工具方法 Function 接口
混合工具调用 LLM 自动选择本地或 MCP 工具 统一注册到 ChatClient
动态工具加载 基于角色配置动态加载工具 Mock 数据模拟角色配置
同步对话 一次性返回完整结果 chatClient.prompt().call()
流式对话 实时流式返回结果 chatClient.prompt().stream()
对话历史 支持多轮对话上下文 ChatMemory
工具调用日志 记录工具调用详情 Advisor 拦截器

3.2 项目结构

复制代码
mcp-client-demo/
├── pom.xml                          # Maven 依赖配置
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/mcpclient/
│   │   │       ├── McpClientApplication.java          # 启动类
│   │   │       ├── config/
│   │   │       │   ├── McpRequestCustomizerProperties.java  # MCP 请求定制器配置属性
│   │   │       │   ├── McpClientConfig.java           # MCP Client HTTP 请求定制器
│   │   │       │   └── ChatConfig.java                # ChatClient 配置
│   │   │       ├── tool/
│   │   │       │   └── LocalWeatherTool.java          # 本地天气工具
│   │   │       ├── advisor/
│   │   │       │   └── ToolCallLoggingAdvisor.java    # 工具调用日志拦截器
│   │   │       ├── service/
│   │   │       │   ├── DynamicToolService.java        # 动态工具加载服务
│   │   │       │   └── ChatService.java               # 聊天服务
│   │   │       └── controller/
│   │   │           └── ChatController.java            # REST API 控制器
│   │   └── resources/
│   │       └── application.yml                        # 配置文件
│   └── test/
│       └── java/
│           └── com/example/mcpclient/
│               └── McpClientApplicationTests.java
└── target/

说明:项目结构按依赖关系排列,底层配置和工具类在前,上层服务和控制器在后。

3.3 依赖配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.5</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>mcp-client-demo</artifactId>
    <version>1.0.0</version>
    <name>MCP Client Demo</name>
    <description>轻量的 MCP Client Demo 项目,包含多 Server 连接、本地工具混合、动态加载等功能</description>
    
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.1.5</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>
        <!-- Web 相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- MiniMax 模型 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-minimax</artifactId>
        </dependency>
        
        <!-- MCP Client - 支持 Streamable HTTP -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-client</artifactId>
        </dependency>
        
        <!-- 对话历史支持 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-client-chat</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.4 配置文件(application.yml)

yaml 复制代码
server:
  port: 8081  # Client 端口,与 Server 端(8080)区分开

spring:
  ai:
    # MiniMax 模型配置
    minimax:
      api-key: ${MINIMAX_API_KEY:your-minimax-api-key-here}
      chat:
        options:
          model: abab6.5s-chat  # MiniMax 模型
          temperature: 0.7
    
    # MCP Client 配置 - Streamable HTTP 模式
    mcp:
      client:
        enabled: true
        name: mini-mcp-client
        version: 1.0.0
        type: sync  # sync=同步模式,async=异步模式
        request-timeout: 60s
        # 多 MCP Server 连接配置(streamable-http 传输方式)
        streamable-http:
          connections:
            server-a:
              url: http://localhost:8080
              endpoint: /mcp
              auth:
                type: api-key
                header-name: "X-API-Key"
                header-value: "demo-api-key"
#            server-b:
#              url: http://localhost:8080
#              endpoint: /mcp
#              auth:
#                type: bearer
#                token: "token-for-server-b"

# 日志配置(调试时开启)
logging:
  level:
    io.modelcontextprotocol: debug  # MCP 协议调试日志
    com.example.mcpclient: debug    # 项目调试日志
连接配置说明

每个连接支持以下配置项:

配置项 说明 示例
url MCP Server 的基础 URL http://localhost:8080
endpoint MCP 端点路径 /mcp
headers 静态请求头(所有请求都添加) X-API-Key: "demo-api-key"
auth 认证配置(可选) 见下方认证配置说明
认证配置(auth)

支持两种认证方式:

1. API Key 认证

yaml 复制代码
auth:
  type: api-key
  header-name: "X-API-Key"
  header-value: "your-api-key"

2. Bearer Token 认证

yaml 复制代码
auth:
  type: bearer
  token: "your-bearer-token"

注意 :没有配置 auth 的连接将不添加额外认证头,使用默认的 headers 配置即可。

3.5 启动类

java 复制代码
package com.example.mcpclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * MCP Client 启动类
 * 一个 Demo 项目搞定核心功能:多 Server 连接、本地工具混合、动态加载等
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@SpringBootApplication
public class McpClientApplication {

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

3.6 本地工具实现

java 复制代码
package com.example.mcpclient.tool;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 * 本地天气查询工具
 * 与 MCP 工具互补,提供本地模拟数据
 * 
 * 为什么需要本地工具?
 * - 有些工具不需要远程调用,直接在本地执行更快
 * - 可以模拟数据用于测试
 * - 与 MCP 工具混合使用,LLM 会自动选择最合适的工具
 * 
 * Spring AI 1.1.5 变化:
 * - 使用 @Tool 注解标记工具方法,而不是实现 Function 接口
 * - MethodToolCallbackProvider 会自动扫描 @Tool 注解的方法
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Component("local_weather_query")
public class LocalWeatherTool {

    private static final String[] CONDITIONS = {"晴朗", "多云", "阴天", "小雨", "大雨", "雷雨"};
    private final Random random = new Random();

    /**
     * 工具请求参数
     * JSON Schema 会自动生成,用于告诉 LLM 如何调用这个工具
     */
    @Data
    @JsonClassDescription("查询指定城市的天气信息")
    public static class Request {
        @JsonProperty(required = true, value = "city")
        @JsonPropertyDescription("城市名称,例如:北京、上海、广州")
        private String city;
    }

    /**
     * 工具响应结果
     */
    @Data
    public static class Response {
        private String city;
        private WeatherInfo weatherInfo;

        @Data
        public static class WeatherInfo {
            private Integer temperature;
            private String condition;
            private Integer humidity;
            private Integer windSpeed;
        }
    }

    /**
     * 查询天气的工具方法
     * 使用 @Tool 注解,Spring AI 会自动将其注册为 ToolCallback
     */
    @Tool(description = "查询指定城市的天气信息")
    public Response queryWeather(Request request) {
        Response.WeatherInfo weatherInfo = new Response.WeatherInfo();
        weatherInfo.setTemperature(random.nextInt(35) - 5);  // -5 ~ 30°C
        weatherInfo.setCondition(CONDITIONS[random.nextInt(CONDITIONS.length)]);
        weatherInfo.setHumidity(random.nextInt(80) + 20);    // 20% ~ 100%
        weatherInfo.setWindSpeed(random.nextInt(30) + 1);    // 1 ~ 30 km/h
        
        Response response = new Response();
        response.setCity(request.getCity());
        response.setWeatherInfo(weatherInfo);
        return response;
    }
}

3.7 工具调用日志拦截器

java 复制代码
package com.example.mcpclient.advisor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.stereotype.Component;

/**
 * 工具调用日志记录 Advisor
 * 记录每次工具调用的详细信息,方便调试和监控
 * 
 * Advisor 是什么?
 * - 类似于 Spring 的拦截器,可以在工具调用前后执行逻辑
 * - 用于日志记录、性能监控、权限检查等
 * 
 * Spring AI 1.1.5 变化:
 * - 使用 CallAdvisor 接口替代 CallAroundAdvisor
 * - 使用 ChatClientRequest 和 ChatClientResponse 替代 AdvisedRequest 和 AdvisedResponse
 * - 使用 adviseCall 方法替代 aroundCall 方法
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Component
@Slf4j
public class ToolCallLoggingAdvisor implements CallAdvisor {

    @Override
    public String getName() {
        return "ToolCallLoggingAdvisor";
    }

    @Override
    public int getOrder() {
        return 0;  // 执行顺序,数字越小越先执行
    }

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        // 工具调用前
        log.info("=== 工具调用开始 ===");
        log.info("请求内容: {}", request.prompt().getContents());
        long startTime = System.currentTimeMillis();
        
        // 执行工具调用
        ChatClientResponse response = chain.nextCall(request);
        
        // 工具调用后
        long duration = System.currentTimeMillis() - startTime;
        log.info("响应结果: {}", response);
        log.info("耗时: {}ms", duration);
        log.info("=== 工具调用结束 ===");
        
        return response;
    }
}

3.8 MCP Client 请求定制器配置

java 复制代码
package com.example.mcpclient.config;

import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.common.McpTransportContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.net.http.HttpRequest;

/**
 * MCP Client 配置
 * 负责定制 HTTP 请求,例如添加认证头、日志记录等
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Configuration
@Slf4j
@RequiredArgsConstructor
public class McpClientConfig {

    private final McpRequestCustomizerProperties customizerProperties;

    @Bean
    public McpSyncHttpClientRequestCustomizer mcpHttpClientRequestCustomizer() {
        return (builder, method, endpoint, body, context) -> {
            String requestUrl = endpoint.toString();
            
            // 根据请求 URL 匹配对应的 server 配置
            customizerProperties.getConnections().values().stream()
                    .filter(conn -> requestUrl.startsWith(conn.getUrl()))
                    .findFirst()
                    .ifPresent(conn -> {
                        McpRequestCustomizerProperties.AuthConfig auth = conn.getAuth();
                        if (auth != null) {
                            applyAuth(builder, auth);
                            log.info("MCP request to {} with auth type={}: method={}", 
                                    endpoint.getHost(), auth.getType(), method);
                        } else {
                            log.info("MCP request to {} without custom auth: method={}", 
                                    endpoint.getHost(), method);
                        }
                    });
        };
    }

    private void applyAuth(HttpRequest.Builder builder, McpRequestCustomizerProperties.AuthConfig auth) {
        switch (auth.getType()) {
            case "api-key":
                builder.header(auth.getHeaderName(), auth.getHeaderValue());
                break;
            case "bearer":
                builder.header("Authorization", "Bearer " + auth.getToken());
                break;
            default:
                log.warn("Unknown auth type: {}", auth.getType());
        }
    }
}
配置属性类
java 复制代码
package com.example.mcpclient.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

/**
 * MCP 请求定制器配置属性
 * 绑定 spring.ai.mcp.client.streamable-http 配置
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.ai.mcp.client.streamable-http")
public class McpRequestCustomizerProperties {

    private Map<String, ServerConnection> connections;

    @Data
    public static class ServerConnection {
        private String url;
        private String endpoint;
        private AuthConfig auth;
    }

    @Data
    public static class AuthConfig {
        private String type;
        private String headerName;
        private String headerValue;
        private String token;
    }
}

3.9 ChatClient 配置

java 复制代码
package com.example.mcpclient.config;

import com.example.mcpclient.advisor.ToolCallLoggingAdvisor;
import com.example.mcpclient.service.DynamicToolService;
import com.example.mcpclient.tool.LocalWeatherTool;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * ChatClient 配置
 * 一个配置类搞定所有功能:
 * - 多 MCP Server 工具注入
 * - 本地工具注入
 * - 对话历史管理
 * - 工具调用日志
 * - 动态工具加载服务(基于角色)
 * 
 * Spring AI 1.1.5 变化:
 * - ChatMemory 由 Spring Boot 自动配置,无需手动创建 InMemoryChatMemory Bean
 * - MessageChatMemoryAdvisor 使用 builder 模式构建
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Configuration
public class ChatConfig {

    private final LocalWeatherTool localWeatherTool;
    private final ToolCallLoggingAdvisor toolCallLoggingAdvisor;
    private final ChatModel chatModel;
    private final ToolCallbackResolver toolCallbackResolver;

    public ChatConfig(LocalWeatherTool localWeatherTool, 
                     ToolCallLoggingAdvisor toolCallLoggingAdvisor,
                     ChatModel chatModel,
                     ToolCallbackResolver toolCallbackResolver) {
        this.localWeatherTool = localWeatherTool;
        this.toolCallLoggingAdvisor = toolCallLoggingAdvisor;
        this.chatModel = chatModel;
        this.toolCallbackResolver = toolCallbackResolver;
    }

    /**
     * 默认 ChatClient(包含所有工具)
     * 用于 /api/chat 接口
     * 
     * 注意:
     * - McpSyncClient 由 Spring AI 自动创建
     * - ChatMemory 由 Spring Boot 自动配置(基于 spring-ai-client-chat 依赖)
     * - 根据 application.yml 中的 spring.ai.mcp.client.connections 配置
     *   每个 connection 会自动创建一个 McpSyncClient Bean
     */
    @Bean
    public ChatClient chatClient(ChatMemory chatMemory, List<McpSyncClient> mcpClients) {
        // 1. 收集所有 MCP Server 的工具
        // Spring AI 已自动创建多个 McpSyncClient,每个对应一个 Server
        SyncMcpToolCallbackProvider[] mcpProviders = mcpClients.stream()
                .map(SyncMcpToolCallbackProvider::new)
                .toArray(SyncMcpToolCallbackProvider[]::new);
        
        // 2. 本地工具
        MethodToolCallbackProvider localProvider = MethodToolCallbackProvider.builder()
                .toolObjects(localWeatherTool)
                .build();
        
        // 3. 构建 ChatClient
        return ChatClient.builder(chatModel)
                .defaultSystem("你是一个智能助手,可以帮助用户查询天气、订单、文件等信息。\n" +
                        "当用户询问天气、订单或文件时,请使用相应的工具来获取信息。\n" +
                        "回答要简洁明了,直接给出用户需要的信息。")
                // 注入所有工具(MCP + 本地)
                .defaultToolCallbacks(mcpProviders)
                .defaultToolCallbacks(localProvider)
                // 注入对话历史(使用 builder 模式)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build()
                )
                // 注入工具调用日志
                .defaultAdvisors(toolCallLoggingAdvisor)
                .build();
    }

    /**
     * 动态工具加载服务
     * 用于 /api/role/chat 接口,根据角色动态加载工具
     */
    @Bean
    public DynamicToolService dynamicToolService(ChatModel chatModel, ChatMemory chatMemory, List<McpSyncClient> mcpClients) {
        return new DynamicToolService(chatModel, chatMemory, mcpClients, toolCallbackResolver);
    }
}

3.10 动态工具加载服务

java 复制代码
package com.example.mcpclient.service;

import io.modelcontextprotocol.client.McpSyncClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 动态工具加载服务
 * 根据角色配置动态加载本地工具和 MCP 工具
 * 
 * 使用场景:
 * - 客服助手角色:只能查询天气和订单
 * - 财务助手角色:只能查询订单和处理支付
 * - 管理员角色:可以使用所有工具
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class DynamicToolService {

    private final ChatModel chatModel;
    private final ChatMemory chatMemory;
    private final List<McpSyncClient> mcpClients;
    private final ToolCallbackResolver toolCallbackResolver;
    
    // Mock 角色配置(实际项目中从数据库读取)
    private final Map<Long, ChatRole> roleConfig = Map.of(
        1L, new ChatRole(1L, "客服助手", "你是一个客服助手,可以帮助用户查询天气和订单。",
                List.of("local_weather_query"), List.of("business-server")),
        2L, new ChatRole(2L, "财务助手", "你是一个财务助手,可以帮助用户查询订单和处理支付。",
                List.of(), List.of("business-server", "payment-server"))
    );

    /**
     * 根据角色 ID 创建 ChatClient
     * 自动加载角色关联的工具
     */
    public ChatClient createChatClientByRole(Long roleId) {
        ChatRole role = roleConfig.get(roleId);
        if (role == null) {
            throw new IllegalArgumentException("角色不存在: " + roleId);
        }

        List<ToolCallback> toolCallbacks = new ArrayList<>();

        // 1. 加载本地工具
        role.toolNames().forEach(toolName -> {
            ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
            if (toolCallback != null) {
                toolCallbacks.add(toolCallback);
            }
        });

        // 2. 加载 MCP 工具
        role.mcpClientNames().forEach(mcpClientName -> {
            // 匹配对应的 McpSyncClient
            mcpClients.stream()
                .filter(client -> client.getClientInfo().name().contains(mcpClientName))
                .findFirst()
                .ifPresent(client -> {
                    ToolCallback[] mcpToolCallbacks = 
                        new SyncMcpToolCallbackProvider(client).getToolCallbacks();
                    toolCallbacks.addAll(List.of(mcpToolCallbacks));
                });
        });

        // 3. 构建 ChatClient
        return ChatClient.builder(chatModel)
                .defaultSystem(role.systemMessage())
                .defaultToolCallbacks(toolCallbacks.toArray(new ToolCallback[0]))
                .build();
    }

    /**
     * 角色定义
     */
    public record ChatRole(Long id, String name, String systemMessage, 
                          List<String> toolNames, List<String> mcpClientNames) {}
}

3.11 聊天服务

java 复制代码
package com.example.mcpclient.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;

/**
 * 聊天服务
 * 封装 ChatClient 调用逻辑
 * 
 * 一个 Service 搞定所有功能:
 * - 同步对话
 * - 流式对话
 * - SSE 流式对话
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ChatService {

    private final ChatClient chatClient;

    /**
     * 同步对话
     * @param message 用户消息
     * @return AI 回复
     */
    public String chat(String message) {
        log.info("用户消息: {}", message);
        
        String response = chatClient.prompt()
                .user(message)
                .call()
                .content();
        
        log.info("AI 回复: {}", response);
        return response;
    }

    /**
     * 流式对话
     * @param message 用户消息
     * @return AI 回复(流式拼接)
     */
    public String chatStream(String message) {
        log.info("用户消息(流式): {}", message);
        
        StringBuilder result = new StringBuilder();
        
        Flux<String> stream = chatClient.prompt()
                .user(message)
                .stream()
                .content();
        
        stream.doOnNext(chunk -> {
            result.append(chunk);
            System.out.print(chunk);
        }).then().block();
        
        return result.toString();
    }

    /**
     * SSE 流式对话
     * @param message 用户消息
     * @return SseEmitter
     */
    public SseEmitter chatSse(String message) {
        SseEmitter emitter = new SseEmitter(60000L);  // 60 秒超时
        
        Flux<String> stream = chatClient.prompt()
                .user(message)
                .stream()
                .content();
        
        stream.doOnNext(chunk -> {
            try {
                emitter.send(SseEmitter.event().data(chunk));
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        })
        .doOnComplete(emitter::complete)
        .doOnError(emitter::completeWithError)
        .subscribe();
        
        return emitter;
    }
}

3.12 控制器

java 复制代码
package com.example.mcpclient.controller;

import com.example.mcpclient.service.ChatService;
import com.example.mcpclient.service.DynamicToolService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * 聊天控制器
 * 提供同步、流式、SSE 三种对话接口
 * 支持基于角色的动态工具加载
 * 
 * 接口说明:
 * - POST /api/chat          : 同步对话,一次性返回完整结果
 * - POST /api/chat/stream   : 流式对话,实时返回文本片段
 * - GET  /api/chat/sse      : SSE 流式对话,浏览器可直接使用
 * - POST /api/role/chat     : 基于角色的对话(动态加载工具)
 *
 * @author senfel
 * @date 2023/11/5 14:01
 */
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class ChatController {

    private final ChatService chatService;
    private final DynamicToolService dynamicToolService;

    /**
     * 同步对话接口
     * @param message 用户消息
     * @return AI 回复
     */
    @PostMapping("/chat")
    public String chat(@RequestParam String message) {
        log.info("用户消息: {}", message);
        return chatService.chat(message);
    }

    /**
     * 流式对话接口
     * @param message 用户消息
     * @return AI 回复(流式拼接)
     */
    @PostMapping("/chat/stream")
    public String chatStream(@RequestParam String message) {
        log.info("用户消息(流式): {}", message);
        return chatService.chatStream(message);
    }

    /**
     * SSE 流式对话接口
     * SSE (Server-Sent Events) 是什么?
     * - 服务器向浏览器推送实时事件的技术
     * - 基于 HTTP,单向通信(服务器 → 客户端)
     * - 浏览器可直接使用 EventSource API 接收
     * 
     * @param message 用户消息
     * @return SSE 流
     */
    @GetMapping(value = "/chat/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatSse(@RequestParam String message) {
        log.info("用户消息(SSE): {}", message);
        return chatService.chatSse(message);
    }

    /**
     * 基于角色的同步对话
     * @param roleId 角色 ID(1=客服助手,2=财务助手)
     * @param message 用户消息
     * @return AI 回复
     */
    @PostMapping("/role/chat")
    public String roleChat(@RequestParam Long roleId, @RequestParam String message) {
        log.info("角色 ID: {}, 用户消息: {}", roleId, message);
        
        // 根据角色动态创建 ChatClient(自动加载对应工具)
        org.springframework.ai.chat.client.ChatClient chatClient = 
                dynamicToolService.createChatClientByRole(roleId);
        
        String response = chatClient.prompt()
                .user(message)
                .call()
                .content();
        
        log.info("AI 回复: {}", response);
        return response;
    }
}

四、一次工具调用的完整链路分析

4.1 完整调用流程图

复制代码
用户                    ChatController              ChatService              ChatClient
 │                          │                          │                        │
 │  "北京天气怎么样?"        │                          │                        │
 │─────────────────────────>│                          │                        │
 │                          │  chat("北京天气怎么样?")  │                        │
 │                          │─────────────────────────>│                        │
 │                          │                          │  prompt().user(msg)    │
 │                          │                          │───────────────────────>│
 │                          │                          │                        │
 │                          │                          │                   构建 Prompt
 │                          │                          │              + 附加工具列表
 │                          │                          │                        │
 │                          │                          │                        ▼
 │                          │                          │                  MiniMax ChatModel
 │                          │                          │                        │
 │                          │                          │  1. 分析用户意图         │
 │                          │                          │  2. 决定调用 getWeather │
 │                          │                          │  3. 生成参数 cityName   │
 │                          │                          │<───────────────────────│
 │                          │                          │                        │
 │                          │                          ▼                        │
 │                          │                    McpSyncClient                  │
 │                          │                    SyncMcpToolCallbackProvider    │
 │                          │                        │                        │
 │                          │                        │  封装 JSON-RPC 请求      │
 │                          │                        │  tools/call              │
 │                          │                        │  HTTP POST /mcp          │
 │                          │                        │                        │
 │                          │                        ▼                        │
 │                          │              ┌─────────────────┐                 │
 │                          │              │  MCP Server     │                 │
 │                          │              │  (localhost:8080)│                │
 │                          │              │  WeatherTool    │                 │
 │                          │              └─────────────────┘                 │
 │                          │                        │                        │
 │                          │                        │  返回天气数据            │
 │                          │                        │<───────────────────────│
 │                          │                          │                        │
 │                          │                          ▼                        │
 │                          │                    MiniMax ChatModel              │
 │                          │                        │                        │
 │                          │                        │  基于工具结果生成回复     │
 │                          │                        │<───────────────────────│
 │                          │                          │                        │
 │                          │  返回 AI 回复              │                        │
 │                          │<─────────────────────────│                        │
 │  "北京今天天气晴朗..."    │                          │                        │
 │<─────────────────────────│                          │                        │

4.2 调用链路详细说明

步骤 组件 动作 说明
1 用户 发送自然语言请求 "北京天气怎么样?"
2 ChatController 接收请求,调用 Service POST /api/chat?message=北京天气怎么样?
3 ChatService 调用 ChatClient chatClient.prompt().user(message).call()
4 ChatClient 构建 Prompt,注入工具列表 将所有工具(MCP + 本地)转换为 JSON Schema
5 MiniMax ChatModel 分析用户意图 理解用户想要查询天气
6 MiniMax ChatModel 决定调用工具 选择 getWeather 工具,生成参数 {cityName: "北京"}
7 McpSyncClient 封装 JSON-RPC 请求 转换为 MCP 协议格式
8 Transport HTTP POST 到 Server POST http://localhost:8080/mcp
9 MCP Server 执行工具方法 WeatherTool.getWeather("北京")
10 MCP Server 返回结构化结果 {city: "北京", temperature: 22, condition: "晴朗"}
11 MiniMax ChatModel 基于工具结果生成回复 "北京今天天气晴朗,温度 22°C..."
12 ChatClient 返回最终结果 返回给用户

五、端到端测试

5.1 启动服务

bash 复制代码
# 1. 先启动 MCP Server(端口 8080)
# 参考:SpringAI+MCPServer实战-StreamableHTTP协议打造企业级AI工具服务.md
cd ../mcp-server
mvn spring-boot:run

# 2. 启动 MCP Client(端口 8081)
cd mcp-client-demo
mvn spring-boot:run

5.2 测试用例

bash 复制代码
# 测试 1:本地工具调用(天气查询)
curl -X POST "http://localhost:8081/api/chat" \
  -d "message=北京今天天气怎么样"

# 测试 2:MCP 工具调用(订单查询)
curl -X POST "http://localhost:8081/api/chat" \
  -d "message=帮我查一下订单 ORD-1001 的状态"

# 测试 3:多工具调用(本地 + MCP)
curl -X POST "http://localhost:8081/api/chat" \
  -d "message=先查一下北京天气,再帮我看看 ORD-1002 订单发货没"

# 测试 4:流式对话
curl -X POST "http://localhost:8081/api/chat/stream" \
  -d "message=今天适合外出吗?"

# 测试 5:SSE 流式对话(浏览器可直接访问)
curl -X GET "http://localhost:8081/api/chat/sse?message=上海天气怎么样"

# 测试 6:基于角色的对话(客服助手角色)
curl -X POST "http://localhost:8081/api/role/chat" \
  -d "roleId=1&message=北京天气怎么样"

# 测试 7:基于角色的对话(财务助手角色)
curl -X POST "http://localhost:8081/api/role/chat" \
  -d "roleId=2&message=订单 ORD-1001 发货没"

5.3 预期效果

测试 1:本地工具调用

复制代码
用户:北京今天天气怎么样
AI:北京今天天气晴朗,温度 18°C,湿度 45%,风速 12 km/h。天气不错,适合外出!

测试 2:MCP 工具调用

复制代码
用户:帮我查一下订单 ORD-1001 的状态
AI:订单 ORD-1001 购买的是 iPhone 16 Pro,当前状态为"已发货",预计 2 天后送达,价格 8999.00 元。

测试 3:多工具调用

复制代码
用户:先查一下北京天气,再帮我看看 ORD-1002 订单发货没
AI:北京今天多云,温度 22°C。订单 ORD-1002(MacBook Air M3)当前状态为"处理中",预计 5 天后送达。

5.4 调试日志示例

复制代码
2025-06-06 10:30:15.123 DEBUG [main] i.m.client.McpSyncClient : Initializing MCP client...
2025-06-06 10:30:15.456 DEBUG [main] i.m.client.McpSyncClient : Connected to server: business-server v1.0.0
2025-06-06 10:30:15.789 DEBUG [main] i.m.client.McpSyncClient : Connected to server: filesystem-server v1.0.0
2025-06-06 10:30:16.123 DEBUG [main] i.m.client.McpSyncClient : Connected to server: payment-server v1.0.0
2025-06-06 10:30:16.456 DEBUG [main] i.m.client.McpSyncClient : Discovered tools: [getWeather, getOrderStatus, listFiles, processPayment]
2025-06-06 10:30:16.789 INFO  [main] c.e.m.config.ChatConfig : Registered local tool: local_weather_query
2025-06-06 10:30:20.123 INFO  [http-nio-8081-exec-1] c.e.m.service.ChatService : 用户消息: 北京今天天气怎么样
2025-06-06 10:30:21.456 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : === 工具调用开始 ===
2025-06-06 10:30:21.456 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : 工具名称: local_weather_query
2025-06-06 10:30:21.456 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : 工具参数: {city=北京}
2025-06-06 10:30:21.789 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : 工具结果: {city=北京, weatherInfo={temperature=22, condition=晴朗, ...}}
2025-06-06 10:30:21.789 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : 耗时: 333ms
2025-06-06 10:30:21.789 INFO  [http-nio-8081-exec-1] c.e.m.advisor.ToolCallLoggingAdvisor : === 工具调用结束 ===
2025-06-06 10:30:22.123 INFO  [http-nio-8081-exec-1] c.e.m.service.ChatService : AI 回复: 北京今天天气晴朗,温度 22°C...

六、部署方案

6.1 Docker 部署

dockerfile 复制代码
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

COPY target/mcp-client-demo-1.0.0.jar app.jar

EXPOSE 8081

ENV MINIMAX_API_KEY=your-api-key-here

ENTRYPOINT ["java", "-jar", "app.jar"]
bash 复制代码
# 构建镜像
docker build -t mcp-client-demo:1.0.0 .

# 运行容器
docker run -d -p 8081:8081 \
  -e MINIMAX_API_KEY=your-real-api-key \
  --name mcp-client-demo mcp-client-demo:1.0.0

6.2 Kubernetes 部署

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-client-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: mcp-client-demo
  template:
    metadata:
      labels:
        app: mcp-client-demo
    spec:
      containers:
      - name: mcp-client-demo
        image: mcp-client-demo:1.0.0
        ports:
        - containerPort: 8081
        env:
        - name: MINIMAX_API_KEY
          valueFrom:
            secretKeyRef:
              name: minimax-secret
              key: api-key
        - name: SPRING_AI_MCP_CLIENT_CONNECTIONS_BUSINESS-SERVER_URL
          value: "http://business-mcp-server/mcp"
        - name: SPRING_AI_MCP_CLIENT_CONNECTIONS_FILESYSTEM-SERVER_URL
          value: "http://filesystem-mcp-server/mcp"
        - name: SPRING_AI_MCP_CLIENT_CONNECTIONS_PAYMENT-SERVER_URL
          value: "http://payment-mcp-server/mcp"
---
apiVersion: v1
kind: Service
metadata:
  name: mcp-client-demo
spec:
  selector:
    app: mcp-client-demo
  ports:
  - port: 80
    targetPort: 8081
  type: ClusterIP
---
apiVersion: v1
kind: Secret
metadata:
  name: minimax-secret
type: Opaque
data:
  api-key: eW91ci1iYXNlNjQtZW5jb2RlZC1hcGkta2V5

七、总结

通过本文的实战,我们完成了一个轻量的 MCP Client Demo 项目,包含:

  1. 多 MCP Server 连接:同时连接天气、订单、文件、支付等多个 Server
  2. 本地工具与 MCP 工具混合使用:LLM 自动选择最合适的工具
  3. 动态工具加载:基于角色配置动态加载工具(Mock 数据)
  4. 同步和流式对话:支持多种对话模式
  5. 对话历史管理:支持多轮对话上下文
  6. 工具调用日志:记录工具调用详情,方便调试
  7. 完整的端到端测试:从用户请求到工具调用再到 AI 回复的全流程

关键要点回顾

要点 说明
模型选择 MiniMax abab6.5s-chat,Spring AI 1.1.5 原生支持
依赖配置 spring-ai-starter-model-minimax + spring-ai-starter-mcp-client
工具注入 SyncMcpToolCallbackProvider 将 MCP 工具转换为 ToolCallback
多 Server connections 配置多个 MCP Server,自动发现合并工具
流式支持 使用 Reactor Flux 实现流式对话
工具选择 LLM 根据工具描述自动选择,不需要代码逻辑判断

适用场景建议

  • 企业内部 AI 助手:连接多个业务系统的 MCP Server,提供统一 AI 入口
  • 微服务架构:MCP Client 作为 API Gateway,后端连接多个 MCP Server 微服务
  • 角色权限管理:基于角色动态加载工具,不同角色使用不同工具集

感谢各位看官的一路陪伴,大家都再接再厉!

上一篇回顾:《Spring AI MCP Server 实战:用 Streamable HTTP 协议打造企业级 AI 工具服务》

相关推荐
zyk_computer7 小时前
AI Agent ,让循环收敛的那套闭环控制系统
人工智能·后端·python·ai·架构·agent·ai agent
IPHWT 零软网络8 小时前
从PBX到IP-PBX:基于SIP协议的企业通信架构演进与AI集成实践
架构设计·ai agent·企业通信
DogDaoDao9 小时前
【GitHub】last30days-skill 深度技术解析
深度学习·程序员·大模型·github·ai agent·agent skill
取个鸣字真的难1 天前
OpenYabby 深度解读:一个语音驱动的开源多智能体项目执行系统
多智能体·ai agent·claude code
没有腰的嘟嘟嘟1 天前
Easy-agent介绍
ai·llm·agent·rag·skill·spring ai·mcp
Devin~Y2 天前
大厂 Java 面试实战:从 Spring Boot 微服务到 AI RAG 音视频平台全链路解析
java·spring boot·redis·spring cloud·微服务·rag·spring ai
要开心吖ZSH2 天前
AI医疗分诊与健康咨询助手agent开发——(2)让AI输出可控:结构化分诊与安全规则
java·ai·agent·健康医疗·spring ai
人工小情绪3 天前
Antigravity 2.0 更新:它不只是一个 AI IDE 了
ide·人工智能·ai agent·antigratity
心之伊始4 天前
Spring AI MCP Client 实战:让 Java 后端通过 stdio 调用本地工具服务
java·spring boot·agent·spring ai·mcp