乐于分享
好东西不私藏

从工具耦合到分布式智能体:Spring AI MCP Client 企业级落地方案深度拆解

从工具耦合到分布式智能体:Spring AI MCP Client 企业级落地方案深度拆解

从工具耦合到分布式智能体:Spring AI MCP Client 企业级落地方案深度拆解

摘要:很多团队第一次接入 Spring AI 时,会把一批 @Tool 直接塞进单个 Spring Boot 应用里。前期开发很快,后期却会迅速踩中四类问题:工具与主应用强耦合、多个智能体无法复用同一批工具、工具发布影响整站、并发上来后链路不可观测也不可治理。
本文不再停留在“怎么把 MCP 跑起来”,而是从企业架构、协议原理、工程治理、高并发设计、生产级代码、分布式扩展几个层面,系统讲透 Spring AI MCP Client 的落地方法。目标不是做一个 Demo,而是建设一套能长期演进的智能体工具基础设施。


一、问题不是“会不会调工具”,而是“工具如何平台化”

1.1 从 @Tool 快速起步,到系统性失控

很多团队的第一版 AI 应用大致长这样:

用户请求
   ->
ChatClient / Advisor / PromptTemplate
   ->
本地 Tool 集合
   |- queryOrder()
   |- queryShipment()
   |- queryInventory()
   |- createTicket()
   |- approveRefund()

这种结构在 PoC 阶段几乎是最优解,但到了生产环境,问题会集中爆发:

  • • 工具和主应用打成一个包,任何工具改动都要重发整站
  • • 订单工具、库存工具、客服工具无法被多个 Agent 共享复用
  • • 工具代码依赖数据库、RPC、缓存、鉴权,导致 AI Host 越来越臃肿
  • • 一个高耗时工具阻塞线程池,拖垮整条对话链路
  • • 工具元数据散落在注解里,缺少统一治理、灰度、审计、限流和版本控制

这说明企业真正要解决的不是“让模型调用函数”,而是“把工具从应用内嵌能力升级为可独立治理的服务能力”。

1.2 MCP 的价值:把工具调用从代码绑定升级为协议绑定

MCP(Model Context Protocol)的核心价值,不是再发明一套 RPC,而是为大模型和工具之间建立统一的能力协商标准。

对企业来说,它至少解决了四个关键问题:

  1. 1. 工具发现标准化:模型侧不再硬编码工具列表,而是通过协议动态发现能力。
  2. 2. 参数描述标准化:工具输入输出以结构化 Schema 暴露,降低模型误调用概率。
  3. 3. 调用链路解耦:AI Host 与工具服务分离部署,支持独立伸缩和灰度。
  4. 4. 治理能力前置:工具可以像微服务一样接入网关、鉴权、审计、限流与观测。

一句话概括:MCP 让 Tool 从“框架内功能点”变成“架构中的能力节点”。

1.3 本文适用的典型场景

  • • 企业客服:订单、物流、退款、工单等工具被多个客服 Agent 共享
  • • 企业助手:HR、OA、CRM、BI、知识库等系统统一发布为 MCP Server
  • • 智能运营:需要工具编排、长链路追踪、灰度发布和高并发弹性伸缩
  • • Agent 平台化:希望把“模型、记忆、工具、工作流、观测”统一纳管

二、先把底层看透:MCP 到底解决了什么问题

2.1 MCP 的本质:面向智能体的协议层

MCP 本质上是一层协议抽象,位于 LLM Runtime 与企业能力系统之间:

用户 / 业务系统
   ->
AI Host(Prompt、Memory、Planner、ChatClient)
   ->
MCP Client
   ->
MCP Server(暴露 tools / resources / prompts)
   ->
订单、库存、CRM、工单、知识库、审批系统

它不替代企业内部已有的 REST、gRPC、MQ、数据库访问,而是把这些异构能力统一包装为模型可理解、可发现、可调用的接口。

2.2 协议工作机制:不是“发请求”,而是“协商能力”

MCP 的关键不只是远程调用,而是初始化阶段的能力协商。

一个典型会话大致如下:

1. Client 建立连接
2. Client / Server 交换 initialize
3. Server 返回支持的 capabilities
4. Client 发起 tools/list
5. Server 返回工具清单与 JSON Schema
6. 模型推理后决定是否 tool call
7. Client 发起 tools/call
8. Server 执行业务逻辑并返回结构化结果
9. Client 将结果回填给模型继续推理

这个流程和传统 RPC 的关键差异在于:

  • • 调用前要先发现工具,而不是编译期硬编码
  • • 工具定义是给模型看的,不只是给工程师看的
  • • 返回结果不仅是数据,更是下一轮推理的上下文

2.3 为什么 JSON Schema 非常关键

企业里很多工具调用失败,不是服务不可用,而是模型“不会正确调用”。

根因通常有三类:

  • • 参数名业务语义不清晰,比如 id 到底是用户 ID、订单 ID 还是租户 ID
  • • 描述信息太弱,模型不知道什么时候该调用
  • • 返回结构混乱,模型无法提炼有效结果

