很多玩AI编程助手的朋友应该都遇到过这个场景:模型很聪明,写代码、改bug、做重构都不在话下,但一旦让它“帮我把这个视频下载下来”,它就傻眼了。不是它不会,而是它手里没有工具。这正好引出了我今天想聊的核心——Skills。简单说,Skills就是把AI Agent能执行的某个具体能力,打包成一份“说明书加脚本”的组合。而我这篇文章要做的,就是讲清楚如何借助Skill Creator这类工具,把一个现成的GitHub仓库——以yt-dlp这个老牌开源视频下载工具为例——完整地转化成一个AI可以直接调用的Skill。
这事的价值在哪?很多人都在用Claude Code、Codex、OpenCode这些编程Agent,它们默认只能操作文件、跑命令,偶尔能写几段代码,但对于“下载某个网页视频、提取音频、抓字幕”这类非常具体的网络任务,往往需要你手把手教它参数、帮它查报错。如果把这些操作封装成Skill,Agent就能在你提出需求时自动命中技能,自己完成从URL识别、参数拼装到文件保存的全过程。这篇文章适合所有在做Agent工具链整合、或者想把自己的开源项目改造成Skill的开发者。我会从设计思路、工具选型、实际封装步骤到问题排查,把整个技术转化逻辑完整走一遍。
1. 首先要搞明白:Skills到底是个什么东西
1.1 AI Agent的“手”:从对话到调用工具
大模型本身是没有“手”的。你问它“怎么下载视频”,它能给你写出十条命令,但它自己没法去执行网络请求、没法把文件写进磁盘。Agent架构解决的就是这个问题:通过编排模型与外部工具的交互,让模型输出结构化调用指令,再由运行时环境去真实执行。这里的关键是,模型并不知道某个工具什么时候能用、参数是什么、结果怎么解析——除非你提前把这些信息告诉它。
Skills就是干这个的。它本质上是一份目录,里面有一个SKILL.md文件加若干辅助脚本。SKILL.md用模型友好的方式描述“这个技能是干什么的、什么时候触发、怎么调用、需要什么参数、输出是什么规范”。辅助脚本则是真正干活的代码,通常是Python、Shell或者Node脚本。模型通过读取SKILL.md,就知道可以用这个技能,然后拼出正确的调用命令,让脚本去执行。
我经常用一个生活化的类比:你给实习生一份工作手册,手册里写明“当客户提出某某需求时,你按第几步操作,使用哪个工具,输出格式是什么”,再给他一个装好常用工具的工具箱。Skills就是这份工作手册加工具箱,AI Agent就是那个实习生。没有手册,实习生再聪明也容易瞎干;有了手册,他就能稳定且高效地完成重复性工作。
1.2 现成的GitHub仓库,为什么要“转换”成Skills
这时候你可能会问:yt-dlp本来就是一个命令行工具,Agent直接用shell调用不就行了,为什么要多一步封装?
这个问题我实测下来确实值得说道说道。直接调用确实可行,但你会发现体验非常不稳定。第一,Agent对yt-dlp的参数记忆通常是模糊的,它可能把--format写成--format-select,把--output的模板变量搞错,遇到报错也不知道怎么调整。第二,yt-dlp输出信息非常庞杂,下载进度、日志、警告全混在stdout里,Agent很难判断任务到底成没成功、文件存在哪了。第三,也是最关键的,Agent不知道怎么把一次“视频下载请求”映射到正确的命令组合上,比如拿到一个B站链接,它可能想都不想就乱填参数。
而把GitHub仓库转换成Skill,就是在两者之间加了一层“翻译与约束”。SKILL.md明确告诉Agent:哪些URL是支持的、推荐用什么参数组合、限制哪些功能不要碰。脚本层则负责把Agent传进来的参数规范化,执行完后输出结构化的结果,比如文件的绝对路径、大小、格式。这样一来,Agent就不再是“猜着用工具”,而是“照着规范用工具”,成功率和稳定性大幅提升。
2. Skill Creator:选型与核心机制
2.1 从手工封装到自动化生成:为什么要用工具
最早的Skill开发方式是完全手写。你得自己研究工具的所有参数,精心设计SKILL.md里的描述,再写脚本做参数映射。这样做不是不行,我曾经手写过两三个简单的Skill,比如“批量重命名图片”和“调用系统计算器”,效果也还凑合。但一旦工具比较复杂,比如yt-dlp这种上百个参数、支持上千个网站的庞然大物,手写的成本就非常高了。
Skill Creator这类工具解决的就是“从已有仓库到Skill”的转换问题。它会去扫描你指定的GitHub仓库,分析README文档、CLI入口、依赖文件,尝试理解这个工具的核心能力,然后自动生成一个可用的Skill骨架。它本身不可能做到完全智能,但能把最繁琐、最机械的部分——比如目录结构、基础SKILL.md、参数清单提取——一次性做好,再由开发者去裁剪和微调。
我实际用过几个方案,除了标题提到的Skill Creator之外,还有类似“harness creator skill”等社区方案。它们底层逻辑大同小异,都是“解析仓库结构,生成技能描述,产出标准目录”。我最终选择Skill Creator作为主力,是因为它对CLI工具的识别做得比较完整,会主动探测类似argparse定义、--help输出这样的信息,这正好匹配yt-dlp这种以命令行参数为核心的工具形态。
2.2 Skill Creator的转换工作流
完整的转换流程可以分成三个阶段。第一阶段是“能力发现”:Skill Creator会拉取或读取你指定的GitHub仓库代码,重点看README、入口脚本、依赖声明,找出工具对外暴露的主要能力。对yt-dlp来说,它发现的能力就是视频下载、音频提取、字幕抓取、批量任务、速度限制这一批。第二阶段是“骨架生成”:工具根据发现的能力,生成SKILL.md草稿和scripts目录,里面可能包含一个参数说明文件和一个示例调用脚本。第三阶段是“人工收尾”:你作为开发者,需要把生成的草稿改得更精准,补充Agent真正需要的触发词、参数白名单、输出规范,再测试调整。
这里要特别强调一句:工具生成的是骨架,不是最终成品。我见过不少人以为跑一遍Skill Creator就万事大吉,结果Agent调用时一塌糊涂。原因是自动生成的描述往往过于泛化,把工具所有功能都写进去,模型反而不知道什么场景该触发。真正有价值的转换逻辑,恰恰是后面的人工裁剪和边界约束。Skill Creator的意义在于帮你省掉从零搭建的体力活,而不是替你做决策。
3. yt-dlp 技术转化逻辑:从命令行参数到Skill能力声明
3.1 先给yt-dlp的能力划一个边界
yt-dlp能做多少事?单看它的README,你可能会被吓到:支持上千个网站、支持各种格式选择、可以下载字幕、可以提取音频、可以断点续传、可以限速、可以批量处理播放列表、甚至可以调用外部后处理工具。但如果把这些能力全部暴露给Agent,SKILL.md会膨胀到几千行,模型很容易被海量参数迷惑,最终效果反而更差。
所以做转换的第一步,是给工具划能力边界。我基于实际使用频率和可靠性,从yt-dlp上百个参数里筛出了五个最核心的能力场景:
| 能力场景 | 对应的yt-dlp核心参数 | Skill中暴露的配置项 |
|---|---|---|
| 单视频下载 | -f、-o、--merge-output-format | url、format、output_dir |
| 提取音频 | -x、--audio-format、--audio-quality | audio_only、audio_format |
| 抓取字幕 | --write-subs、--sub-langs、--write-auto-subs | write_subs、sub_langs |
| 批量下载 | --playlist-items、-o模板 | playlist_items、max_items |
| 稳健性控制 | --retries、--limit-rate、--socket-timeout | retries、limit_rate |
这不是说其他能力不重要,而是要在Skill层面做取舍。我的原则是:只暴露“Agent能安全使用、结果可控”的能力子集。类似--cookies-from-browser这种涉及个人账号权限的参数,我会从Skill的默认参数中剔除,只在脚本层面保留一个可选开关,并加上明确的使用提示。
3.2 参数白名单:降低Agent犯错的概率
为什么一定要做参数白名单?我踩过坑。最初我试过把yt-dlp的参数大部分都写进SKILL.md,期望Agent能灵活选择,结果它经常自作聪明,比如下载一个公开视频时莫名其妙加了需要登录才能用的参数,或者把-o的输出模板写得匪夷所思,导致文件保存位置完全不可控。
参数白名单的设计逻辑是:在SKILL.md里只列出少数几个经过验证的参数组合,并且每个参数都给出明确取值建议。比如format一项,我就会明确写:“推荐传best[height<=720]或者mp4这类简单值,不要传过于复杂的格式选择表达式”。Agent本身有很强的推理能力,但它缺乏对具体工具的“手感”,我们提供越清晰的约束,它就越不容易跑偏。
另外,在脚本层面我也做了校验。任何不在白名单里的参数直接拒绝,而不是透传。这样做还有个额外好处:防止Agent在调用时夹带私货,比如想通过命令注入的方式执行其他操作。脚本用argparse严格解析输入,未知参数直接报错退出,给Agent返回明确的错误信息。
3.3 输入输出规范:让Agent拿到干净结果
Agent调用Skill之后,怎么知道成功还是失败?这是封装时最容易忽略、但恰恰最影响体验的一环。yt-dlp原生输出是给人看的,有进度条、有颜色、有大量噪音。Agent去解析这种输出,简直是在一团乱麻里找针。
我在封装yt-dlp Skills时,把stdout输出规范改成了两段式:常规日志走stderr,最终结果走stdout。脚本执行成功后,只在stdout输出一行JSON,包含下载状态、文件路径、文件大小、视频标题、时长。Agent只需要解析这一行JSON,就能拿到所有需要的信息。这个设计我强烈建议你照抄,它能把Agent判断任务结果的准确率提升一大截。
还有一个细节是退出码。脚本通过sys.exit()返回0表示成功,1表示网络错误或参数错误,2表示目标URL不支持或文件无法访问。Agent拿到退出码后,不需要再费劲去猜“这堆日志里到底有没有Error单词”,直接根据退出码走对应的处理分支。
4. 实操回放:用Skill Creator把yt-dlp仓库做成Skill
4.1 环境准备与目录规划
在开始动手之前,先把环境准备好。我的环境是Ubuntu 22.04 + Python 3.10,下面是完整准备清单:
# 1. 安装yt-dlp,推荐用pip而不是直接下载二进制,方便后续升级 pip install yt-dlp # 2. 克隆Skill Creator项目,如果你在本地已经有了仓库,可以直接指定本地路径 git clone https://github.com/{你的目标}/Skill-Creator.git cd Skill-Creator # 3. 安装Skill Creator的依赖,不同项目略有差异,这里以pip install -r requirements.txt为例 pip install -r requirements.txt整个过程如果发现网络不稳定、克隆失败,不要反复重试同一个命令,换个时间或者直接用GitHub官方发布的zip包解压也一样用,重点是别把时间耗在拉代码上。
接下来是目标目录规划。我希望最终产出的Skill目录长这样:
yt-dlp-downloader/ ├── SKILL.md ├── scripts/ │ ├── download_video.py │ └── help.txt └── assets/ └── example_output.jsonSKILL.md是Agent先读的说明书,scripts/download_video.py是核心执行脚本,help.txt是脚本的详细参数说明,assets目录放一个示例输出,让Agent知道成功后的JSON长什么样。这种结构是Skills的通用约定,Claude Code、Codex、OpenCode基本都认。
4.2 生成SKILL.md骨架并人工校准
我先把yt-dlp仓库的本地路径作为输入喂给Skill Creator。它会自动扫描,输出一份初始SKILL.md。生成的草稿质量其实还行,已经把主要能力列出来了,但存在我前面说的问题:描述太宽泛、参数列表太长、没有能力边界。所以生成之后,我立刻开始人工校准。
我最终写定的SKILL.md关键部分如下:
--- name: yt-dlp-downloader description: 用于下载网络视频、提取音频、抓取字幕。当你需要把某个网页视频保存到本地、提取视频里的音频、下载视频字幕时,使用此Skill。输入通常是一个URL。不支持需要付费订阅才能观看的内容。 ---这个description写得非常收敛,只写了三个核心场景,并且明确写了“不支持需要付费订阅的内容”。这是为了从源头减少Agent误用的概率。
然后我在SKILL.md里放了一个简短的使用示例:
## 使用示例 用户输入:“帮我把这个视频下载下来,只要音频” Agent调用: python scripts/download_video.py --url "https://example.com/watch?v=123" --audio-only true --output-dir "/home/user/Videos" 返回结果:如 {"status": "success", "file_path": "/home/user/Videos/视频标题.mp3", "duration": 372}这里的关键是:示例里的URL、参数、返回JSON三者必须完全对得上。Agent非常依赖示例来学习调用方式,示例不准确等于教它犯错。我见过有人SKILL.md里示例用的参数名和实脚本不一致,导致的后果就是Agent每次调用都报“unrecognized arguments”,这种低级错误排查起来非常浪费时间。
4.3 核心脚本封装:Python API还是subprocess
脚本层我推荐用yt-dlp的Python API,而不是用subprocess去调用命令行。Python API能直接拿到进度回调和结果对象,也方便我们控制stdout输出。下面是我的核心脚本主体:
#!/usr/bin/env python3 """yt-dlp Skill 封装脚本:只暴露白名单参数,输出结构化结果。""" import argparse import json import sys try: import yt_dlp except ImportError: print(json.dumps({"status": "error", "message": "yt_dlp未安装,请先执行 pip install yt-dlp"})) sys.exit(2) def parse_args(): parser = argparse.ArgumentParser(description="yt-dlp skill wrapper") parser.add_argument("--url", required=True, help="目标视频URL") parser.add_argument("--format", default="best[height<=720]", help="格式选择,默认720p以内") parser.add_argument("--output-dir", default="./downloads", help="保存目录") parser.add_argument("--audio-only", action="store_true", help="仅提取音频") parser.add_argument("--audio-format", default="mp3", help="音频格式") parser.add_argument("--write-subs", action="store_true", help="写入字幕") parser.add_argument("--sub-langs", default="zh", help="字幕语言") parser.add_argument("--retries", type=int, default=5, help="网络重试次数") parser.add_argument("--limit-rate", default=None, help="下载限速,如 1M") args = parser.parse_args() return args def main(): args = parse_args() ydl_opts = { "format": args.format, "outtmpl": f"{args.output_dir}/%(title)s.%(ext)s", "retries": args.retries, "noplaylist": True, } if args.audio_only: ydl_opts.update({ "extractaudio": True, "format": "bestaudio/best", "postprocessors": [{ "key": "FFmpegExtractAudio", "preferredcodec": args.audio_format, }], }) if args.write_subs: ydl_opts.update({ "writesubtitles": True, "subtitleslangs": [args.sub_langs], }) if args.limit_rate: ydl_opts["ratelimit"] = args.limit_rate try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(args.url, download=True) result = { "status": "success", "title": info.get("title"), "file_path": ydl.prepare_filename(info), "duration": info.get("duration"), } # 如果音频提取有postprocessor,需要手动修正扩展名,这里简化为替换 if args.audio_only: result["file_path"] = result["file_path"].rsplit(".", 1)[0] + "." + args.audio_format print(json.dumps(result, ensure_ascii=False)) except Exception as e: print(json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)) sys.exit(1) if __name__ == "__main__": main()这个脚本我拆开讲几个设计点。第一,noplaylist: True默认关闭播放列表,防止Agent拿到一个合集链接时一次性下载几十个视频,把磁盘塞爆。第二,音频提取依赖FFmpeg后处理,所以环境里必须装ffmpeg,这个是可预见的坑,必须在帮助文档里写明。第三,prepare_filename返回的是模板展开后的文件名,但经过FFmpeg后处理扩展名会变,所以我手动做了修正,不然Agent拿到的路径根本不存在。
4.4 挂载到Agent环境并验证
Skill做好之后,还需要挂载到Agent的Skills目录。不同工具有各自的约定目录,以我常用的几个工具为例:
| Agent工具 | Skills默认目录 | 备注 |
|---|---|---|
| Claude Code | ~/.claude/skills | 直接放目录即可 |
| Codex | ~/.codex/skills | 社区实现,以实际版本为准 |
| OpenCode | ~/.opencode/skills | 可配置 |
# 把Skill目录复制到Claude Code的Skills目录 cp -r yt-dlp-downloader ~/.claude/skills/挂载完成后,我习惯用一个裸URL做冒烟测试,不传任何额外参数,看Agent是否会自动命中Skill。比如直接输入“帮我下载这个视频:https://example.com/watch?v=测试”,然后观察Agent的行为。如果它在没有提示的情况下调用了download_video.py,说明Skill的description触发逻辑是正常的。
接下来再测音频提取和字幕抓取,每个场景跑一遍。验证的要点是看输出JSON是否正常解析、文件是否真的存在。我还会故意传入一个不存在的URL,看Agent能不能根据错误JSON做出合理反馈,而不是报一个“解析失败”就结束。
5. 常见问题与排查实录
5.1 问题速查表
把yt-dlp封装成Skills的过程中,我踩了不少坑,这里整理成一份问题速查表,大家可以直接对着排查:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| Agent完全没调用Skill,而是尝试自己写代码 | SKILL.md里的description触发词不准确 | 把description改成含“下载视频、提取音频、抓字幕”等明确动作词的表述 |
脚本报unrecognized arguments | SKILL.md示例与脚本参数名不一致 | 逐项核对所有--开头的参数,保证文档、示例、代码三处完全统一 |
下载报Unsupported URL | yt-dlp版本过旧,不认识新网站或新格式 | pip install -U yt-dlp升级到最新版本 |
| 音频提取失败 | 缺少FFmpeg | 安装FFmpeg,并在help.txt里注明依赖 |
| 下载到一半中断 | 网络不稳定或源站限制 | 脚本默认retries=5,可加--fragment-retries处理分段下载 |
| Agent说“下载成功”但文件不在预期位置 | 输出路径是相对路径,Agent的工作目录和预期不符 | 调用时明确传绝对--output-dir,脚本内部也改为os.makedirs自动创建 |
| 批量链接导致一次性下载大量文件 | 没关播放列表功能 | 脚本默认noplaylist: True,必要时用--playlist-items显式控制 |
这里面“下载成功但文件找不到”是我遇到过最隐蔽的问题。Agent的执行目录可能因为会话上下文、工作区切换等因素发生变化,而脚本如果默认相对路径,就会把文件悄悄下载到其他地方。解决的方案很简单:脚本里统一用os.path.abspath解析输出目录,并在返回JSON前确认文件存在,不存在就返回错误,而不是盲目报成功。
5.2 关于cookies和授权边界的提醒
yt-dlp确实支持加载浏览器cookies,这在下载一些需要登录才能访问的公开内容时很有用。但在Skill设计上,我非常不建议默认暴露这个能力。原因有两个:一是cookies涉及个人账号凭据,如果Agent调用脚本时不小心把cookies写进了日志或者传给其他工具,有泄露风险;二是有些网站的内容虽然有登录权限,但并没有授权你下载分发,Skill如果默认帮忙,容易把你推向版权争议。
我最终的取舍是:脚本不接收cookies参数,但在help.txt里预留了一段说明,告诉使用者如果需要下载需要登录的合法内容,可以手动在脚本配置里指定--cookies-from-browser,但必须确认自己有权限这么做。这个设计不能让Skill变得万能,但能让它在绝大多数场景下安全地使用。
5.3 渐进式披露:SKILL.md不要写成字典
还有一个技巧我非常想分享:不要在SKILL.md里把脚本所有参数都展开描述。SKILL.md只需要写清楚触发条件、核心用法、一两个示例,以及一句话的注意事项。详细的参数说明单独放在scripts/help.txt里。Agent读完SKILL.md,只需要知道“用这个脚本能下载视频,具体参数怎么传,自己去看help.txt”,这就够了。
这个模式叫渐进式披露。好处很明显:SKILL.md简单,模型就能更精准地判断触发时机;而真正需要具体参数时,Agent可以主动读取help.txt,信息完整性和决策准确性两头都占。我之前把SKILL.md写成了一篇中篇教程,结果Agent反而变得畏手畏脚,经常为了找参数把自己绕晕。
写在最后的个人体会
把GitHub仓库转换成Skill这件事,我前后做了差不多四五个项目,从最早的图片处理工具到这次的yt-dlp,最大的感受是:转换工具本身只是脚手架,真正的难点在于你对这个工具的边界理解有多深,以及你愿不愿意花时间做约束设计。Skill Creator这类工具能帮你把90%的机械劳动省掉,但剩下那10%——裁剪能力、统一输出、设定安全边界——恰恰决定了这个Skill是好用还是鸡肋。
最后再分享一个小技巧:测试Skill时,不要一上来就下载大文件,先用小视频把链路跑通。我习惯找一些几十秒的公开测试视频,跑通下载、验证JSON、确认文件存在,再逐步增加音频提取和字幕场景。链路通了之后再考虑限速、批量这些高级功能。一次性把所有场景都铺开测试,出了问题很难定位到底哪个环节不对。先跑最小可用闭环,再迭代扩展,这个思路不只对Skill开发有效,做任何工具封装都应该这样来。