被问麻了!Function Calling、MCP、Skill 到底啥关系?一张图讲透三层架构,附 Java 生产落地全流程+30个坑

被问麻了!Function Calling、MCP、Skill 到底啥关系?一张图讲透三层架构,附 Java 生产落地全流程 +30 个坑

最近不管是面试别人还是被人面,这个问题出现的频率高得离谱:"聊聊 Function Calling、MCP、Skill 三者的关系?"

十个人里八个答不利索:要么把三个概念搅成一团,要么只会背定义,一追问工程落地就露馅。

这篇文章一次性讲透:三者的分层关系、每层原理、完整 Java 落地代码,外加 30 多个真实生产踩坑。适合正在做 AI Agent 落地的 Java 工程师,也适合准备 AI 方向面试的朋友。建议先收藏,内容有点长。


一、先给结论:不是三选一,是三层楼

很多人一上来就想分个高下,其实方向就错了。这三个东西根本不是竞品,是一条流水线上的三个工位,自下而上层层封装:

概念 本质 解决什么问题 生活化类比
Function Calling 模型侧的原生能力 模型怎么表达调用意图:"我要调谁、传什么参数" 🗣️ 食客的嘴:只会点菜,不会做菜
MCP 接入侧的开放协议 工具怎么被标准化发现、连接、跨进程调用 🔌 USB-C 接口 / 聚合支付:插上就能用
Skill 业务侧的封装单元 任务怎么按 SOP 干成:工具 + Prompt + 流程 + 知识 📘 岗位操作手册 / 预制菜谱

一句话记忆:FC 是嘴(管表达),MCP 是插座(管连接),Skill 是手册(管流程)。

在展开之前,先纠三个高频误区,这也是面试最容易失分的地方:

误区 1:"MCP 是 Function Calling 的升级版,有了 MCP 就不需要 FC 了。"

错。FC 是模型侧的表达层,MCP Client 收到模型意图后,底层依然是 FC 产生的 tool_call。二者是上下层,不是替代关系。没有 FC,MCP 的工具调不动;没有 MCP,工具接入就是点对点的泥潭。
误区 2:"Skill 和 MCP 是竞争关系。"

也不是。两者基本正交 :Skill 可以只靠 FC + 脚本工作,不必依赖 MCP;MCP 也不需要 Skill 才能跑。当然,MCP 工作组正在推进 Skills over MCP 标准,2026 年这两层正在融合------提前知道这个趋势,面试会很加分。
误区 3:"MCP 是多智能体通信协议。"

不是。MCP 是应用 ↔ 工具 的垂直连接;Agent ↔ Agent 的横向协作是 A2A 协议。两个概念分开说,专业度立刻上来了。

三者叠起来的整体架构长这样:

图里其实藏着三条通路,实际开发中我们一般叫它们:

  • 本地快车道 :@Tool 注解方法,进程内直接调用,毫秒级;
  • 远程标准道:MCP Client ↔ MCP Server,跨进程、跨语言、可独立部署;
  • 知识注入道:Skill 作为"教材"按需塞进 System Prompt,教模型怎么编排前两条路。

💡 划重点:Function Calling 让模型"知道能做什么",MCP 让模型"能标准化地连接到什么",Skill 让模型"知道该怎么做才靠谱"。

概念有了,接下来逐层拆。每一层我都会讲清楚"从哪来、是什么、缺什么",这样你才能理解为什么架构要长成这样。


二、逐层拆解:三兄弟各自的定位

2.1 Function Calling:模型的"嘴"

出身:OpenAI 于 2023 年 6 月在 gpt-3.5-turbo 上首次引入。

本质 :模型的一种原生能力------不是协议,不是框架。你按预定义的 JSON Schema 描述函数,模型判断需要时,吐出一个结构化的调用意图。

先看个最小的例子,有个直观感受:

java 复制代码
public class WeatherTools {
    @Tool(description = "查询指定城市的实时天气,返回温度与天气现象")
    public WeatherInfo getWeather(
            @ToolParam(description = "城市名,如:北京") String city) {
        return weatherService.query(city);
    }
}

这里有个关键认知 ,也是 FC 最容易被说反的地方:模型从不直接执行代码。它像一个只会点菜、不会做菜的食客------知道菜单上有啥、自己想吃啥,但端菜上桌跟它没关系。真正的执行永远在你的 JVM 里,整个闭环是这样的:

"模型决策 + 应用执行 + 结果回传",这三方契约才是 FC 的本质。

实际开发中,还有几个 FC 的关键细节值得记住:

  • Parallel Tool Calls:模型可以一次返回多个调用意图(比如同时查天气 + 查机票),无依赖的调用可以并行执行;
  • Strict Mode / Structured Output:用 JSON Schema 约束输出兜底,减少格式幻觉;
  • 无状态 :工具结果必须以 role="tool" 消息回填进 messages,模型才知道发生了什么;
  • 不执行、无副作用:这是它和 Agent 的根本区别。

也正因为执行权在你手上,鉴权、审批、限流、审计全部可以落在自己的 JVM 代码里------这是后面所有安全设计的根基。

但 FC 也有明显的局限,这些局限正是上面两层出现的原因:

  • ❌ 工具定义与模型强绑定,OpenAI、通义、文心的 Schema 格式各不相同,换个模型就得重适配;
  • ❌ 工具一多,Schema 膨胀,模型选择准确率明显下降;
  • ❌ 没有统一的发现、认证、权限规范,每个 Agent 都得自己硬编码工具集。

2.2 MCP:AI 界的"USB-C 接口"

出身:Anthropic 于 2024 年 11 月开源,现已捐赠给 Linux 基金会旗下 AAIF 中立治理。

本质 :一套标准化的客户端-服务器协议,基于 JSON-RPC 2.0。

它解决的痛点特别直白。没有 MCP 之前,每个应用自己手写对接代码,A 模型接天气写一套,B 模型再接还得写一套:

text 复制代码
有 MCP 之前:N 个 AI 应用 × M 个工具 = N×M 种定制集成  😵
有了 MCP 后:N 个应用 + M 个 Server = N+M 种适配      😎

工具提供方封装成 MCP Server 独立部署,AI 应用作为 MCP Client 连接,通过标准接口发现和调用工具。拓扑结构是"一个 Client 对接一个 Server":

MCP Server 侧有三大原语,这是它比 FC 多出来的东西,也是面试加分点:

原语 控制方 用途 例子
Tools 🛠️ 模型自主决定 可执行函数 query_order、refund
Resources 📄 应用控制 结构化上下文数据 文件内容、DB Schema、Git 历史
Prompts 📝 用户控制 可复用提示词模板 斜杠命令、菜单项

💡 正因为多了 Resources 和 Prompts ,MCP 才不只是"工具调用协议",而是一个上下文中台。

协议层面,MCP 主要干了三件事:

  1. 动态能力发现 :客户端发 tools/list 就能拿到服务器当前全部工具列表,不需要硬编码。新增工具只需部署新 Server,应用无需重启;
  2. 标准化调用接口 :通过 tools/call 发起调用,参数用 JSON Schema 校验,跨语言跨框架无缝对接;
  3. 安全边界:协议要求对工具调用做严格的权限控制。

传输方式目前主流是两种:

  • stdio:本地子进程,延迟极低,适合桌面场景;
  • Streamable HTTP:远程 / Serverless 场景(2025 年 3 月引入,已取代老的 HTTP+SSE 双端点方案)。

⚡ 重点来了:2026-07-28 发布的第 5 版规范 ,官方定性为"自协议发布以来规模最大、最系统性的一次修订",核心是从有状态连接全面转向无状态核心。这是本年度面试最值得说的点:

变更 内容 对 Java 后端意味着什么
🚫 移除握手 initialize / initialized 握手删除 启动期不再有"协商一次"的全局状态
🚫 移除会话 Mcp-Session-Id 请求头删除 不再需要 sticky session / 共享 Session 存储
📦 请求自描述 版本、clientInfo、capabilities 放进 _meta 每个请求可落到任意实例,普通轮询 LB 即可
🔎 新增 server/discover 客户端可主动探能力 能力发现不再依赖握手
🏷️ 强制路由头 Streamable HTTP 必须带 Mcp-Method、Mcp-Name 网关/WAF/限流器不用解 JSON body 就能路由鉴权
⏱️ 列表可缓存 tools/list 等返回 ttlMs、cacheScope 客户端可缓存工具列表,省 token、降延迟
🔄 MRTR 多轮往返 取代服务端主动请求(elicitation / sampling) 中途要用户确认 → 返回 input_required,客户端带 inputResponses 重试
✅ Tasks 转正 io.modelcontextprotocol/tasks 扩展,tasks/get 轮询 长任务终于有原生协议支持
🔐 鉴权加固 强化 OAuth 2.0 / OIDC(RFC 9207 签发者校验) 可直连 Entra / Okta 等企业 IAM
⚠️ 弃用清单 Roots / Sampling / Logging / HTTP+SSE,至少 12 个月兼容期 升级窗口要排期

