找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3454

积分

0

好友

458

主题
发表于 3 小时前 | 查看: 4| 回复: 0

引言:为什么企业级 AI 系统需要 MCP

做过企业 AI 落地的团队,很快就会撞上一道现实的墙:模型很聪明,但真正有价值的能力,并不在模型里,而在企业已运转多年的系统里。

以支付平台为例,客服想用自然语言查订单状态,风控想让 AI 自动判断交易风险,运维希望用对话方式看失败率,运营要按时间窗口统计退款率——这些能力分散在订单服务、支付服务、风控服务、数据仓库、监控平台、知识库系统里。

过去,每接一个 AI 应用,往往都要做一轮定制集成,最终形成典型的 N × M 问题:N 个 AI 应用 × M 个业务系统,每一对都得单独做协议适配、鉴权、参数映射、超时控制和可观测埋点。

MCP(Model Context Protocol,模型上下文协议)的核心价值,不是“让模型多调几个函数”,而是把 AI 与企业工具之间的交互标准化。它把“工具发现、参数定义、上下文读取、执行反馈”抽象成统一协议层,使 AI Host、MCP Client、MCP Server 之间职责清晰,让系统从“脚本拼接”升级为“可治理架构”。

这篇文章不做概念演示,而是从架构师视角,完整讲清楚一套基于 Spring Boot + Spring AI + MCP 的生产级落地方案,包括协议原理、业务建模、生产级工具暴露、高并发与安全治理,以及 Kubernetes 部署——一条可执行的从 PoC 到生产的演进路径。

一、MCP 协议到底解决了什么

1.1 从函数调用到标准化工具总线

很多团队第一次接触 MCP 时,会把它理解为“函数调用协议”。这个理解不算错,但远远不够。

函数调用只解决“模型调用某个方法”这一步,而 MCP 解决的是一整条链路:Host 如何发现可用工具;工具的输入输出如何标准化描述;工具之外的上下文资源如何暴露;提示模板如何参数化复用;调用过程中的状态、异常、进度如何反馈;多个 Server、多个实例、多个租户如何治理。

因此,MCP 更像 AI 时代的“工具总线协议”,而不是一个简单的 SDK。

1.2 MCP 的三端协作模型

在工程上,MCP 至少包含三类角色:

  • Host:承载 AI 应用的运行环境,比如 Claude Desktop、Cursor、自研 Agent 平台
  • Client:Host 内部或旁路的协议代理,负责与 MCP Server 建立连接、发现工具、发起调用
  • Server:对外暴露工具、资源、提示模板的服务端,一般由业务系统实现

它们之间的职责分工非常关键:

角色 关注点 典型职责
Host 用户意图与对话编排 Prompt 编排、工具选择、结果融合
Client 协议通信与连接管理 能力发现、协议协商、超时控制、负载路由
Server 业务能力封装与治理 工具实现、参数校验、审计、鉴权、限流

这意味着什么?AI 决策与业务能力不再强耦合。 模型负责“决定要不要调用工具”,业务系统负责“安全、稳定、正确地执行工具”。

1.3 MCP 的四类能力模型

真正完整的 MCP 系统,至少涉及四类能力:

Tools:模型可调用的函数型能力,如查询订单状态、统计退款率、评估交易风险、创建退款工单。

Resources:可读取的上下文对象,如风控规则文档、退款政策说明、渠道 SLA 配置、商户信息快照。

Prompts:参数化提示模板,用于把领域经验沉淀为结构化模板,而不是散落在代码中的字符串。

Sampling:Server 反向请求 Host 或模型进行推理,适合复杂推理链、工具内嵌模型判断、审核流等高级协作模式。

很多文章只讲 Tools,但企业级实践里,Resources + Prompts + Tools 的组合,才是做出稳定 AI 系统的关键。

1.4 为什么生产环境优先选择 Streamable HTTP

MCP 的传输方式决定了你的系统上限。常见方案大致如下:

传输方式 适用场景 优点 局限
STDIO 本地开发、IDE 插件 接入简单、延迟低 无法天然水平扩展
SSE 轻量远程接入 实现直接 长连接多、代理链路复杂
Streamable HTTP 服务化生产部署 无状态、易负载均衡、云原生友好 需要更完整的服务治理

生产环境优先推荐 Streamable HTTP,原因有三:更适合无状态部署,天然适配容器与弹性扩缩容;更容易接入网关、限流、鉴权、链路追踪等企业基础设施;避免大量长连接在高并发场景下占用连接数、线程和代理资源。

一句话总结:PoC 阶段可以用 STDIO 或 SSE,生产阶段优先用 Streamable HTTP。

二、真实业务场景建模:支付平台的 AI 工具化改造

为避免文章只停留在 Demo 层,我们用一个真实的企业场景来建模。

2.1 业务目标

我们要把一个 Spring Boot 支付平台改造成 MCP Server,对外提供如下能力:

  • getPaymentStatus:查询某笔交易的支付状态
  • getRefundMetrics:统计指定时间范围退款率
  • assessFraudRisk:基于规则和画像评估风险等级
  • createRefundTicket:在异常场景下创建退款处理工单
  • queryMerchantHealth:按商户查看近 5 分钟支付健康度

2.2 业务域拆分

从架构上,不建议把所有逻辑直接写在 Tool 方法里。更合理的分层是:

MCP 接入层
  ├─ Tool 定义
  ├─ Tool 参数校验
  ├─ Tool 鉴权与审计
  └─ Tool 结果封装

应用服务层
  ├─ PaymentQueryService
  ├─ RefundAnalysisService
  ├─ FraudDecisionService
  └─ TicketOrchestrationService

领域能力层
  ├─ 支付领域模型
  ├─ 风控评分规则
  ├─ 退款策略规则
  └─ 商户运营规则

基础设施层
  ├─ PostgreSQL / Redis / Kafka
  ├─ 第三方支付渠道 API
  ├─ 工单系统 API
  └─ 监控与日志系统

这层拆分的好处是:Tool 只是协议适配层,不污染领域逻辑;领域服务可以被 HTTP API、批处理任务、MCP 工具共同复用;后续替换 MCP 框架或增加新 Agent,不需要重写业务核心。

三、生产级架构设计

3.1 单实例能跑,不代表架构成立

很多团队第一版就一行:AI Client -> MCP Server -> 数据库

这能工作,但有四个明显问题:没有流量治理能力;没有多实例会话策略;没有统一鉴权与审计出口;没有异步削峰和故障隔离。

真正可上线的架构应该至少长这样:

┌───────────────────────────────────────────────────────────────┐
│                        AI Host / Agent                        │
│      Claude Desktop / Cursor / 自研 Copilot / 企业 Agent Hub   │
└──────────────────────────┬────────────────────────────────────┘
                           │ MCP
┌──────────────────────────▼────────────────────────────────────┐
│                    MCP Gateway / API Gateway                   │
│         OAuth2 / 限流 / 负载均衡 / 审计 / Trace 透传            │
└───────────────┬───────────────────────┬───────────────────────┘
                │                       │
┌───────────────▼──────────────┐  ┌────▼────────────────────────┐
│ Payment MCP Server Cluster   │  │ Fraud MCP Server Cluster    │
│ Tool 暴露 / 读写分离 / 缓存    │  │ 风控规则 / 特征评分 / 审核   │
└───────────────┬──────────────┘  └────┬────────────────────────┘
                │                       │
      ┌─────────▼──────────┐   ┌───────▼────────────────────┐
      │ PostgreSQL / Redis  │   │ Kafka / Rule Engine / ES   │
      └─────────────────────┘   └────────────────────────────┘

3.2 高并发设计原则

MCP 工具本质上是“被模型驱动的远程函数调用”。一旦进入生产,你要面对的不是单个用户,而是多个 Agent 同时调用、一个会话里多个工具串行或并行调用、模型重试导致的幂等问题,以及高峰期大量统计查询和聚合计算。

