1. 从堆提示词到堆能力:AI Agent 扩展思路的一次转换
最近和不少做 AI Agent 的朋友聊天,几乎都绕到同一个瓶颈:模型本身已经挺聪明,真正决定一个 Agent 能干什么、干得稳不稳的,反而是外面包着的那层"能力系统"。最近反复出现在讨论里的 MCP 和 Skills,本质上是两条不同的扩展路径——MCP 负责把外部工具变成标准接口,Skills 负责把领域经验变成可复用能力包。这篇文章不打算复述官方文档,而是想把自己在真实项目里的理解、拆解和踩坑记录串起来,给正在搭 Agent 的同学一份可以直接用的参照。
1.1 提示词在复杂工具链面前的脆弱
先说个我自己经历过的事情。去年我做过一个客服工单系统,要求 Agent 能查订单、翻历史记录、改地址、发邮件。最开始的做法很朴素:把所有操作都写成自然语言规则,塞进 system prompt。结果也很有代表性——简单问答没问题,链路一长,模型开始"自由发挥"。它知道要先调get_order,但遇到超时不知道该重试还是报错;它理解"用户不满意",把它转成投诉工单的字段时,格式却经常不稳定。
问题不在模型,而在我们把"工具调用协议、异常处理规则、业务流程节点"全部压给了提示词。提示词的本质是模糊的文本,而工具调用恰恰要求强约束。后来改成 Function Calling,情况好了不少,但每次新功能上线就得同步改一堆工具定义,反而更混乱。更麻烦的是,工具说明塞得越多,模型越容易混淆,甚至把 A 工具的参数抄到 B 工具上。
走到这一步我才意识到:需要的不是更长的 prompt,而是更结构化的能力放置方式。这也解释了为什么现在业内会把能力扩展拆成两个方向——不是突发奇想,是被真实痛点逼出来的。
1.2 横向接入与纵向沉淀:扩展能力的两个方向
我习惯把 Agent 能力扩展拆成两个维度来看。
横向扩展是"让 Agent 能连接到更多外部系统"。数据库、文件、浏览器、设计稿、企业内系统,不住进这些真实环境里,模型再聪明也读不到数据、执行不了真实操作。MCP(Model Context Protocol,模型上下文协议)属于这一层,它解决的是接入问题。
纵向扩展是"让 Agent 更会做事"。同一个工具,新手用来和老师傅用来结果完全不同,差异在于经验。Skills 要做的事情,就是把某个领域的方法论、完成步骤、边界条件、质量标准搬到 AI 可复用的知识包里,它解决的是做事水平问题。
一个管触达范围,一个管执行质量,正好补在不同的位置。"双引擎"这个比喻之所以成立,就是因为这两个机制分别驱动了不同的增长维度。接下来我会把两个引擎分别拆开讲,最后给出一套能直接抄作业的组合用法。
2. MCP:给 Agent 一个标准化的外部接入层
2.1 MCP 的定位:工具连接界的"统一插口"
MCP 是 Anthropic 在 2024 年推出的开放协议,目标是标准化大模型应用和外部数据源、工具之间的通信。如果你接触过很多 Agent 项目,就知道过去最烦的事情是:每个工具都有自己的接入方式,你要为每一对"Agent + 工具"单独写适配层。数据库接法一套、GitHub 接法一套、企业内部系统又一套,重复劳动非常重。
MCP 做的事情,有点像把所有充电口统一成 USB-C。服务端按协议暴露能力,Agent 端按同一套协议消费能力。工程上的好处立竿见影:你写了一个文件系统的 MCP server,不仅这个 Agent 能用,团队里其他支持 MCP 的 Agent、IDE、SDK 全都能用,不用再重复适配。
我实际感知到 MCP 的生态拐点,是越来越多专业工具开始主动接进来。除了日常的 GitHub、数据库、浏览器自动化,连游戏引擎 Unreal、EDA 工具 Altium Designer、逆向工具 IDA 这些偏专业领域的软件,都开始提供 MCP 接口或社区方案。这说明它已经不只是办公工具层面的小协议,而是逐渐变成"软件对外暴露智能体接口"的一种通用语言。
2.2 协议核心不止 Tool,还有 Resources 和 Prompts
很多人一上来就把 MCP 等同于"工具调用标准",其实 MCP 协议里有三类核心原语,分工不太一样:
- Tools:可执行的函数或操作,用 JSON Schema 描述参数,比如
search_github、query_database、send_email。Agent 根据任务决定要不要调用。 - Resources:只读的数据源,按 URI 暴露,比如数据库表、文件内容。它更适合被 Agent 作为上下文主动拉取,不需要"调用"这种感觉。
- Prompts:服务端预设的提示模板,可以给用户或 Agent 一些"推荐用法"。
日常使用中 Tools 最频繁,但 Resources 在多步任务里很有价值。举个例子,一个订单系统的 MCP server 可以把客户基本信息暴露成 Resource,Agent 在做分析时直接引用这个资源,不需要先调工具、等返回、再拼上下文。这样省掉一次往返,也减少上下文碎片。
要提醒的是,MCP 协议里的 Prompts 和后面要聊的 Skills 是两码事。MCP 的 Prompts 是"服务端提供的推荐指令",Skills 更像"客户端加载的领域能力包",一个偏接口,一个偏经验,别混淆。
2.3 接入 MCP 时,真正值得关注的工程细节
MCP 本身只解决"连接",不解决"可用"。
工具能调通,不代表 Agent 能在一堆工具里选对。实际项目里我总结出几个关键点:
- 工具命名和描述要克制:不要写又长又抽象的 description。给模型看到的工具说明,应该短到能一眼判断什么时候用、什么时候不用。
- 服务端错误要结构化:比如订单不存在,返回
{"error": "ORDER_NOT_FOUND", "message": "..."},比返回一个空数组更不容易让 Agent 产生幻觉。空数组很容易被当成"查询成功但没结果"。 - 客户端仍要做参数校验:MCP 帮你统一了协议,但模型填参数仍然可能出错,客户端该拦的还是要拦。
另外,接入 MCP 服务时最好从本地或内网服务开始。直接用不可信的第三方 server,意味着你把文件系统、数据库这类敏感能力交到了别人手里。安全边界的问题,我会在第六部分集中讲。
3. Skills:把经验和流程装进"能力包"
3.1 Skills 到底是什么
Skills 是跟着 Claude Agent SDK 这类产品流行起来的思路:把一组指令、示例、脚本、规则塞进一个目录,由 Agent 按需加载。它本质上就是提示工程的模块化管理,解决两个问题:一是让稳定流程可以跨项目复用,二是防止系统提示词无限膨胀。
和传统 prompt 最大的区别有三点:
- 结构化:技能不是一段贴在 system prompt 里的文字,而是有固定目录结构的包。
- 按需加载:不是所有技能内容都常驻上下文,而是调度层根据当前任务,把对应技能读进来。
- 可执行:技能包里除了文本规则,还能带校验脚本、转换脚本这类模型做不到百分百稳定的操作,由真实脚本兜底。
举个我身边很典型的"前端开发 skills"例子。写页面还原类 Agent 时,如果只是往 system prompt 里写一句"按设计稿还原页面",不同设计师的产出差异会非常大。但如果挂一个webpage-builder技能包,里面带上响应式检查脚本、组件命名规范、可访问性规则、常见间距规范,模型输出的代码风格会明显稳定下来。那些用传统方式要花几千字才能写完的约束,被拆成了"技能文本 + 实际校验脚本",既省 token,又更可靠。
3.2 一个 Skills 包到底长什么样
参考 Claude Agent Skills 风格,一个典型技能包的目录结构是这样:
skills/webpage-builder/ ├── SKILL.md ├── scripts/ │ ├── extract_design_tokens.py │ └── check_a11y.py ├── assets/ │ ├── dom_style_guide.md │ └── spacing_notes.md └── rules/ └── component-rule.mdSKILL.md是核心入口,通常带 frontmatter,描述name和description,正文包含详细规则、触发条件、步骤和示例。scripts/放可执行脚本,可以由 Agent 按工具方式调用,用来做校验、清洗、转换;assets/放静态参考文档;rules/放不可违反的硬性约束。
这里最关键的其实是description写得准不准。它决定了调度环节能不能在合适的时机把技能调出来。我见过太多技能包写得很厚,但 description 一句"用于代码生成"的泛话,结果模型根本不知道什么时候该触发它。正确的写法应该是场景化的,比如:"当需要从设计稿生成响应式页面时使用,忽略纯文本内容页面。"这种描述命中率会高很多。
3.3 按需加载:为什么这不是换了个名字的巨型提示词
Skills 最容易踩的误区,是把技能文本直接全拼进 system prompt。如果那样做,其实和一份巨型提示词没有区别,token 很快就会被吃光,模型注意力也会被大量无关信息稀释。
真正的做法是"技能路由":Agent 在规划阶段根据任务描述选择一个或多个技能包,把对应的SKILL.md加载进上下文,任务结束再释放。这就像真实团队里的专家顾问,平时不常驻,遇到对应问题才被叫进来讨论。我在对比测试里验证过:同样一个代码生成任务,带两个相关技能的效果,比带八个无关技能的稳定很多。原因不复杂——模型推理时每多一份无关约束,都是在跟关键约束抢注意力。
不过也有一条经验:技能包内容本身不要太厚。一个技能文本控制在 1500~3000 token 以内比较合理。如果确实需要很多背景资料,就把静态文档放进assets/,让 Agent 需要时通过工具读取,而不是一次性全部加载。这样既能保留完整知识,又不压爆上下文。
4. 双引擎如何分工:MCP 管数据入口,Skills 管决策质量
4.1 一个典型任务里的分工实例
拿"生成产品版本说明"这个自动化场景来拆解。Agent 要做的事情是:收集最近 30 天的代码提交和 Issue,理解改动内容,最后按团队模板写出版本说明。
如果只靠 MCP,Agent 能从 GitHub 拉数据,但写出来的版本说明格式五花八门;如果只靠 Skills,Agent 知道怎么写版本说明,但没有实时数据可写。把两者合起来才顺——通过 GitHub 的 MCP server 获取commits和issues列表,这是数据入口;同时加载一个"版本说明撰写"技能包,里面包含标题格式、技术词使用规范、发布细则、示例段落。MCP 保证信息真实且实时,Skills 保证输出符合团队标准。
我实际跑这种任务时最大的体感是:两个引擎各管一摊,出问题时特别好定位。格式不对就去改技能包,数据不准就去查 MCP 接口,而不是像以前一样在一大坨 prompt 里翻来覆去找原因。
4.2 Skills 可以做调度者,MCP 提供原子能力
很多人以为 Skills 只是给 Agent 提供"工作步骤说明",其实它还可以更进一步,指导 Agent 按什么顺序调用 MCP 工具、如何处理返回结果。这实际上是把流程本身也给模型固化了。
举个例子,一个"数据质量分析"技能包,里面可以明确写:先调用 schema 相关工具获取表结构,再写 SQL 验证关键字段,然后用统计工具做分布分析。Agent 按这个顺序执行,就不会东一榔头西一棒槌地乱试。
所以我通常把这一层叫做"元策略":MCP 提供的是可复用的原子能力,Skills 沉淀的是按业务场景编排的元策略。原子能力是基础设施,元策略是业务资产。这个区分想清楚之后,你就不会再纠结某个动作到底应该做成 MCP 工具还是写进 Skills 里了——如果它是通用的连接能力,放 MCP;如果它是特定业务的做法,放 Skills。
4.3 上下文预算:两个引擎都会吃窗口,必须设上限
做双引擎架构时,上下文预算是最容易被忽视的问题。两种扩展机制占用上下文的方式不同:MCP 工具返回是在执行后动态进入上下文的,体量不可控;Skills 内容是在执行前按任务加载的,可以通过设计控制。
我的经验是做两层限制。第一,给单次任务里的工具返回设置 token 上限,比如 3000 token,超过部分做截断或摘要。第二,给技能加载设上限,一个技能包不超 1500~3000 token,如果确实厚,就把长文档放到工具侧按需读。加了这个机制之后,长流程任务在做第二次、第三次工具调用时,上下文不会堆着前面全部历史,撞窗口天花板的概率小很多。
顺带一提,现在主流 Agent 框架,比如 LangGraph、Dify、Coze、AutoGen、Claude Agent SDK,设计上都越来越向这两个方向靠拢。这对团队来说反而是件好事——你在一个框架里摸熟的能力分层逻辑,换框架之后仍然适用。
5. 一个能直接复现的最小样例:FastAPI + LangGraph + MCP + Skills 串起来
5.1 架构选择和前提
不少团队在调研 Agent 的时候都会问一句"能不能扛并发",我就用一个最小示例把架构串起来说明:FastAPI 提供 HTTP 接口,LangGraph 做状态编排,MCP Client 连接自建 MCP server,Skills 以按需读取SKILL.md的方式注入上下文。
选 FastAPI 是因为它异步能力强、生态成熟;选 LangGraph 是因为节点式编排让"哪一步取数据、哪一步注入技能"一目了然,方便排查。要提前说明,这是能跑通的最小演示,不是生产级方案。生产级还要考虑鉴权、可观测性、失败重试策略这些,后面我会逐个提醒。
5.2 MCP Server 和 Client 的连接实操
MCP server 端,Python SDK 封装了FastMCP,写起来很简单。下面是一个"本地文档索引查询"的示例:
# server.py from mcp.server.fastmcp import FastMCP mcp = FastMCP("doc-index") @mcp.tool() def search_docs(keyword: str, limit: int = 5) -> str: # 实际会去本地索引里查,这里为了演示返回模拟结果 return f"found docs: {keyword} matched {limit} items" @mcp.tool() def get_doc_summary(doc_id: str) -> str: return f"summary of {doc_id}: some key points..." if __name__ == "__main__": mcp.run()客户端这边,用 Python SDK 连接。假设 server 通过 stdio 方式启动:
# client.py from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client server_params = StdioServerParameters(command="python", args=["server.py"]) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() result = await session.call_tool( "search_docs", {"keyword": "FastAPI", "limit": 3} ) print(result)要把大模型落进来,流程就是经典的 Function Calling 循环:把 MCP 列出的 tools 转成模型可用的函数定义,模型决定调哪个,客户端执行session.call_tool,再返回给模型继续推理。和手写工具定义相比,唯一的不同就是工具来源变成了 MCP,而不是你自己 hardcode 在代码里的 JSON。
5.3 Skills 注入上下文的实现方式
在 LangGraph 里,我习惯用一个节点专门做"上下文构建"。它的职责是在调用模型之前,根据任务描述选择技能并读取SKILL.md,拼到系统上下文里。
# context_builder.py from pathlib import Path def route_skill(task_desc: str) -> str: # 可以用关键词规则,也可以用一次轻量模型调用做分类 if "版本说明" in task_desc or "release note" in task_desc.lower(): return "release-note-writer" if "页面" in task_desc: return "webpage-builder" return "" def load_skill(skill_name: str) -> str: skill_path = Path("skills") / skill_name / "SKILL.md" if not skill_path.exists(): return "" return f"## Active Skill: {skill_name}\n{skill_path.read_text()}" def build_messages(state): task_desc = state["task"] active_skill = route_skill(task_desc) skill_block = load_skill(active_skill) system = BASE_SYSTEM + skill_block return {"system": system, "messages": state["messages"]}注意 skill 包里的脚本不能只是躺在目录里。如果你想让它生效,常见做法是把scripts/*.py也暴露成 Agent 可调用的本地工具,类似 LangGraph 的 Tool Node。这样技能包就同时携带了"经验说明"和"可执行校验",模型按规则做,脚本按规则检,可靠性会高很多。
5.4 并发怎么处理:连接池、异步和结果校验
聊到" AI Agent 怎么扛并发",最容易出问题的其实是连接管理,而不是模型调用。
如果 MCP server 是本地 stdio 进程,每个任务都另起一个子进程,开销太大。更合适的做法是:
- 同一个 Agent 进程里维护一个 MCP 客户端实例,用异步 IO 处理并发请求;
- 远程 MCP server 用 HTTP 或 SSE 时,客户端连接尽量复用或做连接池;
- 对重复查询做结果缓存,对超长返回做截断。
LangGraph 支持异步图执行,只要工具调用是可等待的,就能在一个事件循环里并发跑多个任务。但要注意,模型提供方通常都有速率限制,要配重试和指数退避,不然并发一高就开始报限流错误。Skills 部分没有连接压力,它只是读本地文件、加载上下文,天然无状态,可以随意并行。
另外,不是所有任务都适合无脑并发。只读查询和写入操作应该分开路径,写操作如果因为某个并发读请求失败而被回滚,是很讨厌的事。这也是我强调"按任务粒度拆分"的原因。
6. 落地踩坑记录:双引擎组合最容易出问题的四个地方
6.1 工具定义爆炸,Agent 选择开始漂移
第一次把 MCP server 管起来后,lists 里能看到几十个工具,我一开始还挺高兴,结果测试效果反而变差了:模型经常选错工具,把search_docs的参数填到get_doc_summary上。
排查下来,根因是工具太多、说明又都写得太泛。模型面对几十个相似描述时,注意力被迅速稀释。
解决办法我用了两层:第一层,在客户端按任务分组,只暴露当前任务需要的工具子集;第二层,重写工具描述,要求每条控制在两句话以内,必须包含"什么时候用、什么时候别用"。做了这两个改动之后,选择准确率提升得非常明显。
6.2 Skills 加载不触发,等于白装
另一个让人抓狂的问题是:技能包明明写好了,模型却从来不用。我一开始以为是模型不行,后来把route_skill的执行日志打出来才发现,调度环节根本没把技能选出来。
原因基本都在技能描述太宽泛,或者调度规则写得太粗糙。我的解决思路是:先在离线环境里准备一组有明确预期的测试任务,用它们扫一遍技能路由结果,把命中率不高的技能 description 反复调优,再让它上真实任务。另外,在系统提示里加一个"技能菜单",列出可用技能和触发关键词,让模型在规划阶段主动参考。就我实测,加了技能菜单之后命中率高了很多,这个开销非常值得。
6.3 MCP 权限边界模糊,工具越强越危险
接文件系统 MCP 服务时我踩过一次坑:Agent 不只读该读的目录,把用户主目录下的配置、文档也扫了个遍。原因很直接——server 端没有任何路径白名单,客户端也没有控制工具暴露范围。
现在我的项目里会强制做三件事:
- MCP server 端设置路径白名单,尽量不开放根目录或
~; - Agent 侧按环境区分权限,比如开发、生产各用不同的 MCP server 实例;
- 对写操作类工具(如发邮件、删文件、改数据库)统一要求二次确认。
说白了,工具能力越来越强的时代,"权限最小化"不能停留在文档里。权限给宽了就等于没限制,这个认知越早建立越省事。
6.4 长流程里错误累积,并发越大错得越离谱
做多数据源调研任务时,我遇到过一种特别隐蔽的失败:Agent 在第一步调用工具时参数用错了,服务端返回了"空结果";但模型把空结果当成了"查询成功、真没数据",写进结论之后,后续所有步骤全被带偏。更麻烦的是,这个问题在并发跑多个任务时几乎是批量出现的。
解决方法是加一层"结果检查节点"。服务端要把成功和失败说得清清楚楚,客户端要对"空数组且无错误说明"的结果自动标记为异常,触发重试或人工确认。同时,我会把"什么样的结果算合理"这类校验规则写进 Skills 里,让技能包在流程中起到兜底作用。长任务的可靠性,靠的不是祈祷模型每一步都聪明,而是每一步之后都有结构化的检查。
收个尾:一点实际体会
如果你想在团队里推广这套双引擎设计,我的建议是先别把 MCP 和 Skills 当成两个独立项目来搞,而是找一块真实业务整体改造一遍:数据层用 MCP,知识层用 Skills,可观测性和失败重试放在编排层。跑通一个闭环之后,再把工具和技能剥离开,沉淀成可复用的内部包。
我已经把团队里多个 Agent 任务改成了这种结构,最直观的感受是:每次新增业务时,不用再翻山越岭地改提示词,接一个 MCP server 或加一个 skill 目录,其他逻辑照跑。这种"插一块能力、得整体增长"的感觉,才是双引擎真正值钱的地方。希望这篇能帮你少走几个我走过的弯路。