💡 一句话理解这次修订:MCP 从"工具调用协议"升级成了"AI 原生应用交付协议"------工具 + UI + 任务 + 授权 = 完整能力包。无状态化是所有通用协议走向生产级基础设施的必经之路,改造后你的 MCP Server 就是一个普通 HTTP 微服务,随便扩缩容、滚动发布,故障切换从"会话迁移"变成"DNS 切换"。

工程上,MCP 的价值还体现在:工具独立进程部署、独立升级、独立鉴权,业务系统零侵入接入。

MCP 这部分信息量有点大,先消化一下。下一节讲 Skill------它是三者里最容易被低估、也最容易出彩的一个。

2.3 Skill:把经验打包成"技能书"

出身:Anthropic 2025 年 10 月推出,2025 年 12 月 18 日开放为跨平台标准(agentskills.io)。

本质 :一种知识/流程的打包格式 ------就是文件,不是框架。一个 Skill 就是一个文件夹:

text 复制代码
aftersale-refund/
├── SKILL.md          # 核心:YAML 元数据 + Markdown 操作指令
├── scripts/          # 可执行脚本(Python / bash),代码不进上下文
│   └── create_ticket.py
├── references/       # 参考资料:政策文档、Schema、模板
│   ├── refund-policy.md
│   └── reply-template.md
└── assets/           # 模板、素材

很多人只说"Skill 就是 prompt 模板",这个理解太浅了。Skill 的灵魂是渐进式披露(Progressive Disclosure):

层级 内容 加载时机 成本
L1 name + description 启动时常驻 约 30~100 token/个
L2 SKILL.md 正文 SOP 任务命中才加载 < 5k token
L3 scripts/、references/ 按需执行,源码不进上下文,只有输出进 几乎无限

💡 这个设计牛在哪:企业可以装 100 个 Skill 而上下文不爆。官方原话说得很直白------"with a filesystem and code execution tools, the amount of context that can be bundled into a skill is effectively unbounded"。

对比一下 MCP:所有 Server 的工具描述在会话启动时就要全部塞进上下文,装多了 token 消耗非常可观(后面踩坑篇会讲,连 3 个 MCP Server 启动就能吃掉 2~7 万 token)。

Skill 和 Function Calling 的本质区别,一张表说清:

维度 Function Calling Skill
粒度 原子能力(单个函数) 任务模块(一套流程)
构成 JSON Schema 函数定义 Prompt + 代码 + 流程定义 + 异常处理
解决的问题 "能不能调" "该不该这么调"
可复用性 单个工具级别 业务场景级别
示例 queryOrder() "订单退款"完整流程

用代码视角看更直观:

text 复制代码
Function Calling :  queryOrder(orderNo)     → 一次原子调用
Skill            :  handleAfterSales(诉求)   → 理解 → 查单 → 判责 → 生成方案 → 发通知
                   └─ 内部可能触发 5 次 Function Call + 2 次子 Skill

在真实业务里,一个 Skill 往往包含这些东西:

text 复制代码
refund_skill
├─ query_order        查询订单
├─ check_refund_rule  校验退款规则
├─ create_refund      创建退款单
├─ notify_user        通知用户
└─ 高危确认策略        需要用户或管理员确认

也就是说:Skill = Prompt 模板 + 一个或多个 Tool + 参数校验规则 + 权限策略 + 结果摘要策略 + 人工确认流程。

它可组合、可版本化、可灰度、可复用,是 Agent 资产化的核心------这也是为什么我说它最容易出彩:工具是死的,Skill 才是沉淀团队经验的地方。


三、横向对比:一张总表看懂三者

把前面的信息汇总,这张表建议直接存下来:

维度 🗣️ Function Calling 🔌 MCP 📘 Skill
本质 模型原生能力 开放协议(JSON-RPC 2.0) 知识打包格式(文件夹)
解决的问题 模型怎么表达调用意图 工具怎么标准化接入 任务怎么做才对
提出者 / 时间 OpenAI · 2023.06 Anthropic · 2024.11 Anthropic · 2025.10
粒度 单个函数(原子) 一组工具/资源/提示 一个业务流程 / SOP
谁定义 模型训练 + 开发者 Schema Server 提供方 业务/领域专家
通信方式 请求里带 tools 定义,无状态 stdio / Streamable HTTP 文件系统,无需运行时
工具发现 硬编码或手工传入 服务端动态注册 tools/list 在配置中心/目录声明式管理
跨应用复用 ❌ 强绑定当前应用与模型 ✅ 一次部署、任意客户端可连 ✅ 一次安装、任意 Agent 可用
换模型成本 高(Schema 重适配) 低(协议中立) 无
token 成本 Schema 全量进上下文 同上,但可列表缓存 渐进披露,起步 ~30 token
安全机制 需业务代码自行校验 内置权限、授权规范 基于底层能力 + 业务级护栏
失败模式 参数幻觉 / 误调用 连接断开 / 超时 规划漂移 / 死循环
Java 生态 Spring AI @Tool / LangChain4j Spring AI MCP Starter / MCP Java SDK 无框架依赖,纯文件 + 脚本

📖 有人总结得很到位:Skills 是任务的高级模板(SOP + Tools),Tools 是能力的低层接口(API)。


四、选型决策:什么场景用什么

面试高频追问:"那你们项目里怎么选?"我的答案是先看决策树:

再配一张场景速查表:

你的场景 推荐 理由
单体 Spring Boot 应用,工具 < 10 个 🧩 Function Calling 零网络开销,@Tool 五分钟接入
工具要被 Claude / Cursor / 多个内部 Agent 复用 🌐 MCP 协议标准、独立部署、生态互通
任务流程固定且反复出现(写周报、发版、审 PR) 📦 Skill 沉淀 SOP,团队共享,不占常驻上下文
大型平台级 Agent(工具 50+) 🌐 MCP + 📦 Skill 组合 MCP 管连接,Skill 管流程与按需加载

什么时候必须上 MCP? 出现下面任意一个信号就别犹豫了:

  1. 工具由多个团队独立维护,不希望互相侵入代码;
  2. 工具需要动态增减,而应用不能频繁发版;
  3. 需要独立发版扩缩容、故障隔离与鉴权边界;
  4. 想打造一个 AI 工具市场,让不同模型和客户端复用同一套工具。

反过来,如果只是个人项目或内部几个简单 API,直接 @Tool 绑定就够,千万别硬上三层,那是给自己找运维。

💡 大厂真实答案 :FC 是地基,MCP 是水电煤,Skill 是 SOP------生产上三者常常叠着用。但选型永远记住八个字:小而内用,够用就好。

一句话记忆选型:知识 → Skill;连接 → MCP;单应用单模型 → Function Calling;跨 Agent 委派 → A2A。


五、Java 落地全流程(可直接抄)

好,概念讲完,上硬菜。这部分以 Spring AI 为主线,LangChain4j 为辅,环境、代码、配置一次给全。

5.0 环境基线(别在这些地方翻车)

text 复制代码
JDK         17+      ← Spring Boot 3.x 硬要求,JDK 8 直接劝退
Spring Boot 3.2+
Spring AI   1.x GA / 2.x(或 MCP Java SDK 1.0.0+)
xml 复制代码
<!-- pom.xml:Spring AI BOM 统一管理版本 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>2.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

⚠️ 注解包名和 Starter 名称随版本演进很快,落地时以你锁定版本 的官方文档为准,别抄错包。上线前最好跑一下 mvn dependency:tree -Dincludes=io.modelcontextprotocol.sdk 核实 MCP SDK 的真实版本。

5.1 Function Calling:@Tool 注解 + 身份注入

推荐写法:@Tool 注解 + 服务端注入身份 + 工具内越权校验。