所以一个高质量工具定义,必须把三件事说清楚:

  1. 1. 什么时候调用
  2. 2. 需要什么参数
  3. 3. 返回什么可继续推理的数据

例如下面这个描述就明显优于“查询订单”:

@Tool(description = "根据订单号查询订单状态、支付状态、发货状态和最近物流节点。适用于用户询问订单是否支付、是否发货、何时送达等场景。")

这不是文案优化,而是降低模型决策歧义

2.4 传输层怎么选:Stdio、SSE、Streamable HTTP

企业落地时最常见的三个传输形态如下:

传输方式
适用场景
优点
风险
stdio
本地开发、桌面工具、CLI 集成
配置简单、零网络依赖
不适合服务化和集群治理
SSE
早期远程 MCP 服务
实现门槛低
双端点模型复杂,代理层兼容性一般
Streamable HTTP
服务化、K8s、网关接入
单端点、易治理、易观测
对服务端和客户端协议实现要求更规范

生产环境建议优先选择 Streamable HTTP,原因很实际:

  • • 更容易接入 API Gateway / Ingress
  • • 更容易做鉴权、限流、WAF、审计
  • • 更符合企业现有网络治理习惯
  • • 更便于灰度、旁路观测、压测和故障排查

2.5 Spring AI MCP Client 在链路中的职责

Spring AI MCP Client 不只是一个连接器,它实际承担了四层职责:

  1. 1. 建立与多个 MCP Server 的会话连接
  2. 2. 拉取远端工具定义并转换成 Spring AI ToolCallback
  3. 3. 在模型需要工具时完成远程调用
  4. 4. 将工具结果重新注入模型上下文,继续完成推理

也就是说,ChatClient 看到的是“本地工具回调”,但真正执行的可能是远端服务。

这层抽象的意义非常大:应用侧可以像使用本地 Tool 一样使用远端 Tool Mesh。


三、企业级目标架构:不是接一个 MCP Server,而是构建 Tool Mesh

3.1 推荐的三层架构

┌──────────────────────────────────────────────────────┐
│                    Agent 应用层                      │
│  智能客服 Agent | 数据分析 Agent | 运营助手 Agent      │
├──────────────────────────────────────────────────────┤
│              AI Runtime / MCP Client 层             │
│  ChatClient | Advisor | Memory | Planner | Tool Mesh │
│  路由 | 限流 | 重试 | 观测 | 鉴权 | 熔断 | 灰度        │
├──────────────────────────────────────────────────────┤
│                  MCP Server 能力层                   │
│  订单工具 | 物流工具 | CRM 工具 | 工单工具 | BI 工具     │
├──────────────────────────────────────────────────────┤
│                  企业基础服务层                      │
│  MySQL | Redis | Kafka | ES | REST | gRPC | MQ       │
└──────────────────────────────────────────────────────┘

这套架构里,最重要的设计原则有三条:

  • • Agent 应用只负责业务意图编排,不直接承载复杂工具逻辑
  • • MCP Server 只对外发布稳定能力,不暴露内部复杂依赖细节
  • • 工具治理能力集中在 Client Mesh 或 Gateway 层,而不是散落在每个 Agent 内

3.2 为什么要把 MCP Server 看成“能力服务”

工具服务一旦服务化,就应该按微服务标准建设,而不是把它当成“给模型调用的小函数”。

一个合格的 MCP Server 至少要具备:

  • • 独立部署与弹性伸缩
  • • 独立版本控制与兼容策略
  • • 明确的超时、幂等、重试和错误码语义
  • • 请求审计和敏感字段脱敏
  • • 工具级限流与权限控制
  • • 与后端依赖隔离,避免雪崩传导

换句话说,MCP Server 不是 Tool 容器,而是面向智能体场景的微服务。

3.3 企业里最容易忽略的一层:Tool Governance

很多文章写到这里就结束了,但企业落地真正的难点,其实在治理层。

一个成熟的 Tool Mesh,通常需要以下能力:

  • • 工具注册与下线
  • • 名称冲突解决
  • • 工具版本兼容
  • • 实例发现与负载均衡
  • • 调用配额与租户隔离
  • • SLA 监控与异常熔断
  • • 审计追踪与安全合规

如果没有这一层,MCP 最终只是把本地工具耦合替换成了远程工具混乱。


四、落地设计原则:从可用到可运营

4.1 工具领域边界要稳定,不要直接暴露内部对象

错误做法很常见:

  • • 直接把数据库 Entity 暴露为 Tool 返回对象
  • • 把内部 RPC DTO 原样暴露给模型
  • • 让工具入参直接复用 Controller 请求对象

问题在于,这会让模型侧接口和内部实现边界混在一起,稍有字段调整就会造成工具契约漂移。

更稳妥的做法是:

  • • 单独定义 Tool Input / Tool Output
  • • 对外字段命名偏业务语义,不偏内部表结构
  • • 返回信息服务于“模型下一步推理”,而不是服务于“前端页面展示”

4.2 一个工具只做一件事,避免“万能工具”

