最近一段时间,我经常在技术群和面试复盘里被问到同一个问题:"Function Calling、MCP、Skill,这三者到底啥关系?" 问的人有后端同事,也有正在准备 Java 岗位的朋友。大家普遍把它们当成"三选一"来看,好像选了一个,另外两个就不用管了;甚至有人直接说 "MCP 不就是 Function Calling 换个名字吗?"
有一次我在电话里给人解释,解释到一半自己都有点绕。因为这三个词压根不在同一个抽象平面上:Function Calling 是模型的一种原生能力,MCP 是工具接入的协议标准,Skill 则是把知识和流程打包成可复用技能的封装方式。它们不是竞争关系,而是协作关系。
这篇文章我会先用一个三层框架把关系彻底讲清楚,然后给出一套 Java 落地全流程:用 Spring AI 的 @Tool 实现 Function Calling,用官方 MCP SDK 搭建 MCP Server / Client,再用自研的 SkillRegistry 把"订单售后顾问"这种能力打包复用,最后跑通一个真实场景。适合两类人看:一类是在 Java 业务里接入 AI Agent 的开发者,另一类是面试前想把概念边界理清的朋友。
1. 三者不在一个平面上:先记住"能力、协议、封装"六个字
1.1 一张表看穿三者的位置
我自己的习惯是先给一张对照表,把概念钉在对应的抽象层级上,后面再怎么聊都不会乱:
| 概念 | 抽象层级 | 解决什么问题 | 类比 |
|---|---|---|---|
| Function Calling | 模型能力层 | 模型如何在回复中声明"我要调用某个函数,参数是什么" | 员工会说"我要用计算器" |
| MCP | 协议/标准层 | 工具如何被标准化地发现、调用、返回结果 | 插座与插头的统一标准 |
| Skill | 应用封装层 | 如何把知识、多步流程、工具依赖打包成可复用技能 | 员工的作业指导书 |
拆开解释:
- Function Calling 回答的是"模型能不能调用"的问题。它是一种模型侧的行为:模型在生成文本之外,还能输出一段结构化 JSON,表示"我决定调用 findOrder,参数是 orderId=xxx"。至于这个 findOrder 在哪里、怎么执行,模型完全不管。
- MCP 回答的是"工具怎么插进来"的问题。它是一套协议,定义了 AI 应用(Client)如何与工具服务(Server)建立连接、列出工具、发起调用、接收结果。它的生态意义在于:任何工具实现方只要实现一次 MCP Server,所有支持 MCP Client 的应用就都能用。
- Skill 回答的是"遇到某类任务该怎么干活"的问题。它把触发条件、处理步骤、话术规则、依赖的工具清单打包成一个技能包。模型和工具都有了之后,没有 Skill 的 Agent 只能"随机应变";有了 Skill,Agent 面对特定任务时才能"按套路输出"。
1.2 用同一个 Agent 场景把三层串起来
纸上谈兵容易,我拿一个实际场景"订单售后顾问"来说明:
用户说"我的订单 OD-20240315 还没发货,帮我查查,能不能催一下"。
系统收到这句话后,大概会发生三件事:
- Skill 层先判断:这个请求命中了"订单售后顾问"技能,于是把售后流程的 system prompt 注入会话,同时把工具白名单收紧为"订单查询、物流查询、售后登记"这几个。
- 模型收到用户消息后,认为需要先查订单状态,于是触发 Function Calling,输出一个 tool_call:函数名 query_order_status,参数 orderId=OD-20240315。
- 执行器发现 query_order_status 不是本地方法,而是通过 MCP 协议暴露的远端工具,于是充当 MCP Client,把这次的 tools/call 请求转给远端 MCP Server,Server 再背后的订单系统里查出结果,返还给模型。
整个过程里:Skill 管"按什么流程做",Function Calling 管"什么时候决定调工具",MCP 管"工具请求怎么标准化地传出去"。三者同时在场,缺一个,Agent 都会变成跛脚鸭。
2. Function Calling:模型如何"假装"调用你的代码
2.1 机制:模型只是在输出一行结构化 JSON
早期大家是怎么让模型"调用工具"的?很原始:在 prompt 里写"如果用户想查天气,请输出 WEATHER_QUERY:beijing",然后后端解析字符串。这种 hack 在 demo 里能跑,但模型经常输出偏差,你也不知道该相信哪种格式。
Function Calling 把这件事做成了 API 层面的标准行为。在支持 tool calling 的模型接口里,你传入一个 tools 数组,每个工具用 JSON Schema 描述名字、说明和参数结构。模型生成回复时,会二选一:要么直接回复文本,要么输出一个 tool_call。这个 tool_call 本身也是结构化 JSON,包含函数名和参数。
这里有一个最基础也最容易被忽略的点:模型不会真的执行你的代码。它不会帮你查数据库,不会真的调用订单服务,甚至不会校验参数是否存在。模型只是"宣告"了调用意图。真正拿着 tool_call 去执行函数的是你的宿主程序。执行完,宿主程序把结果作为 toolResult 塞回会话,模型再基于结果继续生成第二段回答。
所以 Function Calling 的本质,是把"模型需要外部数据才能完成回答"这件事,用稳定的结构表达了出来。它是模型能力的一次升级,而不是后端框架的某个 API。
2.2 为什么工具描述(description)比参数本身更重要
模型选择工具靠的不是 Java 反射,也不是方法签名,而是语义理解。模型对你每个工具的感知全部来自 tools 数组里的这份"说明书"。因此:
- 函数名:要像变量名一样自解释。queryOrderStatus 是好的命名,f0x121_doJob 是灾难。
- description:必须写清三件事——触发条件、禁止条件、参数约束。
- 参数 schema:每个字段的类型、必填性、枚举取值要明确。
我举一个真实翻车案例:我之前给一个工具写过 description = "查询订单",结果模型把"查用户积分"的请求也调它。后来改成"仅当用户提供完整订单号如 OD-20240315 时调用;没有订单号不得调用;订单状态包括待支付/已支付/已发货/已取消",模型表现立刻正常。
社区里常有人把这类问题归结为"模型太笨",其实根因往往是工具描述给的信息量不够。你可以把模型理解成一个非常聪明、但对你的代码零知识的新同事——你把说明书写成一句话,他就只能按一句话执行。
2.3 这东西的边界在哪
Function Calling 解决的问题非常具体,所以边界也很清楚:
第一,它不解决"工具怎么注册、怎么运维"的问题。你的工具是本地方法、是 HTTP 接口、还是别人家的服务,Function Calling 一概不管。
第二,不同模型厂商的 tool schema 细节有差异。OpenAI、Anthropic、国产模型基本都参考了类似格式,但 Anthropic 早期对函数名有"只能字母数字下划线"之类的限制,某些模型对并发 tool_call 的支持程度也不同。这也是 MCP 这类统一协议存在的意义之一。
第三,Function Calling 只是一次调用决策。一个完整的 Agent 任务往往需要"模型调用工具 -> 拿结果 -> 再决策 -> 再调用"循环很多轮。这个循环不是模型 API 自动给的,是应用层的 Agent 编排代码写的。忘记了这一点,你会对"为什么我调了一次工具,模型就不接着做事了"感到困惑。
3. MCP:给工具装一个标准插座,插拔即用
3.1 从一个"到处都要写适配器"的痛点说起
在 MCP 出现之前,做一个 AI 应用要接外部能力,基本是一地鸡毛:
- 接 OpenAI,要按它的 tool schema 写函数描述;
- 接 Claude,要把同样的函数转成 Claude 的 tool 格式;
- 接私有化模型,可能还得退回 prompt 解析的老路;
- 每个工具的鉴权、超时、重试、错误码,全是要单独处理的细节。
这套东西没有标准,每个 AI 应用和每个工具之间都是点对点适配。今天你做了一个"订单查询函数"给 ChatGPT 用,明天想给 Claude 用,又得重写一层。
MCP 的思路非常朴素:与其两两适配,不如大家统一一个协议。工具提供方实现 MCP Server,AI 应用实现 MCP Client,中间用 JSON-RPC 2.0 通信。这样工具方只需要写一次暴露逻辑,所有支持 MCP 的 AI 产品都能用;AI 产品也只需要实现一次协议,所有 MCP 工具都能接。
3.2 MCP 的骨架:Client、Server、原语与传输
MCP 本身不是"某一个库",而是一套协议规范。架构上分三部分:
- MCP Host:跑着 AI 应用的主体,比如 Claude Desktop、JetBrains 插件、你的 Spring Boot 服务。
- MCP Client:Host 内部负责和 Server 建连、发请求的组件。
- MCP Server:暴露能力的一方,可以是本地子进程,也可以是一个远程 HTTP 服务。
协议里有三个原语,我建议用一句话记:
- Tools:可执行动作,本质上就是 Function Calling 里的 function。
- Resources:可读取的数据,比如数据库 schema、项目文档、配置文件,用于给模型补充上下文。
- Prompts:可复用的提示词模板,以标准方式提供给 Client。
传输层则常见四种:stdio(本地子进程通信,开发最方便)、SSE(旧版 HTTP 流式)、Streamable HTTP(新一代 HTTP 模式)、WebSocket(WSS)。Java 官方 SDK 都支持。
生态现状基本不用多说了:Playwright MCP 让 AI 能直接操作浏览器做自动化验证,Chrome DevTools MCP 让 AI 能帮你看前端调试信息,安全测试工具也有自己的 MCP Server,越来越多后端框架和 SaaS 工具都开始暴露 MCP 接口。
3.3 MCP 能解决什么,解决不了什么
MCP 能解决的是"接入标准化"和"能力复用"两件事。写好的"订单查询 Server"可以同时给桌面客户端、IDE 插件、你自己的后端 Agent 用,不用为每个消费方各写一套对接。
但记住,MCP 解决不了的事同样重要:
- MCP 不负责模型怎么决定调用哪个工具。这个决策仍然是模型推理的事,通常还要靠 Function Calling 触发。
- MCP 不管 Agent 的多步流程编排。先查订单还是先查物流、查完要不要发话术,这些得你在业务代码或 Skill 里定义。
- MCP 不包含任何业务逻辑。工具本身的鉴权、权限、数据质量,全是工具方自己的责任。
所以"用了 MCP"不等于"Agent 变聪明了"。MCP 只是让 Agent 的"手"能够标准化地触达外部世界,脑子还是要靠模型和你的编排层。
4. Skill:把知识、流程和工具调用打包成可复用能力
4.1 Skill 不是 Tool,也不是 MCP:它是"作业指导书"
Skill 这个概念最近明显变流行了,但很多人对它理解偏了。说到底,Skill 就是把"遇到某类任务该怎么做"打包成一个可以复用的技能包。
它和 Tool 的区别最直接:Tool 是单个可执行动作,Skill 是一整套流程和知识。比如"查订单状态"是 Tool,"给出完整的售后话术并判断该不该发起退货"是 Skill。
它和 MCP 的区别也要说清:MCP 是协议,解决工具怎么被调用;Skill 是内容,解决模型面对任务时按什么步骤执行。两者经常配合使用——一个 Skill 的内部流程里,会写明"先调用查订单工具,再调用物流工具,最后按规则生成话术",而这些工具本身可能是通过 MCP 暴露的。
生活化类比:Tool 是厨房里的锅和刀,MCP 是统一的"厨具插口标准",Skill 是菜谱。你有整套好厨具(Tools),也统一了接口(MCP),但没有菜谱(Skill),新来的厨师还是不知道先切菜还是先烧水。
4.2 一个 Skill 的内部结构长什么样
参考 Agent Skills 的实践和我自己在项目里的做法,一个工程化的 Skill 通常包含几块:
- 元信息:id、名称、版本、适用场景。
- 指令模板:告诉模型何时激活这个技能、按什么步骤做、输入输出要求、哪些事不能做。
- 依赖工具清单:这个 Skill 会用到哪些工具,显式列出,方便做工具权限过滤。
- 示例:给一两个 few-shot 输入输出,提高模型执行的稳定性。
- 资源引用:相关的文档、脚本、配置。
拿"Git 代码审查 Skill"举例更直观:输入是一个 PR 的 diff;流程是先调 git 工具拿到改动内容,然后按技能里的规则逐条审查(只查逻辑缺陷,不做风格建议,不重复 lint 已经报的问题),最后输出问题列表和严重级别。这里的"规则文本"和"审查步骤"就是 Skill,真正执行 git 命令的则是 MCP 暴露的 Tools。
4.3 三者怎么在一个项目里协作
最后把三层协作关系固定下来。项目里遇到一个问题时,你可以按这个顺序问自己:
| 你想问的问题 | 对应概念 |
|---|---|
| 模型怎么决定调用哪个工具、传什么参数? | Function Calling |
| 工具怎么标准化暴露给 AI,跨应用复用? | MCP |
| 怎么把多步流程、业务规则、话术模板打包复用? | Skill |
工程上的协作链路是:Skill 命中后注入指令并筛选工具清单 -> 模型在清单内决策 -> 模型输出 tool_call(Function Calling)-> 执行器通过 MCP 协议把 tool_call 转发给对应工具 -> 结果回填 -> 模型继续。三个概念各司其职,缺位任何一个环节,项目都会在某个具体场景里卡壳。
5. Java 落地全流程:从 Spring AI 把三者串成可跑 Agent
5.1 选型:Spring AI / LangChain4j / 原生 MCP SDK,怎么挑
Java 生态里落地这三个概念,主流有三级路线:
- Spring AI:Spring 官方在推,和 Spring Boot 结合最好,@Tool 注解开发体验最简单。适合团队已经以 Spring 为主、想最快跑通第一版 Agent 的场景。
- LangChain4j:老牌 Java LLM 框架,概念丰富,社区活跃。适合需要复杂 Agent 编排、且愿意接受框架更多概念的团队。
- modelcontextprotocol/java-sdk:官方 Java SDK,最贴近协议底层,灵活度最高,但 Agent 循环、上下文管理都得自己写。
我下面演示的组合是 Spring AI + 官方 MCP SDK。这不是唯一答案,但我在实际项目里跑过,比较顺手:Spring AI 管模型对话和本地 @Tool,MCP SDK 管外部工具协议层,Skill 用轻量级注册表管理。这套组合最大的好处是每一层都能被单独替换。
5.2 工程脚手架:JDK 17 + Maven 依赖 + 模型配置
基础环境用 JDK 17 + Spring Boot 3.x。pom.xml 里主要依赖这样加(版本号以你引入时官方最新为准,我当前用的是 1.0.x):
<properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> <mcp-sdk.version>0.10.0</mcp-sdk.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>io.modelcontextprotocol</groupId> <artifactId>sdk</artifactId> <version>${mcp-sdk.version}</version> </dependency> </dependencies>application.yml 里主要配两样:模型接入信息和 MCP 连接信息。模型部分我用 OpenAI 兼容协议,这样换国产模型也只需要改 base-url 和 api-key:
spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL:https://api.openai.com} chat: options: model: ${OPENAI_MODEL:gpt-4o-mini}注意:不同小版本 Spring AI 的配置前缀偶尔会调整,我写的是当前版本可用的写法。如果你的 IDE 自动提示里前缀不一样,以提示为准,逻辑不变。
5.3 第一步:用 @Tool 实现 Function Calling
Spring AI 里实现函数调用非常直接:写一个 Spring Bean,方法上加 @Tool 注解,框架会自动收集这些方法并把它们转成模型 API 需要的 tool schema。
import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Service; @Service public class OrderToolService { @Tool(description = "根据订单ID查询订单状态,订单ID格式如 OD-20240315;只有当用户提供完整订单号时才调用") public String queryOrderStatus(String orderId) { // 真实项目里这里调订单服务,demo 直接返回 return """ 订单号: %s 状态: 已发货 发货时间: 2024-03-16 10:30 承运商: 顺丰 运单号: SF1234567890 """.formatted(orderId); } }这里有两个细节值得说:
- 方法名 queryOrderStatus 会被作为工具名,所以命名要清晰。
- 返回值用 String 是最稳的。Spring AI 虽然也能帮你序列化对象,但一旦序列化链路出问题,模型拿到的可能是无意义的 toString 结果。
有了这个工具,用户问"OD-20240315 啥时候发货"时,模型就会决策调用这个函数。但你可能会问:如果订单数据不在自己系统,而是另一个团队用 MCP Server 提供的呢?这就到了第二步。
5.4 第二步:把工具通过 MCP Server 暴露出去
假设"订单查询"不是本地方法,而是另一个系统提供的 MCP 能力。此刻你的应用只需要当一次 MCP Client。
用官方 SDK 连一个远端 MCP Server 的典型代码结构如下:
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.spec.McpSchema; // 远端 Server 地址,假设它暴露了 query_order_status 工具 McpSchema.Implementation clientInfo = new McpSchema.Implementation( "order-service-demo", "1.0.0" ); McpClient client = McpClient.sync(clientInfo, new HttpClientTransport( "http://order-server.local/mcp" )); client.initialize(); // 列出远端 Server 提供的所有工具 List<McpSchema.Tool> remoteTools = client.listTools();拿到 remoteTools 之后,把它们和自己的本地 @Tool 合并成一个统一的工具列表,再交给 ChatClient。这样一次模型请求里,模型能同时看到本地函数和 MCP 远端工具。
整个调用链会是这样的:
- 模型输出 tool_call:函数名 query_order_status。
- 执行器判断:这个函数不在本地 @Tool 列表里,而是注册在 MCP Client 的 remote tool 列表里。
- 执行器把参数交给 MCP Client,由它按协议发 tools/call 给远端 Server。
- Server 执行真实查询,返回 JSON。
- 执行器把结果作为 toolResult 塞回会话。
- 模型基于结果生成最终回答。
如果你的需求是把"自己的工具"提供给别的 AI 应用用,那就反过来,在系统里起一个 MCP Server。用官方 SDK 可以这样暴露:
McpSchema.Tool queryOrderTool = McpSchema.Tool.builder() .name("query_order_status") .description("根据订单ID查询订单状态") .inputSchema(""" { "type": "object", "properties": { "orderId": {"type": "string"} }, "required": ["orderId"] } """) .build(); McpServer server = McpServer.sync("order-service-server") .transport(new HttpServerTransport("/mcp", 8081)) .tool(queryOrderTool, this::handleQueryOrder) .register();到这里,Function Calling 和 MCP 已经在 Java 工程里串起来了。最后一步补上 Skill。
5.5 第三步:把 Skill 做成可注册的"技能包"
Spring AI 没有一个现成的 Skill 类,但这不影响我们做。我习惯的做法是定义一个轻量的 Skill 注册表:
public record SkillDefinition( String id, String activateCondition, String systemPrompt, List<String> requiredTools ) {}然后注册"订单售后顾问"这个技能:
SkillRegistry registry = new SkillRegistry(); registry.register(new SkillDefinition( "order_after_sale", "用户咨询订单状态、发货、物流、售后相关问题", """ 你是一名订单售后顾问。处理流程: 1. 先调用 query_order_status 获取订单状态。 2. 如果已发货,再调用 query_logistics 查询物流。 3. 如果超过承诺发货时间,向用户致歉并说明原因。 4. 不要编造物流信息,查不到就如实说明。 """, List.of("query_order_status", "query_logistics") ));真正的执行逻辑很简单:每次收到用户消息时,先在 SkillRegistry 里做一次意图命中判断;命中某个 Skill 后,把它注入到 ChatClient 的 system 消息里,并且只加载该 Skill 白名单里的工具,其他工具对模型全部隐藏。
这里没有魔法。Skill 本质上是"系统性组织 prompt + 工具权限"的工程化方案。它的价值在项目变大后马上体现:不同业务域的规则不再散落在聊天逻辑里,而是一个个可注册、可复用、可单独调整的包。
5.6 完整跑通:一个"订单售后顾问"的调用链
最后,把三件套合体跑一遍。用户消息:"订单 OD-20240315 到底发货没有?"
完整链路如下:
- 意图识别层命中 Skill "order_after_sale",注入售后顾问指令,启用工具白名单。
- ChatClient 把 system + user 消息、白名单工具一并发给模型。
- 模型输出 tool_call:{"name": "query_order_status", "arguments": {"orderId": "OD-20240315"}}。
- 执行器检查发现该函数由 MCP Client 注册,于是按 MCP 协议发起 tools/call 请求。
- 远端 MCP Server 查出订单状态,返回 JSON:状态已发货、承运商顺丰、运单号 SF1234567890。
- 执行器把结果作为 tool 消息塞回会话。
- 模型看到结果后,再调用一次 query_logistics 查到最新物流节点。
- 模型按售后顾问的话术规则,告诉用户"已发货,预计明天送达"。
把每一步日志打出来,你能非常清楚地看到三层各干了什么:模型何时产生了调用决策(Function Calling),数据以什么协议走了一圈(MCP),最终回答被什么规则约束了(Skill)。这三个层次在一个 Agent 项目里不是可选项,而是互补项。
6. Java 实战里最容易被坑的 6 个点
6.1 工具返回类型:String 与结构化对象的恩怨
Spring AI 的 @Tool 方法返回 String 是最稳妥的,因为模型 API 的 toolResult 字段本身就是一个字符串。你可以返回一个序列化好的 JSON 字符串,模型读起来完全没问题。但如果方法返回一个 Order 对象,框架可能做了序列化,也可能直接 toString,一旦 toString 输出com.example.Order@1a2b3c这种垃圾,模型看到的就是一串无意义地址,后续回答自然一团糟。
我的建议:工具方法一律返回 String;如果你确实需要结构化结果,把工具调用结果当作数据,在执行器层自己解析,而不是在工具方法里给模型返回一个半死不活的对象。
6.2 description 写得糊,模型就瞎调工具
这是 Function Calling 里最常见的调优点,也是最容易被忽略的。一个工具能不能被正确触发,大部分取决于 description 写得好不好。
我踩过的一个典型坑:给工具写 description = "查询订单信息",结果模型在用户报出手机号的时候也调它,因为模型觉得"手机号也能查订单"。后来我把 description 改成"仅当用户提供完整订单号(格式如 OD-20240315)时调用,手机号查询不在本工具支持范围内",误调率立刻降为接近零。
经验格式是固定的:description 里要写清三件事——什么情况下触发、什么情况下禁止触发、参数遵守什么格式。
6.3 同步阻塞:慢工具会把整个 Agent 拖死
Function Calling 的执行模型是"一次 tool_call 回来,执行完再继续生成"。如果你的工具要跑 30 秒,整轮 Agent 就得干等 30 秒。Java 里最常见的坑是工具里直接调外部 HTTP 或者数据库慢查询,没有超时控制。
解法有两个层次:
- 第一层:给所有工具调用加超时。HTTP 客户端设置 connectTimeout / readTimeout,数据库查询设置 statement timeout,不让某个工具无限阻塞循环。
- 第二层:对长任务不要同步硬等。改成"任务受理 -> 返回任务ID -> 模型告诉用户正在处理 -> 提供查询任务状态的工具",让 Agent 异步轮询结果。这比同步等待优雅得多,但要注意给模型提供任务状态查询入口,否则模型无法自行确认结果。
6.4 死循环:模型反复调用同一个工具
模型是有概率陷入工具调用死循环的,尤其是在工具执行失败但错误信息不够明确时。典型情况:某个查询工具一直返回错误字符串,模型反复调用同一个工具重试,白烧了十几轮 token 还没结果。
我在工程里会用一道保险:单轮任务限制 tool_call 次数,默认 8 次。一旦超限,把最后一次工具结果替换成"已达到最大工具调用次数,请直接基于已有信息回复用户,不要再调用任何工具"。这样模型会被强制收尾,而不是无限空转。
6.5 MCP 连接:stdio 进程与远程连接的生命周期
本地开发的时候,stdio 模式的 MCP Server 用起来很舒服:SDK 直接拉起一个子进程,通过标准输入输出通信。但到了生产环境,stdio 模式会变成运维噩梦——进程挂了谁来重启?多实例怎么扩容?连接状态怎么管理?
我推荐生产环境使用 Streamable HTTP 或者 WebSocket 方式部署 MCP Server,把 MCP Server 独立成一个 HTTP 服务,应用侧只管发 HTTP 请求。同时要注意,MCP 连接不是完全无状态的:初始化握手、会话管理、连接复用、重连策略,这些都要纳入考虑,不能像普通 REST 接口一样完全裸奔。
6.6 安全红线:别把 MCP 工具无脑暴露给模型
最后一个最容易翻车,也最容易被无视:MCP 生态里有很多"通用工具",比如 Shell 执行、文件读写、外网访问。插上就能用,体验很好,但如果你的模型拥有自主决策调用这些工具的权限,本质上是给 Agent 开了一个不受控的执行后门。
我在内部 demo 里就见过一次事故:模型因为一个 shell 工具的误用,把测试环境搅得一塌糊涂。事后复盘,问题不在模型,而在权限设计。
现在我把这些规则写进上线 checklist:
- 对模型可见的工具做最小白名单,Skill 的 requiredTools 正好就是落地这个机制的抓手。
- 远程 MCP Server 必须鉴权,不能裸挂在公网。
- 写入、删除、命令执行这类高风险工具,调用前必须有人工确认环节。
- 所有工具调用要记录日志,便于事后审计。
我最后悔的一件事,是第一次做 Agent 时没有把安全清单前置,结果 demo 现场差点把测试机搞挂。现在我把工具权限检查放在任何 Agent 项目的第一条,宁可少接几个工具,也不让模型拿到超出预期的执行能力。