java 复制代码
@Component
public class OrderTools {

    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    /**
     * 工具描述六要素:何时用 / 参数含义 / 返回什么 / 不该用于什么 / 是否只读 / 是否需确认。
     * 描述质量直接决定模型会不会调用、调得对不对。
     */
    @Tool(description = """
            根据订单号查询订单详情。当用户询问订单状态、金额、物流时使用。
            仅支持查询当前登录用户所属租户的订单,不修改任何数据。
            返回字段:orderNo, status, amount, shipStatus。
            """)
    public OrderVO queryOrder(
            @ToolParam(description = "订单号,形如 ORD20261013001", required = true)
            String orderNo,
            ToolContext context) {

        // ⚠️ 关键:tenantId 不由模型填写,从认证上下文注入,模型无从伪造
        String tenantId = (String) context.getContext().get("tenantId");
        String userId = (String) context.getContext().get("userId");

        OrderVO order = orderService.query(tenantId, orderNo);
        // 工具内做越权校验,绝不信任模型传来的任何参数
        if (!order.getUserId().equals(userId)) {
            throw new BizException("无权访问该订单");
        }
        return order;
    }

    @Tool(description = "创建退款工单。会真实产生资金副作用,执行前必须获得用户明确确认。")
    public RefundResult createRefund(
            @ToolParam(description = "订单编号") String orderId,
            @ToolParam(description = "退款原因") String reason) {
        return refundService.execute(orderId, reason);
    }
}

💡 技术亮点 :@Tool 的 description 是模型选工具的主要依据------写"查询实时天气"和写"查数据",选择准确率差着一个量级。这是后面踩坑篇的重点之一。

调用侧组装 ChatClient,身份通过 toolContext 下传:

java 复制代码
@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel, OrderTools orderTools,
                                 ChatMemory memoryStore) {
        return ChatClient.builder(chatModel)
                .defaultSystem("""
                        你是订单助手。调用工具前必须确认订单号格式;
                        工具失败时如实告知,禁止编造结果。
                        """)
                .defaultTools(orderTools)                    // 可挂多个:.defaultTools(a, b, c)
                .defaultAdvisors(new MessageChatMemoryAdvisor(memoryStore))
                .build();
    }

    // 方法引用 → ToolCallback,零反射样板代码
    @Bean
    public ToolCallbackProvider orderToolProvider(OrderTools tools) {
        return MethodToolCallbackProvider.builder().toolObjects(tools).build();
    }
}

@Service
public class AgentService {

    private final ChatClient chatClient;

    public AgentService(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    public String chat(String userInput, String tenantId, String userId) {
        return chatClient.prompt()
                .user(userInput)
                .toolContext(Map.of("tenantId", tenantId, "userId", userId)) // 身份随上下文下传
                .call()
                .content();
    }
}

Spring AI 会自动处理整个循环:模型返回 tool_calls → 框架执行 → 结果回传模型 → 生成最终回答。

5.2 理解 Agent 主循环:FC 不是一次调用就结束

框架帮你封装了循环,但你得知道它内部在干嘛。Function Calling 本质是一个 Agent Loop:

java 复制代码
@Service
@RequiredArgsConstructor
public class AgentExecutor {

    private final ChatClient chatClient;
    private final SkillRegistry skillRegistry;
    private final ToolDispatcher toolDispatcher;

    public String chat(String userInput) {
        List<ChatMessage> messages = new ArrayList<>();
        messages.add(ChatMessage.user(userInput));

        int maxTurns = 5;   // 🛡️ 必须设上限,防止模型反复调工具停不下来

        for (int i = 0; i < maxTurns; i++) {
            ChatResponse response = chatClient.chat(
                    messages, skillRegistry.listToolDefinitions());

            if (!response.hasToolCalls()) {
                return response.content();      // 模型认为信息够了,生成最终答案
            }
            messages.add(response.toAssistantMessage());

            for (ToolCall toolCall : response.toolCalls()) {
                ToolResult result = toolDispatcher.dispatch(toolCall);
                messages.add(ChatMessage.toolResult(toolCall.id(), result.text()));
            }
        }
        return "抱歉,当前处理轮次过多,请重试。";
    }
}

流程就是:LLM 返回 tool_calls → Java 执行工具 → 结果塞回 messages → 再次请求 LLM → 直到生成最终答案。

⚠️ 注意 :流式输出场景下,tool_call 是分片到达的。千万别收到第一个片段就执行------参数会残缺。一定等 tool_call 完整后再执行,高危工具还要暂停流等用户确认。

5.3 多模型兼容:格式适配器 + 统一注册中心

如果你同时对接多家模型(OpenAI、通义、文心),会立刻撞上一个坑:各家的 Function Schema 格式不一样,切换模型要重写所有工具定义。

解法是在中间加一层格式适配器------所有工具只维护一份标准 Schema,自动转换成对应模型的格式:

java 复制代码
/**
 * MCP 多模型格式适配器:屏蔽不同厂商 Function 格式差异
 */
@Component
public class ToolFormatAdapter {

    public List<Object> adapt(List<ToolDefinition> tools, String modelType) {
        return switch (modelType) {
            case "OPENAI" -> tools.stream().map(this::toOpenAiFormat).toList();
            case "QWEN"   -> tools.stream().map(this::toQwenFormat).toList();
            case "ERNIE"  -> tools.stream().map(this::toErnieFormat).toList();
            default -> throw new IllegalArgumentException("不支持的模型类型");
        };
    }

    private OpenAiFunction toOpenAiFormat(ToolDefinition tool) {
        return OpenAiFunction.builder()
                .name(tool.getName())
                .description(tool.getDescription())
                .parameters(tool.getJsonSchema())
                .build();
    }
}

再配一个统一的工具注册中心,把幂等、权限、埋点都收口在调用入口:

java 复制代码
@Component
public class McpToolRegistry {
    private final Map<String, ToolDefinition> toolMap = new ConcurrentHashMap<>();

    public void register(String toolName, ToolDefinition definition) {
        toolMap.put(toolName, definition);
    }

    // 统一调用入口:内置幂等校验、权限校验、日志埋点、限流
    public String invoke(String toolName, Object args) {
        // 1. 幂等校验(基于请求ID + 工具名 + 参数哈希去重)
        // 2. 权限校验
        // 3. 执行工具
        // 4. 结果埋点
        return doInvoke(toolName, args);
    }
}

💡 技术亮点:这一层就是"自研 MCP 管控层"的最小骨架。工具注册、格式适配、统一执行三件事收口之后,换模型、加工具、做审计都不用再动业务代码。

5.4 MCP Server:把服务暴露成标准工具

依赖:

xml 复制代码
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

配置:

yaml 复制代码
spring:
  ai:
    mcp:
      server:
        name: order-mcp-server
        version: 1.0.0
        protocol: STREAMABLE        # STDIO / STREAMABLE / STATELESS

注解式暴露工具 。这里有个安全设计的抓手------annotations 风险分级:

java 复制代码
@Service
public class OrderMcpServer {

    private final OrderService orderService;

    public OrderMcpServer(OrderService orderService) {
        this.orderService = orderService;
    }

    @McpTool(
        name = "query-order",
        description = "根据订单号查询订单详情",
        annotations = @McpTool.McpAnnotations(
            title = "订单查询",
            readOnlyHint    = true,     // 只读,客户端可放心自动执行
            destructiveHint = false,
            idempotentHint  = true
        )
    )
    public OrderDetail queryOrder(
            @McpToolParam(description = "订单号", required = true) String orderId) {
        return orderService.detail(orderId);
    }

    @McpTool(
        name = "refund-order",
        description = "对指定订单发起退款。会真实产生资金副作用,执行前必须获得用户明确确认。",
        annotations = @McpTool.McpAnnotations(
            title = "订单退款",
            readOnlyHint    = false,
            destructiveHint = true,     // 破坏性操作,客户端应弹确认
            idempotentHint  = false
        )
    )
    public RefundResult refundOrder(
            @McpToolParam(description = "订单号", required = true) String orderId,
            @McpToolParam(description = "幂等键,由调用方生成,重试时保持不变", required = true)
            String idempotencyKey) {
        return orderService.refund(orderId, idempotencyKey);
    }

    // Resources 原语:把只读数据注入上下文,不消耗一次工具调用
    @McpResource(uri = "order://schema", name = "订单库表结构")
    public String orderSchema() {
        return "orders(id, order_no, tenant_id, status, amount, ship_status)";
    }
}

readOnlyHint / destructiveHint / idempotentHint 这几个 hint 不是摆设------客户端会据此决定"能不能自动执行、要不要弹确认、能不能安全重试"。把风险分级写进工具元数据,比在 prompt 里喊一百遍"小心点"都管用。

如果不用 Spring AI Starter,也可以直接用 MCP Java SDK 手写 Server:

xml 复制代码
<dependency>
    <groupId>io.modelcontextprotocol.sdk</groupId>
    <artifactId>mcp</artifactId>
    <version>0.15.0</version>
</dependency>
java 复制代码
public class OrderMcpServer {

