1. 从“skills”这个标题说起:它到底指什么
第一次看到“skills”这个标题,很多人会以为是某个泛泛而谈的能力清单,或者一份简历上的技能罗列。但结合热搜词里反复出现的 Agent Skills、Google Cloud、npx、AI agents、claude agent skills、codex skills 这些词,基本可以确定,这里说的 skills 不是人类的能力项,而是给 AI Agent 使用的一套可插拔能力包。你可以把它理解成给一个刚入职的智能体发的“岗位操作手册加工具箱”:它本来只会聊天,装上 skills 之后,就能按固定流程去查资料、跑命令、调接口、生成文件、做测试。
我最早接触这个概念,是在折腾一个自动化脚本的时候。当时想让 AI 帮我处理一批前端项目的构建检查,结果发现它每次都要我重新解释一遍“先装依赖、再跑 lint、再跑 build、失败要看哪几个日志”。后来有人告诉我,可以把这套流程写成一个 skill,让 Agent 自己按步骤执行。那一刻确实有种“打开新世界”的感觉,因为这不是让 AI 更聪明,而是让 AI 更可控、可复用、可交接。
这篇文章适合三类人看:第一类是想把日常重复操作交给 AI 的开发者;第二类是在团队里做 AI Agent 落地、需要统一流程的人;第三类是对 skills 开发、安装、调试感兴趣,但被 npx、安装失败、市场来源这些问题卡住的新手。我会从设计思路、核心细节、实操过程、常见问题四个方向展开,尽量把每个“为什么”讲清楚,让你看完能自己动手做一个能跑的 skill。
2. 内容整体设计与思路拆解
2.1 为什么 skills 会成为一种独立形态
要理解 skills 的价值,先看一个现实问题:大模型本身是一个“通才”,但它没有稳定的操作记忆。你这次告诉它用 A 方法,下次换个对话窗口,它可能就用 B 方法。对于个人玩玩无所谓,但对于团队协作、生产环境、重复任务,这种不确定性是致命的。
skills 的出现,本质上是把“提示词”升级成了“能力模块”。提示词是一段话,skills 是一个带结构、带依赖、带执行边界的包。它通常包含几个部分:触发条件、执行步骤、依赖工具、输入输出约定、失败处理方式。这样 Agent 在遇到对应场景时,不是靠临场发挥,而是按预设路径走。
我自己的判断是,skills 解决的不是“AI 会不会”的问题,而是“AI 稳不稳”的问题。一个 skill 写得好,换一个模型、换一个会话、换一个人来用,结果应该大差不差。这才是它值得被单独拿出来讨论的原因。
2.2 方案选型:为什么很多 skills 围绕 npx 和命令行展开
热搜词里 npx 出现频率很高,这不是偶然。npx 是 Node.js 生态里执行包命令的工具,它最大的好处是“不用全局安装也能跑”。对于 skills 来说,这意味着你可以把能力包发布成一个 npm 包,用户通过 npx 直接调用,不需要手动配环境变量、不需要改全局路径。
从设计角度看,这种选型有几个明显优势。第一,分发简单,一个包名加版本号就能定位。第二,依赖隔离,不同 skill 可以用不同版本的依赖,不会互相打架。第三,跨平台相对友好,Windows、macOS、Linux 都能跑,只要 Node 环境在。第四,和现有前端、Node 工具链无缝衔接,前端开发者几乎零学习成本。
当然,它也有代价。npx 第一次执行要下载包,网络不好时会卡;某些系统权限设置严格,缓存目录写不进去;如果 skill 依赖浏览器内核,还会遇到 playwright install 失败这类问题。这些后面会专门讲。
2.3 一个合格 skill 的边界应该怎么划
我见过不少人写 skill,恨不得把整个项目流程都塞进去,结果就是又长又脆,一处失败全盘卡住。我的经验是,一个 skill 只做一件事,而且这件事的输入输出要非常明确。
比如“检查前端项目构建”可以是一个 skill,“生成分镜脚本”可以是另一个 skill,“自动挖洞测试”如果指的是安全测试里的漏洞扫描,那又是另一个独立 skill。不要把“安装依赖、跑测试、发通知、写报告”全揉在一起。拆开之后,每个 skill 可以单独测试、单独替换、单独复用。
边界清晰的另一个好处是排查问题快。如果构建失败,我只需要看构建 skill 的日志;如果是通知没发出去,那和构建逻辑无关。这种模块化思维,和写函数是一样的。
2.4 面向 AI Agent 的 skills 和传统脚本有什么区别
传统脚本是“人写给人看,机器执行”。skills 是“人写给 Agent 看,Agent 理解后执行”。这个区别很关键。
传统脚本里,你可以写if [ -f package.json ]; then npm install; fi,机器严格按字符执行。skills 里,你更多是描述意图和约束,比如“如果项目根目录存在 package.json,则安装依赖;如果安装失败,记录错误并停止后续步骤”。Agent 会结合上下文判断,但这也意味着它可能“自由发挥”。
所以写 skill 时,我通常会加两类约束:一类是硬性命令,能写死就写死,减少歧义;另一类是判断规则,告诉 Agent 在什么条件下走什么分支。两者结合,既保留灵活性,又不至于失控。
3. 核心细节解析与实操要点
3.1 skill 的目录结构和关键文件
一个典型的 skill 包,目录结构通常不会太复杂。下面是我常用的一个模板,基于常见实践整理,不是唯一标准,但足够跑通大多数场景。
my-skill/ ├── package.json ├── skill.md ├── scripts/ │ ├── main.js │ └── helpers.js ├── config/ │ └── default.json └── README.mdpackage.json负责声明包名、版本、入口、依赖和 bin 命令。skill.md是给 Agent 看的说明文件,里面写清楚这个 skill 做什么、什么时候触发、需要什么输入、输出什么结果。scripts/放实际执行逻辑。config/放默认参数,比如超时时间、重试次数、日志级别。README.md给人看,说明安装和使用方式。
这里有个细节很多人忽略:skill.md不是普通文档,它是 Agent 的“操作契约”。我一般会把它写成半结构化格式,包含“适用场景”“前置条件”“执行步骤”“成功判定”“失败处理”几个固定段落。这样 Agent 解析起来更稳定,人看起来也清楚。
3.2 触发条件怎么写才不容易误触发
触发条件是 skill 的第一道门。写得太宽,Agent 会在无关场景乱用;写得太窄,该用的时候又用不上。
我的做法是同时用“关键词”和“上下文”两个维度约束。比如一个前端构建检查 skill,触发条件可以写成:当用户提到“构建失败”“打包报错”“lint 不通过”,并且当前工作目录存在package.json时触发。这样既有关键词匹配,又有环境判断。
另外,我建议在触发条件里明确写“不适用场景”。比如“本 skill 不负责修复代码,只负责定位和报告问题”。这能防止 Agent 越界去改代码,避免引入新问题。
3.3 依赖管理:为什么版本要锁死
skills 依赖外部工具时,版本浮动是灾难。你今天用某个包的最新版跑通了,明天它发了个 breaking change,整个 skill 就挂了。
所以我在package.json里坚持用精确版本,不用^或~。比如"playwright": "1.42.0",而不是"playwright": "^1.42.0"。同时提交package-lock.json或pnpm-lock.yaml,确保任何人安装出来的依赖树一致。
如果 skill 依赖系统级工具,比如某个命令行程序,我会在skill.md里写明最低版本要求,并在启动脚本里做版本检查。版本不满足就直接报错退出,而不是硬跑然后产生一堆莫名其妙的错误。
3.4 输入输出约定:让 skill 可组合
一个 skill 的输出,往往是另一个 skill 的输入。所以输入输出格式必须稳定。
我通常用 JSON 作为交换格式,字段名用英文小写加下划线。比如构建检查 skill 的输出可以是:
{ "status": "failed", "stage": "build", "error_count": 3, "errors": [ {"file": "src/main.js", "line": 12, "message": "Unexpected token"} ], "log_path": "./logs/build-20240101.log" }这样下一个 skill 拿到status就知道要不要继续,拿到errors就知道怎么展示,拿到log_path就能去读详细日志。字段含义在skill.md里写死,不允许随意改名。
注意:输入输出约定一旦发布,就不要轻易改字段名。如果必须改,加新字段,旧字段保留至少一个版本,给调用方迁移时间。
3.5 日志和可观测性:出问题时你能看到什么
skill 跑在 Agent 里,很多时候是自动执行的,人不在旁边。所以日志必须写清楚。
我的习惯是分三级:info记录关键步骤开始和结束,warn记录可恢复的异常,error记录导致失败的问题。每条日志带时间戳、skill 名称、步骤编号。日志同时输出到控制台和文件,文件按日期切分。
另外,我会在 skill 结束时输出一个“执行摘要”,包含总耗时、成功步骤数、失败步骤数、关键产物路径。这样即使不看详细日志,也能快速判断这次执行是否正常。
4. 实操过程与核心环节实现
4.1 环境准备:Node 和 npx 的最小可用配置
开始之前,确认本机有 Node.js 和 npm。打开终端执行:
node -v npm -v npx -v如果npx -v报错,说明 npm 版本太老,升级一下:
npm install -g npm@latestNode 版本建议用 LTS,比如 18 或 20。太新的版本有时和某些依赖不兼容,太老的版本缺少现代语法支持。我一般用 nvm 管理 Node 版本,切换方便。
nvm install 20 nvm use 20Windows 用户如果不用 nvm,可以直接去 Node 官网下载 LTS 安装包。安装时勾选“Add to PATH”,省得手动配环境变量。
4.2 初始化一个 skill 项目
新建目录,初始化 package.json:
mkdir my-first-skill cd my-first-skill npm init -y然后修改package.json,加上 bin 字段和依赖:
{ "name": "my-first-skill", "version": "1.0.0", "description": "A demo skill for build checking", "bin": { "my-first-skill": "./scripts/main.js" }, "dependencies": { "execa": "8.0.1" } }bin字段告诉 npx 执行哪个文件。execa是一个比原生child_process更好用的命令执行库,能方便地捕获输出和错误。
4.3 编写主执行脚本
创建scripts/main.js,写入以下内容:
#!/usr/bin/env node const { execa } = require('execa'); const fs = require('fs'); const path = require('path'); async function main() { const cwd = process.cwd(); const pkgPath = path.join(cwd, 'package.json'); if (!fs.existsSync(pkgPath)) { console.error(JSON.stringify({ status: 'failed', stage: 'precheck', message: 'package.json not found' })); process.exit(1); } const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const scripts = pkg.scripts || {}; const steps = ['lint', 'build'].filter(s => scripts[s]); for (const step of steps) { console.log(`[info] running ${step}...`); try { await execa('npm', ['run', step], { cwd, stdio: 'inherit' }); console.log(`[info] ${step} passed`); } catch (err) { console.error(JSON.stringify({ status: 'failed', stage: step, message: err.message })); process.exit(1); } } console.log(JSON.stringify({ status: 'success', steps_run: steps })); } main();这个脚本做了几件事:检查package.json是否存在,读取 scripts,按顺序跑 lint 和 build,任何一步失败就输出 JSON 错误并退出。成功时输出 JSON 摘要。
给脚本加执行权限:
chmod +x scripts/main.js4.4 编写 skill.md 说明文件
skill.md是给 Agent 看的,内容要结构化。我一般这样写:
# Build Check Skill ## 适用场景 当用户提到构建失败、打包报错、lint 不通过,且当前目录存在 package.json 时使用。 ## 前置条件 - Node.js 18+ - 项目已安装依赖 ## 执行步骤 1. 检查 package.json 是否存在 2. 读取 scripts 字段 3. 依次执行 lint 和 build 4. 输出 JSON 结果 ## 成功判定 输出 JSON 中 status 为 success。 ## 失败处理 输出 JSON 中 status 为 failed,包含 stage 和 message。不要尝试自动修复代码。这个文件不需要很长,但每个部分都要有。Agent 读完之后,就知道什么时候用、怎么用、用完看什么。
4.5 本地测试:用 npx 直接跑
在 skill 项目目录下执行:
npx .或者在另一个有package.json的项目里,用本地路径调用:
npx /path/to/my-first-skill如果一切正常,你会看到 lint 和 build 的输出,最后是一段 JSON 摘要。如果失败,会看到错误 JSON。
我第一次跑的时候忘了加chmod +x,结果报权限错误。后来养成习惯,创建脚本后第一件事就是加执行权限。
4.6 发布到包仓库供他人使用
本地跑通之后,可以发布到 npm 仓库。先登录:
npm login然后发布:
npm publish发布成功后,其他人就可以用npx my-first-skill直接调用。如果包名被占用,改一个带作用域的名字,比如@yourname/my-first-skill。
注意:发布前把版本号改好,npm 不允许重复发布同一版本。每次改动后手动升版本,或者用
npm version patch自动升。
4.7 在 Agent 中注册和调用 skill
不同 Agent 平台的注册方式不一样,但核心逻辑类似:告诉 Agent 这个 skill 的名称、调用命令、说明文件位置。常见做法是在 Agent 的配置文件里加一段:
{ "skills": [ { "name": "build-check", "command": "npx my-first-skill", "description_path": "./skills/build-check/skill.md" } ] }Agent 在遇到匹配场景时,会读取skill.md,然后执行command。执行结果按 JSON 解析,决定下一步动作。
这里的关键是description_path要指向正确的文件。我踩过一次坑,路径写成了相对路径,但 Agent 的工作目录和我想的不一样,结果读不到说明文件。后来改成绝对路径,问题解决。
5. 常见问题与排查技巧实录
5.1 npx 执行失败:从网络到缓存的排查顺序
npx 失败是最常见的问题,原因通常分几类。我一般按以下顺序排查:
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 卡在下载 | 网络慢或源不可达 | 换网络重试 | 配置国内镜像源 |
| 报 404 | 包名写错或未发布 | npm view 包名 | 核对包名和版本 |
| 权限错误 | 缓存目录不可写 | 查看错误路径 | 修改缓存目录权限 |
| 版本冲突 | 全局和本地版本不一致 | npx 包名 -v | 指定精确版本 |
配置镜像源可以加快下载:
npm config set registry https://registry.npmmirror.com这个命令把默认源换成国内镜像,下载速度会明显提升。如果公司有私有源,换成公司地址。
5.2 playwright install 失败:依赖缺失和权限问题
如果 skill 依赖 playwright 做浏览器自动化,安装时经常遇到npx playwright install失败。常见原因有三个。
第一,系统缺少浏览器运行库。Linux 上需要装一些系统包,比如libnss3、libatk1.0-0等。可以用 playwright 自带的命令安装:
npx playwright install-deps第二,下载浏览器内核时网络中断。可以设置下载源,或者手动下载后放到缓存目录。
第三,权限不足,缓存目录写不进去。检查~/.cache/ms-playwright目录权限,必要时改所有者。
我遇到最多的是第一种,尤其是在干净的容器环境里。跑一次install-deps基本能解决。
5.3 skill 不触发:触发条件太窄或说明文件没被读到
有时候 skill 写好了,但 Agent 就是不用。先检查两件事:触发条件是不是太窄,说明文件是不是真的被读到了。
触发条件太窄的典型表现是,用户换个说法就不匹配了。比如只写了“构建失败”,用户说“打包挂了”就不触发。解决办法是增加同义词和模糊匹配。
说明文件没被读到,通常是路径问题。可以在 Agent 日志里搜索 skill 名称,看它有没有尝试加载。如果没有,检查配置里的路径是否正确,文件是否有读取权限。
5.4 执行结果不稳定:环境差异和状态残留
同一个 skill,在 A 机器上跑通,在 B 机器上失败,多半是环境差异。常见差异包括 Node 版本、系统命令版本、环境变量、工作目录。
我的做法是在 skill 启动时打印环境摘要,包括 Node 版本、操作系统、当前目录、关键环境变量。这样出问题时一眼就能看出差异。
状态残留是另一个坑。比如上一次执行留下的临时文件,影响了这一次。解决办法是在 skill 开始时清理工作目录,或者每次用独立的临时目录。
5.5 常见问题速查表
| 问题 | 快速检查 | 常用解决 |
|---|---|---|
| npx 找不到包 | 包名和版本 | 核对后重试 |
| 脚本无执行权限 | ls -l scripts/ | chmod +x |
| JSON 解析失败 | 输出是否纯 JSON | 去掉多余日志 |
| Agent 不调用 | 触发条件和路径 | 放宽条件、改绝对路径 |
| 依赖版本冲突 | lock 文件 | 锁死版本、重装 |
| 超时 | 步骤耗时 | 增加超时、拆分 skill |
5.6 几个我踩过的坑和对应技巧
第一个坑是日志混进 JSON 输出。我在脚本里用console.log打日志,结果 Agent 解析输出时把日志也当 JSON 解析,直接报错。后来改成日志走stderr,JSON 走stdout,问题解决。
第二个坑是 skill 之间互相依赖,但没有声明。A skill 的输出格式改了,B skill 没跟着改,结果 B 解析失败。后来我在skill.md里明确写“依赖 A skill 的输出格式版本 v1”,改格式时同步升版本。
第三个坑是忽略退出码。脚本执行失败但退出码是 0,Agent 以为成功了。后来我在每个关键步骤后检查退出码,非零就立即输出错误 JSON 并退出。
第四个坑是路径里有空格。Windows 上项目路径经常带空格,命令拼接时没加引号,直接失败。解决办法是用execa这种参数化执行方式,不要手动拼字符串。
6. 进阶方向:让 skills 真正融入日常工作流
6.1 把重复操作沉淀成 skill 的判断标准
不是所有操作都值得写成 skill。我的判断标准有三条:第一,这个操作每周至少重复三次;第二,操作步骤固定,不需要太多临场判断;第三,失败后的处理方式明确。
满足这三条,就值得写。比如每日构建检查、代码格式校验、生成固定格式的报告、批量处理图片尺寸。不满足的,比如一次性的数据迁移、需要大量人工判断的代码重构,就不适合。
6.2 skill 的版本管理和团队协作
团队里用 skill,版本管理很重要。我建议每个 skill 独立仓库,独立版本号,独立发布。团队内部可以搭一个私有包仓库,或者直接用 Git 仓库加 tag。
协作时,skill.md要写清楚维护者、更新日志、兼容性说明。谁改了触发条件,谁改了输出格式,都要记录。这样别人升级时知道会不会影响现有流程。
6.3 从单个 skill 到 skill 组合
单个 skill 解决单点问题,多个 skill 组合起来能解决复杂流程。比如“构建检查”加“测试报告生成”加“通知发送”,三个 skill 串起来,就是一个完整的 CI 辅助流程。
组合的关键是输入输出格式统一。我一般定义一个团队内部的“skill 交换格式”,所有 skill 都遵守。这样任意两个 skill 都能对接,不用为每个组合单独写适配。
6.4 安全边界:skill 能做什么,不能做什么
skill 跑在 Agent 里,权限控制很重要。我的原则是:skill 只做被明确授权的事,不碰敏感数据,不执行破坏性命令,不访问未声明的外部服务。
具体做法包括:在skill.md里写明权限范围;脚本里对危险操作加确认步骤;输出里不包含密钥、令牌、个人隐私信息;定期审查 skill 的依赖,移除不再维护的包。
注意:如果一个 skill 需要访问外部服务,务必在说明文件里写清楚访问目的和数据范围,方便审查和审计。
6.5 我个人的使用体会
用了大半年 skills 之后,最大的感受是:它把“我知道怎么做”变成了“系统知道怎么做”。以前很多操作只存在我脑子里,换个人就得重新讲一遍。现在写成 skill,新人直接调用,结果一致,我也省心。
另一个体会是,写 skill 的过程本身就是梳理流程的过程。很多步骤我以前是凭感觉做的,写的时候被迫想清楚每一步的条件和结果,反而发现了不少可以优化的地方。
最后分享一个小技巧:刚开始写 skill,不要追求大而全。先写一个最小的、能跑通的版本,用起来,再根据实际遇到的问题逐步加功能。我第一个 skill 只有二十行代码,现在迭代了十几个版本,反而比一开始就设计得很复杂的那些更稳定。