企业里最危险的工具不是太小,而是太大。

例如这样的方法看起来省事,实际最难维护:

@Tool(description = "根据传入的类型执行订单、退款、物流、发票、工单等任意操作")
public
 Object execute(String type, Map<String, Object> payload) { ... }

这种设计会带来三个直接后果:

  • • 模型不容易准确选择参数
  • • 鉴权粒度和审计粒度过粗
  • • 错误定位困难,SLA 无法拆分

生产环境建议按业务动作拆工具,例如:

  • • queryOrderDetail
  • • queryShipmentTrace
  • • createRefundTicket
  • • escalateManualService

4.3 工具要“可回退”,不要只“能调用”

真正的生产工具需要考虑调用失败后的行为:

  • • 模型是否可以降级回答
  • • 是否可以改用只读工具替代写工具
  • • 是否需要提示人工接管
  • • 是否需要保留待补偿任务

因此工具返回值最好包含业务状态,而不只是原始数据,例如:

public record ToolResult<T>(
        boolean
 success,
        String code,
        String message,
        T data,
        boolean
 retryable,
        boolean
 userVisible) {
}

这种结构对智能体很重要,因为它决定了模型后续是继续推理、提示用户、还是触发人工升级。


五、生产级代码实现:从 Demo 到企业骨架

下面给出一套适合企业改造的实现方式。示例以“客服 Agent + 订单 MCP Server”为主线。

5.1 Maven 依赖

<properties>
    <java.version>
21</java.version>
    <spring.boot.version>
3.3.5</spring.boot.version>
    <spring.ai.version>
1.1.0</spring.ai.version>
    <resilience4j.version>
2.2.0</resilience4j.version>
</properties>


<dependencyManagement>

    <dependencies>

        <dependency>

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

        <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>

    <dependency>

        <groupId>
org.springframework.ai</groupId>
        <artifactId>
spring-ai-starter-openai</artifactId>
    </dependency>


    <dependency>

        <groupId>
org.springframework.ai</groupId>
        <artifactId>
spring-ai-starter-mcp-client</artifactId>
    </dependency>


    <dependency>

        <groupId>
org.springframework.ai</groupId>
        <artifactId>
spring-ai-starter-mcp-server-webflux</artifactId>
    </dependency>


    <dependency>

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


    <dependency>

        <groupId>
io.github.resilience4j</groupId>
        <artifactId>
resilience4j-spring-boot3</artifactId>
        <version>
${resilience4j.version}</version>
    </dependency>


    <dependency>

        <groupId>
org.springframework.boot</groupId>
        <artifactId>
spring-boot-starter-data-redis</artifactId>
    </dependency>


    <dependency>

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


    <dependency>

        <groupId>
io.micrometer</groupId>
        <artifactId>
micrometer-tracing-bridge-otel</artifactId>
    </dependency>

</dependencies>

实践建议:mcp-server 端优先使用 WebFlux,不要同时引入 spring-boot-starter-web,避免运行时容器冲突和阻塞链路污染。

5.2 MCP Server:稳定暴露订单能力

5.2.1 Server 配置

server:
  port:
 8088

spring:

  application:

    name:
 order-mcp-server
  ai:

    mcp:

      server:

        enabled:
 true
        type:
 ASYNC
        name:
 order-service
        version:
 1.0.0
        instructions:
 >
          该服务提供订单明细、物流跟踪、退款资格检查等能力。
          所有订单号格式为 ORD-YYYYMMDD-XXXX。
  data:
    redis:

      host:
 127.0.0.1
      port:
 6379

management:

  endpoints:

    web:

      exposure:

        include:
 health,info,prometheus,metrics

5.2.2 Tool Input / Output 定义

public record OrderDetailQuery(
        @JsonProperty(required = true)

        String orderId,
        String tenantId,
        String operatorId) {
}

public
 record OrderToolView(
        String orderId,
        String status,
        String payStatus,
        String shipmentStatus,
        BigDecimal amount,
        String logisticsCompany,
        String latestTrackingNode,
        LocalDateTime createdAt)
 {
}

public
 record ToolEnvelope<T>(
        boolean
 success,
        String code,
        String message,
        T data,
        boolean
 retryable) {

    public
 static <T> ToolEnvelope<T> ok(T data) {
        return
 new ToolEnvelope<>(true, "OK", "success", data, false);
    }

    public
 static <T> ToolEnvelope<T> fail(String code, String message, boolean retryable) {
        return
 new ToolEnvelope<>(false, code, message, null, retryable);
    }
}

5.2.3 工具实现

@Slf4j
@Service

@RequiredArgsConstructor

