第一次看到 OpenClaw 里的 Skills 目录时,我心想这不就是插件系统换了个名字吗?等真正把它跑起来,看着模型在对话里自动选技能、传参数、拿结果继续推理的时候,我才意识到这玩意儿和传统插件根本不是一回事。
如果你也是搞 Agent 开发的,应该能理解那种痛苦:把工具函数一股脑塞给模型,结果提示词越长,模型越“迷糊”,选错工具、参数传错是家常便饭。OpenClaw 的 Skills 体系本质上是在回答一个问题:能不能把“能力描述”和“能力实现”彻底分开,让模型只看精简的说明书,执行时才去触碰真正的代码?这篇文章我会把它的技术架构拆开讲清楚,从技能包结构、加载链路到沙箱执行,再附上我自己调试过程中踩过的坑。适合正在做 Agent 工程、或者想给开源框架贡献技能包的读者,看完应该能自己动手写出第一个顺手好用的 Skills。
1. Skills 在 OpenClaw 里的定位:不只是“插件”
1.1 我最初对 Skills 的误解
刚开始接触 OpenClaw 时,我习惯性地把 Skills 等同于传统插件。传统插件的逻辑是“注册-调用”:程序里有一份固定清单,开发者手动调用某个能力。而 OpenClaw 的 Skills 不同,它服务的对象不是开发者,而是模型本身。技能包内的描述信息会被注入到模型上下文中,模型根据用户当前需求自行判断“此刻该调用哪个技能”,然后以工具调用的形式触发脚本。这意味着技能和技能之间不是并列的代码模块,而是模型决策空间里的一组可选项。
这个区别很重要。传统插件是“人找工具”,而 Skills 是“模型挑工具”。架构上自然带来了两个要求:第一,技能的描述必须极其精炼且准确,因为描述质量直接决定模型能不能在恰当的时候选中它;第二,技能的执行必须被隔离,因为模型不可控,你不能让一个被模型触发的脚本随意访问系统资源。
1.2 Skills 与普通工具调用的本质区别
很多人问过我:OpenClaw 里有 Skills 也有普通的 tools,两者有什么区别?我实际用下来,最大的区别在封装粒度。普通工具往往是一个个细粒度的函数,比如“读取文件”“发送HTTP请求”;而 Skills 是面向“任务”的完整方案,它内部可以串联多个工具调用、执行多步脚本、甚至调用其他技能。
举个例子,同样做“批量重命名图片文件”,普通工具方式可能是模型先调“列出目录”工具,再调“重命名”工具,活生生写出一大串步骤,中间任何一步理解偏差都可能让整个流程断掉。而 Skills 方式是把整件事封装成一个名为“batch_rename_images”的技能包,模型只需要在合适时机触发它,剩下的文件遍历、扩展名判断、防冲突逻辑全在脚本里完成。模型省心,开发者也不用把实现细节塞进提示词,这就是架构上分层带来的好处。
2. 拆开看:一个技能包到底由什么组成
2.1 SKILL.md:给模型看的说明书
每个技能包的根目录下必须有一个 SKILL.md 文件,这是整个技能包的“门面”。它的头部是 YAML 格式的 frontmatter,声明技能的 name、description、参数 schema,正文部分则是给模型看的详细使用说明。我自己写技能的时候,会把 frontmatter 里的 description 控制在 100 字以内,用一句话说清楚“这个技能解决什么问题”,不堆形容词,不写实现细节。
这里有个特别容易犯的错:把 SKILL.md 写得像技术文档。要记住,读这份文档的是模型,不是程序员。模型需要通过 description 判断“当前用户需求是否命中这个技能”,所以在 description 里写“该技能基于 Python 3.10 实现,使用 Pillow 库处理图像”毫无意义。应该写的是“将图片批量转换为指定格式并压缩到目标体积”。模型不关心你怎么实现,只关心你能帮它完成什么任务。
正文部分则是给模型看的“使用说明书”。OpenClaw 的加载器会把正文内容也拼进上下文,模型在决定调用技能后,会根据正文理解参数语义、知道脚本的行为边界。我习惯在正文里写清楚:脚本输入什么格式的参数、输出放在哪里、有哪些已知限制。这能显著降低模型传错参数的概率。
2.2 scripts 目录:真正干活的代码
scripts 目录存放技能的实际可执行脚本,OpenClaw 支持多种语言,但官方推荐 Python 和 Bash,因为模型训练语料里这两种语言的样本最多,生成可靠性最好。脚本的入口约定是 main 函数或者直接可执行的脚本文件,参数通过命令行或者标准输入传入,结果通过标准输出返回 JSON。
在架构上,技能脚本被设计成“无状态”的:脚本启动时读取参数,处理完输出结果,然后退出。不推荐在脚本里维持长连接、守护进程或者写入内存状态。这么设计的原因是为了沙箱回收方便——每次调用都是全新进程,用完就销毁,不存在状态泄漏和资源占用。如果你需要技能之间有状态共享,应该把状态写进磁盘文件或者外部存储,而不是想着让脚本常驻。
2.3 resources:技能自带的“弹药库”
resources 目录放技能运行所需的静态资源,比如模板文件、参考数据、配置文件。它们会随技能包一并加载,在沙箱内以只读方式挂载。我初次接触时以为 Resources 只是“放杂物的”,后来才发现这其实是为了解决一个棘手问题:模型输出有随机性,你没法保证它每次都能生成完全正确的规则文件,所以把不变量放进 resources 里固定好,运行时只读引用,稳定得多。
举个例子,你写一个“生成周报”技能,周报模板就是一个典型的资源文件。脚本运行时从 resources 读取模板文件,填充模型传入的业务数据,再输出成稿。这样模板的维护脱离了代码,改样式的时候只需要替换资源文件,不用动脚本逻辑。
2.4 依赖与权限声明
技能包还需要声明依赖和权限。依赖声明文件放在技能目录下,比如 requirements.txt 或 package.json;权限声明则写在 SKILL.md 的 frontmatter 里,列出技能运行时需要访问的资源类型,比如文件系统、网络、环境变量等。OpenClaw 的沙箱在启动脚本前会检查权限声明,如果技能要访问网络但权限列表里没声明,请求会被直接拦截。
这个设计的思路是“最小权限”:技能默认什么都不能碰,只有显式声明了才能用。我自己刚开始写技能时嫌麻烦,图省事把权限全部放开了,结果有一次一个技能脚本因为拼写错误误删了同目录下的备份文件,从那以后我再也没偷过懒。权限声明不是形式主义,是最后一道安全闸门。
3. 核心执行链路:从模型选技能到跑出结果
3.1 技能注册与描述注入
OpenClaw 启动时会扫描配置指定的技能目录,逐个读取技能包,解析 SKILL.md 的 frontmatter,把技能名和 description 登记到技能注册表。然后,运行时会在每次会话启动时把注册表中所有技能的描述信息渲染成一段结构化的工具说明,注入到系统提示词中。因为上下文窗口有限,description 的每个字都在占宝贵的 token 预算,这也是为什么描述必须精简。
实际测试下来,如果一个会话里注册了超过 15 个技能,模型对边缘技能的选择准确度会明显下降。这不是 OpenClaw 的问题,是语言模型本身的注意力局限。所以如果你的技能库数量很大,建议按场景拆分不同的技能配置,而不是一股脑全挂上。会话开始前只加载当前场景可能用到的 5-8 个技能,效果最好。
3.2 参数提取与校验
当模型决定调用某个技能时,它会以结构化参数的形式输出调用请求,OpenClaw 运行时收到请求后先做参数校验。校验逻辑是:拿 frontmatter 里声明的参数 schema 逐个比对类型、必填项、取值范围。校验失败会把错误信息返回给模型,让它重新生成参数。这个过程对用户是透明的,但调试时可以从日志里看到完整的“拒绝-重试”记录。
参数校验是我觉得 OpenClaw 做得比较踏实的一层。有了它,脚本里不需要写大量的防御性代码,因为运行时已经挡住了一部分明显不合理的参数。但要注意:schema 里声明类型只是运行时校验,模型传参时还可能出现“语义对但格式偏了”的情况,比如把金额单位万元传成了元。这类问题靠 schema 解决不了,需要在 SKILL.md 正文里写明白参数的单位和具体含义。
3.3 沙箱执行与结果回传
参数校验通过后,运行时会在沙箱中启动一个新的子进程执行技能脚本。沙箱的核心隔离手段是文件系统隔离、网络权限控制和进程资源限制。文件系统方面,技能只能访问自己的 resources 目录和临时工作目录;网络方面,默认禁止外部连接,除非在权限声明里显式开启;资源限制方面,CPU 时间和内存上限都有配额,防止某个失控脚本拖垮整个 Agent 进程。
执行完以后,脚本的标准输出被捕获并解析。约定是输出 JSON 字符串,格式为{"status": "success", "data": {...}}或{"status": "error", "message": "..."}。解析成功后会把这个 JSON 作为工具调用结果返回给模型,模型再基于结果继续后续的推理和回复。这里有一个小细节:如果脚本输出里混入了非 JSON 内容,解析会失败,所以脚本里所有日志都要写到 stderr,标准输出只留给最终结果。
3.4 超时与并发控制
OpenClaw 对技能脚本执行设置了超时上限,默认是 60 秒,可在技能配置里覆盖。超时后沙箱会强制终止进程,并向模型返回超时错误。并发方面,全局默认同时只允许有限数量的技能执行,多余的请求排队等待。设计成有并发限制,是因为模型偶尔会“自己跟自己打起来”,连续触发多个高负载技能,把 CPU 占满。
我遇到过一个实际问题:某个图像处理技能处理高分辨率图片时需要 40 秒,模型又有“先处理这批再处理下一批”的连续调用习惯,结果两个任务排队,总耗时快两分钟,用户体验很糟糕。后来我把技能拆成“单张处理”和“批量调度”两个层级,单张处理控制在 10 秒内,批量调度循环调用单张处理并主动在脚本里加进度上报,体验一下就顺了。
4. 实操:手写一个“批量重命名”技能
4.1 第一步:搭目录
先创建一个技能目录,比如skills/batch-rename/,里面建好scripts和resources两个子目录,再新建 SKILL.md 文件。目录命名我用短横线分隔的小写单词,这是 OpenClaw 的命名规范。实际开发中你会有多个技能目录同时存在,所以命名一定要见名知意,别用my_skill_v2这种名字。
4.2 第二步:写 SKILL.md
这是整个技能的核心。frontmatter 里我声明了技能名、描述和参数 schema。描述要精准,我最后敲定的版本是:“批量重命名目录下的文件,支持添加前缀、后缀,替换指定字符串,以及按序号批量命名。适用于整理下载文件夹、图片库等场景。”
参数我设计了三个可选项:目标目录、操作类型、操作参数。所有参数都标注了类型和是否必填,并且在正文里进一步解释了参数语义。写完这个文件,模型在执行调用前就能根据描述把技能和用户需求关联起来。
提示:SKILL.md 里不要出现“你可以尝试”“请根据需要”这类含糊话。模型的 prompt 遵循能力,你不明确,它就自行发挥,结果往往不是你想要的。
4.3 第三步:写执行脚本
脚本我用的 Python,因为文件操作逻辑简单可靠。入口接收 JSON 参数,处理完成后输出 JSON 结果。核心逻辑是遍历目录、按操作类型处理每个文件、跳过隐藏文件和目录本身。处理过程中所有日志写到 stderr,最后只把统计结果输出到 stdout。
脚本本身不复杂,但有一个细节值得说:我在脚本开头加了if __name__ == "__main__":入口,并且用argparse或者sys.stdin接收参数。OpenClaw 默认会把参数序列化成 JSON 通过命令行传入,脚本里解析即可。写完脚本后先在本地手工执行一遍,确认逻辑正确,再进沙箱测试。
4.4 第四步:测试与加载
把技能目录放到 OpenClaw 配置的技能搜索路径下,重启会话后技能就注册成功了。你可以通过交互式对话直接测试:描述一个批量重命名的需求,看模型是否会触发这个技能。如果想快速验证脚本本身没问题,可以用 OpenClaw 内置的技能测试命令,直接指定技能名和参数,绕开模型选择环节,只看脚本执行结果。
这一步是我最推荐的调试顺序:先测脚本、再测触发。脚本都跑不通,就别纠结为什么模型不选中你的技能了。
4.5 一个踩坑记录:描述写得太“文学”导致模型选错
我这个技能最早一版的描述写的是“智能整理散乱文件,让一切井然有序”。听起来很唬人,结果实际测试时,用户说“我想把图片文件按修改日期分类”,模型直接选了它,而不是我准备好的另一个分类技能。原因很明显:描述里用了太多抽象词汇,命中了模型对“整理”这个意图的宽松匹配。
后来我把描述改得很“直白”,明确写出“重命名”这个动作,以及“添加前缀后缀、替换字符串、按序号命名”这些具体能力,模型选错的概率才降了下来。这算是我学到的第一课:技能描述不是海报文案,是给模型看的函数签名,越直白越好。
5. 和模型协作的边界:哪些事适合塞进 Skills
5.1 适合做技能的三类场景
基于我这一段时间的实践,适合封装成 Skills 的任务大致有三类。第一类是“步骤稳定且有固定套路”的任务,比如批量转换文件格式、生成特定结构的报表,这类任务步骤清晰,模型临场发挥反而容易出错。第二类是“需要访问本地资源”的任务,比如读写文件、操作命令行工具,沙箱已经隔离好了,技能脚本在权限范围内可以做很多事情。第三类是“单次执行耗时较长”的任务,比如批量抓取网页数据、处理大量图片,如果不封装成技能,模型在单轮对话里很难稳定完成这么长的动作序列。
5.2 不适合做成技能的(以及替代方案)
有些场景强行做成技能反而添乱。纯文本推理类任务,比如“总结这段文章的核心观点”,逻辑全在模型推理里,封装成技能没有任何收益。需要实时交互的任务,比如“帮我一步步调试这段代码并解释每步输出”,脚本没法动态感知中间状态,更适合让模型直接对话处理。还有一次性临时任务,用完就扔,没必要专门维护一个技能包。
我个人的判断标准是:如果同一个操作模式你调用了三次以上,才值得封装成技能。否则只是给技能库增加噪音,拉低模型选择准确率。技能库不是越大越好,而是越“精”越好。
6. 常见问题排查实录
6.1 技能加载了但模型就是不调用
这是最常遇到的问题。技能注册成功,但不管怎么对话,模型就是不去触发它。我的排查顺序是:先看描述是否具体到“动作+对象”级别,再模拟一次“模型读取描述”的视角——假设你对这个领域一无所知,单看这段描述能不能理解技能是干嘛的。大概率是描述写得太模糊。
如果你确定描述没问题,那就检查是不是技能数量太多导致模型注意力分散。我一般会把当前会话不相关的技能目录临时移出搜索路径,只留下概率最高的几个技能,效果立竿见影。
6.2 脚本报错但日志里什么都没留下
真相是日志多半写到了 stdout,而 OpenClaw 已经丢弃了脚本 stdout 里的非 JSON 内容。所以技能脚本里所有调试日志一定要写到 stderr,标准输出只留给结果。踩过一次坑之后,我现在写脚本的第一步就是配好日志格式,统一输出到 stderr。否则出错的时候,你只能看到一句“执行失败:无法解析输出”,完全不知道脚本内部发生了什么。
6.3 沙箱里没有网络权限
脚本需要访问外部 API 的时候,经常遇到网络被拦截的报错。OpenClaw 默认禁网,必须在 SKILL.md 的权限声明里显式开启allow_network: true,并把允许访问的域名加入白名单。这里要注意,白名单是域名级的,如果 API 切换了域名,需要同步更新权限声明,否则技能会在某个周末的半夜突然哑火。
6.4 依赖安装失败
技能声明的依赖在沙箱创建时会自动安装,但有些包含编译步骤的依赖在沙箱里装不上。我的经验是优先选择纯 Python 实现的依赖,或者干脆把依赖体积控制到最小。如果你的技能需要很强的第三方库,又没法在沙箱里装成功,可以考虑把技能拆成“主逻辑 + 外部服务调用”的方式,外部服务单独跑,技能只负责发请求拿结果。
6.5 技能版本混乱
OpenClaw 支持一个技能目录对应一个版本,但如果目录结构复制来复制去,很容易出现索引记录和实际文件对不上的情况。我的做法是给每个技能目录加上独立的分支管理和版本描述字段,改动前先看当前版本,改动后立即递增并把变更点记录在技能的 CHANGELOG 文件里。看似多了一步,省下的却是半夜排查“为什么这个技能行为前后不一致”的时间。
7. 让你少走弯路的一套细节清单
7.1 命名与目录规范
技能名用短横线分隔小写字母,目录名和技能名保持一致。脚本文件统一放在 scripts 下,资源文件统一放在 resources 下。别把资源文件和脚本混在一起,OpenClaw 扫描的时候是按目录类型走的,乱放可能导致资源没被正确挂载。另外,技能目录内不要放.git文件夹和大型二进制文件,加载速度会明显变慢。
7.2 描述长度控制
frontmatter 里的 description 建议控制在 50-100 字。太短,模型没法理解能力边界;太长,占用上下文窗口还稀释注意力。正式发布前可以做一轮“盲测”:把描述拿给同事看,让他猜这个技能是干什么的。猜得准,说明描述写到位了;猜不准,就得重写。这个方法比反复改 prompt 高效得多。
7.3 调试小技巧
- 先用内置工具测试脚本执行,绕开模型选择环节,快速定位脚本问题。
- 在技能脚本里加入详细模式,环境变量设了
OPENCLAW_DEBUG=1的时候输出额外的执行信息。 - 沙箱没法实时调试,但可以在本地用相同的 Python 版本和依赖手工跑一遍脚本,大多数问题在本地就能复现和定位。
- 出问题先看 stderr 日志,再看退出码,最后才看标准输出。这三样信息加起来能覆盖九成以上的技能故障场景。
最后再分享一个小经验:我在实际使用中逐渐形成了一个习惯,每次新建技能包之前,先问自己一个问题——“如果让我用 200 字的描述把这个能力讲给一个刚入行的人听,我会怎么讲?”技能描述本质上就是在做这件事,只不过听众是模型。说得清楚,模型就用得好;说得含糊,模型就乱来。OpenClaw 的 Skills 架构在我看来最巧妙的地方,就是强迫你把“能力”这件事用人类语言精心打磨一遍,这种打磨本身,就是在提升整个 Agent 系统的质量上限。