Skills vs MCP:Agent能力扩展的双螺旋
摘要:Skills和MCP是Agent能力扩展的两大路径,各有优劣。本文深度对比Skills与MCP的架构差异、开发模式、适用场景,探讨两者融合使用的双螺旋模型。
Skills vs MCP Agent能力扩展的双螺旋
我在做一个数据分析Agent项目的时候,遇到了一个让我困惑了好几天的问题。我给Agent注册了一个MCP工具叫"query_database",同时又在Skills配置里声明了一个同名能力"query_database"。结果Agent运行的时候,有时候走MCP,有时候走Skills,行为完全不可预测。
折腾了两天我才搞明白,Skills和MCP虽然都是给Agent加能力的,但它们的定位、运行机制和适用场景完全不同。混在一起用会出大问题。今天这篇,我把两者的本质区别、互补关系和正确用法讲清楚。
Skills是什么
Skills是一种声明式的能力描述机制。你不需要写完整的工具实现代码,只需要用一个声明文件告诉Agent"你能干什么"以及"干这个事需要什么参数"。Agent拿到这个声明后,自己根据描述来决定怎么完成这个任务。
举个最直白的例子。你想让Agent具备"发邮件"的能力,用Skills的方式是这样的。
# skill定义文件,声明式地描述Agent能力
name: send_email # 能力名称
description: "发送邮件给指定收件人" # 能力描述
version: "1.0.0" # 版本号
# 参数定义,告诉Agent这个能力接受什么输入
parameters:
to: # 收件人邮箱
type: string
required: true
description: "收件人邮箱地址"
subject: # 邮件主题
type: string
required: true
description: "邮件主题"
body: # 邮件正文
type: string
required: true
description: "邮件正文内容"
# 执行提示,告诉Agent如何完成这个任务
# 注意这里不是代码,而是自然语言指令
prompt: |
你现在需要发送一封邮件。请按照以下步骤操作:
1. 确认收件人邮箱格式正确
2. 构造邮件内容
3. 调用SMTP服务发送
收件人: {{to}}
主题: {{subject}}
正文: {{body}}
你看,这里没有一行可执行代码。Skills只是声明了"我能发邮件"以及"发邮件需要什么参数"。具体怎么发,由Agent自己根据prompt里的自然语言指令去完成。它可能调用某个内置的邮件服务,也可能通过MCP去调一个邮件API。
MCP是什么
MCP我们在前面的文章里讲了很多了。这里简单回顾一下。MCP是协议级的工具调用机制,它定义了Agent如何发现工具、如何调用工具、如何获取工具返回结果的完整协议。
同样的"发邮件"功能,用MCP的方式是这样的。
"""
MCP Server实现发邮件工具
和Skills不同,这里有完整的可执行代码
"""
from mcp.server import Server
import smtplib
from email.mime.text import MIMEText
# 创建MCP Server实例
server = Server("email-server")
@server.tool("send_email")
async def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件的MCP工具实现"""
# 构造邮件消息对象
msg = MIMEText(body) # 设置邮件正文
msg["Subject"] = subject # 设置邮件主题
msg["From"] = "agent@example.com" # 设置发件人
msg["To"] = to # 设置收件人
# 连接SMTP服务器并发送
with smtplib.SMTP("smtp.example.com", 587) as smtp:
smtp.starttls() # 启用TLS加密
smtp.login("user", "pass") # 登录SMTP服务器
smtp.send_message(msg) # 发送邮件
# 返回发送结果
return f"邮件已发送至 {to}"
if __name__ == "__main__":
# 启动MCP Server
server.run()
区别一目了然。MCP有完整的可执行代码,你调它就是真的在发邮件。Skills没有代码,只有声明,Agent需要自己想办法去完成。
本质区别对比
我整理了一张详细的对比表,把两者的核心差异列出来。
| 维度 | Skills | MCP |
|---|---|---|
| 本质 | 声明式能力描述 | 协议级工具调用 |
| 代码 | 无可执行代码,只有声明和提示词 | 有完整可执行代码 |
| 执行方式 | Agent自行决定如何完成 | 直接调用预定义的函数 |
| 灵活性 | 高,Agent可以根据上下文调整 | 低,行为固定 |
| 可靠性 | 依赖Agent的推理能力,有不确定性 | 结果确定,每次调用行为一致 |
| 开发成本 | 低,写个声明文件就行 | 中,需要写完整实现 |
| 适用场景 | 开放式任务、创意类任务 | 确定性操作、系统级操作 |
| 调试难度 | 难,行为不完全可预测 | 易,输入输出固定 |
这张表里最关键的是"执行方式"和"可靠性"这两行。Skills的执行结果取决于Agent当时的推理状态,同一个Skill可能每次执行的路径都不同。MCP则完全不同,同样的输入永远得到同样的输出。
两者的互补关系
看到这里你可能会问,既然MCP更可靠,为什么还需要Skills?因为有些场景天然不适合用固定代码来实现。
比如"写一首关于春天的诗"这个能力。你不可能写一个函数来生成诗歌,因为这需要创意和语言理解能力。用Skills的话,你只需要声明这个能力,告诉Agent参数和提示词,Agent自己用大语言模型的能力来完成。
再比如"执行SQL查询"这个能力。这个就不适合用Skills,因为SQL查询是确定性的操作,参数固定、行为固定、结果可预期。用MCP写一个工具来实现最合适。
所以正确的做法是Skills和MCP搭配使用。Skills管那些需要Agent推理和创意的能力,MCP管那些需要精确执行的能力。就像DNA的双螺旋一样,两条链缠绕在一起,各司其职,共同构成Agent的完整能力体系。
| 能力类型 | 适合Skills | 适合MCP | 原因 |
|---|---|---|---|
| 发送邮件 | 推荐 | 确定性操作,参数固定 | |
| 写文章 | 推荐 | 需要创意和语言理解 | |
| 查询数据库 | 推荐 | SQL执行需要精确 | |
| 总结文档 | 推荐 | 需要理解语义 | |
| 文件操作 | 推荐 | 系统级操作 | |
| 头脑风暴 | 推荐 | 开放式创意任务 | |
| 调用外部API | 推荐 | 需要精确的请求和响应处理 | |
| 角色扮演 | 推荐 | 需要灵活的对话能力 |
完整项目演示
下面我搭一个完整的项目,同时使用Skills和MCP,让Agent同时具备"写诗"(Skills)和"查天气"(MCP)两种能力。
项目结构
dual_capability_agent/
├── agent.py # 主Agent逻辑
├── skills/ # Skills定义目录
│ └── write_poem.yaml # 写诗Skill定义
├── mcp_server.py # 天气查询MCP Server
├── capability_router.py # 能力路由器
└── requirements.txt
requirements.txt
# Anthropic SDK,用于调用Claude API
anthropic==0.34.0
# PyYAML,解析Skills的YAML定义文件
pyyaml==6.0.1
# httpx,异步HTTP请求
httpx==0.27.0
skills/write_poem.yaml
# 写诗Skill的声明式定义
# 这个文件不含可执行代码,只有能力描述和提示词
name: write_poem # 能力名称
description: "根据主题写一首诗" # 能力描述
version: "1.0.0" # 版本号
# 参数定义
parameters:
topic: # 诗歌主题
type: string
required: true
description: "诗歌的主题,比如春天、友情、离别"
style: # 诗歌风格
type: string
required: false
default: "古典"
description: "诗歌风格,可选古典或现代"
# 执行提示词,Agent根据这个提示来执行
prompt: |
请根据以下信息写一首诗:
主题: {{topic}}
风格: {{style}}
要求:
1. 诗歌要押韵
2. 意象要生动
3. 情感要真挚
4. 控制在4到8句
mcp_server.py
"""
天气查询MCP Server
这是一个有完整可执行代码的工具
"""
import json
import httpx
from mcp.server import Server
# 创建MCP Server实例
server = Server("weather-server")
@server.tool("get_weather")
async def get_weather(city: str) -> str:
"""查询指定城市的天气信息"""
# 模拟调用天气API
# 生产环境替换为真实的天气API
async with httpx.AsyncClient() as client:
# 调用免费天气API
resp = await client.get(
f"https://wttr.in/{city}",
params={"format": "j1"} # 返回JSON格式
)
# 解析天气数据
data = resp.json()
# 提取当前天气
current = data.get("current_condition", [{}])[0]
# 构造天气描述
weather_info = {
"city": city, # 城市名
"temp": current.get("temp_C", "N/A"), # 温度
"humidity": current.get("humidity", "N/A"), # 湿度
"desc": current.get("weatherDesc", [{}])[0].get("value", "未知"), # 天气描述
}
# 返回JSON格式结果
return json.dumps(weather_info, ensure_ascii=False)
if __name__ == "__main__":
# 启动MCP Server
server.run()
capability_router.py
"""
能力路由器,负责区分请求应该走Skills还是MCP
这是解决两者冲突的关键组件
"""
import yaml
import os
class CapabilityRouter:
"""能力路由器,统一管理Skills和MCP能力"""
def __init__(self):
# Skills能力注册表,存储已加载的Skill定义
self.skills = {}
# MCP能力注册表,存储已注册的MCP工具
self.mcp_tools = {}
# 冲突解决策略配置
self.conflict_strategy = "mcp_first" # 默认MCP优先
def load_skills(self, skills_dir: str):
"""从目录加载所有Skills定义文件"""
# 遍历Skills目录
for filename in os.listdir(skills_dir):
# 只处理YAML文件
if filename.endswith(".yaml") or filename.endswith(".yml"):
filepath = os.path.join(skills_dir, filename)
# 读取YAML文件
with open(filepath, "r", encoding="utf-8") as f:
skill = yaml.safe_load(f)
# 注册到Skills表
self.skills[skill["name"]] = skill
print(f"已加载Skill: {skill['name']}")
def register_mcp_tool(self, name: str, description: str, endpoint: str):
"""注册一个MCP工具"""
# 添加到MCP工具表
self.mcp_tools[name] = {
"name": name, # 工具名
"description": description, # 工具描述
"endpoint": endpoint, # 工具服务地址
"type": "mcp" # 标记为MCP类型
}
print(f"已注册MCP工具: {name}")
def resolve_capability(self, name: str) -> dict:
"""
解析能力请求,决定走Skills还是MCP
这是处理同名冲突的核心方法
"""
# 检查是否同时存在于Skills和MCP中
in_skills = name in self.skills
in_mcp = name in self.mcp_tools
if in_skills and in_mcp:
# 同名冲突,根据策略决定
if self.conflict_strategy == "mcp_first":
# MCP优先策略
print(f"[冲突解决] {name} 同时存在于Skills和MCP,选择MCP")
return self.mcp_tools[name]
elif self.conflict_strategy == "skills_first":
# Skills优先策略
print(f"[冲突解决] {name} 同时存在于Skills和MCP,选择Skills")
return self.skills[name]
else:
# 报错策略,禁止同名
raise ValueError(f"能力 {name} 同时存在于Skills和MCP,请解决冲突")
elif in_skills:
# 只有Skills中有
return self.skills[name]
elif in_mcp:
# 只有MCP中有
return self.mcp_tools[name]
else:
# 都没有
return None
def list_all_capabilities(self) -> list:
"""列出所有可用能力"""
all_caps = []
# 添加Skills能力
for name, skill in self.skills.items():
if name not in self.mcp_tools: # 排除已由MCP处理的同名能力
all_caps.append({
"name": name,
"type": "skill",
"description": skill.get("description", "")
})
# 添加MCP能力
for name, tool in self.mcp_tools.items():
all_caps.append({
"name": name,
"type": "mcp",
"description": tool.get("description", "")
})
return all_caps
agent.py
"""
主Agent,同时使用Skills和MCP两种能力
演示两种能力的协同工作
"""
import json
from capability_router import CapabilityRouter
class DualCapabilityAgent:
"""同时支持Skills和MCP的Agent"""
def __init__(self):
# 创建能力路由器
self.router = CapabilityRouter()
# 加载Skills定义
self.router.load_skills("skills")
# 注册MCP工具
self.router.register_mcp_tool(
name="get_weather", # 工具名
description="查询城市天气", # 工具描述
endpoint="http://localhost:8000" # MCP Server地址
)
async def execute(self, capability_name: str, params: dict):
"""执行一个能力请求"""
# 通过路由器解析能力
cap = self.router.resolve_capability(capability_name)
if cap is None:
# 能力不存在
return {"error": f"能力 {capability_name} 不存在"}
if cap.get("type") == "mcp":
# 走MCP路径,调用工具
return await self._call_mcp(cap, params)
else:
# 走Skills路径,用提示词执行
return await self._execute_skill(cap, params)
async def _call_mcp(self, tool: dict, params: dict):
"""调用MCP工具"""
import httpx
# 构造MCP调用请求
async with httpx.AsyncClient() as client:
# 发送工具调用请求到MCP Server
resp = await client.post(
f"{tool['endpoint']}/mcp/tools/call",
json={
"tool_name": tool["name"],
"arguments": params
}
)
return resp.json()
async def _execute_skill(self, skill: dict, params: dict):
"""执行Skill"""
# 获取Skill的提示词模板
prompt_template = skill.get("prompt", "")
# 用参数填充提示词模板
prompt = prompt_template
for key, value in params.items():
# 替换模板中的占位符
prompt = prompt.replace(f"{{{{{key}}}}}", str(value))
# 生产环境这里要调用LLM来执行
# 简化版直接返回构造好的提示词
return {
"capability": skill["name"],
"type": "skill",
"prompt": prompt,
"note": "实际执行需要调用LLM"
}
def list_capabilities(self):
"""列出Agent的所有能力"""
caps = self.router.list_all_capabilities()
print("Agent当前可用能力:")
for cap in caps:
print(f" [{cap['type'].upper()}] {cap['name']}: {cap['description']}")
return caps
# 运行演示
async def main():
"""主入口函数"""
agent = DualCapabilityAgent()
# 列出所有能力
agent.list_capabilities()
# 执行Skill:写诗
print("\n--- 执行Skill: write_poem ---")
poem_result = await agent.execute("write_poem", {
"topic": "秋天",
"style": "古典"
})
print(f"结果: {json.dumps(poem_result, ensure_ascii=False, indent=2)}")
# 执行MCP:查天气
print("\n--- 执行MCP: get_weather ---")
weather_result = await agent.execute("get_weather", {
"city": "Beijing"
})
print(f"结果: {json.dumps(weather_result, ensure_ascii=False, indent=2)}")
if __name__ == "__main__":
import asyncio
asyncio.run(main())
效果验证
先启动MCP Server,再运行Agent。
# 终端1:启动天气查询MCP Server
python mcp_server.py
# 终端2:运行Agent
python agent.py
预期输出是Agent先列出所有能力,然后分别执行写诗Skill和查天气MCP工具,两种能力各走各的路径,互不干扰。
独家踩坑经验 同名能力优先级冲突
回到我开头说的那个问题。Skills和MCP同时注册同名能力时,会发生什么?
实际情况是这样的。我在能力路由器里同时注册了一个叫"query_database"的Skill和一个叫"query_database"的MCP工具。Agent运行时,能力解析的逻辑可能是这样的。
# 错误示范:没有冲突解决机制的路由逻辑
def resolve_capability_buggy(name: str):
"""有bug的能力解析,没有处理同名冲突"""
# 先查Skills,找到就返回
if name in skills:
return skills[name]
# 再查MCP,找到就返回
if name in mcp_tools:
return mcp_tools[name]
return None
这个逻辑看着没问题,但实际运行时非常不稳定。因为Skills和MCP的加载顺序不确定,如果MCP先加载完,Skills还在加载中,这时请求进来可能走到了不同的分支。更可怕的是,如果是多线程环境,两个注册操作同时发生,结果完全不可预测。
我排查这个问题的时候,Agent有时候用Skills的提示词去"想象"数据库查询结果,有时候又正确走了MCP调真实数据库。两种结果差异巨大,但日志里看不出区别,因为能力名字是一样的。
我的解决方案是在能力路由器里加一个明确的冲突解决策略。上面代码中的CapabilityRouter类已经实现了这一点,核心逻辑如下。
# 正确做法:显式冲突解决
def resolve_capability(self, name: str) -> dict:
"""正确的能力解析,显式处理同名冲突"""
in_skills = name in self.skills # 是否在Skills中
in_mcp = name in self.mcp_tools # 是否在MCP中
if in_skills and in_mcp:
# 同名冲突,根据策略决定
if self.conflict_strategy == "mcp_first":
# MCP优先,因为MCP更可靠
return self.mcp_tools[name]
elif self.conflict_strategy == "skills_first":
# Skills优先
return self.skills[name]
elif self.conflict_strategy == "error":
# 直接报错,强制开发者解决
raise ValueError(
f"能力 {name} 同时存在于Skills和MCP中,"
f"请在注册时使用不同的名称"
)
# 只有一个来源的情况,直接返回
elif in_skills:
return self.skills[name]
elif in_mcp:
return self.mcp_tools[name]
return None
除了在路由器层面解决,我还建议在注册阶段就做检查,提前发现冲突。
def register_mcp_tool(self, name: str, description: str, endpoint: str):
"""注册MCP工具时检查是否与Skills冲突"""
# 检查是否与已有Skill同名
if name in self.skills:
# 打印警告信息
print(f"[警告] MCP工具 {name} 与已注册的Skill同名!")
print(f" Skill描述: {self.skills[name].get('description')}")
print(f" MCP描述: {description}")
print(f" 当前冲突策略: {self.conflict_strategy}")
# 根据策略决定是否继续注册
if self.conflict_strategy == "error":
# 严格模式,直接拒绝注册
raise ValueError(f"能力名 {name} 冲突,拒绝注册")
# 执行注册
self.mcp_tools[name] = {
"name": name,
"description": description,
"endpoint": endpoint,
"type": "mcp"
}
这个问题解决后,我的Agent行为终于可预测了。核心教训就是,当你同时使用Skills和MCP时,一定要在系统初始化时就建立命名规范。比如所有MCP工具名加mcp_前缀,所有Skills名加skill_前缀。虽然不那么优雅,但能彻底避免冲突。
什么时候用Skills什么时候用MCP
最后给你一个实用的判断准则。
用MCP的场景
- 需要精确执行的操作,如数据库查询、文件读写、API调用
- 结果必须可复现,同样的输入必须得到同样的输出
- 涉及外部系统交互,需要认证和错误处理
- 性能敏感的场景,不能依赖Agent的推理速度
用Skills的场景
- 需要创意和语言理解的任务,如写作、总结、翻译
- 开放式任务,执行路径不固定
- 需要根据上下文灵活调整行为的场景
- 快速原型验证,不想写太多代码
两者都用的场景
- 复杂工作流,既有确定性步骤又有创意性步骤
- Agent需要同时操作外部系统和生成内容
- 团队中有人擅长写声明有人擅长写代码,各取所长
常见问题与避坑
Q:Skills可以替代MCP吗?
不能。Skills没有可执行代码,它依赖Agent的推理能力来完成。对于需要精确执行的操作,Skills的不可靠性是不可接受的。
Q:一个能力能否同时用Skills和MCP两种方式实现?
可以但不建议。如果你确实需要两种方式都支持,务必使用不同的名称,比如"analyze_data_skill"和"analyze_data_mcp",然后在路由器里做区分。
Q:Skills的提示词写多长合适?
控制在200字以内。太短了Agent不知道该干什么,太长了会干扰Agent的其他指令。核心信息是"做什么"和"怎么做",参数细节由声明文件的parameters部分处理。
Q:MCP工具和Skills哪个开发效率更高?
Skills更快。写个YAML文件就行,不用写代码不用调试。但代价是执行结果不够可靠。如果是快速验证想法,先用Skills跑通流程,确认需求后再用MCP重写。
小结
Skills和MCP是Agent能力扩展的两种互补方式。Skills是声明式的,灵活但不精确,适合创意类任务。MCP是协议级的,精确但开发成本稍高,适合确定性操作。两者搭配使用时,一定要建立命名规范和冲突解决机制,否则同名冲突会让你 debug 到怀疑人生。
下一篇我们聊MCP工具市场生态,看看开源社区里有哪些现成的MCP Server可以直接用,以及怎么安全地使用第三方MCP工具。
相关推荐
更多推荐



所有评论(0)