被问麻了!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 主要干了三件事:
- 动态能力发现 :客户端发
tools/list就能拿到服务器当前全部工具列表,不需要硬编码。新增工具只需部署新 Server,应用无需重启; - 标准化调用接口 :通过
tools/call发起调用,参数用 JSON Schema 校验,跨语言跨框架无缝对接; - 安全边界:协议要求对工具调用做严格的权限控制。
传输方式目前主流是两种:
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? 出现下面任意一个信号就别犹豫了:
- 工具由多个团队独立维护,不希望互相侵入代码;
- 工具需要动态增减,而应用不能频繁发版;
- 需要独立发版扩缩容、故障隔离与鉴权边界;
- 想打造一个 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 把款退了,这种事故不是段子。三层防线缺一不可:
- 工具层 :风险分级,破坏性工具打
destructiveHint=true; - 服务端层:参数校验 + 身份注入 + 幂等键;
- 执行层 :高危操作走人工确认(新规范的 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 实际版本
三条落地建议:
- 从
@Tool起步。先把三五个本地工具跑通,把描述写好、把身份注入做对,再考虑 MCP; - 治理先行。幂等、审计、高危确认这三件事,从第一个写操作工具就要有,别等出了资损再补;
- 用 Skill 沉淀经验。反复出现的流程(发版、周报、退款 SOP)尽早技能化,这是团队 AI 资产复利的开始。
Java 工程师在 AI 时代的核心价值,其实就是这句话:把模型那点"不确定性",用幂等、熔断、校验、可观测这些确定性的工程手段,收敛成生产可用的能力。
聊了这么多,留个互动问题:你们项目里现在给模型挂了多少个工具?有没有遇到过模型选错工具、或者重复调用的坑?最后是怎么解的? 评论区聊聊,我挑几个典型的下期展开讲。
觉得有用的话,点赞、在看、转发三连支持一下,这个「AI 工程化落地」系列会持续更新实战干货,关注不迷路。我们下期见!