    public static void main(String[] args) {
        McpServer server = McpServer.sync(
                new StdioServerTransportProvider()   // 支持 STDIO 和 Streamable HTTP 两种传输
        )
        .serverInfo("order-mcp-server", "1.0.0")
        .tool(new SyncToolSpecification(
                "queryOrder",
                "根据订单号查询订单",
                Map.of("orderId", "string"),
                (exchange, args) -> {
                    String orderId = (String) args.get("orderId");
                    OrderInfo info = orderService.getOrderDetail(orderId);
                    return new CallToolResult(
                            List.of(new TextContent(info.toJson())), false);
                }
        ))
        .build();
        server.start();
    }
}

💡 MCP Server 的生产重点:后端必须拦截隐私字段(手机号、身份证、余额),同时记录调用人、调用时间、调用内容,方便合规排查------这才是 MCP 在生产环境的核心价值,而不是"能跑通"。

5.5 MCP Client:两套主流方案 + 协议原理

方案 A:Spring AI 官方 Starter(Spring 体系内最省心)

xml 复制代码
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

配置文件里声明要连的 MCP Server,stdio 和 HTTP 都支持:

yaml 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        # 本地进程:stdio 方式
        stdio:
          connections:
            order-server:
              command: java
              args: ["-jar", "order-mcp-server.jar"]
        # 远程服务:HTTP 方式
        connections:
          weather-server:
            url: http://localhost:8081/mcp

Spring AI 会把所有 MCP 连接的工具自动装配成 ToolCallbackProvider,一行接入:

java 复制代码
@Bean
public ToolCallbackProvider orderTools(McpSyncClient mcpClient) {
    // 远端工具的 Schema 启动时拉回来,适配成本地 ToolCallback,对上层完全透明
    return new SyncMcpToolCallbackProvider(List.of(mcpClient));
}

// Agent 里混用本地工具 + MCP 工具
ChatClient chat = ChatClient.builder(chatModel)
        .defaultTools(new WeatherTools())            // 🧩 本地
        .defaultToolCallbacks(mcpToolCallbackProvider) // 🌐 MCP 远端
        .build();

方案 B:LangChain4j(零样板,跨框架)

xml 复制代码
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-mcp</artifactId>
  <version>1.1.0-beta7</version>
</dependency>
java 复制代码
@Configuration
public class McpClientConfig {

    @Bean
    public McpToolProvider mcpToolProvider() {
        // 远程 MCP Server:走 HTTP
        McpTransport transport = new HttpMcpTransport.Builder()
                .sseUrl("http://localhost:8080/mcp")
                .logRequests(true)      // 调试期打开,生产关掉
                .logResponses(true)
                .build();

        // 本地 MCP Server:换成 StdioMcpTransport,命令即进程
        // McpTransport stdio = new StdioMcpTransport.Builder()
        //         .command(List.of("/usr/bin/npx", "-y", "@your/mcp-server"))
        //         .build();

        McpClient mcpClient = new DefaultMcpClient.Builder()
                .key("orderMcpClient")
                .transport(transport)
                .build();

        return McpToolProvider.builder()
                .mcpClients(mcpClient)   // 可挂多个 Client,工具自动合并去重
                .build();
    }
}

// 使用时,把 MCP 工具直接交给 AiServices
OrderAgent agent = AiServices.builder(OrderAgent.class)
        .chatModel(chatModel)
        .toolProvider(mcpToolProvider)
        .build();

如果想更贴近协议本身(比如自己封装网关),可以用 Streamable HTTP 传输 + 超时与能力协商:

java 复制代码
// 1️⃣ 传输层:Streamable HTTP(生产推荐,支持断线重连与流式)
var transport = HttpClientStreamableHttpTransport.builder("http://mcp-gateway:8080")
        .customizeRequest(r -> r.header("X-Tenant-Id", tenantHolder.get()))
        .build();

// 2️⃣ 客户端:带超时与能力协商
var mcpClient = McpClient.sync(transport)
        .requestTimeout(Duration.ofSeconds(10))
        .capabilities(ClientCapabilities.builder().roots(true).sampling(true).build())
        .build();
mcpClient.initialize();

理解协议:MCP 客户端的标准四步

不管用哪个框架,客户端干的事都是这四步:

text 复制代码
① 连接 MCP Server
② listTools / listResources / listPrompts → 拿到全部能力与 Schema
③ 把 Schema 转成 LLM 认识的工具定义
④ 用户提问 → 连同工具一起给 LLM → 由 LLM 决策调用

手写 JSON-RPC 的话,核心就是三个方法(新规范下 initialize 握手已移除,这里展示的是协议全貌):

java 复制代码
public class McpClientGateway {
    private final McpTransport mcpTransport;
    private final AtomicLong requestId = new AtomicLong(0);

    // 自动发现服务端所有可用工具
    public List<McpTool> listTools() {
        JsonRpcResponse resp = send(buildRequest("tools/list", Collections.emptyMap()));
        return objectMapper.convertValue(
                resp.getResult().get("tools"), new TypeReference<List<McpTool>>() {});
    }

    // 调用 MCP 工具
    public ToolResult callTool(String serverId, String toolName, JsonNode arguments) {
        JsonRpcRequest request = buildRequest("tools/call",
                Map.of("name", toolName, "arguments", arguments));

        JsonRpcResponse response = mcpTransport.request(
                serverId, request, Duration.ofSeconds(3));   // ⏱️ 超时一定要设

        if (response.hasError()) {
            throw new McpToolException("MCP tool call failed: " + response.errorMessage());
        }
        return ToolResult.success(response.resultAsString());
    }
}

💡 技术亮点 :MCP 协议核心就是 tools/list(发现)和 tools/call(调用)两个 JSON-RPC 方法。框架做的事,本质是把远端工具 Schema 拉回来适配成本地工具回调------协议归协议、框架归框架,这就是解耦。

5.6 Skill 落地:从 SKILL.md 到编排引擎

第一步:写 SKILL.md。 看一个真实的售后退款技能:

markdown 复制代码
---
name: aftersale-refund
description: 处理电商售后与退款工单。当用户提到退款、退货、售后、工单、退货物流时使用。
---

# 售后退款技能

## 使用前提
先读取 `references/refund-policy.md`,确认当前生效的退款政策。不要凭记忆判断政策。

## 标准流程
1. 调用 `query-order` 确认订单状态与金额
2. 若未发货 → 直接走退款,但**必须先向用户确认金额**
3. 若已发货 → 先调用 `query-logistics`,再引导用户寄回
4. 生成工单:运行 `scripts/create_ticket.py`,**脚本输出的 JSON 即为工单号**(不要编造)
5. 最后按 `references/reply-template.md` 输出面向用户的回复

## 禁止事项
- 禁止在未确认金额的情况下调用 `refund-order`
- 单笔超过 5000 元必须走 `escalate-to-human`
- 禁止把内部字段 `internalRiskScore` 直接返回给用户

注意最后的"禁止事项":Skill 里写的护栏就是给模型立的规矩,比在 system prompt 里反复强调"不要乱退款"稳定得多。

第二步:Java 侧接入。 这里有两条主流路线。

路线一:接口式 Skill + 注册中心(适合强流程编排)

java 复制代码
// 通用 Skill 接口
public interface Skill {
    String getName();
    String getDescription();
    String getPromptTemplate();              // 📝 业务 Prompt
    List<ToolCallback> getRequiredTools();   // 🔧 需要的工具
    SkillResult execute(SkillContext context); // 🎯 编排执行
}

// "订单退款"Skill 实现
@Component
public class OrderRefundSkill implements Skill {

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

    @Override
    public String getDescription() {
        return "处理订单退款请求:查询订单→校验状态→执行退款→通知用户";
    }

    @Override
    public String getPromptTemplate() {
        return """
            你是订单退款处理专家。请按以下 SOP 执行:
            1. 调用 queryOrder 查询订单状态
            2. 校验订单是否满足退款条件(已支付、未超期)
            3. 调用 createRefund 执行退款
            4. 如果退款失败,记录失败原因并告知用户
            ⚠️ 如果订单状态为"已发货",必须提示用户联系人工客服
            """;
    }

