1. 在WorkBuddy里"写技能"到底在写什么
很多人第一次接触WorkBuddy社区时,看到别人分享的Skill包,第一反应是"这不就是个文件夹加一篇说明文档吗"。这个判断对了一半。Skill在WorkBuddy里确实表现为一个目录、几个脚本和一份Markdown说明书,但真正决定它能不能被Agent正确调用、能不能被社区其他人装走就用,靠的是那份说明书里的声明式格式——SKILL.md。你写的不是代码,是一份让Agent理解"什么时候该用你、怎么用你、用了之后输出什么"的契约。
我在社区维护过十几个Skill,从简单的文件批量重命名,到需要调第三方接口的查询类技能,踩了不少坑。最大的体会是:WorkBuddy的Agent本身已经具备很强的对话和推理能力,它缺的不是"能力",而是触发条件和调用方式的精确描述。你把描述写得含糊,Agent就会在用户问A事的时候错误调用你的B技能;你把参数格式写得随意,Agent传进来的数据就乱七八糟。所以社区里流传一句话:"Skill写得好不好,一半看代码,一半看说明书。"
这篇教程不打算讲太深的前端或算法,而是沿着我实际开发Skill的完整流程走一遍:先理解Skill在WorkBuddy里的定位,然后拆解SKILL.md的文件结构,再以"批量重命名文件"这个最经典的入门场景为例,手把手写一个能跑的Skill,最后聊调试、发布和维护。适合刚接触WorkBuddy社区、想把自己手上的小工具或者脚本打包成Skill分享出去的开发者,也适合已经写过一两个Skill但总觉得Agent调用不够精准的人。
2. Skill包的结构和SKILL.md格式规则
2.1 一个标准的Skill包目录长什么样
WorkBuddy社区目前对Skill包的结构约定比较统一,遵循"一个目录一个技能"的原则。我习惯这样组织:
rename-files/ ├── SKILL.md ├── scripts/ │ ├── rename.py │ └── utils.py ├── assets/ │ └── example.png └── requirements.txt这里每个文件都有用处:SKILL.md是Agent唯一会主动读取的入口文档,它决定了Agent在什么场景下想起这个技能;**scripts/**放实际执行的脚本,Agent最终会调用这些脚本;**assets/**放示例图片或模板文件,社区详情页展示用;requirements.txt声明Python依赖,社区安装器会自动根据它配置环境。
我见过有人把几百行代码全塞进SKILL.md的代码块里,或者反过来把SKILL.md写得像产品PRD,这两种极端都不对。SKILL.md的核心任务是描述意图和接口,不是承载实现。Agent读取SKILL.md之后,会把脚本路径和参数组装成系统命令来执行,所以脚本必须是"可被命令行调用"的形态。
2.2 SKILL.md的YAML头部:元信息决定分发
每个SKILL.md文件开头都有一个YAML格式的元信息块,这是社区识别的关键。我贴一份当前推荐的最小示例:
--- name: rename-files description: 批量重命名文件。当用户需要按照规则批量修改文件名、为文件添加前缀或后缀、将文件名统一为某种格式时使用。 version: 1.0.0 author: community license: MIT ---几个容易忽略的点:
description是Agent判断是否调用Skill的第一依据。它要回答三个问题:技能操作对象是什么(文件)、动作是什么(重命名)、典型场景是什么(批量加前缀/后缀/统一格式)。描述里不要出现"这是一个非常有用的工具"这类废话,也不要只写"重命名文件"四个字,因为Agent做意图匹配时是拿用户原话跟description做语义对比的。我用过一个反面例子,某开发者把description写成"for renaming stuff",结果Agent在用户问"修改文档标题"时也能匹配上这个技能,完全跑偏。
name字段要稳。它会被用作调用时的标识,社区里Skill的name规则是纯小写加连字符,不要用下划线,更不要带版本号。我一开始给技能起名"file_rename_v2",结果社区安装器解析时就报了格式警告,因为连字符才是社区解析器认可的单词分隔符。
version建议遵循语义化版本规则。社区详情页会把版本号展示出来,维护者更新包时version是唯一让社区判断"有新版"的依据。我踩过的一个坑是:更新脚本后忘了改version,导致用户装到的还是旧缓存包。从那时候起我把"更新代码后第一件事改version"写进了自己的检查清单。
2.3 正文里的三块关键内容:变量、运行方式和注意事项
YAML头部之下是Markdown正文。社区约定俗成推荐包含三个小节,虽然不强校验,但对Agent能否正确调用影响很大。
第一块是输入变量(Input Variables)。WorkBuddy的Skill支持在SKILL.md里声明输入参数,Agent会像填表单一样把从用户对话里抽取到的信息填进来。示例:
## Input Variables - `directory`: 要重命名的目标目录路径(必填) - `prefix`: 要添加到文件名前的内容(可选,默认空) - `suffix`: 要添加到文件名后的内容(可选,默认空) - `dry_run`: 是否只输出预览结果不实际执行(可选,默认 false)这里有一个重要的技巧:每个变量都要写清楚是必填还是可选,以及默认值。Agent做参数抽取时,如果没有默认值概念,遇到用户没提的参数就容易留空或者编造一个值。比如用户只说"把下载目录里所有图片加个前缀project_",那么suffix变量Agent会留空,如果你的脚本没有默认值处理,就直接崩了。把"可选+默认空"写明白,Agent就会在调用命令时忽略这个参数。
第二块是运行方式(Execution)。你要告诉Agent脚本用什么解释器跑、参数怎么传。社区一般推荐写成一段简短的代码块:
python scripts/rename.py --directory "$directory" [--prefix "$prefix"] [--suffix "$suffix"] [--dry-run]这段描述看起来只是重复一下命令行,但它其实是在给Agent"规定动作"。Agent在真实场景里会根据这段代码决定调用命令的长相。如果有人在这里写成一个含糊的"run the script with the directory",Agent可能就不知道怎么组合参数。
第三块是注意事项(Important Notes)。这里写给Agent看,也写给使用Skill的人看。比如"脚本只处理文件、不会递归子目录""目标目录不存在时会先创建""文件名冲突时自动添加序号避免覆盖",这些边界行为写得越清楚,Agent越不容易在用户提出奇怪需求时瞎猜。
3. 手写一个"批量重命名文件"Skill:从函数设计到Agent调用
3.1 脚本设计:先定义边界,再写功能
写Skill的脚本和普通脚本有个微妙区别:普通脚本的默认假设是"人在终端使用",Skill脚本的默认假设是"一个看不见的Agent在替你调用"。所以参数处理要极其宽容,容错要做足。我以rename脚本为例,先列设计要点:
- 所有参数都支持命令行传参,同时支持环境变量兜底
- 目录不存在时自动创建,而不是抛异常
- 文件名冲突时自动加序号,绝不覆盖已有文件
- 提供dry-run模式,先预览再执行
- 每次执行打印结构化结果,供Agent判断成功与否
第一版脚本我直接用了os.rename,代码很短,但很快在社区反馈中发现一个典型问题:用户让Agent把某目录下所有IMG_001.jpg改名为vacation_001.jpg,Agent抽取参数时没问目录,结果脚本抛了"目录不存在"。用户的预期是"帮我整理相册",但Agent拿到的目录是对话里从未出现的字段。这个问题的解决方案不是让脚本更聪明,而是在SKILL.md的Input Variables里把directory写成必填,并加一句"如果用户没有提供目录,询问用户具体路径,不要猜测"。
这就是Skill开发的常态:你一半的时间在写业务逻辑,一半时间在写引导Agent正确提问的约束说明。
下面是rename.py的完整实现,我保留了核心功能,去掉了一些平台相关装饰:
#!/usr/bin/env python3 import argparse import os import sys from pathlib import Path def safe_rename(src: Path, dst: Path) -> Path: if not dst.exists(): dst = dst else: stem = dst.stem suffix = dst.suffix counter = 1 while dst.exists(): dst = dst.with_name(f"{stem}_{counter}{suffix}") counter += 1 os.rename(src, dst) return dst def batch_rename(directory: str, prefix: str, suffix: str, dry_run: bool): folder = Path(directory).expanduser().resolve() if not folder.exists(): folder.mkdir(parents=True, exist_ok=True) renamed = [] for item in sorted(folder.iterdir()): if not item.is_file(): continue new_name = f"{prefix or ''}{item.stem}{suffix or ''}{item.suffix}" new_path = item.with_name(new_name) if new_path == item: continue if dry_run: renamed.append((item.name, new_path.name, "预览")) else: final_path = safe_rename(item, new_path) renamed.append((item.name, final_path.name, "完成")) return renamed def main(): parser = argparse.ArgumentParser(description="Batch rename files in a directory") parser.add_argument("--directory", required=True, help="target directory path") parser.add_argument("--prefix", default="", help="prefix to add") parser.add_argument("--suffix", default="", help="suffix to add before extension") parser.add_argument("--dry-run", action="store_true", help="preview only") args = parser.parse_args() results = batch_rename(args.directory, args.prefix, args.suffix, args.dry_run) for old, new, status in results: print(f"{status}: {old} -> {new}") if not results: print("没有可重命名的文件(只处理文件,不递归子目录)") sys.exit(0) if __name__ == "__main__": main()这段代码故意做得非常保守。resolve()和expanduser()处理~路径和相对路径,Agent传参时经常带着~或者相对路径,不处理就是各种"文件找不到";folder.mkdir(parents=True, exist_ok=True)让脚本面对不存在的目录时自动创建,这是考虑到Agent可能在目录尚未建立时就调用技能。safe_rename里的序号冲突处理,保证了脚本在文件重名时不会意外覆盖,这在批量场景下几乎必然会遇到。
3.2 写SKILL.md:让Agent在合适的时机想起它
脚本写好后,动手写SKILL.md。前面YAML头部已经展示过,这里重点说正文怎么组织。我完整的SKILL.md正文大概是这样的:
## Input Variables - `directory`: 要重命名的目标目录路径(必填)。如果用户未提供,请先询问用户,不要假设。 - `prefix`: 要添加到文件名前的内容(可选,默认为空)。 - `suffix`: 要添加到文件名后、扩展名之前的内容(可选,默认为空)。 - `dry_run`: 是否只预览不执行(可选,默认为 false)。 ## Execution 使用以下命令执行: ```bash python scripts/rename.py --directory "$directory" [--prefix "$prefix"] [--suffix "$suffix"] [--dry-run]如果用户没有提供directory,请向用户提问获取路径后再执行,不要使用空字符串或当前目录代替。
Important Notes
- 脚本只重命名目录下的直接文件,不递归处理子目录。
- 目标目录不存在时会自动创建。
- 当目标文件名已存在时,会自动添加数字序号(如
report_1.txt),不会覆盖原有文件。 - 默认情况下脚本会真实执行重命名;如需预览,请使用
dry_run=true。 - 命令输出格式为
状态: 原文件名 -> 新文件名,其中状态可能是 "完成" 或 "预览"。
注意我把"如果用户没有提供directory,请询问用户"同时写在了变量说明和执行方式两处。这不是重复,而是为了增强约束力。Agent读取上下文时,有时候只读了变量定义没看后面的执行说明,两处都写可以降低它乱猜的概率。 ### 3.3 为什么这个Skill适合做第一个练手项目 批量重命名文件几乎是所有Skill开发者的第一个练习,因为它足够小、依赖少、需求场景清晰。完成它之后,你会对Skill开发有个完整的体感:脚本怎么写、SKILL.md怎么定义变量、Agent在什么场景下会调用、发布后别人怎么用。而且重命名这个动作天然适合交给Agent——用户在对话里描述规则,Agent解析成参数,脚本执行,整个过程用户不需要打开终端也不需要手写命令。 更进一步,这个Skill可以很自然地扩展出多个变体:加一个`--recursive`支持递归子目录、加一个`--pattern`支持用正则匹配过滤文件、加一个`--replace`支持替换文件名中的特定关键词。每加一个参数,你就多一次练习"如何把参数语义写清楚"的机会,这些经验会反哺到你后面写复杂Skill的过程中。 ## 4. 调试和验证:把Skill当成一个函数来测 ### 4.1 本地直接跑脚本:最朴素的调试法 Skill本质是"脚本+描述文档",所以最早期的调试完全不需要启动任何平台界面。我习惯先在本地把脚本当普通CLI工具测: ```bash mkdir -p /tmp/rename_test touch /tmp/rename_test/photo_{1,2,3}.jpg python scripts/rename.py --directory /tmp/rename_test --prefix holiday_ --suffix _2025这一步能验证脚本基本逻辑有没有错。参数传错、路径解析错误、文件冲突处理,都在这里暴露。我把这套做法称为"函数式调试":输入一组已知数据,断言输出结果,和写单元测试的思路一样。
本地测试通过后,才是真正验证Skill能不能被Agent正确调用的环节。WorkBuddy社区提供了本地调试模式,你不需要上传Skill包,而是把本地目录挂载到运行环境中。启动时它会读取你当前的SKILL.md并渲染到对话上下文里,然后你直接用自然语言提需求,看Agent是否调用了技能、传的参数对不对。
我在这个阶段最常发现的问题是:描述写得太窄或者太宽。有一次我把一个查天气的Skill的description写成"获取天气信息",结果用户问"今天需要带伞吗",Agent调用了天气技能,但传参时把"带伞"解析成了地点参数,输出自然莫名其妙。后来我把description改成了"获取指定城市当前天气和未来24小时降水概率。当用户询问天气、降雨、带伞、出行建议时使用",Agent的匹配精度立刻上来了。
4.2 关键测试用例:覆盖Agent的九种奇怪习惯
我总结了一份针对Skill的"Agent习惯测试清单",每个Skill在发布前我都会照着过一遍:
| 测试场景 | 典型用户话术 | 验证点 |
|---|---|---|
| 完整参数 | "把下载文件夹里的文件全部加上前缀project_" | 参数抽取正确、脚本执行成功 |
| 缺失必填参数 | "帮我重命名一下文件" | Agent是否主动询问目录 |
| 路径带波浪号 | "处理~/Pictures目录" | expanduser是否生效 |
| 相对路径 | "重命名当前目录的文件" | resolve是否解析到绝对路径 |
| 空目录 | 指定一个空目录 | 输出是否友好提示 |
| 文件名冲突 | 目标名已存在 | 是否自动加序号 |
| 预览模式下执行 | 加了dry-run参数 | 是否真的不修改文件 |
| 多个同扩展名文件 | 批量处理大量文件 | 是否全部处理、顺序是否稳定 |
| 目录不存在 | 指定一个不存在的路径 | 是否自动创建并静默执行 |
第2个场景是最容易翻车的。很多Skill的SKILL.md里把参数写了"默认空",Agent就不会追问,直接用一个空字符串调用脚本,脚本执行后要么报错要么没有效果。我在重命名Skill的说明里专门加了一句"如果用户未提供目录,请先询问用户,不要猜测",Agent就基本不会再犯这种错了。写Skill的本质,是你在教一个什么都不懂但非常听话的实习生怎么干活,规则一旦没写清楚,他就会一本正经地做错。
4.3 调试日志:脚本要把过程说出来
调试Skill时另一个常用技巧是让脚本"话多"。普通CLI工具用户不介意你只在出错时打印一点信息,但Agent需要通过标准输出来判断脚本执行情况。我在rename.py里故意每处理一个文件都打印一条记录,即使只是dry-run模式,也把"预览: a.jpg -> b.jpg"逐条打出来。这样Agent在返回给用户时,可以原样复述这些信息,用户会感觉Agent真的"看到了"执行过程。
反过来,如果脚本只打印一个"成功",Agent回复用户时就只能干巴巴地说"已完成",用户如果再问"具体改了什么名字",Agent就完全无从回答。这也是社区里"好Skill"和"普通Skill"的一个隐性分水岭:脚本输出是否结构清晰、信息完整,直接决定了Agent的解释能力。
5. 发布、安装机制和版本维护的一些细节
5.1 打包检查:发布前的最后一次自检
Skill在WorkBuddy社区有两种分发形式:一种是提交到社区仓库,另一种是分享压缩包。无论哪种,发布前都应该过一遍自检清单。我自己是这么做的:
- 检查目录结构是否完整,有没有遗留的临时文件(
.DS_Store、__pycache__这些一定要清掉) - 确认SKILL.md里的version已经比上一版递增
- 在干净环境里重新安装一遍Skill,跑一次端到端测试,确认没有依赖缺失
- 检查requirements.txt是不是最小依赖集,避免把无关包写进去
- 确认脚本权限是可执行的,至少确保
python能直接运行它
第3步最容易出问题。我遇到过的情况是:本地开发环境里装了某个包,脚本运行正常,但requirements.txt里漏掉了它,用户装完Skill一跑就报ModuleNotFoundError。解决方案是每次发布前都新建一个干净虚拟环境安装依赖并跑通全部测试,这个过程虽然多花五分钟,但能避免社区里一堆"装不上""跑不了"的反馈。
5.2 社区安装器做了什么:理解背后的机制
社区平台的安装器读取Skill包时,核心动作是:解压文件、读取SKILL.md的YAML头部、把目录放到技能目录下、根据requirements.txt安装依赖。整个流程对用户是黑盒,但对Skill作者来说,理解它有助于规避问题。
一个典型坑是文件夹名字和name字段不一致。社区安装器解压后以目录名为准建立技能目录,但SKILL.md里的name字段用于Agent调用时的逻辑标识。如果两者不一致,会出现"包装上了但Agent找不到技能"的诡异情况。另一个坑是requirements.txt里固定了过高的依赖版本,和用户环境里已有包冲突,导致Agent启动时报错。我现在的习惯是限制一个版本下限而不是上限,比如requests>=2.25,除非确有必要才用==精确锁定。
5.3 持续维护:Skill不是写完就结束的
Skill发布之后,维护压力主要来自两头:一是用户的真实使用反馈,二是Agent平台本身对格式要求的变化。社区里的Skill格式已经迭代过好几版,早期一些依赖旧字段的包现在已经检索不到。
我的维护策略是给每个Skill建一个简单的小文档,记录每个版本改了哪些内容、为什么改。这样下次收到"用户反馈某个功能不好用"的时候,可以快速定位是脚本逻辑的问题还是描述说明的偏差。有一个经历让我印象很深:某查询类Skill上线后一直表现稳定,后来社区更新了一次Agent的语义匹配模型,突然有用户在反馈区说"明明问的是一个完全不相关的问题,Agent却自动带了天气参数"。排查后发现是description里某个词和天气模块的description有语义重叠,改了一下描述措辞就恢复了。这种问题不写版本日志很难定位,因为你不会记得两个月前那个description是怎么写的。
6. 写Skill最容易翻车的四个细节:我的经验笔记
6.1 参数语义模糊:AI会一本正经地猜错
Skill参数不是给机器填的键值对,而是给AI理解的自然语言描述。我见过很多新写的Skill把变量写成input、data、arg1这种名字,SKILL.md里也只有一个孤零零的变量名,没有任何解释。这种技能Agent调用时基本靠猜,猜错是常态,猜对是运气。
正确做法是把变量命名得像自然语言里的"槽位"。比如你要设计一个查天气的技能,变量不叫city叫city_name,描述写清楚"城市名,如北京、上海、广州等具体地名,用户可能使用'我所在的城市',此时需要结合对话上下文判断或澄清"。这样Agent在抽取参数时才能从"带伞吗"这类模糊表达里提取出有效信息。
6.2 脚本的退出码和错误处理
用户反馈过一类问题:技能执行时报错,但Agent回复"已完成"。查下去根因是脚本里某个子流程发生异常,但被一个宽泛的try-except吞掉了,脚本照样exit(0)。这个教训让我此后非常重视退出码和异常信息的传递。
WorkBuddy的Agent在脚本执行结束后,会读取标准输出和退出码判断是否成功。如果脚本打印了明显的"ERROR: xxx"字样,Agent通常能识别为失败并如实告知用户;但如果脚本只是静默失败,Agent大概率会把"命令执行完成"当作"任务成功"。所以现在我在所有Skill脚本里约定了一条规则:任何捕获到的异常必须打印到标准错误并采用非零退出码,绝不吞掉异常。
6.3 参数里带空格和特殊字符的传递问题
这是命令行类Skill的经典翻车点。Agent把用户话术里的目录路径直接塞给命令时,如果路径里有空格(比如Windows的C:\Users\My Documents),裸传参数就会把路径拆成两段。我在rename脚本里之所以用--directory加=号的风格,就是为了规避一部分空格问题,但对于Shell来说,正确做法还是包装一层引号。
社区的处理惯例是在Execution描述里明确写出命令模板并用引号包裹参数,例如--directory "$directory"。同时,脚本内部也应该对路径做一次标准化处理。经验是:永远不要假设Agent传入的参数是干净的,它在对话里拿到的原始值可能有空格、有中文、有括号、有~,你的脚本必须全部容忍。
6.4 演示文件和图片的尺寸
社区详情页会给每个Skill渲染一组展示图片。很多人上传一张巨大无比的工作区截屏,结果详情页加载很慢,甚至图片边缘被裁掉。我踩过一次这个坑之后,专门用一个脚本把assets里的示例图统一压缩到1200px宽、体积控制在500KB以内。这个细节看似和"写Skill"无关,但真实影响体验:一次我分享的图片加载超时,好几个用户直接放弃了安装,转去了另一个功能类似的包。
7. 从"能用"到"好用":几个进阶思路
如果一个Skill在社区里收获了一些关注,值得花时间做一轮"体验优化"。我推荐三个方向:
第一个方向是丰富描述场景。参考社区热门的同类技能,看它们的description怎么写的。如果一个场景自己没想到,但用户反馈里反复出现,就把它补进去。比如重命名Skill,最初只写了"批量重命名、加前缀后缀",后来有用户拿来整理下载文件夹,我就在description里加了"整理下载目录、批量归类文件"这些具体场景词,之后这类请求的命中率明显上升。
第二个方向是增加启发式行为。比如脚本在处理完文件后,可以顺手生成一份变更清单写到目录下的.rename_log.txt,这样用户事后能追溯。Agent读到这个行为后,在回复里也会主动提及"已生成变更日志",体验完整度会高很多。
第三个方向是保持兼容性迭代。每次社区平台更新Skill格式,我会第一时间用老包跑一遍新机制,发现警告就顺手修掉。这类维护看起来没有"新功能上线"那么光鲜,但它决定了你的Skill在社区里是"持续可搜索"还是"逐渐沉底"。
写Skill这件事,越往后越会发现,技术实现的门槛真的不高,难的是让一个看不见推理细节的Agent准确理解你的设计意图。每一次发布后的反馈,都是你重新审视"契约写得好不好"的机会。熟练之后,定描述、写脚本、测场景这些流程大概半小时就能走完,但这个"契约思维"的打磨,是一个Skill作者在社区里真正积累下来的东西。