所以在设计时要遵循四条原则:

原则一:同步工具尽量短小。 适合同步返回的工具应该是单表查询、小范围聚合、快速规则判断、毫秒级缓存读取。不要把“大查询、跨系统事务、复杂报表”强行做成同步 Tool。

原则二:重任务异步化。 生成月度商户分析报告、全量风控回放、历史退款聚合计算,这些更适合“提交任务 + 返回任务 ID + 轮询或回调取结果”的模式。

原则三:工具必须幂等。 模型可能因为超时、重试、上下文漂移而重复调用同一工具。任何写操作型工具都必须具备幂等键、去重校验和重复请求保护。

原则四:把状态从连接中解耦。 不要把会话、上下文、任务进度绑死在单机内存里。生产环境必须把关键状态外置到 Redis、数据库或事件流中。

四、项目搭建与依赖选型

4.1 Maven 依赖

下面给出一套偏生产环境的依赖组合:

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

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

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
        <version>1.1.2</version>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>

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

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot3</artifactId>
    </dependency>

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
</dependencies>

4.2 推荐运行时版本

  • JDK:21
  • Spring Boot:3.4.x
  • Spring AI MCP Starter:与 Spring Boot 版本保持兼容
  • 容器基线:eclipse-temurin:21-jre

选择 JDK 21 的核心原因不是“追新”,而是虚拟线程对高并发 IO 密集型工具调用更友好,新一代 GC 和运行时诊断能力更成熟,适合作为中长期 LTS 生产基座。

五、领域模型与应用服务设计

在暴露工具之前,先把业务层设计清楚。

5.1 订单查询服务

package com.example.payment.application;

import com.example.payment.domain.Payment;
import com.example.payment.domain.PaymentStatus;
import com.example.payment.infrastructure.PaymentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Service
public class PaymentQueryService {

    private final PaymentRepository paymentRepository;

    public PaymentQueryService(PaymentRepository paymentRepository) {
        this.paymentRepository = paymentRepository;
    }

    @Transactional(readOnly = true)
    public Optional<Payment> findByTransactionId(String transactionId) {
        return paymentRepository.findByTransactionId(transactionId);
    }

    @Transactional(readOnly = true)
    public long countByStatusBetween(PaymentStatus status, LocalDateTime start, LocalDateTime end) {
        return paymentRepository.countByStatusAndCreatedAtBetween(status, start, end);
    }

    @Transactional(readOnly = true)
    public long countAllBetween(LocalDateTime start, LocalDateTime end) {
        return paymentRepository.countByCreatedAtBetween(start, end);
    }
}

5.2 风控评分服务

生产环境里,风控不应只返回一个分数,还要返回解释项,以便 AI 能生成可信回复。

package com.example.payment.application;

import org.springframework.stereotype.Service;

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

@Service
public class FraudDecisionService {

    public FraudAssessment assess(FraudCommand command) {
        int score = 0;
        List<String> reasons = new ArrayList<>();

        if (command.amount() >= 10000) {
            score += 30;
            reasons.add("高金额交易");
        }
        if ("NEW".equalsIgnoreCase(command.clientType())) {
            score += 20;
            reasons.add("新客首单");
        }
        if (command.hour() >= 1 && command.hour() <= 5) {
            score += 20;
            reasons.add("凌晨时段交易");
        }
        if (command.orderCount24h() >= 30) {
            score += 15;
            reasons.add("24小时内订单数偏高");
        }
        if (command.successRate7d() < 0.75) {
            score += 15;
            reasons.add("近7日成功率偏低");
        }

        String level = score >= 70 ? "HIGH" : score >= 40 ? "MEDIUM" : "LOW";
        return new FraudAssessment(score, level, reasons);
    }

    public record FraudCommand(
            double amount,
            String clientType,
            int hour,
            int orderCount24h,
            double successRate7d
    ) {}