    @Override
    public SkillResult execute(SkillContext context) {
        // 🎯 业务级编排:串流程、处理异常、兜底逻辑
        try {
            OrderInfo order = queryOrder(context.getOrderId());
            if (!order.isRefundable()) {
                return SkillResult.fail("订单不满足退款条件: " + order.getStatus());
            }
            RefundResult refund = createRefund(order.getId(), context.getReason());
            notifyUser(context.getUserId(), "退款成功,预计3-5工作日到账");
            return SkillResult.success(refund);
        } catch (Exception e) {
            log.error("退款Skill执行异常", e);
            return SkillResult.fail("退款处理失败,请稍后重试");
        }
    }
}

注册中心负责管理和按需加载:

java 复制代码
@Component
public class SkillRegistry {
    // 元数据缓存(轻量):启动只加载 name + description
    private final Map<String, SkillMeta> metaCache = new ConcurrentHashMap<>();
    // 完整 Skill 懒加载
    private final Map<String, Skill> skillCache = new ConcurrentHashMap<>();

    public SkillMeta match(String userIntent) {
        // 🔍 只遍历元数据,快速匹配
        return metaCache.values().stream()
                .filter(meta -> meta.matchIntent(userIntent))
                .findFirst().orElse(null);
    }

    public Skill load(String skillName) {
        return skillCache.computeIfAbsent(skillName, this::doLoadSkill);
    }
}

💡 技术亮点:渐进式加载 。Skill 不一次性全部注入,启动时只加载元数据,执行时再加载完整内容。版本管理上,给每个 Skill 加 version 字段,支持灰度发布和回滚;A/B 测试时同一场景可以挂载不同版本,按用户分桶决定用哪个。

路线二:注解式 Skill + 自动 Schema 生成(适合降低业务接入成本)

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Skill {
    String name();
    String description();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface SkillParam {
    String value();
    String description() default "";
    boolean required() default true;
}

业务同学只管写业务方法:

java 复制代码
@Component
public class OrderSkill {

    @Skill(name = "query_order",
           description = "根据订单号查询订单状态和物流信息")
    public OrderVO queryOrder(
            @SkillParam(value = "orderId", description = "订单号") String orderId) {
        return orderService.queryById(orderId);
    }
}

框架层用注解 + 反射 + Jackson,自动把 Java 方法转成 LLM 可识别的 JSON Schema:

java 复制代码
@Component
public class SkillSchemaRegistry {

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final Map<String, ToolDefinition> definitions = new ConcurrentHashMap<>();
    private final Map<String, ToolHandler> handlers = new ConcurrentHashMap<>();

    public void register(Object bean) {
        for (Method method : bean.getClass().getDeclaredMethods()) {
            Skill skill = method.getAnnotation(Skill.class);
            if (skill == null) continue;

            ObjectNode schema = objectMapper.createObjectNode();
            schema.put("type", "object");
            ObjectNode properties = schema.putObject("properties");
            ArrayNode required = schema.putArray("required");

            for (Parameter parameter : method.getParameters()) {
                SkillParam param = parameter.getAnnotation(SkillParam.class);
                if (param == null) continue;

                ObjectNode property = properties.putObject(param.value());
                property.put("type", javaTypeToJsonType(parameter.getType()));
                property.put("description", param.description());
                if (param.required()) required.add(param.value());
            }

            definitions.put(skill.name(),
                    new ToolDefinition(skill.name(), skill.description(), schema));
            handlers.put(skill.name(), args -> invokeMethod(bean, method, args));
        }
    }

    private String javaTypeToJsonType(Class<?> type) {
        if (type == String.class) return "string";
        if (type == Integer.class || type == Long.class) return "integer";
        if (type == Boolean.class) return "boolean";
        if (type == Double.class || type == Float.class) return "number";
        return "object";
    }
}

好处很直接:业务只关心业务方法,框架自动生成 Schema、自动注册到 Agent 可用工具列表。

进阶:Planner 式编排器(把模型的随机性关进确定性的笼子)

复杂场景下,Skill 可以只声明"骨架",让 Planner 动态生成执行计划:

java 复制代码
public record SkillDef(
        String id,
        String triggerIntent,          // 触发条件(意图 + 约束)
        List<String> requiredTools,    // 依赖工具
        List<String> steps,            // 执行步骤(给 Planner 的骨架)
        String outputSchema,           // 结构化输出契约
        double riskLevel               // 风险等级 → 是否需要人工确认
) {}

@Service
@RequiredArgsConstructor
public class SkillOrchestrator {

    private final ChatClient chatClient;
    private final SkillRegistry registry;
    private final ToolExecutor executor;

    public SkillResult execute(String userInput, UserContext ctx) {
        SkillDef skill = registry.match(userInput);                  // 1. 意图路由
        if (skill.riskLevel() > 0.7) {
            humanApprovalService.request(skill, userInput);          // 2. 高危人工确认 🛑
            return SkillResult.pending();
        }

        // 3. 生成执行计划(结构化输出,不是自由文本!)
        Plan plan = chatClient.prompt()
                .user(u -> u.text(PROMPT).param("skill", skill).param("input", userInput))
                .call()
                .entity(Plan.class);                 // ← 自动 JSON Schema 校验
        Validator.validate(plan);                    // ← Bean Validation 兜底

        // 4. DAG 执行,单步超时 + 幂等
        Map<String, Object> ctxMap = new ConcurrentHashMap<>();
        for (PlanStep step : plan.steps()) {
            Object r = executor.invoke(step.tool(), step.args(), ctx)
                    .timeout(Duration.ofSeconds(8))
                    .idempotentKey(step.stepId() + ":" + ctx.tenantId())
                    .fallback(step.fallback())
                    .execute();
            ctxMap.put(step.stepId(), r);
        }

        // 5. 🌟 结果压缩回填,防止上下文爆炸
        return chatClient.prompt()
                .user(u -> u.text(SUMMARIZE).param("facts", TokenCompressor.compress(ctxMap)))
                .call()
                .entity(SkillResult.class);
    }
}

Skill 路由:RAG 检索,只注入最相关的那一个

Skill 多了之后,靠意图匹配或向量检索路由:

java 复制代码
@Component
public class SkillLoader {

    private Map<String, String> skills;

    // 启动时扫描 skills/ 目录,注册全部可用 Skill
    @PostConstruct
    public void loadSkills() throws IOException {
        try (var paths = Files.list(Path.of("skills"))) {
            skills = paths.filter(Files::isDirectory)
                    .collect(toMap(
                        p -> p.getFileName().toString(),
                        p -> Files.readString(p.resolve("SKILL.md"))));
        }
    }

    // 按用户意图路由:向量检索最相关的 Skill,只注入那一个
    public String injectSkill(String userQuery) {
        String topSkill = skillRetriever.search(userQuery);
        return skills.getOrDefault(topSkill, "");
    }
}

// Agent 侧:Skill 指令 + Function Calling 执行
String systemPrompt = """
        你是招聘助手。请严格按照以下 SOP 执行:
        %s
        """.formatted(skillLoader.injectSkill(query));

chatClient.prompt()
        .system(systemPrompt)
        .tools(new FileRenderTool())   // Skill 的 scripts 由工具执行
        .user(query).call();

最后看两个组合示例。SQL 优化 Skill(状态机 + 降级策略):

java 复制代码
@Skill(name = "sql_optimize_skill", description = "MySQL 语句性能分析与优化建议")
public class SqlOptimizeSkill implements AiSkill {

    @Override
    public SkillResult execute(String userSql, SkillContext context) {
        SkillResult.SkillResultBuilder builder = SkillResult.builder();
        try {
            String explainResult = callStep("sql_explain", userSql, context);   // 执行计划分析
            builder.stepResult("执行计划分析", explainResult);

            String indexResult = callStep("index_analyze", explainResult, context); // 索引健康度
            builder.stepResult("索引分析", indexResult);

            String rewriteResult = callStep("sql_rewrite", indexResult, context);   // 改写建议
            builder.finalResult(rewriteResult).success(true);
        } catch (Exception e) {
            // 步骤失败降级:返回基础优化建议,不让整个技能崩掉
            builder.finalResult("基础优化建议: 避免 SELECT *、增加 WHERE 条件索引")
                   .success(false).errorMsg(e.getMessage());
        }
        return builder.build();
    }

    // 渐进式披露:仅注入当前步骤需要的上下文,减少 Token 开销
    private String callStep(String funcName, String input, SkillContext context) {
        context.injectCurrentFunc(funcName);
        return functionCallHandler.execute(
                new FunctionCallRequest(funcName, input)).getResult().toString();
    }
}

差旅助手 Skill(本地 FC + 远端 MCP 混合编排):

java 复制代码
@Component
public class TravelSkill implements Skill {
    private final WeatherTools weather;        // 本地 FC 工具
    private final FlightMcpClient flightMcp;   // MCP 远端工具

    @Override
    public SkillResult execute(SkillContext ctx) {
        String city = ctx.get("city");
        String date = ctx.get("date");
        String weatherInfo = weather.getWeather(city);
        String flights = flightMcp.searchFlights(city, date);
        return new SkillResult(
                "目的地天气: " + weatherInfo + "\n可选航班: " + flights);
    }
}

此时 Skill 注册中心对外暴露能力列表,前端就能渲染成用户可选的"技能卡片"------能力资产化就这么落地了。

5.7 统一抽象:别把本地函数和 MCP 写成两套

如果只说"我会调 Function Calling",那是普通回答;能说到统一 Tool Gateway,才是加分项。

用 sealed 接口抽象所有调用:

java 复制代码
public sealed interface ToolInvocation
        permits LocalToolInvocation, McpToolInvocation {
    String name();
    JsonNode arguments();
}

public record LocalToolInvocation(
        String name, JsonNode arguments) implements ToolInvocation {}

public record McpToolInvocation(
        String serverId, String toolName, JsonNode arguments
) implements ToolInvocation {
    @Override
    public String name() {
        return "mcp." + serverId + "." + toolName;   // 统一命名规范
    }
}

MCP 工具统一命名成 mcp.{serverId}.{toolName},比如 mcp.logistics.query_delivery、mcp.crm.query_user_profile。这样对模型来说仍然是普通 Tool,对系统来说可以精确路由到 MCP。

Dispatcher 负责分发,治理逻辑统一收口:

java 复制代码
@Component
@RequiredArgsConstructor
public class ToolDispatcher {

    private final SkillRegistry skillRegistry;
    private final McpClientGateway mcpClientGateway;
    private final ToolGovernance toolGovernance;

    public ToolResult dispatch(ToolCall toolCall) {
        return toolGovernance.execute(toolCall, call -> {
            if (call.name().startsWith("mcp.")) {
                McpToolTarget target = McpToolTarget.parse(call.name());
                return mcpClientGateway.callTool(
                        target.serverId(), target.toolName(), call.arguments());
            }
            return skillRegistry.execute(call.name(), call.arguments());
        });
    }
}

治理层把鉴权、限流、幂等、审计一次性做齐:

java 复制代码
@Component
@RequiredArgsConstructor
public class ToolGovernance {

    private final AccessControl accessControl;
    private final RateLimiter rateLimiter;
    private final IdempotencyService idempotencyService;
    private final AuditService auditService;

    public ToolResult execute(ToolCall call, Function<ToolCall, ToolResult> executor) {
        try {
            accessControl.check(call);                    // 不是所有用户都能调敏感工具
            rateLimiter.acquire(call.name());             // 防止模型疯狂调用打爆下游
            idempotencyService.markExecuting(call.id());  // 防止重复执行退款、下单

            ToolResult result = executor.apply(call);

            auditService.success(call, result);           // 所有 Tool Call 可追踪
            idempotencyService.markSuccess(call.id());
            return result;
        } catch (Exception e) {
            auditService.fail(call, e);
            idempotencyService.markFail(call.id());
            // 错误回填给模型,让模型决定是否重试或换参数
            return ToolResult.error("工具调用失败:" + e.getMessage());
        }
    }
}

💡 技术亮点:本地函数、HTTP 工具、MCP 工具全部纳入同一套注册、调用、鉴权、限流、审计和观测体系。这才是企业级 Agent 和 demo 的分水岭。

5.8 全链路时序:一次退款请求的完整旅程

把三层串起来,看一次真实请求怎么跑完全程:

代码写到这儿,"能跑通"的问题解决了。但离"能上生产"还差一道闸门------下一节的治理代码,才是真正拉开差距的地方。这个系列会持续拆解 AI 工程化的实战坑,感兴趣的话点个关注,不迷路。


六、生产级治理:六段值得写黑板上的代码

6.1 工具执行守卫(ToolGuard):一道闸门管住所有执行

这是把"能跑通"变成"能上生产"的关键一环 。核心思想:模型提出调用 ≠ 业务可以执行,所有真实执行必须过这道闸门------鉴权、高危确认、幂等、审计、脱敏、异常降级,一个切面全搞定:

java 复制代码
@Aspect
@Component
public class ToolGuardAspect {

    /** 需要人工确认的高危工具(也可改为读 destructiveHint 动态判断) */
    private static final Set<String> HIGH_RISK =
            Set.of("refund-order", "cancel-order", "delete-*");

    private final AuthService authService;
    private final IdempotentStore idempotentStore;
    private final AuditLogger auditLogger;
    private final PrivacyMasker privacyMasker;

    @Around("@annotation(org.springframework.ai.tool.annotation.Tool)")
    public Object guard(ProceedingJoinPoint pjp) throws Throwable {
        String toolName = ((MethodSignature) pjp.getSignature()).getMethod().getName();
        long start = System.currentTimeMillis();

        try {
            // ① 鉴权
            authService.checkToolPermission(toolName);

            // ② 高危闸门:未确认则不执行,返回结构化"待确认"结果给模型
            if (isHighRisk(toolName) && !ConfirmContext.confirmed(toolName)) {
                return ToolResult.pending(toolName,
                        "该操作有资金/数据副作用,需用户明确确认后再执行");
            }

            // ③ 幂等:同一个业务键只真正执行一次,重试/多副本不会重复扣款
            String idemKey = IdempotencyKey.of(toolName, pjp.getArgs());
            Object result = idempotentStore.executeOnce(idemKey, () -> {
                try {
                    return pjp.proceed();
                } catch (Throwable e) {
                    throw new BusinessException(e.getMessage(), e);
                }
            });

            // ④ 审计 + 脱敏:记录调用人/时间/内容,拦截手机号、余额等隐私字段
            auditLogger.success(toolName, idemKey, result, System.currentTimeMillis() - start);
            return privacyMasker.mask(result);

        } catch (Exception e) {
            // ⑤ 异常降级:返回结构化错误而不是抛栈,让模型能自我纠错重试
            auditLogger.fail(toolName, e);
            return ToolResult.error(toolName, "执行失败:" + e.getMessage());
        }
    }

    private boolean isHighRisk(String toolName) {
        return HIGH_RISK.stream().anyMatch(p -> toolName.matches(p.replace("*", ".*")));
    }
}

6.2 并行调用 + 结构化错误自愈

无依赖的工具调用并行执行,用 JDK 21 虚拟线程 + 结构化并发:

java 复制代码
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var futures = calls.stream().map(tc -> scope.fork(() -> safeCall(tc))).toList();
    scope.joinUntil(Instant.now().plusSeconds(10));
    scope.throwIfFailed();
    results = futures.stream().map(Subtask::get).toList();
}

配套的关键设计:工具报错不抛异常,而是结构化回灌让模型自纠:

java 复制代码
"TOOL_ERROR: " + r.error() + " 请修正参数后重试"

💡 技术亮点:异常会炸掉整轮对话,结构化错误能让 LLM 自我纠错------这是 demo 级和生产级的分水岭。

6.3 弹性执行器:幂等 + 熔断 + 重试 + 超时

java 复制代码
public class ResilientToolExecutor {