public
 class OrderMcpTools {

    private
 final OrderApplicationService orderApplicationService;
    private
 final PermissionService permissionService;

    @Tool(description = """
            根据订单号查询订单详情、支付状态、履约状态和最新物流节点。
            适用于用户询问订单是否支付、是否发货、何时送达、订单当前状态等场景。
            如果订单不存在或当前操作者无权限访问,返回失败信息。
            """)

    public
 ToolEnvelope<OrderToolView> queryOrderDetail(OrderDetailQuery query) {
        if
 (!permissionService.canAccess(query.tenantId(), query.operatorId(), query.orderId())) {
            return
 ToolEnvelope.fail("ORDER_ACCESS_DENIED", "当前用户无权访问该订单", false);
        }

        return
 orderApplicationService.findOrder(query.orderId())
                .map(this::toView)
                .map(ToolEnvelope::ok)
                .orElseGet(() -> ToolEnvelope.fail("ORDER_NOT_FOUND", "订单不存在", false));
    }

    @Tool(description = """
            检查订单是否满足退款申请条件。
            适用于用户询问是否可以退款、退款前置条件、是否已超出退款窗口等场景。
            """)

    public
 ToolEnvelope<RefundEligibilityView> checkRefundEligibility(RefundEligibilityQuery query) {
        try
 {
            RefundEligibilityView
 view = orderApplicationService.checkRefundEligibility(query.orderId());
            return
 ToolEnvelope.ok(view);
        } catch (TransientDependencyException ex) {
            log.warn("refund eligibility dependency timeout, orderId={}", query.orderId(), ex);
            return
 ToolEnvelope.fail("DEPENDENCY_TIMEOUT", "退款资格服务暂时不可用,请稍后重试", true);
        }
    }

    private
 OrderToolView toView(OrderAggregate aggregate) {
        return
 new OrderToolView(
                aggregate.orderId(),
                aggregate.status().name(),
                aggregate.payStatus().name(),
                aggregate.shipmentStatus().name(),
                aggregate.amount(),
                aggregate.logisticsCompany(),
                aggregate.latestTrackingNode(),
                aggregate.createdAt());
    }
}

这里有三个生产细节值得注意:

  • • 先做权限校验,再查业务数据,避免越权数据泄漏
  • • 返回统一信封对象,给模型明确的成功/失败语义
  • • 对可重试错误和不可重试错误做区分,方便 Agent 决策

5.2.4 Server 侧幂等与审计

对于写操作工具,例如“创建退款工单”“提交审批”,一定要补幂等和审计。

@Component
@RequiredArgsConstructor

public
 class ToolAuditInterceptor {

    private
 final StringRedisTemplate redisTemplate;

    public
 boolean acquireIdempotency(String requestId) {
        return
 Boolean.TRUE.equals(
                redisTemplate.opsForValue().setIfAbsent(
                        "tool:idempotent:"
 + requestId, "1", Duration.ofMinutes(10)));
    }
}
@Tool(description = "创建退款申请工单,仅在用户明确要求退款且已通过退款资格校验时调用。")
public
 ToolEnvelope<RefundTicketView> createRefundTicket(CreateRefundTicketCommand command) {
    if
 (!toolAuditInterceptor.acquireIdempotency(command.requestId())) {
        return
 ToolEnvelope.fail("DUPLICATE_REQUEST", "重复请求已被拦截", false);
    }
    return
 ToolEnvelope.ok(refundService.create(command));
}

企业里最怕的不是查询失败,而是写工具被模型或用户重复触发。

5.3 AI Host:把远程工具注入 ChatClient

5.3.1 Client 配置

server:
  port:
 8090

spring:

  application:

    name:
 customer-agent-host
  ai:

    openai:

      api-key:
 ${OPENAI_API_KEY}
      chat:

        options:

          model:
 gpt-4.1-mini
          temperature:
 0.2
    mcp:

      client:

        enabled:
 true
        type:
 ASYNC
        request-timeout:
 10s
        connect-timeout:
 3s
        toolcallback:

          enabled:
 true
          name-prefix-generation:
 auto
      clients:

        order-service:

          transport:
 http
          http:

            url:
 http://127.0.0.1:8088/mcp
        shipment-service:

          transport:
 http
          http:

            url:
 http://127.0.0.1:8089/mcp

management:

  tracing:

    enabled:
 true
  endpoints:

    web:

      exposure:

        include:
 health,info,prometheus,metrics

5.3.2 ChatClient 组装

@Configuration
public
 class AgentConfiguration {

    @Bean

    ChatClient customerSupportChatClient(ChatClient.Builder builder,
                                         ToolCallbackProvider toolCallbackProvider,
                                         CustomerSupportAdvisor customerSupportAdvisor)
 {
        return
 builder
                .defaultSystem("""
                        你是企业客服智能体。
                        优先根据用户问题判断是否需要调用工具。
                        不要编造订单、物流、退款状态。
                        当工具返回失败时,必须基于失败信息给出明确解释,不要虚构成功结果。
                        涉及退款、改地址、补开发票等写操作前,必须先确认用户意图。
                        """
)
                .defaultAdvisors(customerSupportAdvisor)
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
                .build();
    }
}

这里的关键点不在“如何注册工具”,而在“如何约束模型使用工具”。

只配工具,不写系统提示词,会导致两个问题:

  • • 模型在可回答与可调用之间摇摆
  • • 工具失败时模型容易自行补全结果

5.4 增强一层 Tool Mesh:限流、熔断、超时和降级