    public record FraudAssessment(int score, String level, List<String> reasons) {}
}

5.3 异步工单服务

写操作型工具不应直接在请求线程里串行调用多个外部系统,建议事件化。

package com.example.payment.application;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.UUID;

@Service
public class RefundTicketService {

    private final KafkaTemplate<String, RefundTicketCreatedEvent> kafkaTemplate;

    public RefundTicketService(KafkaTemplate<String, RefundTicketCreatedEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public String submit(RefundTicketCommand command) {
        String ticketId = "RT-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
        RefundTicketCreatedEvent event = new RefundTicketCreatedEvent(
                ticketId,
                command.transactionId(),
                command.reason(),
                command.operator(),
                Instant.now()
        );
        kafkaTemplate.send("refund-ticket-created", ticketId, event);
        return ticketId;
    }

    public record RefundTicketCommand(String transactionId, String reason, String operator) {}

    public record RefundTicketCreatedEvent(
            String ticketId,
            String transactionId,
            String reason,
            String operator,
            Instant createdAt
    ) {}
}

这类设计的价值在于:峰值写入先落 Kafka,降低同步链路压力;下游工单系统短暂故障时不阻塞 MCP 主链路;可以做失败重试、死信队列、审计追踪。

六、MCP 工具暴露:从业务方法到生产级 Tool

6.1 为什么 Tool 层不能直接返回“随便一段字符串”

很多 Demo 里 Tool 返回一个字符串就结束了,但生产环境里还得考虑:返回结构是否稳定?是否便于模型消费?是否便于日志审计?是否便于未来版本演进?

建议内部使用强类型 DTO,必要时在 Tool 层转换成模型更易理解的结构化文本或 JSON。

6.2 Tool 参数对象与校验

package com.example.payment.mcp.dto;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

public record FraudRiskRequest(
        @NotNull @Min(1) double amount,
        @NotBlank @Pattern(regexp = "NEW|RETURNING") String clientType,
        @Min(0) @Max(23) int hour,
        @Min(0) int orderCount24h,
        @NotNull @Min(0) @Max(1) double successRate7d
) {}

参数对象化有三个价值:校验逻辑集中,不散落在 Tool 方法中;接口契约更清晰,方便模型理解;后续可复用于 HTTP API、测试、审计对象。

6.3 MCP Tool 实现

package com.example.payment.mcp;

import com.example.payment.application.FraudDecisionService;
import com.example.payment.application.PaymentQueryService;
import com.example.payment.application.RefundTicketService;
import com.example.payment.domain.PaymentStatus;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Component
public class PaymentMcpTools {

    private final PaymentQueryService paymentQueryService;
    private final FraudDecisionService fraudDecisionService;
    private final RefundTicketService refundTicketService;

    public PaymentMcpTools(PaymentQueryService paymentQueryService,
                           FraudDecisionService fraudDecisionService,
                           RefundTicketService refundTicketService) {
        this.paymentQueryService = paymentQueryService;
        this.fraudDecisionService = fraudDecisionService;
        this.refundTicketService = refundTicketService;
    }

    @Tool(description = """
            根据交易ID查询支付状态、金额、时间和失败原因。
            当用户询问某一笔订单是否支付成功、为什么失败、是否已退款时调用。
            如果用户只是询问整体趋势或统计概览,不要调用该工具。""")
    public String getPaymentStatus(
            @ToolParam(description = "交易ID,例如 TXN-20260430-000123") String transactionId) {
        return paymentQueryService.findByTransactionId(transactionId)
                .map(payment -> """
                        transactionId=%s
                        status=%s
                        amount=%.2f
                        createdAt=%s
                        failureReason=%s
                        """
                        .formatted(
                                payment.getTransactionId(),
                                payment.getStatus(),
                                payment.getAmount(),
                                payment.getCreatedAt(),
                                payment.getFailureReason() == null ? "NONE" : payment.getFailureReason()))
                .orElse("transactionId=%s\nresult=NOT_FOUND".formatted(transactionId));
    }

