首页 › tutorials › Skill可执行脚本封装开发实战

Claude Code Skill 可执行脚本封装开发实战:从零构建 schema-forge

概述

本教程面向已经会用 Claude Code 交互、但还没亲手写过 Skill 的开发者。我们将用大约 60 分钟,从空白目录开始,构建一个名为 schema-forge 的真实 Skill——它能从任意 JSON 样本数据推断出符合 Draft 2020-12 规范的 JSON Schema。教程的核心独特价值在于深入讲解 Skills 体系里最强大却最少被正确使用的能力:内置可执行脚本(scripts/ 目录)的封装,以及如何用三级渐进披露把一个"提示词包"升级为"确定性工具包装器"。读完之后,你将具备独立设计、编写、测试、调优一个生产级 Skill 的全部能力。

前置条件

不需要任何 Python 第三方依赖——这是一个刻意的设计选择,理由会在第五节解释。


一、先建立正确的心智模型:Skill 到底是什么

很多人把 Skill 理解成"一段复用的提示词",这只说对了一半。更精确的定义是:

Skill 是一个文件夹,里面装着「指令 + 可选脚本 + 可选参考文档 + 可选素材」,Claude 只在判断它与当前任务相关时,才把这些内容按需加载进上下文。

这个定义里有三个关键词,构成了 Skill 体系的全部设计哲学:

  1. 文件夹——Skill 是文件系统里的一个目录,不是单条配置。它天然支持多文件、多脚本、多语言资源。
  2. 按需加载——这是 Skills 区别于普通系统提示词的根本所在,技术上叫"渐进披露"(Progressive Disclosure)。
  3. 相关时才加载——Claude 怎么判断"相关"?唯一依据是 frontmatter 里的 description 字段。这是你必须刻进脑子里的铁律。

1.1 三级渐进披露架构

一个设计良好的 Skill,其内容按"加载成本从低到高"分成三层。Claude 像剥洋葱一样,只在需要更深处时才往下剥一层:

flowchart TD
    A["第 0 层:所有 Skill 的 frontmatter<br/>~100 tokens / 个 · 始终在上下文"] --> B{"description 是否<br/>匹配当前任务?"}
    B -- 否 --> Z["该 Skill 完全不加载<br/>零 token 成本"]
    B -- 是 --> C["第 1 层:SKILL.md 主体<br/>完整指令 · 仅激活时加载<br/>建议 < 500 行 / < 5000 tokens"]
    C --> D{"是否需要确定性计算<br/>或精确执行?"}
    D -- 是 --> E["第 2 层:scripts/*.py 等<br/>按需执行 · 100% 确定性输出"]
    D -- 否 --> F{"是否需要深层参考资料?"}
    F -- 是 --> G["第 2 层:references/*.md<br/>按需读取 · 仅相关章节进上下文"]
    F -- 否 --> H["直接按主体指令完成任务"]
    E --> H
    G --> H
    style A fill:#e8f5e9
    style Z fill:#ffebee
    style E fill:#fff3e0
    style G fill:#e3f2fd

这张图揭示了一个反直觉但至关重要的优化原则:一个 Skill 占用上下文的成本,不是看它总共有多少内容,而是看它在当前任务里被剥到了第几层。 一个写了 3000 行参考文档的 Skill,如果用户的任务只用到第 1 层,那么第 2 层那 3000 行一行都不会进上下文。这就是为什么 Skills 可以承载远超普通提示词的知识量而不爆上下文。

1.2 "模型—作者契约"

理解了渐进披露,你就能理解 Skill 作者与 Claude 之间本质上是在签订一份契约:

契约破裂最常见的两种形式,都是作者的责任:(a) description 写得太宽泛(例如"用于代码任务"),导致 Skill 该激活时不激活、或不该激活时乱激活;(b) 主体指令含糊(例如"生成一个好的 schema"),把本该确定性的工作丢给模型凭空发挥。本教程会逐条教你避开这两个坑。


二、搭建 Skill 骨架

个人级 Skill 放在 ~/.claude/skills/<skill-name>/,项目级 Skill 放在 <项目>/.claude/skills/<skill-name>/。前者对你所有项目生效,后者只在当前项目生效。我们这次做个人级的。

先建目录结构:

mkdir -p ~/.claude/skills/schema-forge/scripts
mkdir -p ~/.claude/skills/schema-forge/references

最终我们要产出的目录树长这样:

~/.claude/skills/schema-forge/
├── SKILL.md                       # 必需:frontmatter + 主体指令
├── scripts/
│   └── generate_schema.py         # 可执行脚本:确定性推理核心
└── references/
    ├── type-mapping.md            # JSON→Schema 类型映射详解
    └── merging-strategy.md        # 多样本合并策略详解

