一直以为 OpenClaw 装好就能直接干活的人,基本都会在第二天放弃。默认技能确实能处理点简单问答、跑跑命令,但真到自己手头的工作流——比如把散落各处的周报整理成一份摘要,或者让本地的小模型按固定格式输出 JSON——默认那套东西根本不够用。所以我决定动手写自定义技能,从 Windows 部署踩坑到技能上线,前后折腾了两周,这篇实战教程就是这次过程的完整记录。
如果你只是把 OpenClaw 当聊天机器人用,那确实不用往下读了。但如果你想让 AI 代理替你完成重复性的操作,比如每天早上汇总监控指标、把新下载的资料按项目归档、给不同格式的文档统一转成 Markdown,你就绕不开自定义技能。接下来的内容我不会把官方文档抄一遍,只讲我在实际搭建和调试中验证过的思路、结构以及最容易翻车的地方。整个过程会涉及 WSL 环境、Node.js 运行时、本地模型接入和技能脚本编写,新手可以按顺序跟着做,老手可以直接跳到第 3 章看技能设计思路。
1. 为什么自定义技能才是 OpenClaw 的核心玩法
OpenClaw 的技能机制,说白了就是把“什么时候做、让模型怎么思考、最终执行什么动作”这三件事打包成一个文件。它比单纯给模型写提示词更硬核一点,因为动作是真实脚本在跑;它又比传统程序开发更灵活一点,因为中间那层思考是由语言模型完成的。你不需要把每个分支都写死,只需要给模型一套规则和一个可执行的工具,剩下的由模型来决定怎么调用。
很多人刚接触时觉得默认技能已经够用,但用两周就会发现天花板很明显。默认技能通常只覆盖通用场景:查天气、翻译、执行简单 shell 命令。一旦任务涉及你自己的目录结构、私有数据格式、固定报表模板,默认技能就抓瞎了。它既不知道你的周报长什么样,也不知道你的项目文件夹命名规则,更不可能替你维护一套分类逻辑。这时候自定义技能的价值就体现出来了。
我自己的经验是,自定义技能带来的收益主要有四个:
- 输出稳定:把提示词和脚本都固化下来,模型每次执行都遵循同一套规则,不会今天输出 Markdown、明天输出 JSON。
- 节省 token:复杂的背景说明、少样本示例只需要在技能文件里写一次,不用每次对话都重复灌输。
- 可复用:同类任务换台机器、换个项目,把技能目录复制过去就能用。
- 可审计:所有动作都经过脚本执行,脚本日志会留下痕迹,不会出现“模型嘴上说做了,实际什么都没干”的尴尬。
我平时给团队搭自动化流程时,习惯把一个技能拆成四层:触发层、提示词层、执行层、解析层。触发层决定技能什么时候被激活;提示词层决定模型如何理解任务;执行层是真正的 Python 或 Shell 脚本;解析层决定脚本返回的结果如何被展示或继续传递。这四层各自独立,方便调试。如果技能没触发,就去改触发层;如果模型理解错了,就去改提示词层;如果脚本跑挂了,就去看执行层报错。这是整篇文章的主线,后面所有内容都围绕着这四层展开。
需要泼一盆冷水的是:自定义技能不是一上来就写的。先花几天把 OpenClaw 默认的技能跑熟,理解它的日志文件在哪、怎么手动触发、怎么传参,再开始动手写自己的。跳过这一步直接写,你会在调试的时候分不清问题是出在平台配置,还是出在你自己写的脚本里。
2. 先把环境收拾利索:Windows 部署 OpenClaw 的常见坑
OpenClaw 在 Windows 上的部署体验,比 Ubuntu 上要曲折不少。最典型的症状就是“看起来装好了,一启动就报错”。网上很多人卡在同一个地方:PowerShell 里提示无法安全验证环境,后面跟着一句让你运行wsl -- status的说明。这句话本身没有错,但它只说了一半——你真正要做的不是跑一次状态命令,而是把 WSL 侧的环境整个检查一遍。
2.1 别跳过 WSL 状态检查
先在 PowerShell 里按顺序跑三条命令:
wsl --status wsl --list --verbose wsl --update第一条看 WSL 内核版本和默认发行版状态,第二条看当前安装了哪些 Linux 发行版、分别是什么版本,第三条把 WSL 内核升级到最新。很多“无法安全验证”的报错,根因其实就是 WSL 内核版本太旧,或者压根没有一个被标记为 default 的发行版。升级完内核之后,务必重启一次终端,让环境变量和路径重新加载。
如果你执行wsl --list --verbose发现没有任何发行版,那就先安装一个,我建议直接用 Ubuntu:
wsl --install -d Ubuntu-22.04装完进入 Ubuntu,执行sudo apt update && sudo apt upgrade -y,把系统基础软件包更新一遍。这里有一个容易忽略的点:OpenClaw 的核心进程实际跑在 Linux 侧,Windows 侧只是启动器和托管进程,所以 Linux 子系统的健康程度直接决定 OpenClaw 能否顺利运行。我自己就遇到过 Ubuntu 发行版里 Python 版本不对,导致 OpenClaw 某个依赖原生模块编译失败,折腾了一个多小时才定位到问题。
另外强烈建议把数据和工作目录放在 WSL 原生文件系统里,也就是~/下面的路径,不要放在/mnt/c/这种挂载目录。WSL 访问 Windows 宿主磁盘的 IO 速度慢,而且文件权限经常出问题,脚本里执行chmod可能无效,进程也可能拿不到执行权限。第一次部署时我把整个技能目录放在D:\skills下,结果模型一直反馈“权限不足”,把目录复制到~/.openclaw/skills之后就正常了。
2.2 运行时版本与基础依赖
很多人以为装 OpenClaw 需要去 Node.js 官网下载什么包,其实 Node.js 只是它需要的运行时。OpenClaw 本体还是从项目仓库拉取,Node 只是底层运行环境。安装时不要图新,选 LTS 版本就好。就我实测的经验,Node.js 18 和 20 的 LTS 版本都比较稳,太新的版本反而可能遇到依赖原生模块没跟上导致的报错。
装完 Node 后检查一下:
node -v npm -v版本没问题,但很多原生依赖依然可能编译失败。这是因为 OpenClaw 的依赖里有一部分需要调用系统编译工具,比如sharp、esbuild这类包在安装时会触发 node-gyp 编译。在 WSL 里先补齐基础工具链:
sudo apt install -y build-essential python3 make g++这一条在官方文档里常常一笔带过,但它能帮你避开不少奇奇怪怪的安装错误。我在一台几乎全新的 Ubuntu 上安装时,就是因为没有build-essential,依赖安装阶段直接报“node-gyp failed”,补上之后再执行安装就非常顺滑。
2.3 “无法安全验证环境”这类报错的完整排查链路
如果上面两步都做了,还是出现安全验证失败,那就不能只盯着那句提示了。按我排错的经验,接下来要按顺序检查四样东西:
| 检查项 | 命令 | 预期结果 | 不通过时的处理 |
|---|---|---|---|
| WSL 内核版本 | wsl --status | 显示内核版本且无“更新可用”提示 | wsl --update升级内核 |
| 默认发行版 | wsl --list --verbose | 某个发行版标记为默认 | wsl -s Ubuntu-22.04设置默认 |
| PowerShell 执行策略 | Get-ExecutionPolicy | 不是 Restricted | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser |
| 依赖目录完整性 | ls ~/.openclaw/ | 关键目录存在且非空 | 重新执行安装命令 |
执行策略这个问题很容易被忽略。Windows 默认的 PowerShell 执行策略可能会拦截 OpenClaw 的启动脚本,报出来的错误五花八门,最终都会指向“无法安全验证”。解决办法是给当前用户放开执行策略:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser这个设置只对当前用户生效,不影响系统全局,安全性可控。
如果以上都检查了仍然失败,最有效的办法是直接看日志。OpenClaw 的日志一般写在~/.openclaw/logs/下面,用tail -f实时盯着,然后把启动命令重新跑一遍。真实报错往往藏在日志最后几十行里,比界面提示准确得多。我曾经遇到过安全软件把wsl.exe调用拦下来,导致 OpenClaw 子进程起不来,把目录加入白名单后立刻恢复。这类问题不看日志根本猜不到。
如果实在不想在 Windows 上折腾,直接用一台 Ubuntu 服务器或者虚拟机安装会顺利很多。Windows 侧的部署逻辑和 Linux 侧没有本质区别,只是多了 WSL 和执行策略这两道坎。
3. 先把需求拆成技能设计:触发规则、提示词与动作的边界划分
环境跑通之后,最容易犯的错误就是直接动手写代码。实际做下来你会发现,技能设计阶段花的时间应该比编码更多。我第一个技能“整理下载目录”光拆需求就花了一个晚上,真正写脚本反而只用了四十分钟。原因很简单:模型不擅长理解模糊的意图,你必须把需求翻译成清晰的边界、规则和可执行步骤。
3.1 一个技能的最小组成
无论技能的复杂程度如何,都可以用一张表来描述它的设计:
| 组成部分 | 作用 | 典型示例 |
|---|---|---|
| 触发规则 | 决定技能何时被激活 | 包含关键词“整理下载”,或每天 09:00 定时触发 |
| 提示词 | 定义模型的人设、任务理解方式、输出约束 | “你是文件管理助手,只输出 JSON,不要解释” |
| 执行动作 | 真正跑起来的脚本命令 | python3 tidy_files.py --dir 参数 |
| 解析逻辑 | 处理脚本输出并决定如何展示 | 把 stdout 按 JSON 解析并渲染成列表 |
这四部分里,提示词层和执行动作层需要划分得非常清楚。我的原则是:所有有副作用的操作(移动文件、删除、写入、网络请求)一律放到执行层,由脚本控制;模型只负责产出参数和决策分类。这样做有一个显而易见的好处——你可以在正式执行前让脚本进入 dry-run 模式,只输出将要做的事情清单,而不是立刻动手。这个安全阀在调试阶段极其重要。
3.2 范例:把“整理下载目录”拆成技能
假设需求是“把下载目录里的文件按类型整理好”。这句话对模型来说太模糊了,必须拆成五个步骤:
- 扫描指定目录,列出所有文件。
- 根据扩展名或文件名关键字判断类型。
- 移动到目标分类子目录,遇到同名文件自动追加时间戳。
- 不删除任何文件,除非显式传入
--trash参数。 - 输出一份变更清单,方便用户确认。
拆完之后,触发规则就很好定了:包含“整理下载”“归类文件”“file tidy”等关键词。提示词只需要告诉模型三个要点:下载目录的路径从哪里取、文件分类规则以脚本内置映射表为准、不要自己臆造目标路径。执行动作就是一个 Python 脚本,接收目录参数和--dry-run标志,输出 JSON。解析逻辑把 JSON 里的变更列表渲染成表格。
这里要特别注意“边界”的设计。文件移动这个动作一旦执行就不可反悔,所以我把删除操作默认禁掉,而且所有操作都必须记录到变更清单里。模型永远不能直接执行文件操作,只能生成参数供脚本使用。这个设计让技能既灵活又可审计,哪怕模型某次判断失误,最多是文件被移到了错误分类目录,而不是被删掉。
3.3 让技能挂上本地模型:qwen2.5-3b 这类开源模型怎么接
热词里出现“qwen2.5-3b 关联到 openclaw”,这其实是很多人的真实需求。把 OpenClaw 默认的云端模型切换成本地模型,好处是数据不出内网、没有按次计费、离线也能跑。我的建议是优先选择支持 OpenAI 兼容接口的本地推理服务,OpenClaw 接入时只需要配置一个base_url指向本地端口。
以qwen2.5-3b-instruct为例,启动推理服务后,技能配置大致是这样:
model: provider: openai-compatible base_url: "http://127.0.0.1:8000/v1" name: "qwen2.5-3b-instruct" api_key: "local" temperature: 0.2注意api_key这里填什么都可以,因为本地服务通常不做校验,但字段不能省,否则客户端会报鉴权错误。temperature建议调低到 0.2,尤其是需要结构化输出的时候,温度越高越容易跑偏格式。
3B 级别的模型上下文窗口有限,不能指望它像大模型那样记住长篇背景。针对小模型,我的经验是:提示词越短越好,把所有复杂细节放到脚本里解决,模型只负责“理解意图、抽取参数、返回分类结果”这三件事。如果真的要它总结长文档,先在动作脚本里把文档切成片段,逐段让模型处理,再在脚本侧合并结果。这样既绕开了上下文窗口限制,执行速度也快很多。
4. 把技能真正写出来:目录结构、清单与执行脚本
设计文档写清楚之后,编码反而是水到渠成的事。这里我给出一套我实际在用的目录结构,你可以直接作为模板:
~/.openclaw/skills/file_tidy/ ├── skill.yaml ├── prompts/ │ └── system.md ├── actions/ │ ├── tidy_files.py │ └── requirements.txt └── README.md每个技能一个独立目录,放在 OpenClaw 的skills目录下。skill.yaml是技能清单,负责把触发条件、模型参数、执行命令串起来;prompts/system.md是给模型的提示词;actions目录放所有可执行脚本;README.md写使用说明,方便自己和团队其他人查阅。
4.1 技能清单 skill.yaml 怎么写
技能清单是整个技能的核心胶水。我用的是 YAML 格式,字段名在旧版本里可能略有差异,但逻辑是通用的:
name: file_tidy description: 按文件类型整理指定目录 version: 1.0.0 trigger: type: keyword matches: ["整理下载", "归类文件", "file tidy"] model: provider: openai-compatible base_url: "http://127.0.0.1:8000/v1" name: "qwen2.5-3b-instruct" api_key: "local" temperature: 0.2 action: command: python3 args: ["actions/tidy_files.py"] env: PYTHONUNBUFFERED: "1" timeout: 60 parse: jsontrigger决定了技能是否会被唤醒。matches里放关键词,宁可多放几个同义说法也不要只写一个精确短语。我一开始只放了“整理下载”,结果我对 OpenClaw 说“帮我把下载文件夹归类一下”,技能完全没有响应。后来把“归类文件”“整理下载目录”“file tidy”都加进去,命中率才上来。关键词的选取建议用自己的口语习惯反推,你平时怎么说,就把那些说法加进去。
action.timeout一定要设。脚本如果卡在等待网络响应或者死循环里,超时机制会强制终止,避免整个代理会话被拖死。我习惯设 60 秒,大文件处理场景会调到 120 秒。
4.2 执行脚本:输入、输出与可观测性
执行脚本是技能里真正干活的角色。我写文件整理脚本时,对它的要求有三个:参数从命令行和 stdin 读取、输出严格 JSON、非零退出码表示失败。
下面是一个精简版脚本框架,你可以直接参考:
#!/usr/bin/env python3 import argparse import os import shutil import json import time CATEGORY_MAP = { ".pdf": "documents", ".docx": "documents", ".xlsx": "documents", ".png": "images", ".jpg": "images", ".jpeg": "images", ".mp4": "videos", ".zip": "archives", } def scan_files(directory): result = [] for name in os.listdir(directory): full_path = os.path.join(directory, name) if os.path.isfile(full_path): result.append((full_path, name)) return result def move_file(src, dest_dir, dry_run): os.makedirs(dest_dir, exist_ok=True) base = os.path.basename(src) dest = os.path.join(dest_dir, base) if os.path.exists(dest): root, ext = os.path.splitext(dest) dest = f"{root}_{int(time.time())}{ext}" if dry_run: return {"file": base, "action": "move", "dest": dest} shutil.move(src, dest) return {"file": base, "action": "move", "dest": dest} def main(): parser = argparse.ArgumentParser() parser.add_argument("--dir", required=True) parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() changes = [] for src, name in scan_files(args.dir): ext = os.path.splitext(name)[1].lower() category = CATEGORY_MAP.get(ext, "others") dest_dir = os.path.join(args.dir, category) changes.append(move_file(src, dest_dir, args.dry_run)) print(json.dumps({"code": 0, "changes": changes}, ensure_ascii=False)) return 0 if __name__ == "__main__": exit(main())这段脚本有几点值得注意:输出 JSON 用了ensure_ascii=False,避免中文路径变成\uXXXX转义序列,否则解析层还要二次还原;move_file里做了一次同名文件检查,防止覆盖已有文件;--dry-run模式不会真正移动文件,只会打印计划。
脚本里宁可多写一层检查,也不要信任模型传过来的参数。例如dir参数如果为 None,脚本会直接崩溃,而一个健壮的技能应该捕获异常并返回人类可读的错误信息。比较稳妥的做法是,在技能清单里写清楚参数约束,比如“dir 必须是绝对路径,且必须存在”,同时在脚本里再做一次os.path.isdir校验,双重保险。
4.3 错误处理与超时设计
脚本执行过程中,最常遇到的三类错误是参数错误、权限错误、目标路径不存在。我给技能设计错误处理时,遵循一个原则:内部异常绝不静默吞掉,必须转成 JSON 格式返回给上层。这样可以保持parse: json的解析一致性,模型也能根据错误信息自行调整下一次参数。
def main(): try: # 正常流程 ... except Exception as exc: print(json.dumps({"code": 1, "error": str(exc)}, ensure_ascii=False)) return 1返回非零退出码 + JSON 错误信息,是技能脚本与 OpenClaw 之间最可靠的通信方式。模型看到code: 1,就知道这次执行失败了,不会再强行格式化输出。
超时设计上,我除了在技能清单里设置timeout,还会在脚本内部对可能存在网络请求的步骤设置单独超时。比如按文件类型分类本来不需要网络请求,但如果某个步骤要调用外部接口,我会用urllib或requests的 timeout 参数限制在 5 秒内。防止一个子任务拖垮整个技能。
5. 测试与调试:看起来成功不等于真的能用
技能写完,第一件事不是立刻让 OpenClaw 调用,而是先在命令行里手动执行脚本。很多新手跳过了这一步,直接对代理说“整理下载”,结果脚本报错被模型包装成一段礼貌的道歉,你根本看不出真正原因。正确的调试链路是:先测脚本,再测触发,最后测完整链路。
5.1 手动执行的正确姿势
先跑 dry-run 模式,确认计划输出正确:
python3 actions/tidy_files.py --dir ~/Downloads --dry-run再跑真实移动,确认文件确实被归类:
python3 actions/tidy_files.py --dir ~/Downloads脚本没问题之后,再回到 OpenClaw 里触发技能。如果触发命令你还不熟悉,直接在对话里输入“整理下载目录”就行。成功后立刻看日志:
tail -f ~/.openclaw/logs/openclaw.log日志里能看到技能是否被命中、模型调用了哪个技能、脚本的退出码、以及最终的解析结果。有一次我以为技能没生效,结果日志显示脚本执行成功,但解析层因为模型返回了 Markdown 格式而失败。这种问题看界面根本发现不了,不盯日志很难定位。
5.2 三个高频失败信号
调试过程中,我把最常见的失败整理成了一个表格,方便对照排查:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 技能完全没反应 | 触发关键词没匹配上 | 扩展matches列表,覆盖用户口语说法 |
| 模型提示“我无法完成” | 动作脚本退出码非零 | 看脚本的 stderr,检查参数、权限、依赖 |
| 执行成功但输出乱码 | 解析层拿到非 JSON | 在提示词里强制“只输出 JSON 代码块”,或加正则兜底 |
| 执行超时 | 脚本卡在循环或网络请求 | 检查处理路径,给脚本内部加超时和日志 |
第三类问题尤其常见。小模型对“输出 JSON”的理解不稳定,经常会在 JSON 外面包一层 Markdown 代码块,或者前面加一句“好的,这是结果:”。这种情况在配置里写死“只输出 JSON,不要任何解释文字”能改善大部分问题,但最稳的方案是在脚本里加一个提取 JSON 的兜底函数,用正则从标准输出里把第一个{...}代码块抽出来再解析。
5.3 边界测试必须做
脚本能跑一次成功,不代表它可靠。我建议至少准备三种测试目录:空目录、包含中文文件名的目录、包含超长路径和特殊字符的目录。空目录最容易暴露逻辑漏洞,比如列表为空时脚本是否还能输出合法 JSON;中文文件名在 Windows 和 WSL 之间来回传递时,编码问题也时常出现;超长路径会触发操作系统的路径长度限制,脚本必须能跳过或报错而不是崩溃。
我实际遇到的案例是:文件整理技能在英文文件名目录下跑得很顺,一旦遇到中文文件名,stdout 里出现了编码问题导致解析失败。后来排查发现是我在 Windows 侧执行脚本时,默认编码不是 UTF-8,加上PYTHONUTF8=1环境变量后问题消失。这类边界问题只有靠多场景测试才能暴露,第一次跑通根本不代表什么。
6. 把技能沉淀成可复用资产:版本管理、分享与后续扩展
技能一旦稳定运行,就要考虑把它当成一个正经项目来维护。我见过太多人写好技能之后直接丢在skills目录里,改了几次后就再也记不清改了什么,也没办法回滚到可用版本。自定义技能本质上是一份代码资产,该有的版本管理一套都不能少。
6.1 用 Git 管理技能目录
最简单的方式是给每个技能目录单独git init:
cd ~/.openclaw/skills/file_tidy git init git add . git commit -m "feat: 新增文件整理技能"后续每次改动,按功能提交,不要积攒一大堆改动才提交一次。我在维护技能的时候,给自己定的规矩是:技能目录里永远保留一个可以工作的版本,新增功能先在--dry-run模式下验证,再合并到正式分支。哪怕改坏了,随时可以回滚。
依赖也要记录。Python 脚本就在requirements.txt里写明依赖包和版本,Node 脚本就在package.json里固定。没有依赖锁定的技能,换一台机器大概率跑不起来。实测中比较常见的情况是:本地开发环境装好了所有包,以为技能自带脚本不需要依赖声明,结果换台干净的机器一跑就报ModuleNotFoundError。
6.2 README 与分享规范
如果是打算分享给团队甚至发布到社区,README 至少要包含四块内容:使用场景、安装方法、配置示例、已知限制。使用场景帮助别人快速判断这个技能适不适合自己;安装方法写清楚依赖和技能目录的复制位置;配置示例把关键字段(如模型地址、目标目录)贴出来;已知限制则说明哪些情况下不适合使用,比如“超过 2GB 的目录不建议使用”。
命名规范也值得注意。技能名一律用蛇形命名,如file_tidy、daily_report,不要用带空格和特殊符号的名字。描述字段要写得像产品简介而不是技术笔记,因为触发层很可能依赖描述做语义匹配。描述太啰嗦会让模型难以判断,太简略又容易误触发。
发布时还要关注许可证。脚本代码和提示词文本都可以被复制,但如果要公开发布,最好在仓库里声明许可证类型,让别人知道能不能商用、能不能改。没有许可证的仓库,默认情况下其实是不允许他人自由使用的,这一点很多人没意识到。
6.3 后续扩展:多技能联动与模型回退
技能跑稳定之后,可以开始考虑组合。比如我在整理文件之外还写了一个“生成日报”技能,它会先调用数据提取脚本,再把结果交给模型汇总成日报。这个过程中,整理技能和日报技能并不需要互相知道对方的内部实现,只要统一走 JSON 输出,就能像管道一样串联起来。这也是我把所有技能的输出都设计成 JSON 的原因——任何技能的输出都可以作为另一个技能的输入。
模型回退也是一个值得做的扩展。本地 3B 模型对复杂任务的理解能力确实弱一些,我会在技能里配置一个回退机制:当本地模型连续两次返回格式错误时,自动切换到更大的云端模型。这需要在执行流程里加一个计数器或者让解析层在异常时触发重试,复杂度会上升,但稳定性提升非常明显。
最后分享一个我踩过坑之后养成的习惯:每个技能都必须支持--dry-run。不只是执行脚本支持,整个技能配置里也要预留一个只读环境,让模型先在沙箱里生成参数、查看影响范围,再实跑。很多看似复杂的自动化事故,其实都在差这一步。以我自己维护的这套技能来说,凡是能允许我先模拟一遍操作的,至今没有出过破坏性错误。这个习惯,算是整篇教程里我最想让你带走的东西。