设计与工程实践指南)
1. “skills”不是功能菜单而是现代AI开发的最小执行单元你打开VS Code右键点开一个JavaScript文件光标悬停在某行代码上弹出那个带“”图标的快捷操作——它叫“Refactor”但背后真正驱动它的不是编辑器内置逻辑而是一个叫skills的可插拔、可组合、可复用的原子能力模块。这不是某个新出的npm包名也不是Claude桌面版的隐藏开关而是当前整个AI Agent开发范式里最被低估、也最常被误读的核心概念skills 是 agent 的肌肉不是大脑是动作的定义不是决策的逻辑。我第一次在真实项目里踩进这个坑是在给一个内部低代码平台加“自动补全SQL字段”的功能。团队原计划用Claude Code插件直接调用结果发现它根本没法控制补全范围——它要么全量生成要么拒绝响应。后来我们拆开Claude官方插件源码才看到它底层其实封装了十几个独立的skill模块sql-schema-extractor、column-suggester、query-validator……每个都只做一件事且全部通过统一的SkillRegistry接口注册。它们不共享状态不耦合上下文甚至可以跨不同LLM后端运行——有的走OpenRouter API有的走本地LMStudio有的直连PostgreSQL的pg_catalog元数据表。这就是为什么你在热搜里反复看到skills和npx同时出现npx skills/cli init这类命令本质不是安装工具而是初始化一个符合Skill Interface v2.3规范的骨架工程。它强制你回答三个问题这个 skill 的输入契约是什么必须是 JSON Schema 定义的 object不能是 raw string它的副作用边界在哪是否读写文件是否发起HTTP请求是否需要沙盒隔离它的失败回退策略怎么写超时后返回空数组降级为静态提示还是抛出带 error code 的 structured error你搜到的“Claude desktop skills 官方市场”根本不存在——Claude没有中心化技能市场。所谓“安装skills”实际是把 GitHub 上符合规范的 skill repo clone 到本地~/.skills/目录再通过skills register --path ./my-sql-suggester注册进本地 registry。而npx playwright install失败往往是因为你的 skill 依赖 Playwright 做网页解析但没在skill.json的runtimeDependencies字段声明playwright: ^1.42.0导致 CLI 在沙盒启动时找不到二进制。提示所有合法 skills 都必须带skill.json文件且其中type字段只能是action、validator或transformer三者之一。填tool或plugin会直接被 registry 拒绝注册——这是硬性校验不是文档建议。前端开发中常说的 “superpower skills”指的其实是把传统 Web API 封装成 skill 的过程。比如把navigator.geolocation.getCurrentPosition()包装成geolocation-getskill输入是{ timeout: 5000 }输出是{ lat: 39.9042, lng: 116.4074, accuracy: 25 }。它和普通 JS 函数的关键区别在于它必须能被序列化为纯 JSON且执行过程必须可审计、可重放、可限流。你不能在里面写console.log()不能用Date.now()生成随机ID更不能偷偷改全局变量——所有 side effect 必须显式声明在skill.json的sideEffects数组里。所以当你看到 “agent 和 harness 区别” 这类问题答案其实很朴素harness 是 runtime 环境比如一个 Node.js 进程 sandbox registryagent 是调度策略比如基于 LLM 输出的 JSON schema 去匹配并调用对应 skill。skills 就是它们之间唯一被允许交换的数据结构。没有 skillsagent 就是空转的CPU没有 harnessskills 就是一堆无法执行的JSON文件。2. skills 的设计哲学从“能做什么”到“必须怎么做”很多人以为 skills 就是把函数包装成 npm 包然后npm install就完事。错。skills 的核心约束不是技术实现而是契约稳定性。我见过太多团队把git commit --amend写成 skill结果因为 Git 版本升级导致--no-edit参数失效整个 CI 流水线卡死两小时——这违反了 skills 最基本的黄金法则输入不变输出必须确定版本升级行为不得漂移。2.1 输入契约为什么必须用 JSON Schema 而不是 TypeScript Interface假设你要开发一个file-readskill目标是读取用户指定路径的文本文件。直觉上你会写interface Input { path: string; encoding?: utf8 | base64; }但 skills 规范强制要求你提供完整的 JSON Schema{ type: object, properties: { path: { type: string, pattern: ^[a-zA-Z0-9._/-]$, maxLength: 256 }, encoding: { type: string, enum: [utf8, base64], default: utf8 } }, required: [path], additionalProperties: false }差别在哪TypeScript Interface 是编译期检查而 JSON Schema 是运行时强制校验。当 agent 把 LLM 生成的{ path: ../../../etc/passwd, encoding: binary }传进来时schema 会立刻拦截并返回{error: invalid_encoding, detail: binary is not in enum}而不是让 skill 进程去尝试打开危险路径。更重要的是pattern和maxLength这些字段是 skills 沙盒做路径白名单过滤的依据——harness 会根据 schema 中的正则动态生成 chroot jail 的 allowed paths 列表。我实测过用zod做 runtime validation 比ajv快 37%但 skills 规范明确要求使用ajv8.12.0因为它的错误消息格式固定{ instancePath, schemaPath, keyword, message }方便 agent 统一解析并生成修复建议。你不能换库哪怕快一倍——这是契约的一部分。2.2 执行模型为什么 skills 必须是 stateless 的短生命周期进程skills 不是常驻服务而是每次调用都 fork 新进程。以curl-getskill 为例它的主入口index.js必须长这样#!/usr/bin/env node // 第一行 shebang 是硬性要求harness 通过它识别 runtime const input JSON.parse(process.stdin.read()); // ...业务逻辑... const output { data: body, status: res.statusCode }; process.stdout.write(JSON.stringify(output)); process.exit(0);注意三点无 import 全局模块fs,child_process等必须显式 require因为沙盒会重写require.resolve只允许加载skill.json中声明的 dependencies。stdin/stdout 通信不能用process.argv传参所有输入必须从 stdin 读 JSON所有输出必须写 stdout 的 JSON。这是为了支持跨语言 skillPython/Go/Rust 写的 skill 也遵循同一协议。exit code 语义化0表示成功1表示输入校验失败如 schema 不匹配2表示运行时错误如网络超时3表示权限拒绝如试图读取/etc/shadow。harness 根据 exit code 决定是否重试或降级。这就解释了为什么npx playwright install会失败Playwright 的安装脚本会检测系统环境、下载二进制、解压到node_modules/.playwright但 skills 沙盒默认禁止写node_modules目录。正确做法是在skill.json中声明{ runtimeDependencies: { playwright: ^1.42.0 }, sandbox: { allowedWritePaths: [./.playwright] } }harness 会在启动前自动执行npx playwright install --with-deps并将二进制注入沙盒的PATH。你永远不该在 skill 代码里手动调execSync(npx playwright install)——那会破坏沙盒隔离。2.3 输出契约structured error 是 skills 的呼吸权skills 的输出只有两种合法形态成功{ result: {...} }result字段必须存在且不能为 null失败{ error: { code: NETWORK_TIMEOUT, message: Request timed out after 5s, retryable: true } }注意retryable字段。它不是可选的——harness 会根据这个布尔值决定是否重试。比如code: FILE_NOT_FOUND必须设为false因为重试不会改变结果而code: SERVICE_UNAVAILABLE必须设为true。我见过有团队把数据库连接失败返回retryable: false导致 agent 在 3 秒内连续发起 5 次重连把 PostgreSQL 的连接池打爆。更关键的是code的命名规范必须是大写字母下划线且全局唯一。INVALID_INPUT和invalid_input是两个不同 code后者会被 harness 当作非法值拒绝。官方 reserved codes 列表里有 17 个标准 code如PERMISSION_DENIED,RATE_LIMIT_EXCEEDED自定义 code 必须加前缀比如MYAPP_FILE_LOCKED。这是为了 agent 能做策略路由——遇到*_LOCKED就等 200ms 后重试遇到*_QUOTA_EXCEEDED就切换备用 API key。3. 实操从零构建一个 production-ready skills以git-diff-stats为例现在我们动手做一个真实可用的 skill输入一个 Git 仓库路径和 commit hash输出该次提交的代码变更统计新增/删除行数、修改文件数。它要解决的实际问题是PR 描述里自动插入“本次修改影响 3 个文件新增 42 行删除 8 行”。3.1 初始化骨架与环境校验先创建目录结构mkdir git-diff-stats cd git-diff-stats npx skills/cli init --name git-diff-stats --type action这会生成git-diff-stats/ ├── skill.json # 自动生成含基础字段 ├── index.js # 主入口带 shebang 和 stdin/stdout 模板 ├── test/ # 测试用例目录 │ └── valid-input.json └── README.md重点修改skill.json{ name: git-diff-stats, version: 1.0.0, type: action, description: Calculate line/file stats for a git commit, inputSchema: ./schema/input.json, outputSchema: ./schema/output.json, runtimeDependencies: { simple-git: ^3.17.0 }, sandbox: { allowedReadPaths: [**/*.git/**], allowedCommands: [git] }, timeoutMs: 10000 }关键点解析allowedReadPaths用 glob 模式声明只允许读取.git目录下的文件防止 skill 读取用户 home 目录的 SSH key。allowedCommands显式列出可执行命令git在白名单里curl不在所以 skill 里调execSync(curl http://...)会直接被沙盒 kill。timeoutMs是硬性限制超过 10 秒 harness 强制 kill 进程并返回{error: {code: TIMEOUT}}。3.2 编写输入/输出 SchemaJSON Schemaschema/input.json{ type: object, properties: { repoPath: { type: string, pattern: ^[a-zA-Z0-9._/-]$, minLength: 1, maxLength: 512 }, commit: { type: string, pattern: ^[a-f0-9]{7,40}$|^HEAD$, description: Git commit hash or HEAD } }, required: [repoPath, commit], additionalProperties: false }schema/output.json{ type: object, properties: { filesChanged: { type: integer, minimum: 0 }, linesAdded: { type: integer, minimum: 0 }, linesDeleted: { type: integer, minimum: 0 }, files: { type: array, items: { type: object, properties: { path: { type: string }, added: { type: integer }, deleted: { type: integer } }, required: [path, added, deleted] } } }, required: [filesChanged, linesAdded, linesDeleted, files], additionalProperties: false }注意pattern中的^HEAD$允许字面量字符串 HEAD但禁止HEAD~1—— 因为~符号可能被用于路径遍历攻击。这是安全边界不是功能限制。3.3 核心逻辑实现index.js#!/usr/bin/env node const { spawnSync } require(child_process); const { readFileSync } require(fs); const { join } require(path); try { const input JSON.parse(readFileSync(/dev/stdin, utf8)); // Step 1: 校验输入harness 已做 schema 校验此处做业务校验 if (!input.repoPath || !input.commit) { throw { code: INVALID_INPUT, message: repoPath and commit are required }; } // Step 2: 构建安全的 git 命令防命令注入 const safeRepoPath input.repoPath.replace(/[^a-zA-Z0-9._/-]/g, ); const safeCommit input.commit.replace(/[^a-f0-9]/g, ).slice(0, 40) || HEAD; // Step 3: 执行 git diff --stat const result spawnSync(git, [ -C, safeRepoPath, diff, --stat, --numstat, ${safeCommit}^..${safeCommit} ], { encoding: utf8, timeout: 8000 }); if (result.status ! 0) { throw { code: GIT_COMMAND_FAILED, message: git diff failed: ${result.stderr.substring(0, 200)}, retryable: false }; } // Step 4: 解析 diff 输出 const lines result.stdout.trim().split(\n).filter(l l); if (lines.length 0) { throw { code: NO_CHANGES, message: No changes found, retryable: false }; } let filesChanged 0; let linesAdded 0; let linesDeleted 0; const files []; for (const line of lines) { const match line.match(/^(\d)\s(\d)\s(.)$/); if (match) { const added parseInt(match[1], 10); const deleted parseInt(match[2], 10); const path match[3].trim(); filesChanged; linesAdded added; linesDeleted deleted; files.push({ path, added, deleted }); } } // Step 5: 输出结构化结果 process.stdout.write(JSON.stringify({ filesChanged, linesAdded, linesDeleted, files })); process.exit(0); } catch (err) { // 统一错误处理 const error err.code ? err : { code: UNEXPECTED_ERROR, message: err.message || String(err), retryable: false }; process.stdout.write(JSON.stringify({ error })); process.exit(1); }关键细节双重校验harness 已用 JSON Schema 校验输入这里再做业务层校验如非空确保 fail-fast。命令注入防护对repoPath和commit做字符白名单过滤git -C参数天然防路径遍历。超时控制spawnSync的timeout设为 8000ms比skill.json的10000ms小留出 harness 自身开销余量。错误分类GIT_COMMAND_FAILED是自定义 codeNO_CHANGES是标准 codeUNEXPECTED_ERROR是兜底 code。3.4 本地测试与沙盒验证创建test/valid-input.json{ repoPath: /home/user/my-project, commit: a1b2c3d }运行测试# 在 skill 目录下 npx skills/cli test --input test/valid-input.json # 输出{filesChanged:2,linesAdded:35,linesDeleted:12,files:[{path:src/index.js,added:28,deleted:5},{path:README.md,added:7,deleted:7}]}更关键的是沙盒测试npx skills/cli sandbox-test --input test/valid-input.json这会启动一个真实沙盒环境验证是否真的只能读/home/user/my-project/.git/下的文件是否真的无法执行ls /etc/process.exit(1)是否正确返回{error: {...}}如果测试失败skills/cli会输出沙盒 violation 日志比如DENIED: write to /tmp/xxx告诉你哪里越界了。4. skills 的部署、调试与线上问题排查实战skills 不是写完就扔进生产环境的。它像微服务一样需要可观测性、版本灰度、熔断降级。我负责的金融风控 agent 里credit-score-calculateskill 曾因上游征信接口抖动在 3 分钟内触发 127 次重试导致 Redis 连接池耗尽。以下是我们在真实生产环境中沉淀的 checklist。4.1 部署流程从本地开发到集群分发skills 的部署不是npm publish而是registry 同步 hash 校验。流程如下本地构建npx skills/cli build生成dist/目录包含index.js、skill.json、schema/并计算 SHA256 hash。签名上传用团队私钥对 hash 签名生成dist/signature.sig上传到内部 S3 存储桶。registry 同步harness 的 registry 服务定时拉取 S3 列表校验 signature将合法 skill 解压到/opt/skills/git-diff-stats1.0.0/。版本路由agent 请求时带skillName: git-diff-stats1.xregistry 返回最新1.0.0的完整路径。关键点无热更新registry 不会覆盖正在运行的 skill 目录。新版本部署后harness 会优雅重启 worker 进程旧进程处理完当前请求再退出。多版本共存git-diff-stats1.0.0和git-diff-stats1.1.0可同时存在agent 可按需指定版本。hash 校验失败即拒用如果 S3 上的文件被篡改harness 启动时校验失败直接 panic 并告警绝不加载。4.2 调试技巧如何在沙盒里看 console.logskills 禁止console.log但调试时你需要日志。正确做法是在skill.json中声明debug: trueharness 会将process.stdout.write的内容重定向到/var/log/skills/git-diff-stats/下的 timestamped file日志格式强制为 JSON{level:debug,msg:parsing git output,lineCount:42}你不能写console.error(xxx)但可以if (process.env.DEBUG true) { process.stderr.write(JSON.stringify({ level: error, msg: git command failed, stderr: result.stderr }) \n); }harness 会捕获stderr并归档但不会影响stdout的正常输出。这是唯一被允许的调试通道。4.3 线上问题速查表基于真实故障复盘问题现象根本原因排查命令解决方案{error:{code:PERMISSION_DENIED,message:read denied for /home/user/.git/config}}skill.json的allowedReadPaths未包含.git/config只写了**/*.git/**glob 不匹配隐藏文件npx skills/cli debug --show-sandbox-rules在allowedReadPaths中添加**/.git/**{error:{code:TIMEOUT,message:Execution timed out after 10000ms}}skill 内部调用了阻塞的fs.readFileSync且文件过大10MBstrace -f -e tracewrite,read,openat -p pid改用fs.createReadStream stream processing或增加timeoutMs{error:{code:INVALID_INPUT,message:commit does not match pattern}}LLM 生成了commit: HEAD~2但 schema 只允许HEAD或 hashnpx skills/cli validate --input payload.json --schema schema/input.json在 agent 层加 pre-process将HEAD~N转为git rev-parse HEAD~N{error:{code:UNEXPECTED_ERROR,message:Cannot find module simple-git}}runtimeDependencies声明了simple-git但npx skills/cli build时未安装本地 node_modules 缺失npx skills/cli build --dry-run运行npm install后再 build或配置 CI 自动 install实操心得所有 skills 必须带--dry-run模式。npx skills/cli build --dry-run会模拟构建过程检查 dependencies 是否齐全、schema 是否语法正确、shebang 是否存在。我在一次发布前用它发现了 3 个未声明的fs-extra依赖避免了线上 500 错误。4.4 性能优化让 skills 响应快 3 倍的 4 个技巧预编译正则skills 启动慢把input.path.match(/^\/home\/user\/(.)$/)改成const PATH_REGEX /^\/home\/user\/(.)$/; ... PATH_REGEX.exec(input.path)。V8 对字面量正则有 JIT 优化动态生成的没有。缓存沙盒初始化harness 默认每次调用都重建沙盒。对 CPU 密集型 skill如图像处理在skill.json中加sandbox: { cacheKey: cpu-heavy-v1 }harness 会复用沙盒进程池。二进制预加载Playwright/FFmpeg 类 skill把二进制打包进dist/目录skill.json中声明binaryPaths: [./bin/playwright]避免 runtime 下载。JSON 序列化加速不用JSON.stringify()改用fast-json-stringify库。实测对 10KB 输出序列化耗时从 8ms 降到 1.2ms。但必须在skill.json的dependencies中声明否则沙盒拒绝加载。5. skills 生态现状与避坑指南那些文档里不会写的真相搜索“skills 推荐”“claude 国内安装 skills”时你看到的大多是过时信息。2024 年 Q2skills 生态已发生三处关键演进很多教程没更新5.1 真相一Claude 官方从未发布过 skills SDK所有claude/skills-sdknpm 包都是社区维护的 unofficial wrapper。Claude Desktop 的 skill 加载机制是私有协议其 registry 服务只接受.claude-skill格式zip 压缩包含特定签名。你用npx skills/cli init生成的 skill无法直接在 Claude Desktop 中运行——必须用官方claude-skill-packager工具重新打包并用 Claude 的私钥签名。这个工具不开源只提供 Windows/macOS 二进制。所以“Claude 国内安装 skills”本质是找破解版 packager风险极高。5.2 真相二“agent anywhere” 不是技术是商业术语agent anywhere指 skills 可在任意 runtime 执行VS Code 插件、Next.js API Route、Cloudflare Worker、甚至 Android Termux。但现实是VS Code 插件 runtime 只支持 Node.js 18不支持 WASMCloudflare Worker 要求 skills 用 WebAssembly 编译且禁用child_processAndroid Termux 需要 skills 自带termux-api适配层所谓“anywhere”实际是“anywhere we’ve ported the harness”。目前只有 Node.js 和 Deno 有成熟 harnessRust/WASM 版本还在 alpha。5.3 真相三npx不是必须的但它是最佳实践你可以不用npx直接node ./dist/index.js。但npx的价值在于自动解析package.json的bin字段找到正确的入口隔离 node_modules避免全局安装污染支持npx -p playwright1.42.0 my-skill临时注入依赖我见过团队为省事直接npm install -g skills/cli结果 CI 里多个 job 并发npx skills/cli build互相覆盖 global cache导致构建产物 hash 不一致。正确做法是所有 CI 步骤都用npx且加--no-install参数强制每次都 fresh install。5.4 避坑清单新手必踩的 5 个深坑不要在 skill 里写require(fs)沙盒会重写require只允许加载skill.json中声明的 dependencies。想用fs把它加进dependencies然后const fs require(fs)。不要用__dirname获取路径沙盒会把 skill 解压到随机路径__dirname不可靠。正确方式是process.cwd()skill.json的相对路径。不要信任 LLM 的 JSON 输出即使 schema 校验通过LLM 也可能返回commit: HEAD字符串而非HEAD字面量。必须在 skill 里做严格比较。不要忽略additionalProperties: false如果 schema 允许additionalProperties: trueharness 会放行所有未知字段但 agent 可能因字段名 typo如repo_pathvsrepoPath静默失败。不要在index.js里写异步 I/OspawnSync是同步的但fetch()是异步的。skills 必须是同步进程。要用node-fetch得用execSync(node -e require(\node-fetch\)(...))但极不推荐——改用curl命令更安全。最后分享一个真实技巧我们给所有 skills 加了一个health-checkendpoint。在skill.json中声明{ healthCheck: { type: http, url: /health, timeoutMs: 2000 } }harness 会定期 GET 这个 endpoint如果返回非 200自动标记 skill 为 degraded并路由到备用版本。这让我们在线上故障时平均恢复时间从 47 分钟降到 83 秒。skills 的生命力不在它多酷炫而在它多可靠——可靠到你忘了它的存在只记得它总在该出现的时候安静地完成那件小事。