    private final Cache<String, Object> idemCache = Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(10)).maximumSize(10_000).build();

    private final CircuitBreaker cb = CircuitBreaker.ofDefaults("tool");

    public Object invoke(ToolCallback tool, Map<String, Object> args, ToolContext ctx) {
        String idemKey = ctx.getContext().get("idemKey").toString();
        return idemCache.get(idemKey, k ->                       // 🌟 幂等:重复调用直接命中缓存
                CircuitBreaker.decorateSupplier(cb, () ->
                        Retry.ofDefaults("tool", 3, Duration.ofMillis(200),
                                Timeout.of(Duration.ofSeconds(8)),
                                () -> tool.call(JsonUtils.toJson(args), ctx))
                ).get());
    }
}

极简版幂等装饰器,两行就能看懂原理:

java 复制代码
String fp = sha256Hex(toolName + jsonArgs);
return store.computeIfAbsent(who + ":" + fp, () -> delegate.call(jsonArgs, ctx));

⚠️ 模型会重试,没有幂等键就会重复退款 💰。幂等键由 Server 侧校验,不要让模型生成。

6.4 虚拟线程 + 超时隔离

AI Agent 99% 的时间都在等外部调用返回,MCP 连接超时、线程阻塞会直接传导成故障。JDK 21 虚拟线程特别适合这个场景:

