面试题:MCP和skills有什么区别?
这两个东西经常被混淆,但它们其实工作在完全不同的层面。
一句话区分
-
MCP (Model Context Protocol) 是一套连接协议,解决"Agent 怎么访问外部系统"的问题——连数据库、调 API、读文件系统,它管的是"能做什么"。
-
Skill 是一份指令文档,解决"Agent 应该怎么思考和行动"的问题——什么时候该用哪个工具、遵循什么流程、用什么风格回复,它管的是"该怎么做"。
用个类比说清楚
想象你在培训一个新员工:
- MCP 就像给他办工卡、开通系统权限、配好 VPN——让他物理上能够访问公司的 GitHub、数据库、Slack。
- Skill 就像给他一份《新人手册》——告诉他"接到客户投诉后先查日志再升级,不要直接改生产库"“代码审查要检查这三项”“写周报用这个模板”,教他按公司规矩做事。
没有 MCP,Agent 有再多 Skill 也白搭,因为它连不上系统。
没有 Skill,Agent 虽然能调工具,但不知道什么时候该调、该按什么流程组合使用。
技术上的核心差异
| 维度 | MCP | Skill |
|---|---|---|
| 本质 | 客户端-服务器协议(基于 JSON-RPC) | 提示词文档(Markdown) |
| 作用 | 提供工具调用能力(连接层) | 提供行为指导(指令层) |
| 例子 | 连接 GitHub API、查询 PostgreSQL | “收到 bug 报告后,先用 GitHub MCP 查 issue,再用日志 MCP 查错误” |
| 加载方式 | 启动一个独立进程,持续运行 | 按需加载到上下文窗口 |
| 状态 | 有状态(可以维持连接、缓存认证) | 无状态(纯文本指令) |
| 认证 | 自己管理(OAuth、API key 等) | 不涉及 |
一个完整例子串起来
假设你要让 Agent 处理 GitHub issue:
只有 MCP,没有 Skill:
Agent: "我可以调 github.list_issues,但不知道什么时候该调,也不知道拿到结果后该干嘛。"
只有 Skill,没有 MCP:
Skill 说: "当收到 issue 通知时,去查看 issue 详情,分析严重性,打标签。"
Agent: "好的我懂流程了,但我怎么连 GitHub?工具在哪?"
两者配合:
1. MCP 提供 github.get_issue()、github.add_label() 等工具能力
2. Skill 提供指导:
"收到 issue.created webhook 时:
- 用 github.get_issue() 获取详情
- 如果标题包含 'crash'/'error',打上 'bug' 和 'high-priority' 标签
- 如果是功能请求,打 'enhancement' 标签
- 用 github.add_comment() 回复确认收到"
3. Agent 照着 Skill 的流程,调用 MCP 提供的工具,完成任务
为什么很多人会混淆
因为在一些早期 Agent 框架里(比如 OpenAI 的 GPTs),“tools” 和 “instructions” 是混在一起配置的,没有明确分层。但当系统复杂到一定程度,你会发现:
- 工具能力(MCP 这层)应该是可复用、跨 Agent 共享的——比如一个 GitHub MCP Server 可以给十个不同的 Agent 用。
- 行为指导(Skill 这层)应该是可替换、按场景加载的——同一个 Agent,处理 bug 和处理 feature request 可能加载不同的 Skill 文档。
把它们分开,架构才清晰、才好维护。
所以我的回答总结就是
MCP 和 Skill 不是二选一的关系,而是互补的两层。MCP 是传输层,Skill 是应用层。一个成熟的 Agent 系统两个都需要——MCP 让它有手有脚能干活,Skill 让它有脑子知道怎么干。
如果面试官追问:“那现在业界趋势是?”
我会补充:MCP 目前增长很快(2026 年初数据显示月下载量 9700 万,是增长最快的开源 AI 项目之一),因为它解决了一个长期痛点——以前每个 Agent 框架都自己定义一套工具协议,现在 Anthropic 推 MCP 后有了标准化的趋势。而 Skill 这层,各家叫法不一样(有叫 Custom Instructions、有叫 System Prompts、有叫 Agent Skills),但本质都是在做"给模型加行为约束",只是 Anthropic 把它形式化成了 skill.md 这样的文件规范。
我搜一下 MCP 最新的协议规范和 GitHub server 的实际实现,确保给你的 JSON-RPC 消息格式是准确的。
让我用另一个搜索工具试试:好,我用 GitHub MCP Server 的真实例子给你讲清楚 MCP 协议的运作细节。
MCP 协议具体例子:GitHub 操作全流程
MCP 基于 JSON-RPC 2.0,客户端和服务器之间通过标准化的消息格式通信。我用 GitHub MCP Server 的实际操作演示完整流程。
场景:让 Agent 帮你创建一个 GitHub Issue
假设用户对 Agent 说:“在我的 my-project 仓库创建一个 issue,标题是’修复登录页面崩溃’”。
整个过程涉及四个关键步骤的协议交互。
第一步:建立连接(Initialize)
客户端(比如 Claude Desktop)启动 GitHub MCP Server,双方握手协商能力。
客户端发送:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
}
},
"clientInfo": {
"name": "Claude Desktop",
"version": "1.0.0"
}
}
}
服务器响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "github-mcp-server",
"version": "0.1.0"
}
}
}
这一步确认了双方都支持协议版本 2024-11-05,并且服务器声明自己提供 tools 和 resources 能力。
第二步:发现工具(tools/list)
客户端问服务器:“你都能提供哪些工具?”
客户端发送:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
服务器响应:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "create_issue",
"description": "在指定仓库创建一个新的 GitHub issue",
"inputSchema": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "仓库所有者(用户名或组织名)"
},
"repo": {
"type": "string",
"description": "仓库名称"
},
"title": {
"type": "string",
"description": "Issue 标题"
},
"body": {
"type": "string",
"description": "Issue 正文内容"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "标签列表(可选)"
}
},
"required": ["owner", "repo", "title"]
}
},
{
"name": "list_issues",
"description": "列出指定仓库的 issues",
"inputSchema": {
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"description": "Issue 状态筛选"
}
},
"required": ["owner", "repo"]
}
},
{
"name": "create_pull_request",
"description": "创建一个新的 Pull Request",
"inputSchema": {
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"title": {"type": "string"},
"head": {"type": "string", "description": "源分支名"},
"base": {"type": "string", "description": "目标分支名"},
"body": {"type": "string"}
},
"required": ["owner", "repo", "title", "head", "base"]
}
}
]
}
}
这一步至关重要:服务器把自己能做的所有操作都声明出来,包括每个工具的名称、用途、需要哪些参数(用 JSON Schema 描述)。
客户端拿到这个列表后,会把它作为上下文提供给大模型,让模型知道"我现在可以调用这些工具"。
第三步:模型决策(发生在客户端内部)
这一步不涉及 MCP 协议交互,但要理解流程必须说清楚。
大模型(比如 Claude)看到用户的请求"在 my-project 创建 issue",结合刚才拿到的工具列表,推理出应该调用 create_issue 工具,并构造参数:
{
"name": "create_issue",
"arguments": {
"owner": "user-123",
"repo": "my-project",
"title": "修复登录页面崩溃",
"body": "用户报告在 Safari 浏览器点击登录按钮后出现白屏"
}
}
然后客户端把这个决策转化为 MCP 协议调用。
第四步:执行工具(tools/call)
客户端发送:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_issue",
"arguments": {
"owner": "user-123",
"repo": "my-project",
"title": "修复登录页面崩溃",
"body": "用户报告在 Safari 浏览器点击登录按钮后出现白屏"
}
}
}
服务器执行后响应:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "成功创建 Issue #42\nURL: https://github.com/user-123/my-project/issues/42\n状态: open\n创建时间: 2024-01-15T10:30:00Z"
}
]
}
}
服务器在后台实际调用了 GitHub API(POST /repos/{owner}/{repo}/issues),拿到结果后包装成 MCP 响应返回。
客户端把这个结果再交给模型,模型生成最终回复给用户:“已经在 my-project 仓库创建了 Issue #42,链接在这里……”
协议层的关键设计
1. 工具调用是"懒加载"的
服务器不会一上来就执行所有操作,而是:
- 第一阶段:只声明"我能做什么"(tools/list)
- 第二阶段:等模型决定要调用哪个工具时,才真正执行(tools/call)
这样既安全(模型先思考再行动),又高效(不浪费资源)。
2. 参数校验在两端都做
- 客户端校验:根据
inputSchema提前检查参数类型,避免发送明显错误的请求。 - 服务器校验:再次校验,防止客户端绕过或出错,确保最终调 GitHub API 时参数正确。
3. 错误处理
如果参数不对或操作失败,服务器会返回标准 JSON-RPC 错误:
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Invalid params: 'owner' is required",
"data": {
"missing_field": "owner"
}
}
}
客户端可以把这个错误信息反馈给模型,让模型修正后重试。
一个更复杂的例子:串联多个工具调用
用户:“帮我查看 my-project 仓库的所有开启状态的 issues,如果有标题包含’crash’的,给它们都打上’urgent’标签。”
流程:
-
调用 list_issues
// tools/call { "name": "list_issues", "arguments": { "owner": "user-123", "repo": "my-project", "state": "open" } }服务器返回:
{ "content": [{ "type": "text", "text": "找到 3 个 open issues:\n#40: crash on startup\n#41: feature request\n#42: app crash when..." }] } -
模型分析结果,发现 #40 和 #42 标题包含’crash’。
-
调用 add_label(第一次)
{ "name": "add_label", "arguments": { "owner": "user-123", "repo": "my-project", "issue_number": 40, "labels": ["urgent"] } } -
调用 add_label(第二次)
{ "name": "add_label", "arguments": { "owner": "user-123", "repo": "my-project", "issue_number": 42, "labels": ["urgent"] } }
每一步都是独立的 tools/call 请求,服务器保持无状态——它不需要记住"上一次查询了什么",客户端(或者说模型)负责编排这些调用的顺序和逻辑。
MCP 协议 vs 直接调 REST API 的差异
| 直接调 GitHub API | 通过 MCP | |
|---|---|---|
| 认证管理 | 每次调用都要带 token | MCP Server 统一管理,客户端无感知 |
| 工具发现 | 需要读 GitHub 文档 | tools/list 自动列举 |
| 参数校验 | 调用失败才知道参数错了 | inputSchema 提前校验 |
| 错误处理 | 各 API 错误格式不一 | 统一 JSON-RPC 错误格式 |
| 多服务协同 | 需要自己写胶水代码 | 标准化协议,客户端统一对接 |
实际代码:GitHub MCP Server 的实现片段
这是一个简化的 Python 实现(基于 MCP SDK):
from mcp.server import Server
from mcp.types import Tool, TextContent
import httpx
app = Server("github-mcp-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="create_issue",
description="在指定仓库创建一个新的 GitHub issue",
inputSchema={
"type": "object",
"properties": {
"owner": {"type": "string"},
"repo": {"type": "string"},
"title": {"type": "string"},
"body": {"type": "string"}
},
"required": ["owner", "repo", "title"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "create_issue":
# 实际调用 GitHub API
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://api.github.com/repos/{arguments['owner']}/{arguments['repo']}/issues",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
json={
"title": arguments["title"],
"body": arguments.get("body", "")
}
)
issue = response.json()
return [TextContent(
type="text",
text=f"成功创建 Issue #{issue['number']}\nURL: {issue['html_url']}"
)]
服务器实际运行时,MCP SDK 会把这些函数包装成 JSON-RPC 消息处理器。
总结
MCP 协议的 GitHub 例子展示了:
- 标准化握手:initialize 确认协议版本和能力。
- 工具声明:tools/list 把所有能力暴露出来,用 JSON Schema 描述参数。
- 按需执行:tools/call 只在模型决策后才真正调用后端 API。
- 无状态设计:每次 tools/call 都是独立的,服务器不需要维护会话状态。
这套协议让 Agent 可以用统一的方式对接 GitHub、数据库、文件系统等各种外部服务,不用为每个服务写专门的适配代码——这正是 MCP 要解决的核心问题。
模型是如何选择 Skill 的
关键澄清:不是"只看名字",而是看 metadata
很多人以为模型是从 skill 的名字推断的,这只对了一小部分。实际上模型主要依赖 skill 的元数据(metadata),尤其是 description 字段。
一个 Skill 的典型结构(以 Anthropic 的 SKILL.md 规范为例):
---
name: pdf-form-filler
description: 当用户需要填写 PDF 表单、提取 PDF 中的字段、或对 PDF 做数据回填时使用。支持 AcroForm 和 XFA 格式表单。
---
# PDF 表单填写指南
(这里是详细的操作步骤、代码示例、注意事项……)
模型做选择时,看的不是 name,而是 description。name 只是个标识符,真正告诉模型"什么时候该用我"的是 description。
核心机制:渐进式披露(Progressive Disclosure)
这是理解 skill 选择的关键。Skill 不是一次性全部塞进上下文的,而是分三层逐步加载:
第一层:只加载 metadata(name + description)
↓ 模型判断"这个 skill 跟当前任务相关吗?"
第二层:相关的话,加载完整的 SKILL.md 正文(详细流程、指令)
↓ 模型判断"我需要更细节的资源吗?"
第三层:按需加载 skill 附带的文件(脚本、参考文档、模板)
举个例子,假设系统里有 20 个 skill。对话一开始,模型的上下文里只有这 20 个 skill 的 name + description,可能一共就几百 token。当用户说"帮我把这份 PDF 表单填一下"时:
- 模型扫描所有 description,发现
pdf-form-filler的 description 高度匹配。 - 触发加载它的完整 SKILL.md 正文。
- 如果正文里引用了
fill_form.py脚本,才在真正执行时加载那个脚本。
这样设计的好处:上下文窗口不会被 20 个 skill 的完整内容撑爆,只有被选中的那个才展开。
所以选择的判断依据是什么
模型本质上是在做一个语义匹配 + 推理的过程:
- 把用户当前的意图(“填 PDF 表单”)
- 和每个 skill 的 description(“当用户需要填写 PDF 表单时使用……”)
- 做语义层面的相关性判断
这不是关键词精确匹配,而是模型用它的语言理解能力判断"这个描述说的场景,是不是我现在遇到的场景"。所以 description 写得好不好,直接决定 skill 能不能被正确触发。
名字雷同时怎么区分
这是实践中的真实痛点。假设你有两个 skill 都叫 data-export 或者名字很接近,模型该怎么办?
问题的本质:区分不靠名字,靠 description
既然模型选择依据的是 description 而不是 name,那么名字雷同本身不是大问题,description 雷同才是灾难。
反例(会出问题):
---
name: export-data
description: 导出数据
---
---
name: export-report
description: 导出数据
---
两个 description 都是"导出数据",模型面对"帮我导出数据"时会无法区分,可能随机选一个、选错、或者两个都想用。
正确做法(消除歧义):
---
name: export-data-csv
description: 当用户需要把数据库查询结果导出为 CSV 或 Excel 文件时使用。适用于结构化表格数据的批量导出。
---
---
name: export-report-pdf
description: 当用户需要生成格式化的可视化报告(含图表、公司 logo、排版)并导出为 PDF 时使用。适用于给管理层看的正式报告。
---
现在即使名字都带 “export”,模型也能靠 description 里的触发条件和适用场景清楚区分。
消除歧义的几个实践原则
1. description 要写"什么时候用",而不是"这是什么"
- 差:
description: 处理图片的工具 - 好:
description: 当用户需要压缩、裁剪、格式转换图片,或批量处理多张图片时使用
description 应该描述触发条件(when to use),这是模型做选择时最需要的信息。
2. 明确边界,尤其是容易混淆的 skill 之间
如果两个 skill 功能相邻,在 description 里直接划清界限:
description: 当用户需要查询「历史」订单数据(30天前)时使用。
注意:查询最近30天的订单请用 recent-order-query skill。
主动告诉模型"什么情况下不该用我,该用谁",能显著减少误选。
3. 用命名空间/前缀区分同类 skill
当 skill 数量多时,给名字加领域前缀:
finance-export-csv
finance-generate-invoice
hr-export-employee-list
hr-generate-offer-letter
这既方便人类管理,也给模型一个额外的分类信号。
4. 拆分而不是堆叠职责
如果你发现两个 skill 老是被搞混,往往是因为它们职责边界模糊。与其在 description 里反复解释区别,不如重新设计:让每个 skill 的适用场景互斥且清晰。一个 skill 只干一件定义明确的事,比十个模糊的 skill 好用得多。
如果实在还是分不清怎么办
在成熟的 agent 框架里,还有几个兜底机制:
- 让模型澄清:当多个 skill 相关度接近时,一些框架会让模型主动反问用户"你是想导出 CSV 数据还是生成 PDF 报告?"
- 优先级/评分:框架层面可以给 skill 设置优先级,或用一个轻量的检索/排序(甚至就是前面聊过的语义检索)先筛出 Top-N 候选,再让模型定夺。
- 显式调用:允许用户或上层逻辑直接指定 skill 名,绕过模型的自动选择。
总结
回到你的两个问题:
模型是从名字推断的吗? 不是。模型主要看 skill 的 description(触发条件描述),name 只是标识符。整个过程是"渐进式披露"——先只看所有 skill 的 name+description 做初筛,选中后才加载完整内容。
名字雷同怎么区分? 名字雷同不致命,description 雷同才致命。区分的核心是把每个 skill 的 description 写清楚"什么时候用我、什么时候别用我、跟相邻 skill 的边界在哪"。配合命名空间前缀、职责拆分、以及框架层的澄清/评分机制,就能可靠地消除歧义。
一句话:Skill 选择的质量,本质上取决于 description 写得好不好。 这也是为什么写 skill 时,description 往往比正文更值得反复打磨。
后记
2026年8月12日于上海,在claude opus 4.8辅助下完成。
更多推荐



所有评论(0)