企业项目里,不建议直接让 ChatClient 毫无保护地打远端 MCP Server。更好的方式是在 Client 侧增加一层治理包装。

5.4.1 治理包装器

@Slf4j
@Component

@RequiredArgsConstructor

public
 class ManagedToolExecutor {

    private
 final CircuitBreakerRegistry circuitBreakerRegistry;
    private
 final TimeLimiterRegistry timeLimiterRegistry;
    private
 final MeterRegistry meterRegistry;

    public
 <T> CompletableFuture<T> execute(String toolName, Supplier<CompletableFuture<T>> supplier) {
        CircuitBreaker
 circuitBreaker = circuitBreakerRegistry.circuitBreaker(toolName);
        TimeLimiter
 timeLimiter = timeLimiterRegistry.timeLimiter(toolName);

        Supplier<CompletableFuture<T>> decorated = CircuitBreaker
                .decorateSupplier(circuitBreaker, supplier);

        CompletableFuture<T> future = TimeLimiter
                .decorateFutureSupplier(timeLimiter, decorated)
                .get();

        return
 future.whenComplete((result, ex) -> {
            if
 (ex == null) {
                meterRegistry.counter("mcp.tool.call", "tool", toolName, "status", "success").increment();
            } else {
                meterRegistry.counter("mcp.tool.call", "tool", toolName, "status", "failure").increment();
                log.warn("tool call failed, tool={}", toolName, ex);
            }
        });
    }
}

5.4.2 熔断参数建议

resilience4j:
  circuitbreaker:

    instances:

      order-service_queryOrderDetail:

        sliding-window-size:
 50
        minimum-number-of-calls:
 20
        failure-rate-threshold:
 50
        wait-duration-in-open-state:
 30s
  timelimiter:

    instances:

      order-service_queryOrderDetail:

        timeout-duration:
 2s

为什么这一层很重要?

  • • LLM 自身已经有响应时间,工具不能再无限等待
  • • 多工具串联时,任何一个慢节点都会放大用户感知时延
  • • 当下游故障时,应该尽快失败并引导模型降级,而不是把线程全部占满

5.5 工具结果回填与响应封装

生产环境通常需要把“模型输出”和“工具过程”同时返回,便于前端展示和审计。

@Data
@Builder

public
 class AgentReply {
    private
 String answer;
    private
 String sessionId;
    private
 List<ToolTraceView> toolTraces;
    private
 long elapsedMs;
}
@RestController
@RequestMapping("/api/agent")

@RequiredArgsConstructor

public
 class CustomerSupportController {

    private
 final ChatClient customerSupportChatClient;
    private
 final ToolTraceCollector toolTraceCollector;

    @PostMapping("/chat")

    public
 Mono<AgentReply> chat(@RequestBody AgentChatRequest request) {
        long
 start = System.currentTimeMillis();
        toolTraceCollector.start(request.sessionId());

        return
 customerSupportChatClient.prompt()
                .user(request.message())
                .call()
                .chatResponse()
                .map(chatResponse -> AgentReply.builder()
                        .answer(chatResponse.getResult().getOutput().getText())
                        .sessionId(request.sessionId())
                        .toolTraces(toolTraceCollector.snapshot(request.sessionId()))
                        .elapsedMs(System.currentTimeMillis() - start)
                        .build());
    }
}

这类响应结构非常适合企业后台、质检平台和 A/B 实验平台接入。


六、高并发与可扩展:企业级落地的分水岭

很多文章在“能调通”就收尾,但企业最关心的是高并发下是否稳定。

6.1 高并发下的四个瓶颈点

一个典型请求链路如下:

用户请求
 -> AI Host 接入层
 -> LLM 首轮推理
 -> MCP 工具发现 / 调用
 -> 下游业务系统
 -> 工具结果回填
 -> LLM 二轮推理
 -> 返回用户

真正的瓶颈通常不在单点,而在组合效应:

  1. 1. 模型耗时高
  2. 2. 工具依赖链长
  3. 3. 写操作工具需要审计与幂等
  4. 4. 多轮推理导致总时延叠加

6.2 线程模型建议:虚拟线程 + 响应式 I/O 分层使用

推荐实践不是“全响应式”或者“全阻塞”,而是按职责分层:

  • • MCP Server:I/O 密集型工具优先用 WebFlux
  • • AI Host:对外 API 可用 WebFlux,也可用 MVC + 虚拟线程
  • • 工具内部远程调用:尽量异步化,减少阻塞线程池占用
  • • 写工具:把强一致链路与可异步补偿链路拆开

如果团队主栈是 Spring MVC,JDK 21 的虚拟线程已经足以显著提升吞吐:

@Configuration
public
 class VirtualThreadConfig {

    @Bean

    AsyncTaskExecutor applicationTaskExecutor() {
        return
 new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }
}

适用场景是:

  • • 工具本身仍以同步 JDBC / HTTP Client 为主
  • • 团队不希望全面拥抱响应式编程
  • • 并发量高但单请求 CPU 消耗不大

6.3 工具缓存:不是为了省钱,是为了稳态吞吐