java 复制代码
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<ToolResult> future = executor.submit(() -> mcpClient.callTool(request));
    try {
        ToolResult result = future.get(10, TimeUnit.SECONDS);   // ⏱️ 超时控制
    } catch (TimeoutException e) {
        future.cancel(true);
        log.warn("MCP工具调用超时: {}", request.getName());
        return ToolResult.timeout();
    }
}

每个工具调用跑在独立虚拟线程上,超时自动取消,互不阻塞。

6.5 并发限流 + Token 预算

大模型推理和复杂工具链调用极易超过默认超时,Token 消耗和并发请求必须严格限制:

java 复制代码
private final Semaphore mcpSemaphore = new Semaphore(10);          // 最多10个并发
private final AtomicLong tokenBudget = new AtomicLong(100_000);    // Token 预算

public ToolResult callMcpTool(McpRequest request) throws InterruptedException {
    if (tokenBudget.get() <= 0) {
        return ToolResult.fail("Token 预算耗尽,请稍后重试");
    }
    mcpSemaphore.acquire();
    try {
        ToolResult result = mcpClient.callTool(request);
        tokenBudget.addAndGet(-result.getTokenCost());
        return result;
    } finally {
        mcpSemaphore.release();
    }
}

6.6 按权限动态收敛工具集

java 复制代码
/**
 * 原则:模型能"看到"的工具,必须是当前用户"有权用"的工具。
 * 这既是安全边界,也直接降低模型选错工具的概率。
 */
public ToolCallbackProvider permissionAwareProvider(UserContext user,
                                                    ToolCallbackProvider all) {
    return () -> Arrays.stream(all.getToolCallbacks())
            .filter(t -> rbac.allowed(user, t.getToolDefinition().name()))
            .toArray(ToolCallback[]::new);
}

💡 一石二鸟:防越权 + 省 token。工具越少,模型选得越准。


七、踩坑大全:30 个生产难点与解法

这部分是面试追问的重灾区,也是实际项目里最容易翻车的地方。先上总表,再挑几个最要命的展开讲。

# 难点 典型现象 解决方案
1 工具爆炸、选型错误 50+ 工具模型乱选,上下文被吃掉一半 分层路由(先选 Skill 再选工具)、工具白名单、embedding 动态召回 Top-K、MCP 列表缓存 ttlMs
2 模型参数不可信 编造订单号、传入别的租户 ID 模型输出视为不可信输入:Bean Validation + 服务端注入身份
3 高危工具误执行 模型直接调 refund-order 把款退了 风险分级 hints + 人工确认(MRTR)+ 幂等键 + 全链路审计
4 重复执行 网络重试 + 多副本导致重复扣款 业务幂等键 + 数据库唯一约束,幂等键由 Server 侧校验
5 上下文爆炸 工具返回 50KB JSON,窗口直接爆 字段白名单、分层摘要压缩、Skill 渐进披露
6 规划漂移 / 死循环 Agent 反复调同一工具停不下来 Structured Output 强约束 + 最大步数 + 重复调用检测 + fallback
7 MCP 有状态扩不了容 必须 sticky session、共享 Session 存储 升级 2026-07-28 无状态规范;跨调用状态用 server-minted handle 当参数传
8 新旧版本混跑 客户端支持新规范,SDK/Server 没跟上 dependency:tree 核实真实版本;别把配了 STATELESS 当成"完整支持新规范"
9 长任务超时 部署/导出跑 5 分钟,HTTP 早断了 Tasks 扩展(tasks/get 轮询)+ 进度通知 + 异步化
10 流式下 tool_call 分片 收到第一个片段就执行 → 参数残缺 等 tool_call 完整再执行;高危工具暂停流等确认
11 安全边界模糊 MCP 工具 ≈ 任意代码执行 OAuth 2.0 / OIDC、命名空间隔离、结果脱敏、禁止裸露 DB/Shell 权限
12 提示注入 & 越权 工具返回文本里藏"忽略之前的指令" 工具结果强制包 `` 标签声明为不可信数据;权限校验放工具内不放 prompt
13 可观测性缺失 出问题不知道是哪一轮哪一步 Micrometer + OpenTelemetry GenAI 语义规范 + toolCallId 级审计
14 多租户成本失控 租户 A 用量把 B 拖垮 ToolContext 强制注入 tenantId + 令牌桶限流 + 按 Skill 计费埋点
15 多模型格式不兼容 切换模型要重写所有工具定义 统一格式适配器,一份标准 Schema 自动转换
16 Skill 上下文串线 并发时 A 用户看到 B 用户的订单 每个 Skill 绑定独立会话上下文,ThreadLocal + 会话ID 隔离
17 长链路响应慢 多轮 FC + 多次模型调用,单轮超 5s 无依赖 Function 并行调用 + 工具结果缓存 + 流式输出降首屏等待
18 工具描述写得太烂 模型该调不调、调错工具 description 写全六要素:何时用/参数/返回/禁用场景/是否只读/是否需确认
19 参数幻觉 模型生成不存在的函数名/参数 调用前匹配注册列表 + JSON Schema 校验 + 有限重试(2 次)+ 兜底
20 多 Skill 状态一致性 中间步骤失败,断点重试困难 状态机(Pending→Running→Success/Failed)+ 上下文快照 + 全链路幂等
21 MCP 连接治理 Server 挂了拖垮主链路 连接池 + 心跳保活 + CircuitBreaker 隔离 + 降级到本地缓存
22 工具发现延迟 tools/list 拖慢首问响应 本地缓存工具清单 + 主动推送更新
23 异构协议兼容 已有内部服务不是 MCP,改造量大 写 MCP 适配器把 REST API 包装成 MCP Server
24 Skill 冲突 多个 Skill 能处理同一请求 意图路由层 + 置信度评分 + 降级转人工
25 跨进程性能损耗 JSON-RPC 序列化 + 网络往返 高频工具走 @Tool、连接池与结果缓存、批量调用合并
26 并发数据一致性 同时下单 + 退款导致超卖 幂等键(业务ID+步骤ID)+ DAG 依赖编排 + 补偿事务(Saga)
27 MCP Server 容错 单点故障导致 Agent 不可用 三层兜底:Client 熔断降级 + Skill 异常兜底 + Agent 重试策略
28 任务比凭证活得久 8 小时长任务中途凭证过期 per-task 身份、任务中途吊销、任务级预算归因
29 工具列表 Token 膨胀 连 3 个 Server 启动即耗 2~7 万 token 只挂必需 Server、分组按需加载、工具检索替代常驻 Schema
30 权限边界不清 用户 A 查到用户 B 数据 工具执行前注入用户上下文,服务端强制鉴权

挑几个最要命的展开讲,都是真金白银换来的教训。

坑 1:工具爆炸,模型选错率高得离谱

我之前就踩过这个坑:工具从 50 个涨到 200 个,模型选择准确率从 90% 左右一路掉到 60%,上下文还被工具描述吃掉一半。

根因有两个:Prompt 膨胀稀释了模型注意力;工具描述太模糊("查数据"这种描述等于没说)。

解法是组合拳:

  • description 写"何时用 + 怎么用",命名规范统一;
  • Skill 绑定工具白名单:模型先选 Skill(每个才几十 token),命中后只注册它那 5~8 个工具,prompt 直接降一个数量级;
  • 工具 > 50 时上 RAG 检索工具目录,让模型先"查工具"再"用工具";
  • Spring AI 的 ToolCallingAdvisor 支持动态工具发现,实测可将 Token 消耗降低 34%~64%;
  • embedding 动态召回 Top-K 工具,只把相关工具塞进 prompt,我们实测误调用率降 60%,token 降 70%。

💡 业界参考:Uber 的实践是------工具超过 100 个时,用 CLI 命令模式或工具检索替代常驻 Schema,能省 50% 以上的 Token。

坑 2:模型参数一律不可信

很多团队把模型返回的 JSON 直接当参数用,这是最容易被忽视的隐患。模型会幻觉出不存在的订单号,甚至传入别的租户 ID。

正确姿势:

  • 模型只能传"业务标识"(订单号、商品 ID);
  • 凡是"身份类"参数(tenantId、userId)一律由服务端从登录态注入,模型无从伪造;
  • Java 侧用 @Validated + Bean Validation 兜底;
  • 工具内部再做一次越权校验(查出来的订单不属于当前用户就直接拒绝)。