    @Tool(description = """
            统计指定日期范围内的退款率。
            当用户询问最近退款多不多、某个时间段退款率是多少时调用。
            如果用户需要查询单笔订单,请改用支付状态查询工具。""")
    public String getRefundMetrics(
            @ToolParam(description = "开始日期,格式 yyyy-MM-dd") String startDate,
            @ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate) {
        LocalDateTime start = LocalDate.parse(startDate).atStartOfDay();
        LocalDateTime end = LocalDate.parse(endDate).plusDays(1).atStartOfDay();

        long total = paymentQueryService.countAllBetween(start, end);
        long refunded = paymentQueryService.countByStatusBetween(PaymentStatus.REFUNDED, start, end);
        double ratio = total == 0 ? 0D : refunded * 100D / total;

        return """
                startDate=%s
                endDate=%s
                total=%d
                refunded=%d
                refundRate=%.2f%%
                """
                .formatted(startDate, endDate, total, refunded, ratio);
    }

    @Tool(description = """
            评估一笔交易的欺诈风险等级,并返回评分与触发原因。
            当用户询问某笔交易是否存在风险、是否建议人工复核时调用。""")
    public String assessFraudRisk(
            @ToolParam(description = "交易金额") double amount,
            @ToolParam(description = "客户类型,只能是 NEW 或 RETURNING") String clientType,
            @ToolParam(description = "交易发生的小时,0 到 23") int hour,
            @ToolParam(description = "近24小时订单数") int orderCount24h,
            @ToolParam(description = "近7日成功率,0 到 1") double successRate7d) {

        var result = fraudDecisionService.assess(
                new FraudDecisionService.FraudCommand(amount, clientType, hour, orderCount24h, successRate7d));

        return """
                score=%d
                level=%s
                reasons=%s
                """
                .formatted(result.score(), result.level(), String.join(",", result.reasons()));
    }

    @Tool(description = """
            为退款异常交易创建处理工单。
            仅当用户明确要求创建工单、升级处理、人工介入时调用。
            如果只是查询状态或解释原因,不要调用该工具。""")
    public String createRefundTicket(
            @ToolParam(description = "交易ID") String transactionId,
            @ToolParam(description = "工单原因") String reason,
            @ToolParam(description = "操作人,例如 ai-assistant 或客服工号") String operator) {
        String ticketId = refundTicketService.submit(
                new RefundTicketService.RefundTicketCommand(transactionId, reason, operator));
        return "ticketId=%s\nstatus=SUBMITTED".formatted(ticketId);
    }
}

这段代码里,最容易被忽略但最重要的点有两个:

第一,description 不是注释,而是工具路由策略的一部分。 好的 Tool 描述必须明确:什么情况下调用、什么情况下不要调用、输入参数的业务语义、输出能解决什么问题。

第二,写操作型工具必须有明确触发条件。createRefundTicket 这样的工具,如果描述过于模糊,模型就可能在“仅仅解释退款失败原因”时误创建工单。这是生产事故,不是精度问题。

七、工程化升级:并发、超时、熔断、缓存、幂等

7.1 统一超时与隔离策略

MCP Tool 的超时预算,不能照搬传统后台接口。模型在工具链路里一旦等待过久,整轮对话都会被拖慢,用户感知比普通 API 更差。

建议做法:快查询型工具 300ms ~ 2s;中等聚合型工具 2s ~ 5s;写操作型工具同步确认 1s ~ 3s,重任务异步化。

基于 Resilience4j 的包装示例:

package com.example.payment.mcp.support;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.timelimiter.TimeLimiter;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;

@Component
public class ToolExecutionTemplate {

    private final ExecutorService executorService = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor();