对只读工具,应尽可能引入短 TTL 缓存,特别是:

  • • 订单详情
  • • 物流轨迹
  • • 商品库存快照
  • • 组织架构信息
  • • 静态知识型查询
@Service
@RequiredArgsConstructor

public
 class CachedOrderQueryService {

    private
 final RedisTemplate<String, OrderToolView> redisTemplate;
    private
 final OrderRepository orderRepository;

    public
 Optional<OrderToolView> findByOrderId(String orderId) {
        String
 key = "mcp:order:" + orderId;
        OrderToolView
 cached = redisTemplate.opsForValue().get(key);
        if
 (cached != null) {
            return
 Optional.of(cached);
        }

        return
 orderRepository.findByOrderId(orderId)
                .map(this::toView)
                .map(view -> {
                    redisTemplate.opsForValue().set(key, view, Duration.ofSeconds(30));
                    return
 view;
                });
    }
}

30 秒缓存看似很短,但对高并发客服场景已经足够显著:

  • • 重复咨询命中率高
  • • 物流信息允许秒级延迟
  • • 可以有效削减数据库和三方 API 压力

6.4 多实例扩展:从单个 MCP Server 到分布式 Tool Pool

当订单工具调用量持续增长时,不是扩 AI Host,而是扩“订单 MCP Server 实例池”。

推荐架构:

AI Host
  ->
MCP Client Router
  ->
Nacos / 注册中心
  ->
order-mcp-server-1
order-mcp-server-2
order-mcp-server-3

关键收益有两个:

  • • 工具服务可按热点独立扩容
  • • 不同工具域可以按业务流量差异独立治理

6.5 工具调用并行化:只在无依赖时并行

很多智能体场景会遇到一个问题:用户问的是复合问题,比如:

“帮我看下订单状态,顺便确认物流什么时候送到,如果超时还能不能退款。”

这类问题理论上涉及:

  • • 订单详情
  • • 物流轨迹
  • • 退款资格

如果这三个工具完全串行,时延会很差。更合理的方式是:

  • • 订单详情、物流轨迹可并行
  • • 退款资格可能依赖订单状态,则后置

也就是说,并行化的前提不是“越多越好”,而是明确依赖图

6.6 多租户与隔离:别让一个租户拖垮所有人

企业平台化时必须考虑租户隔离:

  • • 不同租户的工具访问权限不同
  • • 不同租户的限流配额不同
  • • 不同租户的数据源和审计要求不同

推荐在 Tool Input 中显式携带:

  • • tenantId
  • • operatorId
  • • requestId

同时在网关或 Client Mesh 层做:

  • • 租户级 QPS 限制
  • • 租户级工具白名单
  • • 租户级日志脱敏策略

七、从单机到分布式:MCP Client 的企业演进路径

7.1 第一阶段:本地内嵌工具

适合 PoC,优点是开发快,缺点是无法治理和复用。

ChatClient -> Local Tools

7.2 第二阶段:固定地址远程工具

把工具独立成 MCP Server,AI Host 通过配置直连。

ChatClient -> MCP Client -> http://tool-server/mcp

这是大多数团队的第一个生产版本。

7.3 第三阶段:服务发现 + 负载均衡

引入注册中心,把工具从“固定 URL”升级为“动态服务”。

ChatClient -> Tool Router -> Registry -> Tool Instances

这时要解决的不是“能不能连”,而是:

  • • 工具实例上线下线如何感知
  • • 同名工具如何治理
  • • 哪个实例健康、该不该摘除
  • • 如何做灰度、权重和区域路由

7.4 第四阶段:Tool Mesh / Agent Platform

最终形态通常是:

  • • 多个 AI Host 共享一套工具服务体系
  • • 工具注册、路由、鉴权、审计、观测平台化
  • • 工具能力不只服务聊天,也服务工作流、自动化任务和批处理 Agent

这是从“AI 功能”走向“AI 基础设施”的关键一步。


八、基于 Nacos 的分布式发现方案

8.1 Server 注册

如果团队已经有 Spring Cloud Alibaba 体系,可以把 MCP Server 作为普通微服务注册到 Nacos。

spring:
  application:

    name:
 order-mcp-server
  cloud:

    nacos:

      discovery:

        server-addr:
 127.0.0.1:8848
        namespace:
 public
        group:
 MCP_GROUP

实际项目中建议在元数据里增加:

  • • mcpProtocol=streamable-http
  • • mcpPath=/mcp
  • • toolDomain=order
  • • toolVersion=1.0.0

这样 Client Router 在服务发现后,可以根据元数据拼装目标地址并做路由决策。

8.2 Client 动态发现

@Component
@RequiredArgsConstructor

public
 class McpServiceLocator {

    private
 final DiscoveryClient discoveryClient;

    public
 List<URI> findServiceUris(String serviceName) {
        return
 discoveryClient.getInstances(serviceName).stream()
                .map(ServiceInstance::getUri)
                .toList();
    }
}

