1. 从手搓 Agent 到 SKILL.md:一场开发范式的转移
过去大半年,我几乎把市面上能见到的 Agent 框架都折腾了一遍。从最早的 ReAct 循环手写 prompt,到后来用各种编排框架搭工作流,再到接入 MCP 协议打通外部工具,每一步都踩过坑。最深的感受是:Agent 的智能程度,往往不取决于模型本身,而取决于你怎么把"技能"喂给它。手搓 Agent 的时代,我们花大量时间在写工具描述、拼 prompt 模板、处理参数解析,真正用于业务逻辑的精力反而被稀释了。
直到 SKILL.md 这类约定出现,事情开始变得不一样。它本质上是一种用自然语言描述能力的结构化文档,把"这个 Agent 能做什么、怎么调用、输入输出是什么"用统一的格式固化下来。配合 OpenClaw 这类运行时,Agent 不再需要你手写一堆胶水代码去注册工具,而是直接读取 SKILL.md,自动理解并挂载能力。这解决了三个核心痛点:能力描述与代码实现解耦、跨框架复用、以及让非工程背景的人也能参与 Agent 能力建设。
这篇文章适合两类人看:一是正在做 Agent 开发、被工具注册和 prompt 维护折磨的工程师;二是想快速搭建可用 Agent、但不想深陷框架细节的产品或运营同学。我会从设计思路、核心机制、实操落地到问题排查,把 SKILL.md 这套东西讲透,尽量让你看完就能上手改自己的项目。
2. SKILL.md 到底解决了什么问题:设计思路与方案选型
2.1 手搓 Agent 的三大痛点
先说清楚为什么需要 SKILL.md。我早期做 Agent 时,典型流程是这样的:定义一个工具函数,写一段 JSON Schema 描述参数,再在系统 prompt 里用自然语言解释这个工具什么时候用、怎么用。问题在于,这三处信息是分散且容易不一致的。改了函数签名忘了改 Schema,改了 Schema 忘了更新 prompt,最后模型调用时参数对不上,报错排查半天。
第二个痛点是复用困难。我在 A 项目里写了一个"查询天气"的工具,想在 B 项目复用,结果发现两个项目的框架不同、prompt 风格不同、参数命名习惯不同,复制过去还得改一遍。第三个痛点是协作门槛高。产品经理知道业务上需要什么能力,但他不会写代码,只能口头描述给工程师,工程师再翻译成工具函数。这个翻译过程损耗大、周期长。
SKILL.md 的思路是:把能力的描述权交给最懂业务的人,把执行权交给运行时。你只需要用 Markdown 写清楚"这个技能叫什么、什么时候触发、需要什么参数、返回什么结果",运行时负责解析这份文档,自动生成工具注册代码和 prompt 片段。工程师只需要实现真正的执行逻辑,描述性工作全部由文档承担。
2.2 为什么是 Markdown 而不是 JSON Schema
有人会问,为什么不用 JSON Schema 或 OpenAPI 这种更结构化的格式?我的实测体会是:JSON Schema 对机器友好,但对人极不友好。写一个稍微复杂的工具描述,JSON 嵌套好几层,改一个字段要小心翼翼数括号。而 Markdown 是人和模型都能轻松读懂的格式,模型在训练时见过海量 Markdown,对它的结构理解天然就好。
更重要的是,SKILL.md 里可以写自然语言的触发条件。比如"当用户询问股票行情、K线数据、财务指标时使用此技能",这种描述用 JSON Schema 根本表达不了,只能塞进 description 字段里,但那个字段通常很短。Markdown 允许你写大段说明、举例、甚至注意事项,模型读完之后对"什么时候该用"的判断准确率明显提升。我在对比测试中发现,同样的工具,用 SKILL.md 描述后,模型误触发率从大约 15% 降到了 5% 以内。
2.3 SKILL.md 与 MCP 的关系
这里必须澄清一个常见混淆。MCP 解决的是Agent 与外部工具之间的通信协议问题,它定义了"怎么调用";而 SKILL.md 解决的是能力描述与发现问题,它定义了"有什么能力、怎么用"。两者是互补的。你可以把 SKILL.md 理解成一份"能力菜单",MCP 是"点菜和上菜的服务流程"。OpenClaw 这类运行时同时支持两者:读取 SKILL.md 知道有哪些菜,通过 MCP 把菜端上来。
实际项目中,我通常这样分工:SKILL.md 负责面向模型的能力说明和触发逻辑,MCP Server 负责面向系统的实际执行。SKILL.md 里可以引用某个 MCP 工具,也可以直接指向一个本地函数。这种分层让描述和执行各自独立演进,改描述不影响执行,换执行方式也不用重写描述。
3. SKILL.md 的核心结构拆解与编写要点
3.1 一份标准 SKILL.md 的骨架
我经过多个项目迭代,总结出一份 SKILL.md 通常包含这几个部分。不是硬性规定,但按这个结构写,模型理解效果最好:
# 技能名称 ## 描述 一句话说明这个技能做什么。 ## 触发条件 什么情况下应该使用这个技能。尽量具体,举例说明。 ## 输入参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | ... | ... | ... | ... | ## 输出格式 返回结果的结构说明。 ## 使用示例 用户输入示例和对应的调用示例。 ## 注意事项 边界情况、限制、常见错误。这个骨架的关键在于触发条件和注意事项这两块。很多教程只讲参数和输出,但实际用下来,模型最容易出错的地方恰恰是"该不该用"和"用了之后怎么处理异常"。把这两块写清楚,Agent 的稳定性会有质的提升。
3.2 触发条件怎么写才准
触发条件是 SKILL.md 里最需要花心思的部分。我的经验是:正向描述 + 反向排除 + 具体例子,三管齐下。
正向描述要覆盖用户可能的多种表达方式。比如一个查股票数据的技能,不能只写"查询股票行情",要写"当用户询问某只股票的价格、涨跌幅、成交量、K线走势、财务数据时使用"。反向排除要明确什么情况下不要用,比如"当用户只是闲聊提到股票、或询问股票基础知识概念时,不要调用此技能"。具体例子给两到三个,覆盖典型场景。
我踩过的一个坑是:触发条件写得太宽泛,导致模型在用户问"今天天气怎么样"时也去调股票技能。后来加了反向排除,明确"与金融数据无关的日常问题不触发",误触发就基本消失了。另一个坑是写得太窄,用户换个说法模型就不认识了。解决办法是把同义词、口语化表达都列进去。
3.3 参数描述的颗粒度把控
参数描述要精确到模型能直接生成正确值的程度。类型、必填性、取值范围、默认值、格式要求,一个都不能少。我见过太多因为参数描述模糊导致调用失败的案例。
举个例子,一个日期参数,如果只写"日期",模型可能生成"2024年1月1日"、"01/01/2024"、"2024-01-01"各种格式。你必须明确写"格式为 YYYY-MM-DD,例如 2024-01-01"。再比如枚举参数,要把所有可选值列全,并说明每个值的含义。对于有依赖关系的参数,要写清楚"当 A 参数为 X 时,B 参数必填"这类条件逻辑。
提示:参数描述里尽量避免用"等等"、"之类的"这种模糊词。模型会真的去猜,而猜错的比例不低。宁可多写几行,把边界情况列全。
3.4 输出格式与错误处理约定
输出格式要说明返回值的结构,最好给一个真实示例。如果返回的是 JSON,把字段含义解释清楚。如果返回可能为空,要说明空值时的表现。这些细节决定了模型拿到结果后能不能正确地向用户转述。
错误处理这块,我建议在 SKILL.md 里明确列出常见错误码和对应的用户友好提示。比如"当返回 404 时,告知用户未找到相关数据,建议检查输入";"当返回超时时,告知用户服务暂时不可用,稍后重试"。这样模型在遇到错误时不会胡编乱造,而是按你预设的话术回复。实测下来,加了错误处理约定的技能,用户投诉率明显下降。
4. 基于 OpenClaw 的实操落地:从零搭一个带 SKILL.md 的 Agent
4.1 环境准备与 OpenClaw 安装
先说环境。OpenClaw 支持多平台,我在 Mac 和 Linux 上都部署过。Mac 下安装相对简单,用包管理器一行命令搞定;Linux 下要注意依赖版本,尤其是 Node 运行时和 Python 环境的隔离。Windows 用户如果遇到 WSL2 环境校验失败的问题,通常是 WSL 版本过旧或虚拟化未开启,升级 WSL 并确认 BIOS 里虚拟化选项打开即可。
安装完成后,第一件事是初始化工作目录。OpenClaw 默认会读取项目根目录下的skills/文件夹,里面每个子目录放一份 SKILL.md。我建议按业务域分目录,比如skills/stock/、skills/weather/、skills/file/,这样后期维护清晰。初始化命令执行后,运行时会扫描所有 SKILL.md,生成能力清单。
注意:不同版本的 OpenClaw 对 SKILL.md 的字段要求略有差异。安装后先跑一遍官方示例,确认你的版本能正确解析,再开始写自己的技能。我因为版本不匹配浪费过整整一个下午。
4.2 编写第一个 SKILL.md:以本地文件查询为例
拿一个最实用的场景练手:本地文件查询。用户说"帮我找一下项目里所有配置文件",Agent 应该能扫描目录并返回结果。对应的 SKILL.md 大概这样写:
# 本地文件查询 ## 描述 在指定目录下按文件名模式或内容关键词搜索文件。 ## 触发条件 当用户要求查找、搜索、定位本地文件时使用。 例如:"找一下所有 .md 文件"、"搜索包含 TODO 的代码文件"。 当用户只是询问文件系统概念时不要使用。 ## 输入参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | directory | string | 是 | 搜索起始目录,绝对路径 | | pattern | string | 否 | 文件名匹配模式,支持通配符 | | keyword | string | 否 | 文件内容关键词 | | max_results | integer | 否 | 最大返回数量,默认 20 | ## 输出格式 返回匹配文件列表,每项包含路径、大小、修改时间。 ## 使用示例 用户:"找一下 src 目录下所有 .ts 文件" 调用:directory="/project/src", pattern="*.ts" ## 注意事项 - directory 必须是绝对路径,相对路径会导致失败 - pattern 和 keyword 至少提供一个 - 大目录搜索可能较慢,建议设置 max_results写完这份文档,运行时就能自动把它注册成一个可调用技能。工程师只需要实现背后的搜索函数,描述性工作全部由文档承担。
4.3 技能注册与运行时加载机制
OpenClaw 加载 SKILL.md 的流程大致是:扫描目录、解析 Markdown、提取结构化字段、生成工具描述、注入到模型的系统 prompt 中。这个过程对开发者透明,你改完 SKILL.md 重启运行时即可生效,不需要重新编译。
我实测下来,加载速度很快,几十个技能也就一两秒。但有个细节要注意:技能名称不能重复。如果两个 SKILL.md 用了同一个技能名,运行时的行为不确定,可能覆盖也可能报错。我建议用命名空间前缀,比如stock_query、file_search,避免冲突。
另外,运行时通常会缓存解析结果。如果你改了 SKILL.md 但没生效,先检查是不是缓存没刷新。大多数运行时提供热重载选项,开发阶段打开它,改完即生效,效率高很多。
4.4 与 MCP 工具联动:让技能真正干活
SKILL.md 本身只是描述,真正执行要靠背后的实现。如果你的能力已经封装成 MCP 工具,SKILL.md 里可以直接引用。比如你有一个 MCP Server 提供数据库查询能力,SKILL.md 里写清楚"此技能通过 MCP 工具db_query执行",运行时就会把两者关联起来。
这种联动的好处是描述和执行彻底分离。MCP Server 可以独立部署、独立升级,SKILL.md 只负责告诉模型"有这么个能力、什么时候用"。我在一个项目里把数据库查询、文件操作、外部 API 调用都封装成 MCP 工具,然后用 SKILL.md 统一描述,模型侧完全感知不到底层差异,调用准确率很高。
配置 MCP 连接时,注意 token 和 endpoint 的管理。敏感信息不要写进 SKILL.md,放在运行时的环境变量或配置文件中。我见过有人把 API key 直接写在技能描述里,这是大忌。
5. 常见问题与排查技巧实录
5.1 技能不触发或误触发怎么排查
这是最高频的问题。排查思路分三步:先看描述、再看模型、最后看运行时。
技能该触发却没触发,先检查触发条件是不是写得太窄。把用户的实际输入和你的触发描述对照,看有没有覆盖。我遇到过一次,用户说"帮我看看这个文件",我的触发条件写的是"查找文件",模型没匹配上。后来加了"查看、打开、读取文件内容"等同义表达,问题解决。
误触发则相反,通常是触发条件太宽。解决办法是加反向排除,明确列出不该触发的场景。还有一个技巧是在描述里强调优先级,比如"当同时满足多个技能触发条件时,优先使用本技能",帮助模型做选择。
如果描述没问题,那可能是模型本身的能力边界。换个更强的模型试试,或者把触发条件写得更直白。最后检查运行时有没有正确加载技能,看日志里有没有解析错误。
5.2 参数传递错误的典型场景
参数错误通常有三类:格式不对、类型不对、必填缺失。格式问题最常见,尤其是日期、时间、枚举值。解决办法是在参数描述里给死格式和示例,越具体越好。
类型问题多发生在数字和字符串之间。模型有时会把数字写成字符串,或者反过来。在描述里明确类型,并说明"必须是数字,不要加引号"。必填缺失一般是模型没理解哪个参数是必须的,把必填标记写醒目,并在注意事项里再强调一遍。
我整理了一份常见参数错误速查表,贴在项目文档里,团队新人上手快很多:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 日期格式混乱 | 描述未指定格式 | 明确 YYYY-MM-DD 并给示例 |
| 数字被加引号 | 类型描述不清 | 强调"数字类型,不加引号" |
| 必填参数缺失 | 必填标记不醒目 | 表格加粗必填列,注意事项重申 |
| 枚举值超出范围 | 可选值未列全 | 列出所有合法值及含义 |
| 路径参数报错 | 未要求绝对路径 | 明确"必须是绝对路径" |
5.3 多技能冲突与优先级处理
当技能数量多起来,冲突几乎不可避免。用户一句话可能同时匹配好几个技能。我的处理原则是:在 SKILL.md 里显式声明优先级和互斥关系。
比如"查询股票"和"查询基金"两个技能,用户说"查一下我的持仓",可能两个都匹配。这时在描述里写清楚"如果用户提到股票代码或股票名称,用股票技能;提到基金代码,用基金技能;如果都不明确,先询问用户"。把决策逻辑写进文档,模型就有依据了。
另一个技巧是设置兜底技能。当所有技能都不匹配时,用一个通用的对话技能接住,避免模型硬套某个不相关的技能。这个兜底技能的触发条件写"当没有其他技能匹配时使用",能显著降低误触发带来的糟糕体验。
5.4 性能与上下文占用的平衡
SKILL.md 写得太详细,会占用大量上下文窗口。我早期犯过这个错,一个技能写了上千字,十几个技能下来,光技能描述就吃掉大半上下文,留给实际对话的空间所剩无几。
解决办法是分层描述:SKILL.md 里只放核心信息,详细的文档放到外部文件,需要时再加载。OpenClaw 支持引用外部文档,模型在需要深入细节时才去读取。这样既保证了触发准确性,又控制了上下文占用。
另外,定期清理不再使用的技能。我每个季度会 review 一遍技能列表,把半年没触发过的归档。技能不是越多越好,精简的技能集反而让模型判断更准。
6. 我踩过的坑与实战心得
6.1 描述与实现不一致的隐蔽陷阱
最隐蔽的坑是 SKILL.md 描述和实际实现不一致。文档说返回 JSON,实现返回的是纯文本;文档说参数可选,实现里却是必填。这种不一致在测试时可能发现不了,上线后遇到边界情况才暴露。
我的做法是写一个校验脚本,自动比对 SKILL.md 里的参数定义和实现函数的签名。每次提交前跑一遍,不一致就报错。这个脚本不复杂,但省了我大量排查时间。团队协作时尤其重要,别人改了实现忘了改文档,脚本能兜住。
6.2 模型对技能描述的"过度解读"
模型有时候会过度解读技能描述,把没写的功能也当成有的。比如你写"查询股票价格",模型可能自作主张去查"股票历史价格"、"股票预测"。这在描述模糊时特别容易发生。
对策是明确边界。在注意事项里写清楚"本技能仅支持查询当前价格,不支持历史数据、预测、分析"。把不支持的也列出来,模型就不会越界。我还会在描述里加一句"严格按照上述范围执行,不要扩展功能",实测有效。
6.3 版本迭代中的兼容性维护
SKILL.md 会随业务迭代。改参数、加功能、调整触发条件,都可能影响已有调用。我建议给 SKILL.md 加版本号,重大变更时升级版本,旧版本保留一段时间做兼容。运行时可以同时加载多个版本,模型根据上下文选择。
另外,变更日志要记录清楚。哪个版本改了什么、为什么改、影响哪些场景,写明白。团队里有人遇到问题,翻日志就能定位。我见过因为没记日志,改了一个参数导致线上 Agent 大面积失效,排查了半天才发现是文档变更引起的。
6.4 团队协作中的 SKILL.md 管理规范
多人协作时,SKILL.md 的管理需要规范。我们的做法是:技能目录按负责人分,每人维护自己的技能,合并前必须 review。Review 重点看触发条件是否清晰、参数是否完整、有没有和现有技能冲突。
命名规范也很重要。统一用下划线分隔的小写英文,比如stock_query、file_search。避免用中文或特殊字符,减少解析问题。技能描述里的术语要统一,比如"股票"不要一会儿写"个股"一会儿写"证券",模型会困惑。
最后,建一个技能索引文档,列出所有技能的名称、用途、负责人、状态。新人加入时先看索引,快速了解现有能力,避免重复造轮子。这个索引我每周更新一次,成本不高,收益很大。
6.5 从 SKILL.md 到 Agent Skills 的演进思考
用了一段时间 SKILL.md 后,我越来越觉得它代表了一种趋势:Agent 的能力建设正在从"写代码"转向"写文档"。Agent Skills 这个概念本质上就是把技能文档化、标准化、可组合化。未来可能不需要每个团队都从零写技能,而是像装插件一样,从技能市场挑选、组合、定制。
这对开发者的要求也变了。以前拼的是框架熟练度和代码能力,现在拼的是把业务逻辑清晰表达成文档的能力。谁能把技能描述写得让模型一看就懂、一用就对,谁就能更快搭出好用的 Agent。我个人的体会是,花在打磨 SKILL.md 上的时间,回报率远高于花在调框架参数上的时间。
如果你还在手搓 Agent,我的建议是:先挑一个最常用的能力,按 SKILL.md 的格式写一遍,接进 OpenClaw 跑起来。感受一下描述和执行分离带来的清爽,再决定要不要全面迁移。这个过程不会太久,但可能会改变你对 Agent 开发的整个认知。