    public <T> T execute(String toolName, Duration timeout, Supplier<T> supplier) {
        TimeLimiter timeLimiter = TimeLimiter.of(timeout);
        CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults(toolName);

        Supplier<CompletableFuture<T>> futureSupplier =
                () -> CompletableFuture.supplyAsync(supplier, executorService);

        try {
            return circuitBreaker.executeSupplier(
                    timeLimiter.decorateFutureSupplier(futureSupplier)
            ).get();
        } catch (Exception e) {
            throw new ToolExecutionException(toolName, "TOOL_EXECUTION_FAILED", e);
        }
    }
}

7.2 缓存策略

并不是所有 Tool 都要直查数据库。对于热点查询,建议增加 Redis 缓存:商户健康度缓存 30 秒;日级统计缓存 1 到 5 分钟;配置型资源缓存 10 分钟以上。

但要注意:单笔订单状态如果要求强一致,不要盲目缓存过久;风控结果若依赖实时画像,缓存要谨慎;写后读场景要考虑主动失效。

@Service
public class MerchantHealthService {

    @Cacheable(cacheNames = "merchant-health", key = "#merchantId", unless = "#result == null")
    public MerchantHealthSnapshot query(String merchantId) {
        return loadFromWarehouse(merchantId);
    }

    private MerchantHealthSnapshot loadFromWarehouse(String merchantId) {
        return new MerchantHealthSnapshot(merchantId, 0.982, 120, 3);
    }