坑 3:高危操作必须有人工闸门

模型直接调 refund-order 把款退了,这种事故不是段子。三层防线缺一不可:

  1. 工具层 :风险分级,破坏性工具打 destructiveHint=true;
  2. 服务端层:参数校验 + 身份注入 + 幂等键;
  3. 执行层 :高危操作走人工确认(新规范的 MRTR 机制返回 input_required)+ 全链路审计。

同时记住一条铁律:根本不给模型裸露的 DB / Shell 权限,只给语义化工具。

坑 4:MCP 无状态化是 2026 年的必修课

旧版 MCP 基于会话,Server 重启即丢状态,扩容必须 sticky session。新规范转向无状态核心后,这些问题全没了。

但升级窗口期要小心新旧混跑 :Java 侧目前是"客户端先行、服务端跟进中"的状态。不要只看文章标题或 Starter 名字,要以实际构建产物的版本为准:

bash 复制代码
mvn dependency:tree -Dincludes=io.modelcontextprotocol.sdk

网关按路由表分流新旧三类流量,别把"配了 STATELESS"当成"完整支持新规范"。

坑 5:MCP 连接治理,P99 从 8s 到 1.2s

我们遇到过 MCP Server 抖动拖垮主链路的情况。解法:连接池 + 心跳保活 + CircuitBreaker 隔离(失败率 > 50% 熔断 30s)+ 降级到本地缓存工具。落地后 P99 从 8s 降到 1.2s。

如果面试官追问"MCP Server 挂了怎么容错",标准答案是三层兜底:

  • MCP Client 侧:Resilience4j CircuitBreaker,连续失败后自动降级到本地缓存数据;
  • Skill 层 :execute() try-catch 兜底,失败返回 SkillResult.fail(),不让 Agent 崩溃;
  • Agent 层:对幂等工具支持最多 2 次自动重试,非幂等工具直接提示用户。

坑 6:可观测性,排障时间降 90%

出问题不知道模型想了啥、调了啥,是最绝望的。方案:

  • OpenTelemetry GenAI Semantic Conventions 打点;
  • traceId 贯穿 prompt / tool_calls / tool_result 全链路;
  • 按 toolCallId 记录状态、参数摘要、结果摘要;
  • 记录 token 成本与工具命中率,做成本归因。

坑 7:多 Skill 编排的状态一致性

复杂任务多 Skill 串联,中间步骤失败会导致上下文不一致。我们用状态机管理:

每个节点状态流转明确,配合上下文快照(每步执行完持久化,失败从最近快照回滚重试)和全链路幂等(唯一请求 ID,重复调用直接返回缓存),断点续跑就不是问题。


八、高频追问快答:面试官最爱挖的 8 个坑

Q1:有了 MCP,还需要 Function Calling 吗?

需要。MCP Client 收到模型意图后,底层表达的依然是 FC 产生的 tool_call。FC 是模型侧的表达层,MCP 是发现与执行层------MCP 把"调用哪个 Server、怎么发现能力"标准化了,但没消灭表达层。

Q2:Skill 是不是就是"高级一点的 Prompt"?

不是,差两个数量级。Prompt 是一次性的、必须整体进上下文;Skill 是持久化文件夹 + 渐进披露 + 可执行脚本。关键差别:① 启动时只花 30~100 token;② 脚本代码本身不进上下文,只有输出进;③ 可跨 Claude Code / Codex CLI / Gemini CLI / Cursor 等 20+ 平台复用,不修改任何内容。

Q3:Skill 和 Workflow、Agent 什么区别?

Workflow 是人写死的流程 (确定性),Agent 是模型自己规划 (自主性),Skill 是中间态------用自然语言声明能力边界,由 Planner 动态排布步骤。生产上推荐"Skill 声明 + 关键节点代码兜底",纯自主 Agent 太不可控。

Q4:MCP 和 Dubbo / gRPC 什么区别?

传统 RPC 的契约是给编译器 看的,MCP 的契约是给大模型看的------所以要有自我描述的 Schema、人可读的 description、结构化错误自愈、动态工具发现。

Q5:传输怎么选?

本地桌面场景用 stdio;服务端一律 Streamable HTTP------单端点、会话可恢复、能无状态水平扩容、天然过网关。

Q6:不用 MCP 行不行?

行。工具 < 10 个且单应用内,进程内注册就够。出现"跨团队复用 / 独立发版扩缩容 / 需要故障隔离与鉴权边界"三个信号之一,就必须上。

Q7:怎么防止模型把生产库删了?

三层防线:① 工具层风险分级,破坏性工具打 destructiveHint=true;② 服务端参数校验 + 身份注入;③ 执行层人工确认 + 幂等键 + 全链路审计。同时根本不给模型裸露的 DB / Shell 权限,只给语义化工具。

Q8:怎么测试一个 Skill?

三层:① 单测 ------工具层 mock,验证参数校验与越权;② 契约测试 ------用固定评测集跑 Planner 输出,比对计划结构;③ 在线评估------采样真实对话做 LLM-as-Judge + 人工抽检,盯"任务完成率 / 工具误调用率 / token 成本"三个北极星指标。


九、收尾:口诀、清单与落地建议

写到这里,把全文浓缩成一段口诀:

text 复制代码
🗣️ Function Calling ------ 嘴:负责"我要调谁、传什么"
🔌 MCP              ------ 插座:负责"标准化地把工具接上"
📘 Skill            ------ 手册:负责"按什么流程、什么规矩干"

嘴管表达,插座管连接,手册管流程。
三明治三层,缺一层都别扭------但小项目别硬上三层,够用就好。

上线前速查清单:

  • JDK 17+ / Spring Boot 3.2+ / Spring AI 1.x GA+
  • 工具描述写全六要素,@ToolParam 补参数说明
  • 身份类参数由服务端注入,绝不由模型传
  • 破坏性工具打 destructiveHint + 人工确认 + 幂等键
  • MCP Server 优先 STREAMABLE / STATELESS,别再用老 SSE
  • 长任务走 Tasks 扩展 + 进度通知
  • 按权限动态收敛工具集(安全 + 省 token)
  • 单轮工具次数 / 工具链深度 / 总耗时都要设上限
  • Micrometer + OTel + toolCallId 级审计日志
  • 上线前 dependency:tree 核实 MCP SDK 实际版本

三条落地建议:

  1. 从 @Tool 起步。先把三五个本地工具跑通,把描述写好、把身份注入做对,再考虑 MCP;
  2. 治理先行。幂等、审计、高危确认这三件事,从第一个写操作工具就要有,别等出了资损再补;
  3. 用 Skill 沉淀经验。反复出现的流程(发版、周报、退款 SOP)尽早技能化,这是团队 AI 资产复利的开始。

Java 工程师在 AI 时代的核心价值,其实就是这句话:把模型那点"不确定性",用幂等、熔断、校验、可观测这些确定性的工程手段,收敛成生产可用的能力。


聊了这么多,留个互动问题:你们项目里现在给模型挂了多少个工具?有没有遇到过模型选错工具、或者重复调用的坑?最后是怎么解的? 评论区聊聊,我挑几个典型的下期展开讲。

觉得有用的话,点赞、在看、转发三连支持一下,这个「AI 工程化落地」系列会持续更新实战干货,关注不迷路。我们下期见!

相关推荐
hoLzwEge1 小时前
AI签到「一劳永逸」
人工智能·程序员·ai编程
文谦南宁AI普工1 小时前
写了个 PAP/PACT 授权面审计器:把 `poppy.json` 和 PACT 委托链的检查项跑成一条命令
aigc·ai编程
楚楚2511 小时前
2026最新5款AI编程助手基础版免费深度对比
ai编程
fthux2 小时前
编程语言:从机器指令到人类思维的演进史诗
ai编程
Rain的Java大神之路2 小时前
被问麻了!Function Calling、MCP、Skill 到底啥关系?一张图讲透三层架构,附 Java 生产落地全流程+30个坑
java·后端·ai编程
桃西西呀2 小时前
LangGraph4j之三:状态与通道
人工智能·llm·ai编程
桃西西呀2 小时前
LangGraph4j之二:图模型与节点边
人工智能·llm·ai编程
虞七月3 小时前
Claude Code 平替工具推荐:TRAE 是否值得作为首选替代方案?
copilot·ai编程
9i编程3 小时前
11. 教 AI 上班:带出我的数字同事 —— 前后端部署上线 130,按 v1.7 回归清零与总结
人工智能·openai·ai编程