整个骨架里,只有 SKILL.md 是必需的,其余都是可选的增强。但恰恰是 scripts/ 这个可选项,决定了你的 Skill 是"花瓶提示词"还是"真工具"。


三、编写 SKILL.md:契约的落地

SKILL.md 是 Skill 的入口文件,名字固定不能改。它由两部分组成:YAML frontmatter(元数据)和 Markdown 主体(指令)。

3.1 frontmatter:唯一决定触发的字段

frontmatter 目前只关心两个字段:

字段 必需 作用 写作要点
name 是 Skill 的唯一标识符 小写 kebab-case,与目录名一致
description 是 触发判断的唯一依据 用自然语言写清"何时用"+"做什么"

description 是整个 Skill 最值得反复打磨的字段。它永远在上下文里(每个 Skill 约 100 tokens),Claude 拿它和你当前的任务描述做语义匹配。写好它,胜过写一万行指令。

3.2 description 的黄金公式

经过大量实践,一条高触发率的 description 通常遵循这个结构:

能力概述 + 触发场景列举(2–4 个具体场景)+ 关键约束/优先级

对比下面三个版本,体会差异:

# ❌ 反面案例 A:太抽象
description: 一个数据相关的工具

# ❌ 反面案例 B:太宽泛,会和别的 Skill 抢触发
description: 用于处理 JSON 和代码生成任务

# ✅ 正面案例:具体、可匹配、有优先级
description: 从 JSON 样本数据推断并生成 JSON Schema (Draft 2020-12)。当用户提供 JSON 数据、API 响应样本、配置文件示例,要求生成对应的 JSON Schema、校验规则、类型定义,或在编写数据校验/接口契约时使用。优先调用内置的 generate_schema.py 脚本进行确定性推理,避免凭空手写 schema。

正面案例好在三处:① 开头点明能力边界("从 JSON 样本→JSON Schema");② 中间枚举了 Claude 容易在用户消息里捕捉到的具体触发词("生成 JSON Schema""校验规则""接口契约");③ 结尾给出行为优先级("优先调用脚本……避免凭空手写"),这直接关系到第四节要讲的脚本封装。

3.3 主体:给模型的操作手册

主体在 Skill 被激活后才加载,所以这里可以放心写详细操作步骤。但官方建议控制在 500 行以内、5000 tokens 以内——超出部分应该下沉到 references/。一个好的主体结构是:

# schema-forge
(一句话定位)

## 何时使用
(2–4 个触发场景)

## 如何使用
(调用脚本的具体命令,含基本用法/内联用法/多样本用法)

## 规则
(必须遵守的硬性约束,编号列出,便于模型遵循)

## 详细类型映射
(一句话提示:"见 references/type-mapping.md",触发渐进披露)

把完整的 SKILL.md 写出来:

---
name: schema-forge
description: 从 JSON 样本数据推断并生成 JSON Schema (Draft 2020-12)。当用户提供 JSON 数据、API 响应样本、配置文件示例,要求生成对应的 JSON Schema、校验规则、类型定义,或在编写数据校验/接口契约时使用。优先调用内置的 generate_schema.py 脚本进行确定性推理,避免凭空手写 schema。
---

# schema-forge

把 JSON 样本转换为符合 Draft 2020-12 规范的 JSON Schema。

## 何时使用
- 用户给出 JSON 示例,要求"生成 schema / 生成校验规则 / 推断类型"
- 需要为 API 响应、配置文件、数据契约编写 JSON Schema
- 需要把多个样本合并成一份统一的 schema

## 如何使用

脚本位于本 Skill 的 `scripts/generate_schema.py`,纯标准库实现,无需安装任何依赖。