上面这段只是示意。真实项目里通常还要叠加:

  • • 健康状态过滤
  • • 机房/地域优先
  • • 权重路由
  • • 灰度标签
  • • 租户路由

8.3 为什么不建议把工具路由逻辑散落在业务里

如果每个 Agent 自己维护工具地址、注册中心查询和负载均衡策略,会出现三个问题:

  • • 每个应用重复造轮子
  • • 治理策略无法统一升级
  • • 故障定位要跨多个应用排查

因此推荐把发现、负载、重试、熔断沉到统一的 Tool Router 或网关层。


九、真实业务案例:客服智能体如何使用 MCP 工具链

9.1 场景定义

用户说:

“我的订单怎么还没到?如果今天送不到我想退款。”

这类请求表面上是一句话,实际是一个多阶段决策流程:

  1. 1. 识别用户意图:查物流 + 判断退款条件
  2. 2. 查询订单详情
  3. 3. 查询物流轨迹
  4. 4. 根据订单状态判断是否可退款
  5. 5. 返回解释,必要时引导创建退款工单

9.2 推荐的 Agent 编排思路

用户输入
  ->
意图识别 Advisor
  ->
调用 queryOrderDetail
  ->
调用 queryShipmentTrace
  ->
必要时调用 checkRefundEligibility
  ->
生成答复
  ->
若用户确认退款,再调用 createRefundTicket

关键设计点有两个:

  • • 把“查询”和“执行写操作”分层,不要一步到位
  • • 写操作必须要求用户显式确认,避免模型误触发

9.3 一段更接近生产的系统提示词

你是企业客服智能体。
当用户咨询订单、物流、退款问题时:
1. 涉及事实查询时优先调用工具,不要猜测。
2. 工具返回失败时,必须解释失败原因,并给出下一步建议。
3. 涉及退款、改地址、取消订单等写操作时,必须先征得用户明确确认。
4. 如果当前订单不满足退款条件,应说明原因,不要直接承诺退款成功。
5. 不要输出系统内部错误堆栈、数据库字段名或敏感标识。

很多系统效果不好,不是工具不够,而是系统提示词没有把“工具使用纪律”写清楚。


十、生产问题深挖:最常见的 12 个坑

10.1 工具名冲突

多个 MCP Server 都可能有 searchquerygetById 之类的工具名。

解决方式:

  • • 开启自动前缀
  • • 或者在服务端按领域强约束命名

推荐规范:

  • • order_queryOrderDetail
  • • shipment_queryTrace
  • • crm_queryCustomerProfile

10.2 工具 Schema 漂移

服务端字段变更后,客户端缓存的工具定义没有及时刷新,会导致模型继续按旧参数调用。

建议:

  • • 工具参数新增尽量向后兼容
  • • 高风险变更升版本,不直接覆盖
  • • 关键工具变更走灰度验证

10.3 工具描述过短,模型误调用

很多团队只写一句“查询订单”,实际效果很差。

建议把以下信息写进描述:

  • • 适用场景
  • • 必填参数
  • • 不适用场景
  • • 返回内容范围

10.4 把大对象直接塞进上下文,导致 Token 暴涨

工具一旦把完整订单对象、物流全量历史、几十条工单记录全部塞回模型,很容易造成:

  • • Token 成本上升
  • • 首轮/二轮推理耗时增加
  • • 模型抓不住重点

更好的做法是工具结果只保留对话所需摘要字段。

10.5 模型把失败结果“解释成成功”

这是企业里非常危险的一类问题。

解决方式:

  • • 工具返回统一失败结构
  • • 系统提示词显式约束
  • • 对写操作增加二次确认和状态校验

10.6 下游系统超时,MCP 链路雪崩

不要把所有重试都交给模型或 HTTP Client。

建议:

  • • 工具内部最多一次快速重试
  • • Client Mesh 侧统一超时
  • • 达到阈值立即熔断并降级

10.7 工具调用日志与对话日志分离,无法串联

生产排障时经常遇到:

  • • 有对话日志,没有工具日志
  • • 有工具日志,没有模型上下文

应统一透传:

  • • traceId
  • • sessionId
  • • tenantId
  • • toolName
  • • requestId

10.8 对写工具缺少防重

例如“创建退款工单”“发送催办通知”“提交审批”,没有 requestId 和幂等键就非常危险。

10.9 只做服务级限流,不做工具级限流

一个 MCP Server 里可能同时承载轻量查询工具和高成本写工具。只做服务级限流,会让轻量查询被重型工具拖死。

建议至少做到:

  • • 服务级总限流
  • • 工具级细粒度限流
  • • 租户级配额控制

10.10 灰度发布只灰服务,不灰工具描述

很多团队只灰度代码,不灰度工具元信息。实际上模型行为对描述极其敏感,描述变化本身就可能改变调用路径。

10.11 MCP Server 把内部异常直接透出

不要把数据库异常、SQL 语句、三方接口原始报错直接返回给模型和用户。

应该统一转成:

  • • 业务可见错误
  • • 系统不可见错误
  • • 可重试错误

10.12 忽略合规和敏感信息治理

