本教程面向已经会用 Claude Code 交互、但还没亲手写过 Skill 的开发者。我们将用大约 60 分钟,从空白目录开始,构建一个名为 schema-forge 的真实 Skill——它能从任意 JSON 样本数据推断出符合 Draft 2020-12 规范的 JSON Schema。教程的核心独特价值在于深入讲解 Skills 体系里最强大却最少被正确使用的能力:内置可执行脚本(scripts/ 目录)的封装,以及如何用三级渐进披露把一个"提示词包"升级为"确定性工具包装器"。读完之后,你将具备独立设计、编写、测试、调优一个生产级 Skill 的全部能力。
python3(3.8+)和 bash 环境~/.claude/skills/ 目录(个人级 Skill 的存放点)不需要任何 Python 第三方依赖——这是一个刻意的设计选择,理由会在第五节解释。
很多人把 Skill 理解成"一段复用的提示词",这只说对了一半。更精确的定义是:
Skill 是一个文件夹,里面装着「指令 + 可选脚本 + 可选参考文档 + 可选素材」,Claude 只在判断它与当前任务相关时,才把这些内容按需加载进上下文。
这个定义里有三个关键词,构成了 Skill 体系的全部设计哲学:
description 字段。这是你必须刻进脑子里的铁律。一个设计良好的 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 可以承载远超普通提示词的知识量而不爆上下文。
理解了渐进披露,你就能理解 Skill 作者与 Claude 之间本质上是在签订一份契约:
description 里诚实、精准地描述这个 Skill 能干什么、什么时候该用它。description 来决定要不要激活;激活后,忠实地执行 SKILL.md 主体里的指令,必要时调用你提供的脚本和参考文档。契约破裂最常见的两种形式,都是作者的责任:(a) description 写得太宽泛(例如"用于代码任务"),导致 Skill 该激活时不激活、或不该激活时乱激活;(b) 主体指令含糊(例如"生成一个好的 schema"),把本该确定性的工作丢给模型凭空发挥。本教程会逐条教你避开这两个坑。
个人级 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 的入口文件,名字固定不能改。它由两部分组成:YAML frontmatter(元数据)和 Markdown 主体(指令)。
frontmatter 目前只关心两个字段:
| 字段 | 必需 | 作用 | 写作要点 |
|---|---|---|---|
name |
是 | Skill 的唯一标识符 | 小写 kebab-case,与目录名一致 |
description |
是 | 触发判断的唯一依据 | 用自然语言写清"何时用"+"做什么" |
description 是整个 Skill 最值得反复打磨的字段。它永远在上下文里(每个 Skill 约 100 tokens),Claude 拿它和你当前的任务描述做语义匹配。写好它,胜过写一万行指令。
经过大量实践,一条高触发率的 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""校验规则""接口契约");③ 结尾给出行为优先级("优先调用脚本……避免凭空手写"),这直接关系到第四节要讲的脚本封装。
主体在 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 质量上限的部分。
模型直接"凭记忆"生成 JSON Schema 会产生三类典型错误:① 类型判断不准(把 "5" 当成数字);② 漏掉嵌套结构;③ 多样本时无法可靠合并。这些恰恰是确定性的代码最擅长的事。
Skills 允许你在 scripts/ 目录里放进可执行文件(Python / Node / Bash / 任意可执行程序),并在主体指令里告诉 Claude"优先调用它"。于是 Skill 的角色从"给模型提建议"升级为"为模型提供一条确定性的执行路径"。模型负责理解和编排,脚本负责精确执行——这是当前 Agent 工程里最稳健的分工。
刻意只用 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
既然要写脚本,就要写得让模型能可靠调用、能跨机器复现。以下四条是从踩坑里总结出来的:
pip install 一旦失败,Skill 就整体失效。这条直接对应搜索资料里反复强调的"avoid dependency hell"。主体塞不下、又不该让模型每次都读的深层知识,放进 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)。
在一个新对话里(确保上下文干净),分别抛出下面三类提示,观察 schema-forge 是否被激活:
| 类别 | 测试提示 | 期望 |
|---|---|---|
| 正例(应激活) | "我有一段 API 返回的 JSON:{"id":1,"tags":["a"]},帮我生成校验用的 schema" |
✅ 激活并调用脚本 |
| 正例(变体措辞) | "给这份配置样本做接口契约:…" | ✅ 激活(依赖 description 里的"接口契约") |
| 反例(不应激活) | "帮我把这段 Python 重构成类" | ❌ 不激活 |
| 现象 | 根因 | 修复 |
|---|---|---|
| 正例不激活 | 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/,主体只留索引。
scripts/ 与 references/、并通过三类触发测试用例的 Skill,并口头解释其 description 的触发词设计理由。[!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 等场景全覆盖。
