摘要: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:stringrequired:truedescription:"收件人邮箱地址"subject:# 邮件主题type:stringrequired:truedescription:"邮件主题"body:# 邮件正文type:stringrequired:truedescription:"邮件正文内容"# 执行提示,告诉Agent如何完成这个任务# 注意这里不是代码,而是自然语言指令prompt:|你现在需要发送一封邮件。请按照以下步骤操作: 1. 确认收件人邮箱格式正确 2. 构造邮件内容 3. 调用SMTP服务发送 收件人: {{to}} 主题: {{subject}} 正文: {{body}}你看,这里没有一行可执行代码。Skills只是声明了"我能发邮件"以及"发邮件需要什么参数"。具体怎么发,由Agent自己根据prompt里的自然语言指令去完成。它可能调用某个内置的邮件服务,也可能通过MCP去调一个邮件API。
MCP是什么
MCP我们在前面的文章里讲了很多了。这里简单回顾一下。MCP是协议级的工具调用机制,它定义了Agent如何发现工具、如何调用工具、如何获取工具返回结果的完整协议。
同样的"发邮件"功能,用MCP的方式是这样的。
""" MCP Server实现发邮件工具 和Skills不同,这里有完整的可执行代码 """frommcp.serverimportServerimportsmtplibfromemail.mime.textimportMIMEText# 创建MCP Server实例server=Server("email-server")@server.tool("send_email")asyncdefsend_email(to:str,subject:str,body:str)->str:"""发送邮件的MCP工具实现"""# 构造邮件消息对象msg=MIMEText(body)# 设置邮件正文msg["Subject"]=subject# 设置邮件主题msg["From"]="agent@example.com"# 设置发件人msg["To"]=to# 设置收件人# 连接SMTP服务器并发送withsmtplib.SMTP("smtp.example.com",587)assmtp:smtp.starttls()# 启用TLS加密smtp.login("user","pass")# 登录SMTP服务器smtp.send_message(msg)# 发送邮件# 返回发送结果returnf"邮件已发送至{to}"if__name__=="__main__":# 启动MCP Serverserver.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.txtrequirements.txt
# Anthropic SDK,用于调用Claude API anthropic==0.34.0 # PyYAML,解析Skills的YAML定义文件 pyyaml==6.0.1 # httpx,异步HTTP请求 httpx==0.27.0skills/write_poem.yaml
# 写诗Skill的声明式定义# 这个文件不含可执行代码,只有能力描述和提示词name:write_poem# 能力名称description:"根据主题写一首诗"# 能力描述version:"1.0.0"# 版本号# 参数定义parameters:topic:# 诗歌主题type:stringrequired:truedescription:"诗歌的主题,比如春天、友情、离别"style:# 诗歌风格type:stringrequired:falsedefault:"古典"description:"诗歌风格,可选古典或现代"# 执行提示词,Agent根据这个提示来执行prompt:|请根据以下信息写一首诗: 主题: {{topic}} 风格: {{style}}要求: 1. 诗歌要押韵 2. 意象要生动 3. 情感要真挚 4. 控制在4到8句mcp_server.py
""" 天气查询MCP Server 这是一个有完整可执行代码的工具 """importjsonimporthttpxfrommcp.serverimportServer# 创建MCP Server实例server=Server("weather-server")@server.tool("get_weather")asyncdefget_weather(city:str)->str:"""查询指定城市的天气信息"""# 模拟调用天气API# 生产环境替换为真实的天气APIasyncwithhttpx.AsyncClient()asclient:# 调用免费天气APIresp=awaitclient.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格式结果returnjson.dumps(weather_info,ensure_ascii=False)if__name__=="__main__":# 启动MCP Serverserver.run()capability_router.py
""" 能力路由器,负责区分请求应该走Skills还是MCP 这是解决两者冲突的关键组件 """importyamlimportosclassCapabilityRouter:"""能力路由器,统一管理Skills和MCP能力"""def__init__(self):# Skills能力注册表,存储已加载的Skill定义self.skills={}# MCP能力注册表,存储已注册的MCP工具self.mcp_tools={}# 冲突解决策略配置self.conflict_strategy="mcp_first"# 默认MCP优先defload_skills(self,skills_dir:str):"""从目录加载所有Skills定义文件"""# 遍历Skills目录forfilenameinos.listdir(skills_dir):# 只处理YAML文件iffilename.endswith(".yaml")orfilename.endswith(".yml"):filepath=os.path.join(skills_dir,filename)# 读取YAML文件withopen(filepath,"r",encoding="utf-8")asf:skill=yaml.safe_load(f)# 注册到Skills表self.skills[skill["name"]]=skillprint(f"已加载Skill:{skill['name']}")defregister_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}")defresolve_capability(self,name:str)->dict:""" 解析能力请求,决定走Skills还是MCP 这是处理同名冲突的核心方法 """# 检查是否同时存在于Skills和MCP中in_skills=nameinself.skills in_mcp=nameinself.mcp_toolsifin_skillsandin_mcp:# 同名冲突,根据策略决定ifself.conflict_strategy=="mcp_first":# MCP优先策略print(f"[冲突解决]{name}同时存在于Skills和MCP,选择MCP")returnself.mcp_tools[name]elifself.conflict_strategy=="skills_first":# Skills优先策略print(f"[冲突解决]{name}同时存在于Skills和MCP,选择Skills")returnself.skills[name]else:# 报错策略,禁止同名raiseValueError(f"能力{name}同时存在于Skills和MCP,请解决冲突")elifin_skills:# 只有Skills中有returnself.skills[name]elifin_mcp:# 只有MCP中有returnself.mcp_tools[name]else:# 都没有returnNonedeflist_all_capabilities(self)->list:"""列出所有可用能力"""all_caps=[]# 添加Skills能力forname,skillinself.skills.items():ifnamenotinself.mcp_tools:# 排除已由MCP处理的同名能力all_caps.append({"name":name,"type":"skill","description":skill.get("description","")})# 添加MCP能力forname,toolinself.mcp_tools.items():all_caps.append({"name":name,"type":"mcp","description":tool.get("description","")})returnall_capsagent.py
""" 主Agent,同时使用Skills和MCP两种能力 演示两种能力的协同工作 """importjsonfromcapability_routerimportCapabilityRouterclassDualCapabilityAgent:"""同时支持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地址)asyncdefexecute(self,capability_name:str,params:dict):"""执行一个能力请求"""# 通过路由器解析能力cap=self.router.resolve_capability(capability_name)ifcapisNone:# 能力不存在return{"error":f"能力{capability_name}不存在"}ifcap.get("type")=="mcp":# 走MCP路径,调用工具returnawaitself._call_mcp(cap,params)else:# 走Skills路径,用提示词执行returnawaitself._execute_skill(cap,params)asyncdef_call_mcp(self,tool:dict,params:dict):"""调用MCP工具"""importhttpx# 构造MCP调用请求asyncwithhttpx.AsyncClient()asclient:# 发送工具调用请求到MCP Serverresp=awaitclient.post(f"{tool['endpoint']}/mcp/tools/call",json={"tool_name":tool["name"],"arguments":params})returnresp.json()asyncdef_execute_skill(self,skill:dict,params:dict):"""执行Skill"""# 获取Skill的提示词模板prompt_template=skill.get("prompt","")# 用参数填充提示词模板prompt=prompt_templateforkey,valueinparams.items():# 替换模板中的占位符prompt=prompt.replace(f"{{{{{key}}}}}",str(value))# 生产环境这里要调用LLM来执行# 简化版直接返回构造好的提示词return{"capability":skill["name"],"type":"skill","prompt":prompt,"note":"实际执行需要调用LLM"}deflist_capabilities(self):"""列出Agent的所有能力"""caps=self.router.list_all_capabilities()print("Agent当前可用能力:")forcapincaps:print(f" [{cap['type'].upper()}]{cap['name']}:{cap['description']}")returncaps# 运行演示asyncdefmain():"""主入口函数"""agent=DualCapabilityAgent()# 列出所有能力agent.list_capabilities()# 执行Skill:写诗print("\n--- 执行Skill: write_poem ---")poem_result=awaitagent.execute("write_poem",{"topic":"秋天","style":"古典"})print(f"结果:{json.dumps(poem_result,ensure_ascii=False,indent=2)}")# 执行MCP:查天气print("\n--- 执行MCP: get_weather ---")weather_result=awaitagent.execute("get_weather",{"city":"Beijing"})print(f"结果:{json.dumps(weather_result,ensure_ascii=False,indent=2)}")if__name__=="__main__":importasyncio asyncio.run(main())效果验证
先启动MCP Server,再运行Agent。
# 终端1:启动天气查询MCP Serverpython mcp_server.py# 终端2:运行Agentpython agent.py预期输出是Agent先列出所有能力,然后分别执行写诗Skill和查天气MCP工具,两种能力各走各的路径,互不干扰。
独家踩坑经验 同名能力优先级冲突
回到我开头说的那个问题。Skills和MCP同时注册同名能力时,会发生什么?
实际情况是这样的。我在能力路由器里同时注册了一个叫"query_database"的Skill和一个叫"query_database"的MCP工具。Agent运行时,能力解析的逻辑可能是这样的。
# 错误示范:没有冲突解决机制的路由逻辑defresolve_capability_buggy(name:str):"""有bug的能力解析,没有处理同名冲突"""# 先查Skills,找到就返回ifnameinskills:returnskills[name]# 再查MCP,找到就返回ifnameinmcp_tools:returnmcp_tools[name]returnNone这个逻辑看着没问题,但实际运行时非常不稳定。因为Skills和MCP的加载顺序不确定,如果MCP先加载完,Skills还在加载中,这时请求进来可能走到了不同的分支。更可怕的是,如果是多线程环境,两个注册操作同时发生,结果完全不可预测。
我排查这个问题的时候,Agent有时候用Skills的提示词去"想象"数据库查询结果,有时候又正确走了MCP调真实数据库。两种结果差异巨大,但日志里看不出区别,因为能力名字是一样的。
我的解决方案是在能力路由器里加一个明确的冲突解决策略。上面代码中的CapabilityRouter类已经实现了这一点,核心逻辑如下。
# 正确做法:显式冲突解决defresolve_capability(self,name:str)->dict:"""正确的能力解析,显式处理同名冲突"""in_skills=nameinself.skills# 是否在Skills中in_mcp=nameinself.mcp_tools# 是否在MCP中ifin_skillsandin_mcp:# 同名冲突,根据策略决定ifself.conflict_strategy=="mcp_first":# MCP优先,因为MCP更可靠returnself.mcp_tools[name]elifself.conflict_strategy=="skills_first":# Skills优先returnself.skills[name]elifself.conflict_strategy=="error":# 直接报错,强制开发者解决raiseValueError(f"能力{name}同时存在于Skills和MCP中,"f"请在注册时使用不同的名称")# 只有一个来源的情况,直接返回elifin_skills:returnself.skills[name]elifin_mcp:returnself.mcp_tools[name]returnNone除了在路由器层面解决,我还建议在注册阶段就做检查,提前发现冲突。
defregister_mcp_tool(self,name:str,description:str,endpoint:str):"""注册MCP工具时检查是否与Skills冲突"""# 检查是否与已有Skill同名ifnameinself.skills:# 打印警告信息print(f"[警告] MCP工具{name}与已注册的Skill同名!")print(f" Skill描述:{self.skills[name].get('description')}")print(f" MCP描述:{description}")print(f" 当前冲突策略:{self.conflict_strategy}")# 根据策略决定是否继续注册ifself.conflict_strategy=="error":# 严格模式,直接拒绝注册raiseValueError(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工具。
相关推荐
- Agent协议生态全景:MCP/A2A/ARD/AP2/ACP对比与选型
- MCP工具市场生态:发现、分享与使用开源MCP Server
- Tools原语深度解析:从定义到调用全流程