### 基本用法(单文件)
\`\`\`bash
python3 ~/.claude/skills/schema-forge/scripts/generate_schema.py sample.json --root-title "User"
\`\`\`

### 从 stdin 内联(适合粘贴在对话里的 JSON)
\`\`\`bash
echo '{"id":1,"name":"ada","active":true}' | python3 ~/.claude/skills/schema-forge/scripts/generate_schema.py
\`\`\`

### 合并多个样本(显著提升推理准确度)
\`\`\`bash
python3 ~/.claude/skills/schema-forge/scripts/generate_schema.py a.json --merge b.json c.json
\`\`\`

## 规则
1. **优先调用脚本**,不要手动编写 schema。脚本输出是确定性的,比手写更可靠。
2. 脚本默认输出 Draft 2020-12 格式;若用户要求 Draft-07,告知其把输出的 `$schema` 改为 draft-07 的 URL 即可(数组/联合类型语义略有差异)。
3. 优先用多样本合并,对象 `required` 字段与联合类型的准确度会随样本数提升。
4. 输出 schema 后,简要提示可增强的方向(`format`、`pattern`、`enum`、`additionalProperties`),但不擅自改动脚本输出。

## 详细类型映射
完整的 JSON 值 → Schema 关键字映射,见 `references/type-mapping.md`。仅在用户追问某个具体类型的生成逻辑时才查阅。

注意最后一节用一句话引导 Claude 去读 references/,而不是把全部细节堆在主体里——这就是渐进披露的精髓。


四、可执行脚本封装:让 Skill 从"建议"变成"工具"

这是本教程的核心,也是大多数 Skill 教程一笔带过、但实际决定了 Skill 质量上限的部分。

4.1 为什么需要内置脚本

模型直接"凭记忆"生成 JSON Schema 会产生三类典型错误:① 类型判断不准(把 "5" 当成数字);② 漏掉嵌套结构;③ 多样本时无法可靠合并。这些恰恰是确定性的代码最擅长的事。

Skills 允许你在 scripts/ 目录里放进可执行文件(Python / Node / Bash / 任意可执行程序),并在主体指令里告诉 Claude"优先调用它"。于是 Skill 的角色从"给模型提建议"升级为"为模型提供一条确定性的执行路径"。模型负责理解和编排,脚本负责精确执行——这是当前 Agent 工程里最稳健的分工。

4.2 编写 generate_schema.py

刻意只用 Python 标准库(json / argparse / sys / collections),不引入任何 pip 依赖。把下面内容存到 ~/.claude/skills/schema-forge/scripts/generate_schema.py:

#!/usr/bin/env python3
"""schema_forge: 从 JSON 样本推断 JSON Schema (Draft 2020-12)。纯标准库实现。"""
import json, sys, argparse
from collections import OrderedDict

# Python 类型 -> JSON Schema 基本类型
def infer_schema(value):
    if value is None:
        return {"type": "null"}
    if isinstance(value, bool):                      # 注意:bool 必须在 int 之前判断
        return {"type": "boolean"}
    if isinstance(value, int):
        return {"type": "integer"}
    if isinstance(value, float):
        return {"type": "number"}
    if isinstance(value, str):
        return {"type": "string"}
    if isinstance(value, list):
        if not value:
            return {"type": "array"}
        # 数组合并:把每个元素的 schema 合并成统一的 items
        return {"type": "array", "items": merge_many([infer_schema(v) for v in value])}
    if isinstance(value, dict):
        props, required = OrderedDict(), []
        for k, v in value.items():
            props[k] = infer_schema(v)
            required.append(k)
        schema = {"type": "object"}
        if props:
            schema["properties"] = props
            schema["required"] = required
        return schema
    return {}

def merge_two(a, b):
    ta, tb = a.get("type"), b.get("type")
    if ta == tb:
        if ta == "object":                           # 同为对象:合并 properties 与 required
            props = dict(a.get("properties", {}))
            req = list(dict.fromkeys(a.get("required", []) + b.get("required", [])))
            for k, v in b.get("properties", {}).items():
                props[k] = merge_two(props[k], v) if k in props else v
            return {"type": "object", "properties": props, "required": req}
        if ta == "array":
            return {"type": "array", "items": merge_two(a.get("items", {}), b.get("items", {}))}
        return a
    # 类型不同:升级为联合类型(type 数组)
    return {"type": sorted({ta, tb})}

def merge_many(schemas):
    out = schemas[0]
    for s in schemas[1:]:
        out = merge_two(out, s)
    return out

def main():
    ap = argparse.ArgumentParser(description="从 JSON 样本生成 JSON Schema")
    ap.add_argument("input", nargs="?", help="JSON 文件路径,省略则读 stdin")
    ap.add_argument("--merge", nargs="*", default=[], help="额外样本文件,合并推理")
    ap.add_argument("--root-title", default="", help="根 schema 的 title 字段")
    args = ap.parse_args()

    # 收集所有输入源:主输入 + --merge 列表
    sources = ([args.input] if args.input else []) + list(args.merge)
    raw = sys.stdin.read() if (not args.input and not sys.stdin.isatty()) else None

    schemas = []
    if raw is not None:
        schemas.append(infer_schema(json.loads(raw)))
    for path in sources:
        with open(path, encoding="utf-8") as f:
            schemas.append(infer_schema(json.load(f)))

    if not schemas:
        ap.error("需要提供 JSON 输入(文件参数或 stdin)")

    final = merge_many(schemas)
    out = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
    if args.root_title:
        out["title"] = args.root_title
    out.update(final)
    print(json.dumps(out, indent=2, ensure_ascii=False))

if __name__ == "__main__":
    main()

赋予可执行权限:

chmod +x ~/.claude/skills/schema-forge/scripts/generate_schema.py

4.3 脚本封装的四条铁律

既然要写脚本,就要写得让模型能可靠调用、能跨机器复现。以下四条是从踩坑里总结出来的:

  1. 零外部依赖优先——能用标准库就别用第三方包。Skill 可能跑在同事的机器、CI 容器、甚至沙箱里,pip install 一旦失败,Skill 就整体失效。这条直接对应搜索资料里反复强调的"avoid dependency hell"。
  2. 纯函数式的输入输出契约——脚本读 stdin 或文件参数,向 stdout 打印结构化结果(JSON 最佳),不写副作用文件、不依赖当前工作目录的隐藏状态。这样模型调用它就像调用一个纯函数。
  3. 明确的退出码与错误信息——成功返回 0,失败返回非 0 并把人类可读的错误打到 stderr。模型会读 stderr 来自我纠错。
  4. 在 SKILL.md 里给出可直接复制运行的命令——不要让模型去"猜"调用方式。第四节 3.3 里的"如何使用"小节就是这个目的。

五、渐进披露的参考文档

主体塞不下、又不该让模型每次都读的深层知识,放进 references/。Claude 只会在主体提示"见 references/xxx.md"且任务确实需要时,才去读取相关章节。

在 ~/.claude/skills/schema-forge/references/type-mapping.md 写入:

# JSON → Schema 类型映射详解

| JSON 值 | 生成关键字 | 备注 |
|---------|-----------|------|
| `null` | `"type": "null"` | |
| `true/false` | `"type": "boolean"` | Python 中 bool 是 int 子类,须先判 bool |
| `42` | `"type": "integer"` | |
| `3.14` | `"type": "number"` | |
| `"ada"` | `"type": "string"` | 可后续建议追加 `format: date-time` 等 |
| `[]` | `"type": "array"` | 空数组无法推断 items |
| `[...]` | `"type":"array","items":{...}` | 元素 schema 经 merge_many 合并 |
| `{...}` | `"type":"object","properties":{...},"required":[...]` | 全部键默认 required |

## 联合类型
当多个样本里同一字段出现不同类型,merge_two 会把 `type` 升级为数组:
`{"type": ["integer", "string"]}`。

这类内容绝对不要塞进 SKILL.md 主体——它们只在用户追问"为什么这个字段变成了 integer+string 联合类型"时才需要,平时放着纯粹是浪费。

至此,一个结构完整、带确定性执行核心、遵循渐进披露的 Skill 就成型了。


六、触发测试与调优

Skill 写完不算完,必须验证 Claude 会不会在"该用的时候用、不该用的时候不用"。这一步叫触发测试(trigger testing)。

6.1 三类测试用例

在一个新对话里(确保上下文干净),分别抛出下面三类提示,观察 schema-forge 是否被激活:

类别 测试提示 期望
正例(应激活) "我有一段 API 返回的 JSON:{"id":1,"tags":["a"]},帮我生成校验用的 schema" ✅ 激活并调用脚本
正例(变体措辞) "给这份配置样本做接口契约:…" ✅ 激活(依赖 description 里的"接口契约")
反例(不应激活) "帮我把这段 Python 重构成类" ❌ 不激活

6.2 常见失效模式与修复

现象 根因 修复
正例不激活 description 缺少用户实际会用的词 把用户真实措辞补进 description 的触发场景列举
反例乱激活 description 太宽泛(如含"代码""JSON"这类高频词且无边界) 加上能力边界与"用于…时"的限定
激活但模型手写而非调脚本 主体"规则"不够强硬 把"优先调用脚本"写成编号第 1 条,并用"不要手动编写"反向约束
脚本报错 依赖缺失或路径写死 改用标准库;命令里用 ~/.claude/skills/... 绝对路径

调优是个迭代过程:改 description → 新开对话 → 抛测试用例 → 再改。注意每次测试都要开新对话,因为旧上下文会"污染"判断。


七、完整工作流总览

把前面所有步骤串起来,构建并验证一个 Skill 的完整流程如下:

flowchart LR
    A["1. 明确能力边界<br/>确定输入/输出"] --> B["2. 建目录骨架<br/>SKILL.md + scripts/ + references/"]
    B --> C["3. 写 frontmatter<br/>打磨 description(触发核心)"]
    C --> D["4. 写主体指令<br/>何时用/如何用/规则/<500行"]
    D --> E{"需要确定性计算?"}
    E -- 是 --> F["5. 封装 scripts/*.py<br/>零依赖 + 纯函数契约"]
    E -- 否 --> G["6. 下沉深层知识到 references/"]
    F --> G
    G --> H["7. 触发测试<br/>正例激活 / 反例不激活"]
    H --> I{"测试通过?"}
    I -- 否 --> J["8a. 回去调 description<br/>或主体规则"]
    J --> C
    I -- 是 --> K["8b. 发布<br/>个人级 / 项目级 / 插件市场"]
    style C fill:#fff3e0
    style F fill:#e8f5e9
    style H fill:#e3f2fd

八、实战案例:端到端跑通

模拟一个真实场景:后端同事扔给你两个用户对象的 JSON 样本(字段略有差异),要你给出一份覆盖两者的 schema。

样本 a.json:

{"id": 101, "name": "ada", "email": "[email protected]", "roles": ["admin"], "active": true}

样本 b.json:

{"id": 102, "name": "bob", "email": null, "roles": ["admin", "user"], "active": false, "last_login": "2026-06-01"}

在 Claude Code 里直接说:

这两个样本 a.json、b.json 是同一种用户对象,帮我生成覆盖它们的 JSON Schema。

如果 Skill 触发正常,Claude 会调用:

python3 ~/.claude/skills/schema-forge/scripts/generate_schema.py a.json --merge b.json --root-title "User"

得到确定性输出:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "User",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" },
    "email": { "type": ["string", "null"] },
    "roles": { "type": "array", "items": { "type": "string" } },
    "active": { "type": "boolean" },
    "last_login": { "type": "string" }
  },
  "required": ["id", "name", "email", "roles", "active", "last_login"]
}

注意脚本自动完成了三件模型容易出错的事:① email 因两个样本类型不同而升级为 ["string","null"] 联合类型;② roles.items 被合并成统一的 string;③ required 取了所有出现过的键的并集。这就是脚本封装相对于"模型手写"的硬核价值——可复现、可审计、可进化。当业务规则变了,你改的是脚本而不是 prompt,质量不会因为模型某天"状态不好"而波动。


九、常见问题

Q:Skill 和 MCP 服务器有什么区别?什么时候用哪个? Skill 是"打包好的指令+脚本+知识",轻量、零运行时、靠模型按 description 激活;MCP 是"独立运行的工具进程",提供稳定 API、适合长期状态管理和多客户端共享。一句话:一次性、确定性、跟着上下文走的用 Skill;需要常驻服务、跨工具共享、有复杂鉴权的用 MCP。 本例的 schema 推理属于前者。

Q:脚本能不能用 Node.js 而不是 Python? 完全可以。scripts/ 里放什么语言都行,只要 SKILL.md 给出正确的调用命令。选 Python 纯粹是因为标准库覆盖广、零安装。Node 脚本记得提醒模型用 node scripts/xxx.js 调用。

Q:Skill 不生效,连 frontmatter 都没被识别? 最常见三个原因:① 目录名与 frontmatter 的 name 不一致;② SKILL.md 文件名拼错(必须全大写 SKILL.md);③ 放错位置——个人级必须在 ~/.claude/skills/ 下,项目级必须在 <项目>/.claude/skills/ 下。用 /skills 或重启会话让 Claude Code 重新扫描。

Q:把 SKILL.md 写到 800 行会怎样? 不会报错,但每次激活都会把 800 行全塞进上下文,挤占你宝贵的 token 预算,也稀释模型对关键规则的注意力。超过 500 行就该把详尽内容下沉到 references/,主体只留索引。


延伸阅读


训练营适用性评估


参考来源

  1. Equipping agents for the real world with Agent Skills — Anthropic Engineering — Anthropic 官方对 Skill 设计哲学与渐进披露的第一手阐述
  2. Extend Claude with skills — Claude Code Docs — Skill 目录结构、SKILL.md、frontmatter 的权威定义
  3. Skill authoring best practices — Claude API Docs — 500 行/5000 token 建议与 description 写作规范
  4. Inside Claude Code Skills: Structure, prompts, invocation — mikhail.io — 从实现视角解析 Skill 如何被注入模型上下文
  5. Agent Skills Specification — agentskills.io — 跨实现 Skill 规范草案,含三级加载的 token 成本建议

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入