OpenClaw自定义技能开发:从需求拆解到协议设计的实战指南

发布时间:2026/10/17 18:37:22
OpenClaw自定义技能开发:从需求拆解到协议设计的实战指南
1. 动手写代码前先想明白一件事技能不是提示词1.1 我对技能机制的误解是怎么被纠正的前十三篇把OpenClaw的基础功能梳理得差不多了但真正常态化使用是从我下定决心从零写一个自定义技能开始的。一开始我把技能理解成一段更长的提示词以为给模型写得足够详细它就能稳定复现我想要的行为。结果被现实狠狠教育了提示词写得再好模型也会在关键步骤上自由发挥十个任务跑下来能有两种结果。直到我把第一个真正的技能按输入协议 处理逻辑 输出协议的方式落地所有行为才算真正可预期。这里头最关键的认知转变是技能的本质是可复用的代码能力大模型只负责决定什么时候调用它以及把用户的模糊需求翻译成技能要求的参数。技能内部的每一次扫描、判断、移动、重命名走的都是确定性逻辑不依赖当时的模型状态。这有点像一个电话客服外部不管谁来打电话只要报出工单号输入参数客服就按固定流程处理核心逻辑最后给你一个回执输出结果。至于打电话的人语气好不好、心情怎么样不影响处理流程本身。1.2 技能、脚本、工作流、插件到底有什么不一样很多第一次接触OpenClaw的朋友都会问这不就是写脚本吗我把这几种东西摆在一起对比过区别还是挺清楚的。形态触发方式是否涉及大模型判断典型场景普通脚本手动执行 / 定时任务不涉及批量改文件名、数据备份技能Agent在对话中按需调用只负责参数解析与调用决策文件归档、信息汇总、定时提醒工作流固定流程编排按序执行节点间转移可能涉及判断多步骤数据处理管线插件通常扩展平台能力边界视实现而定新增数据源、外部系统连接从这张表能看出来技能最特别的地方在于调用决策被交给了Agent。也就是说技能本身是一段确定的代码但什么时候用、用在哪一批数据上是大模型根据用户对话内容动态判断的。这让技能既保留了脚本的稳定性又比脚本灵活得多。也是因为这个原因技能开发的重点反而不全在代码有一半功夫要花在怎么让Agent能看懂你的技能、知道何时调用它这件事上。1.3 什么样的任务值得做成技能我盘点过自己日常反复做的那些操作发现符合下面几条里的任意两条都值得做成技能行为稳定同一类任务每次的处理规则基本不变。比如把下载目录里散落的文件按扩展名归档规则一年都未必变一次。结果要结构化后续还要被程序或Agent继续消费比如生成一份给数据库导入的JSON清单。涉及多个子步骤不是单条命令能解决的需要扫描、判断、执行、汇总多步配合。会被反复调用每周至少用个三四次的活儿做成技能后每次喊一句话就行。反之如果某个任务每次需求都大不相同、需要大量临场判断那它暂时不适合做技能扔给对话式处理反而更合适。技能开发最关键的第一步不是写代码而是搞清楚这个技能什么时候该出手、什么时候该闭嘴。2. 需求拆解与协议设计从乱糟糟的下载目录到清晰的能力边界2.1 先别急着写代码把需求说成人话我选的练手项目很朴素自动整理下载目录。我的下载目录常年堆着PDF、压缩包、安装包、截图、乱七八糟的文档时间一长根本找不到东西。这个场景够简单又有普遍性而且非常符合行为稳定、结果结构化、反复调用三个条件。动手之前我先把需求写成人话——给定一个目录路径扫描里面的所有文件不递归深层子目录暂不处理子目录按照扩展名把文件归类到对应子目录里比如 PDF 文件放进 Documents/PDF图片放进 Images/压缩包放进 Archives/。移动完成后输出一份清单告诉 Agent 哪个文件被移动到了哪里、哪些文件没处理成功、为什么没成功。这句话看起来朴素但暂不处理子目录和哪些文件没处理成功这两个限定是我刻意加上去的。前一个把问题域缩小到第一版能搞定后一个保证输出不是一句搞定了就结束而是能暴露问题。需求越早知道要说人话后面的实现就越不会跑偏。2.2 把需求拆成子任务再决定每个子任务怎么实现子任务我要的实现依赖扫描目录内容读取路径下的文件和子目录列表pathlib识别文件类别按扩展名映射到预定义类别规则字典生成归档目标路径类别名作为子目录名pathlib执行移动处理重名、权限、占用等异常shutil 自定义兜底输出结构化结果返回JSON格式的清单json我给这个技能定的边界是只处理传入目录下的直接文件不动子目录内容不从磁盘上删除任何文件。删除操作风险太高第一版坚决不碰。先预览、再执行这个原则我从一开始就定死了技能可以在预览模式下只输出将要做什么确认没问题后再切换到执行模式。2.3 为什么输出必须用JSON而不是一段漂亮的日志这是我这次开发里感受最深的一个设计取舍。如果技能跑完只往终端打一行归档完成人看着确实舒服但Agent没法用这个结果做后续决策。技能输出的本质是给Agent吃的数据不是给人看的故事所以必须是结构化、可解析的东西。我设计的输出协议长这样{ status: success, preview_only: false, processed: 128, moved: 3, skipped: 0, failed: 0, items: [ { source: E:/Downloads/项目资料.pdf, target: E:/Downloads/Documents/PDF/项目资料.pdf, action: moved, category: documents/pdf } ] }即使失败也要返回同样的结构{ status: partial, processed: 10, moved: 9, failed: 1, errors: [ { file: E:/Downloads/锁定中的文件.tmp, reason: permission_denied } ] }所有情况和字段都提前约定好Agent拿到结果后就能决定下一步是继续执行还是让用户介入。养成输出协议先行的习惯是从写脚本思维切换到写技能思维的第一道门槛。2.4 当时没人提醒我的一点技能描述文本的分量堪比代码这是我踩过最大的一个隐性坑。技能写完后Agent经常不调用它。我一开始以为是注册有误排查半天最后发现问题是技能声明里的description字段写得一塌糊涂。我最初写的是description: 整理文件。Agent看到这个描述大概率把它理解成一个可以整理所有文件的通用能力但下载目录归档技能明明只管按扩展名归档既不能批量重命名也不能清理重复文件。于是Agent面对用户帮我整一下下载文件夹的需求调用它的意愿非常低因为描述和用户需求的匹配度不高。后来我把描述改成了description: 扫描指定目录下的直接文件根据扩展名自动归档到对应类别子目录。支持图片、文档、压缩包、安装包、音视频、代码文件等常见类型。仅扫描直接文件不处理子目录移动前可预览。当用户要求整理下载文件夹、归档散乱文件或让某目录更有条理时使用本技能。改完之后调用率肉眼可见地上升。原因不复杂Agent靠描述来判断什么时候用哪个技能描述就是连接用户意图和技能实现的桥梁写得越具体匹配越精准反而不消耗太多token。3. 技能骨架落地目录布局、声明文件与核心逻辑3.1 目录布局按什么结构组织技能文件我建议从第一个技能开始就养成清晰的目录习惯别图省事把代码全塞一个文件里。这是我跑了一段时间后固定下来的布局download-archiver/ ├── manifest.yml # 技能声明文件Agent读取它来了解技能 ├── src/ │ └── archiver.py # 核心逻辑纯Python ├── rules/ │ └── categories.yaml # 分类规则外置配置 ├── tests/ │ └── test_archiver.py # 本地测试脚本 └── README.md # 给自己看的说明记录设计决策这个布局的用意很明确声明文件负责告诉Agent这个技能能干什么核心逻辑负责把事情做对规则外置让用户不用改代码就能调整分类策略测试脚本让你不启动整个OpenClaw也能验证逻辑。每一层各司其职将来技能复杂了也不至于变成一坨泥。3.2 声明文件Agent理解技能的入口manifest.yml 是整个技能最先被读取的部分它的质量直接决定技能会不会被正确调用。我实际用下来关键字段就这几个name: download_archiver version: 1.0.0 description: - 扫描指定目录下的直接文件根据扩展名自动归档到对应类别子目录。 支持图片、文档、压缩包、安装包、音视频、代码文件等常见类型。 仅扫描直接文件不处理子目录移动前可预览。 当用户要求整理下载文件夹、归档散乱文件或让某目录更有条理时使用本技能。 inputs: target_dir: type: string required: true description: 要整理的目录绝对路径 preview_only: type: boolean required: false default: true description: 为true时只输出计划不实际移动文件 outputs: - status - moved - failed - items - errorspreview_only默认设为 true 是我刻意为之的。默认情况下实际调用技能时不会真的把文件挪走只计算和展示。等用户在Agent对话里确认就这样执行之后再传 preview_onlyfalse 真正动手。这个设计在早期测试阶段帮我避开了大量误操作。3.3 核心逻辑扫描、分类、归档三步走核心代码我保持了最短的可用实现没有引入任何第三方依赖只用了Python标准库。这样技能部署到哪里都不会因为缺依赖而挂掉。from pathlib import Path import shutil import json import yaml # 需要pyyaml如果不想引入依赖用json存规则也行 from datetime import datetime def load_rules(rules_path: str) - dict: 加载分类规则扩展名 - 类别子目录名 with open(rules_path, r, encodingutf-8) as f: data yaml.safe_load(f) return data def classify(path: Path, rules: dict) - str: 根据扩展名返回类别标识未匹配的归入 others suffix path.suffix.lower().lstrip(.) for category, extensions in rules.items(): if suffix in extensions: return category return others def collect_candidates(target_dir: str, rules: dict) - list: 只扫描直接文件不递归子目录 root Path(target_dir) return [p for p in root.iterdir() if p.is_file()] def build_plan(candidates: list, rules: dict, target_dir: str) - list: 生成移动计划每条计划包含 source、target、category root Path(target_dir) plan [] for src in candidates: category classify(src, rules) category_dir root / category target category_dir / src.name if target.exists(): # 重名则加时间戳前缀避免覆盖 stamp datetime.now().strftime(%Y%m%d%H%M%S) target category_dir / f{stamp}_{src.name} plan.append({ source: str(src), target: str(target), category: category, }) return plan def execute_plan(plan: list) - dict: 执行移动计划单文件失败不中断整体 moved, failed 0, 0 errors [] for item in plan: src Path(item[source]) dst Path(item[target]) try: dst.parent.mkdir(parentsTrue, exist_okTrue) if src.exists(): shutil.move(str(src), str(dst)) moved 1 item[action] moved else: failed 1 errors.append({file: item[source], reason: source_missing}) except PermissionError: failed 1 errors.append({file: item[source], reason: permission_denied}) return {moved: moved, failed: failed, errors: errors}3.4 两个容易忽略但很要命的设计细节第一个细节是失败隔离。归档过程中只要有一个文件被占用、权限不足整个任务不应该崩掉。代码里对每个文件单独做异常捕获该文件失败就记录失败原因继续处理后面剩下的文件。实际跑起来之后才发现这个设计太重要了——我的下载目录里总有那么一两个被其他程序占用的文件放在老写法里会直接导致整批文件处理到一半停下来剩下的全卡住。第二个细节是幂等性。同一个技能跑第二次不应该把已经归档好的文件再次移动或搞乱。因为归档后的文件已经待在类别子目录里了而我的扫描逻辑只处理当前目录下的直接文件不会碰子目录内容所以同一批文件第二次跑的时候根本不会被扫描到。这个天然约束让技能在重复调用时表现稳定也正好构成了只处理直接文件这个边界的意义。4. 注册、单测与排错把技能接到OpenClaw里还要过三道坎4.1 注册技能把目录告诉平台然后验证平台能识别声明文件不同版本OpenClaw的技能注册方式可能略有差别但思路是一致的把技能目录放到平台能找到的位置再通过技能列表命令确认它被正确加载。我当时用到的步骤是# 1. 在技能目录下建立软链或复制技能目录到约定的技能目录中 # 2. 列出技能确认 download_archiver 出现且 manifest 无报错 openclaw skill list # 3. 查看技能详情确认 description 和 inputs 被正确解析 openclaw skill info download_archiver这种验证比直接进Agent对话里试快得多能立刻发现声明文件格式、字段拼写这类低级问题。4.2 绕过Agent先单独验证核心逻辑我强烈建议在把技能接进Agent前先写一个小脚本直接调用核心函数。我当时用命令行手测跑了这么一轮命令# 构造一个带混合文件的临时目录先走预览模式 python -c from src.archiver import load_rules, collect_candidates, build_plan rules load_rules(rules/categories.yaml) candidates collect_candidates(/tmp/mix_dir, rules) for item in build_plan(candidates, rules, /tmp/mix_dir): print(item[source], -, item[target], [ item[category] ]) 这样跑完全不依赖OpenClaw也不牵扯模型调用定位问题非常快。等核心逻辑验证得差不多了再进Agent里测调用链路。分两层验证的好处是一旦最终技能行为不对你能立刻判断问题出在代码层还是出在Agent调用层不用两头猜。4.3 我实打实踩过的三个坑第一个坑是描述与声明不一致。我把技能描述改得很具体之前Agent每次都不调它后来才发现描述里压根没提归档分类下载目录这些触发词。Agent很笨你描述里没有的词它不会脑补出来。修法就是把触发场景和关键词写进描述里越具体越好。第二个坑是输出里混进非JSON内容。我早期想给用户看日志直接在返回结果前打了一行归档完成共处理12个文件。这行字本身没问题但有一次某个接入层的老版本直接把技能标准输出的所有内容都当成结果去解析于是解析直接失败。从那之后我定了一条铁律技能的标准输出必须永远是纯JSON任何给人看的日志走专门的日志文件或者标准错误输出绝不和主结果混在一起。第三个坑是硬编码路径。我最开始在分类规则里写死了一个测试目录路径在本地跑得欢换到另一台机器直接找不到路径。修法很简单路径全部走参数传入代码里不出现任何绝对路径字面量。4.4 测试用例怎么设计才算靠谱技能逻辑不复杂但输入变化特别多偷懒很容易埋雷。我最后沉淀出来的用例集大致是这个范围用例输入内容期望结果空目录一个空文件夹processed0不报错混合类型图片PDF压缩包混在根目录各归各位未知后缀扩展名为.xyz的文件归入 others重名文件子目录里已存在同名文件自动加时间戳不覆盖只读文件目标文件不可写记录失败原因不影响其他文件子目录存在目录里有一个子文件夹子文件夹不被处理每个用例我都用临时目录先铺数据再跑逻辑全程不进Agent。后来进Agent里的那些报错基本都是我自己在接线上出的应用层问题核心归档逻辑几乎没翻过车。5. 让技能从能跑到靠谱多轮实测后沉淀的几个改动点5.1 兜底策略未识别类型不是报错而是归入others第一次跑实测的时候我遇到了不少奇奇怪怪的扩展名比如.crdownload、.tmp、.partial 这种临时文件。一开始我把它们归类成unknown并且直接跳过后来发现这反而让目录里永远有一批残留文件跟没整理一样。后来我调整了策略所有未识别类型一律归入others子目录至少保证根目录是干净的。分类规则里落到一个稳妥的兜底类别比追求绝对正确分类更符合实际使用预期。类似的策略还体现在重名文件上。归档时目标子目录里已经有一个同名文件的情况非常常见我的方案是对后到的文件加时间戳前缀而不是覆盖旧文件。宁可多一个带时间戳的文件也绝对不丢数据。这类宁可保守也绝不误伤的思路在自动化任务里应该贯穿始终。5.2 规则外置让不懂代码的人也能调整分类我最初把分类规则写在代码里每次想加一种扩展名都得改Python文件。后来把规则抽到了 categories.yaml 里问题就简单多了。documents/pdf: - pdf images: - jpg - jpeg - png - gif - webp archives: - zip - rar - 7z - tar - gz installers: - exe - msi - dmg others: # 兜底这里不放具体扩展名也行改动规则只需要改这个YAML核心代码一行都不用动。规则外置这件事的好处在技能要交给别人用的时候尤其明显——你的技能使用者只需要会改文本文件不需要会Python。5.3 安全和性能边界可逆操作优先别让技能失控我对技能的安全边界要求是任何操作都要可逆不可逆的操作必须先经过确认。归档移动算是可逆操作文件还在只是位置变了所以我允许直接执行但删除、覆盖、压缩包解压这类操作必须要有显式的确认开关默认关闭。对这个技能来说我还在执行前加了数量校验——如果本次要移动的文件数量超过预期值的几倍就主动停下来返回异常避免因为规则配置错误导致整个目录被洗一遍。做自动化越久越明白多数事故不是逻辑复杂导致的而是缺少边界保护。性能方面如果目录里有数千个小文件逐个同步移动会慢一些。第一版没有引入并行处理因为绝大多数场景一次归档的文件量撑死在几百个。但我在设计时把执行动作集中到了一处将来真要优化只需要换掉 execute_plan 的底层实现接口不变。5.4 日志复盘让每次运行都留下可追溯的痕迹技能跑得多了总会有某次移动结果不符合预期。我的做法是给技能增加了一个简单的运行日志文件记录每次申请的扫描时间、文件数量、移动了哪些文件、failed列表和reason。这样即使Agent对话上下文丢了也能靠日志复盘那次到底处理了什么。日志也严格走独立文件不影响标准输出的JSON纯净性。说实话这一条最初是我从一个做交付的朋友那儿学来的他的一句话让我印象很深自动化任务不怕花时间怕的是出了问题不知道发生了什么。做自定义技能这段时间最大的收获可以浓缩成一句话技能开发的核心不在代码写得多漂亮而在于能力边界划得清、输出协议立得住、行为结果可预期。这三点搞定了代码多少反而是小事。如果你也想从零搭自己的专属自动化技能我建议不要一上来就搞复杂任务先用一个像整理下载目录这样的小技能把全流程跑通——目录怎么布局、声明怎么写、怎么在Agent里被调用、怎么排查问题——这些经验远比多写几行功能代码值钱。正如我这次自己体验到的打通一次全流程之后再做第二个技能就是流水线作业十几分钟就能从想法到落地了。