客服、金融、医疗等场景尤其需要关注:

  • • 日志脱敏
  • • 工具入参与出参审计
  • • PII 字段最小化暴露
  • • 模型上下文中的敏感数据保留时间

十一、可观测性:没有观测,就没有生产可控性

11.1 指标体系建议

至少监控以下指标:

  • • mcp_tool_call_total
  • • mcp_tool_call_success_total
  • • mcp_tool_call_failure_total
  • • mcp_tool_call_latency_ms
  • • mcp_tool_call_timeout_total
  • • mcp_tool_circuit_open_total
  • • mcp_tool_cache_hit_ratio

11.2 日志结构建议

推荐使用 JSON 日志,并至少包含:

{
  "traceId"
: "2d3c1f...",
  "sessionId"
: "sess-10001",
  "tenantId"
: "tenant-a",
  "toolName"
: "order-service_queryOrderDetail",
  "toolLatencyMs"
: 86,
  "success"
:true,
  "retryable"
:false
}

11.3 链路追踪建议

完整链路至少应覆盖:

API Gateway
 -> AI Host
 -> LLM 调用
 -> MCP Client
 -> MCP Server
 -> 下游订单服务 / DB / Redis

一旦这里断链,问题排查成本会急剧上升。


十二、部署与发布:把文章里的代码带到生产环境

12.1 Dockerfile

FROM eclipse-temurin:21-jre
WORKDIR
 /app
COPY
 target/order-mcp-server.jar app.jar
EXPOSE
 8088
ENTRYPOINT
 ["java","-XX:+UseZGC","-XX:MaxRAMPercentage=70","-jar","/app/app.jar"]

12.2 Kubernetes 部署示例

apiVersion: apps/v1
kind:
 Deployment
metadata:

  name:
 order-mcp-server
spec:

  replicas:
 3
  selector:

    matchLabels:

      app:
 order-mcp-server
  template:

    metadata:

      labels:

        app:
 order-mcp-server
    spec:

      containers:

        -
 name: order-mcp-server
          image:
 registry.example.com/ai/order-mcp-server:1.0.0
          ports:

            -
 containerPort: 8088
          readinessProbe:

            httpGet:

              path:
 /actuator/health/readiness
              port:
 8088
          livenessProbe:

            httpGet:

              path:
 /actuator/health/liveness
              port:
 8088
          resources:

            requests:

              cpu:
 "500m"
              memory:
 "1Gi"
            limits:

              cpu:
 "2"
              memory:
 "2Gi"

12.3 发布策略建议

工具服务建议采用:

  • • 小步快跑的灰度发布
  • • 描述变更单独验证
  • • 写工具优先金丝雀
  • • 高峰期禁止高风险 schema 变更

原因很简单:模型对工具定义的敏感度,往往比传统前后端接口更高。


十三、文章最后给一套可执行的落地清单

13.1 架构层

  • • 将本地 @Tool 逐步拆分为独立 MCP Server
  • • 以业务域划分工具服务,不以开发团队划分
  • • 在 AI Host 与 MCP Server 之间增加治理层

13.2 工程层

  • • 统一 Tool Input / Output 契约
  • • 引入超时、熔断、限流、幂等、缓存
  • • 打通 traceId、sessionId、tenantId 全链路传递

13.3 高并发层

  • • 读工具缓存化
  • • 热点工具独立扩容
  • • 控制工具返回数据体积
  • • 只在无依赖前提下并行调用工具

13.4 安全与合规层

  • • 工具按动作授权,不按服务粗放授权
  • • 写工具必须二次确认
  • • 日志脱敏与审计默认开启
  • • 敏感字段最小化回传给模型

十四、总结:MCP Client 的真正价值,在于让企业拥有“可治理的工具网络”

如果只把 Spring AI MCP Client 当作“远程工具调用器”,它的价值会被低估。

从企业架构视角看,它真正带来的变化是:

  • • 工具从应用内嵌逻辑变成标准化服务能力
  • • 智能体从单体式 Prompt 工程走向分布式能力编排
  • • AI Host 从“工具大杂烩”演进为“面向智能体的业务运行时”

真正成熟的企业实践,绝不会停留在“把工具接进 ChatClient”。
它一定会继续往前走,走向这三件事:

  1. 1. 工具服务化
  2. 2. 调用治理化
  3. 3. 智能体平台化

当你完成这一步,MCP 就不再只是一个协议,而会成为企业智能体基础设施中的关键连接层。


附:一套推荐的章节式认知框架

如果你希望把本文内容沉淀为团队内部方法论,可以直接按下面的顺序培训或评审:

  1. 1. 先统一认知:MCP 解决的是工具标准化,不是替代微服务通信
  2. 2. 再统一架构:AI Host、Tool Mesh、MCP Server 三层分离
  3. 3. 再统一工程:契约、超时、幂等、缓存、熔断、观测
  4. 4. 最后统一治理:注册发现、灰度发布、租户隔离、安全审计

这样团队在落地 Spring AI MCP Client 时,才不会停留在“Demo 能跑”,而是真正进入“企业可运营”阶段。