    public record MerchantHealthSnapshot(
            String merchantId,
            double successRate,
            int qps,
            int failedCount5m
    ) {}
}

7.3 幂等控制

只要是“创建、提交、发起、关闭、扣款、退款”类 Tool,都必须引入幂等键。典型做法:调用方传 requestId;服务端用 requestId + toolName + tenantId 做唯一约束;首次成功后缓存结果,重复请求直接返回之前结果。

@Entity
@Table(name = "tool_idempotency_record", uniqueConstraints = {
        @UniqueConstraint(name = "uk_tool_request", columnNames = {"tool_name", "request_id", "tenant_id"})
})
public class ToolIdempotencyRecord {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String toolName;
    private String requestId;
    private String tenantId;
    private String responseBody;
}

这一步不是可选项。模型发生重试时,如果没有幂等保护,最轻是重复建单,最重可能是重复退款。

7.4 异步化与削峰

当工具执行涉及多下游串行调用、重计算、报表生成、文件导出时,建议改造为两段式:submitXxxTaskqueryXxxTaskResult。其本质是把“长事务工具”转为“任务型工具”,这也是大规模 Agent 系统中非常常见的模式。

八、安全体系:不是加个 Token 就结束了

MCP Server 一旦暴露到远程环境,本质上就是“可被模型驱动的业务操作面”。安全要求应高于普通查询接口。

8.1 OAuth2 Resource Server

package com.example.payment.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health/**").permitAll()
                        .requestMatchers("/mcp/**").authenticated()
                        .anyRequest().denyAll())
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

8.2 工具级授权

认证只解决“你是谁”,不解决“你能调用什么”。建议把权限控制做到 Tool 级别:客服只能查订单、创建普通退款单;风控人员可调用高风险交易分析工具;运维可调用商户健康度与异常指标工具;高危操作工具需要更高权限甚至人工审批。

8.3 审计日志

每次 Tool 调用都应记录 traceIdsessionIdtenantIdtoolName、输入参数摘要、执行耗时、调用结果码、调用人或调用主体。

package com.example.payment.mcp.support;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class ToolAuditLogger {

    private static final Logger log = LoggerFactory.getLogger("TOOL_AUDIT");

    public void log(String traceId, String toolName, String principal, long costMs, String resultCode) {
        log.info("traceId={}, toolName={}, principal={}, costMs={}, resultCode={}",
                traceId, toolName, principal, costMs, resultCode);
    }
}

如果你的系统涉及支付、金融、医疗、政务,审计链路必须从第一天就设计进去。

九、可观测性:看得见,才运维得住

一个 MCP 工具服务上线后,最怕的是“用户说 AI 不稳定,但你根本不知道哪一步出问题”。建议至少建设三类观测能力。

9.1 Metrics

重点指标:Tool 调用次数、调用成功率、P95/P99 延迟、下游依赖耗时、熔断次数、超时次数、缓存命中率。

package com.example.payment.mcp.support;

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class ToolMetricsRecorder {

    private final MeterRegistry meterRegistry;

    public ToolMetricsRecorder(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    public void record(String toolName, long costMs, boolean success) {
        meterRegistry.timer("mcp.tool.latency", "tool", toolName, "success", String.valueOf(success))
                .record(costMs, TimeUnit.MILLISECONDS);
        meterRegistry.counter("mcp.tool.calls", "tool", toolName, "success", String.valueOf(success))
                .increment();
    }
}

9.2 Trace

要把 Agent 请求 -> Gateway -> MCP Tool -> DB/Redis/Kafka/第三方接口 这条链路打通。这样你才能准确判断到底是模型选错工具、Tool 执行超时、下游数据库抖动、网关限流,还是外部渠道返回异常。

9.3 Structured Logging

不要输出一堆难以检索的自然语言日志。MCP 服务要尽量结构化输出,方便检索与告警聚合。

十、配置中心、服务发现与多实例治理

对于企业内部多团队、多工具集、多环境部署的场景,单体式静态配置很快会失效。

建议引入服务注册中心(如 Nacos、Consul)、配置中心(统一管理工具开关、描述、限流参数)、Gateway(统一处理鉴权、流量治理、灰度发布)。

10.1 为什么要做工具元数据动态化

Tool 描述不是写完就不变的。上线后你常常会发现:模型误调用某个工具,某些参数描述不清晰,某类问题应该路由到另一个工具。如果每改一次描述都要重新发版,迭代成本会很高。更好的方式是把这类“工具元数据”放入配置中心做动态更新。

10.2 多实例下的状态管理

在 Streamable HTTP 模式下,服务可以做到基本无状态,但仍有几类状态要外置:

状态类型 存储建议
会话上下文 Redis
幂等记录 MySQL / PostgreSQL
异步任务状态 Redis + DB
配置与元数据 Nacos / Apollo
审计日志 ELK / ClickHouse / Loki

十一、Kubernetes 生产部署

11.1 Dockerfile

FROM eclipse-temurin:21-jdk AS builder
WORKDIR /workspace
COPY . .
RUN ./mvnw clean package -DskipTests

FROM eclipse-temurin:21-jre
WORKDIR /app

RUN useradd -r -s /sbin/nologin spring
USER spring

COPY --from=builder /workspace/target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-XX:+UseZGC", "-XX:MaxRAMPercentage=70", "-jar", "app.jar"]

11.2 Spring Boot 配置

server:
  port: 8080
  shutdown: graceful

spring:
  application:
    name: payment-mcp-server
  threads:
    virtual:
      enabled: true
  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:5432/payment
    username: ${DB_USER:payment}
    password: ${DB_PASSWORD:changeit}
    hikari:
      maximum-pool-size: 30
      minimum-idle: 8
      connection-timeout: 3000
      validation-timeout: 1000
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
  kafka:
    bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
  ai:
    mcp:
      server:
        enabled: true
        name: payment-mcp-server
        version: 1.0.0
        type: async
        protocol: STREAMABLE

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics
  endpoint:
    health:
      probes:
        enabled: true
  metrics:
    tags:
      application: ${spring.application.name}

11.3 Deployment 与 HPA

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-mcp-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payment-mcp-server
  template:
    metadata:
      labels:
        app: payment-mcp-server
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      containers:
        - name: app
          image: registry.example.com/payment-mcp-server:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2"
              memory: "2Gi"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 40
            periodSeconds: 20
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-mcp-server
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-mcp-server
  minReplicas: 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

生产环境还建议补齐 PodDisruptionBudgetanti-affinitytopologySpreadConstraints、灰度发布策略、Secret 管理和镜像漏洞扫描。

十二、实战案例:一条完整调用链长什么样

下面看一个真实场景。

场景:客服处理“支付成功但用户未到账”

用户输入:订单 TXN-20260430-000123 为什么用户说没到账?如果有风险请帮我创建人工处理工单。

推荐的 Agent 行为链路:先调用 getPaymentStatus,若状态异常或失败原因复杂,再调用 assessFraudRisk,若判断需要人工介入,再调用 createRefundTicket

一次典型输出可能是:

transactionId=TXN-20260430-000123
status=SUCCESS
amount=299.00
createdAt=2026-04-30T10:12:31
failureReason=NONE

再结合风控输出:

score=78
level=HIGH
reasons=高金额交易,新客首单,凌晨时段交易

最后触发工单:

ticketId=RT-a8b1c92d2e11
status=SUBMITTED

这一整条链路的关键价值不只是“AI 能回答问题”,而是结果来自真实业务系统,关键动作经过工具级控制,整个过程可审计、可回放、可治理。

十三、常见生产问题与应对策略

13.1 工具描述写得太泛,导致误调用

现象:用户问“退款趋势”,模型却调用单笔订单查询;用户只是想解释失败原因,模型却创建了工单。

解决:在 description 中明确 when-to-call 和 when-not-to-call;给关键写操作工具增加显式触发条件;对误调用日志做定期复盘。

13.2 工具执行太慢,拖垮整轮对话

现象:一个 10 秒的聚合查询导致整轮对话超过 15 秒。

解决:聚合任务异步化;增加缓存;限制单次查询时间窗口;细化数据库索引与预聚合表。

13.3 多实例下重复执行写操作

现象:模型重试叠加网关重试,出现重复建单。

解决:引入幂等键;明确网关与客户端重试策略;写操作结果持久化并支持重复返回。

13.4 工具返回内容不稳定,模型消费困难

现象:有时返回自然语言,有时返回散乱字段,下游 Agent 很难稳定解析。

解决:统一返回格式;保持字段语义稳定;给关键 Tool 做契约测试。

十四、从 PoC 到生产的演进路线

建议按四阶段推进,而不是一开始就堆满所有基础设施。

阶段一:本地验证

单实例、本地数据库、STDIO 或简化 HTTP、验证工具定义与业务闭环。

阶段二:团队测试

单实例远程访问、OAuth2 接入、基础日志与指标、初步 Tool 描述调优。

阶段三:生产试点

Streamable HTTP、多实例部署、Redis 缓存与会话外置、熔断、限流、幂等、审计。

阶段四:平台化治理

MCP Gateway、服务注册发现、工具元数据动态化、多租户隔离、工具市场化管理。

这个节奏比“一步到位设计宇宙飞船”更现实,也更符合多数企业技术团队的推进方式。

总结

MCP 在企业里的真正价值,不是“让模型多了一个插件入口”,而是为 AI 与业务系统之间建立了一条可标准化、可治理、可扩展的工程通道。

从架构角度看,这套体系至少有五个关键结论:

  1. MCP 的本质是标准化能力暴露,而不是简单函数调用。
  2. Spring Boot 非常适合承载 MCP Server,因为它天然具备分层、治理、监控和安全基因。
  3. 生产环境要优先考虑高并发与可扩展,而不是只追求 Demo 跑通。
  4. 写操作工具必须具备幂等、审计、授权和异步化能力。
  5. 真正稳定的 AI 系统,不取决于模型多聪明,而取决于工具链路是否可靠。

如果你要把一句话带走,那应该是:MCP 不是给 AI 加工具,而是给企业 AI 系统加操作系统级的接口规范。

当你把 Tool、Resource、Prompt、鉴权、审计、可观测性和分布式治理串成一套完整架构后,AI 才真正从“演示能力”走向“生产能力”。




上一篇:崩坏星穹铁道AI助手实测:从帕姆帮帮看米哈游的长线游戏AI实践
下一篇:AI算力涨价,用AI代替员工的快开不起工资了!网友:果然还是牛马便宜
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-1 22:45 , Processed in 0.652573 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表