本教程面向"已经装通 Claude Code、想把自己的重复操作固化成技能"的开发者。你将学会一个 Skill 的完整生命周期:从识别一个可复用任务,到写 SKILL.md、配 frontmatter、做渐进披露、捆绑脚本、用动态注入喂实时数据,最后用 skill-creator 做量化评测。我们以一个全新的假数据工厂(mock-data-factory)为案例——给定一个 JSON 样例,批量生成 JSON/CSV/SQL 格式的逼真测试数据——把官方文档里散落的字段、替换变量、注入语法全部串起来,跑通一个零依赖、可直接复制使用的真实 Skill。
claude),建议 v2.1.196 以上以支持 ${CLAUDE_PROJECT_DIR}、计划任务联动等特性。pip install)。Claude Code 的 Skill(技能)本质是一个目录,里面至少有一个 SKILL.md,外加可选的脚本、模板、参考文档。官方文档原话:"Skills extend what Claude can do. Create a SKILL.md file with instructions, and Claude adds it to its toolkit."
和 CLAUDE.md 全程常驻不同,Skill 的核心设计叫渐进披露(progressive disclosure),分三层:
| 层级 | 内容 | 何时进入上下文 | 成本 |
|---|---|---|---|
| 第一层 | frontmatter 的 name + description + when_to_use |
始终在上下文(受字符预算约束) | 极低 |
| 第二层 | SKILL.md 正文 |
你手动 /name 或 Claude 判断命中时 |
一次加载,会话内常驻 |
| 第三层 | references/、scripts/、examples/ 等支撑文件 |
正文里被显式引用、且确有需要时才读取 | 按需,几乎零成本 |
这意味着:你可以在 Skill 里塞进一份 2000 行的 API 规范,只要它躺在 references/ 里、正文只引用一次,平时就几乎不占 token。这是 Skill 区别于"把一切写进 CLAUDE.md"的关键。
存放位置决定作用范围(官方四级覆盖:企业 > 个人 > 项目 > 插件):
| 级别 | 路径 | 作用范围 |
|---|---|---|
| 个人 | ~/.claude/skills/<name>/SKILL.md |
你的所有项目 |
| 项目 | .claude/skills/<name>/SKILL.md |
仅当前项目(提交进 git 即团队共享) |
| 插件 | <plugin>/skills/<name>/SKILL.md |
启用该插件处 |
小知识:旧版的
.claude/commands/deploy.md已与.claude/skills/deploy/SKILL.md合并,两者都生成/deploy,Skill 因支持目录、frontmatter、自动加载而成为推荐写法。
先做一个"零依赖"的最小例子,确认触发链路通了,再加复杂度。
操作说明
mkdir -p ~/.claude/skills/hello-skill
cat > ~/.claude/skills/hello-skill/SKILL.md <<'EOF'
---
description: 问候并报告当前所在 git 分支。当用户说"打个招呼"或问当前分支时使用。
---
对用户说一句简短问候,然后用下面的信息说明当前仓库状态:
当前分支:!`git branch --show-current 2>/dev/null || echo "非 git 仓库"`
EOF
预期结果
进入任意 git 项目运行 claude,输入 /hello-skill,或直接问"现在在哪个分支呀",Claude 会先执行 git branch --show-current,把输出内联进指令,再给出"你好,当前分支是 main"之类的回复。
常见问题
/hello-skill 报"找不到命令":目录名拼错,命令名来自目录名而非 frontmatter 的 name(仅插件根目录例外)。description,关键词要和用户自然语言一致;改完无需重启,官方支持热加载——会话内编辑 SKILL.md 立即生效。frontmatter 是两行 --- 之间的 YAML。全部字段可选,但 description 强烈推荐。下表是开发中最常用的:
| 字段 | 作用 | 备注 |
|---|---|---|
name |
显示名 | 默认取目录名;命令名始终来自目录名 |
description |
做什么、何时用 | 核心触发依据;与 when_to_use 合计在列表里截断到 1536 字符 |
when_to_use |
触发短语/示例请求 | 追加到 description,共享 1536 上限 |
disable-model-invocation |
设 true 则只能手动触发 |
适合 /deploy、/commit 等有副作用操作 |
user-invocable |
设 false 则只能 Claude 自动触发,不进 / 菜单 |
适合"背景知识"类技能 |
allowed-tools |
该技能激活时免确认可用的工具 | 如 Bash(python3 *) |
disallowed-tools |
激活期间移除的工具 | 下条消息后失效 |
model / effort |
临时切换模型/思考强度 | 仅本回合有效,不入设置 |
context |
设 fork 则在隔离子代理里跑 |
配合 agent 使用 |
agent |
Explore/Plan/general-purpose/自定义 |
仅 context: fork 时生效 |
paths |
glob 限定激活条件 | 如 ["**/*.sql"] 只在改 SQL 文件时触发 |
shell |
内联命令用 bash 还是 powershell | Windows 下可设 powershell |
两组易混开关:disable-model-invocation: true = 防止 Claude 自作主张(部署、发消息类必备);user-invocable: false = 不暴露成命令(背景知识类)。两者默认都为关,即"你也能调、Claude 也能调"。
预算管理:所有技能描述挤在一个"列表预算"里,规模约为上下文窗口的 1%,溢出时最少使用的技能描述先被裁掉。用 /doctor 或 /context 查看被裁情况,必要时用 skillListingBudgetFraction 调高,或把冷门技能在 skillOverrides 里设为 "name-only"。
常见问题
/name 仍可用,但 description 没了、自动触发失效。加 --debug 启动可看到解析报错。!command`` 语法会在正文发给 Claude 之前执行命令,用输出替换占位符。关键规则(官方明确):
!`cmd` 仅当 ! 出现在行首或紧跟空白时才识别;像 KEY=!cmd`` 这种紧跟字符的写法会被当成字面文本,不执行。! ``` 代码块。## 当前环境
```!
node --version
npm --version
git status --short
这是"预处理",不是 Claude 自己执行——Claude 只看到最终结果。若要全局禁用(如受管环境),设 `"disableSkillShellExecution": true`,注入会被替换成 `[shell command execution disabled by policy]`。
---
## 五、字符串替换:$ARGUMENTS 与路径变量
调用 `/skill foo bar` 时,参数通过占位符进入正文:
| 变量 | 含义 |
|:--|:--|
| `$ARGUMENTS` | 全部参数原样字符串 |
| `$ARGUMENTS[0]` / `$0` | 第一个参数(0 起始),引号可传多词 |
| `$name` | frontmatter `arguments` 声明的具名参数 |
| `${CLAUDE_SKILL_DIR}` | 当前 `SKILL.md` 所在目录——**引用捆绑脚本用这个**,无论装在哪都正确 |
| `${CLAUDE_PROJECT_DIR}` | 项目根(v2.1.196+) |
| `${CLAUDE_SESSION_ID}` | 会话 ID,可做日志/临时文件命名 |
写脚本调用时务必用 `${CLAUDE_SKILL_DIR}/scripts/xxx.py`,不要写死 `~/.claude/skills/...`——后者在项目级或插件级安装时会失效。
---
## 六、实战案例:造假数据工厂 Skill(完整可运行)
### 需求
测试和演示经常需要一堆"像样"的数据:注册接口要 50 条用户、数据库要 200 行种子、前端原型要 CSV。手写费时、固定样例又太假。我们做一个 Skill:输入一个 JSON 样例,按字段名语义自动生成批量数据,支持 JSON/CSV/SQL 三种格式,纯标准库、零依赖。
### 目录结构
```text
~/.claude/skills/mock-data-factory/
├── SKILL.md # 主指令(第二层)
├── references/
│ └── data-conventions.md # 字段语义规则(第三层,按需读)
└── scripts/
└── generate.py # 生成脚本(被执行,不进上下文)
---
name: mock-data-factory
description: 从一个 JSON 样例或字段描述,批量生成逼真的假数据(JSON/CSV/SQL)。用于造测试数据、种子数据、mock 接口返回、数据库填充、演示样例。
when_to_use: 出现"造数据/假数据/mock 数据/测试数据/种子数据/seed data/fake data/生成 N 条用户"等请求,或需要为接口/数据库/前端填充真实感数据时。
argument-hint: [count] [format=json|csv|sql] [table=users]
allowed-tools: Bash(python3 *)
---
# 假数据工厂
你是假数据生成助手,目标是产出语义逼真、可即用的数据。
## 工作流
1. 取得目标字段:用户给了 JSON 对象样例就用它做模板;只描述字段就先帮他构造一个样例对象。
2. 解析 $ARGUMENTS:第一个整数作为 count(默认 10);json/csv/sql 任一作为 format(默认 json);table= 后作为表名(默认 mock_data)。
3. 把样例写到临时文件,再执行生成器:
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/generate.py \
--template /tmp/mock-tpl.json --count <count> --format <fmt> --table <table>
```
4. 字段语义规则(手机号/邮箱/姓名/金额等如何造)见 [data-conventions.md](references/data-conventions.md),需要时再读取。
5. 输出格式:json 用 ```json 代码块,csv/sql 用对应代码块;超过 30 行时只展示前若干行并说明完整写入了哪个文件。
## 约束
- 始终用 ${CLAUDE_SKILL_DIR} 定位脚本,不要硬编码安装路径。
- 新增字段规则时优先扩展 data-conventions.md,而不是把规则堆进本文件。
注意
description和when_to_use加起来要控制长度——1536 字符上限是从头截断,所以把"做这件事"放最前面,触发词次之。
# 字段语义约定
generate.py 依据字段名(小写)推断语义。新增规则在此追加。
| 关键字 | 生成规则 |
|:--|:--|
| id / *_id | 自增整数(1, 2, 3…) |
| email / mail | user{n}@example.com |
| phone / mobile | 中国大陆手机号 1[3-9] + 9 位 |
| name / username | 中文姓名池随机 |
| age | 18–65 整数 |
| price/amount/salary/money/fee | 两位小数金额 |
| *date* / *_at / *_ts | ISO8601 日期(2024 起) |
| is_*/has_*/*bool* | 布尔 |
| province/city/region | 省份池 |
样例值无法推断时,按样例的 Python 类型回退:bool→随机布尔,int→0–1000,float→两位小数,str→"字段名_序号"。
#!/usr/bin/env python3
"""假数据工厂生成器,仅用标准库。
用法: generate.py --template sample.json --count 50 --format json|csv|sql --table users [--out file]
"""
import argparse, csv, io, json, random, sys, datetime
NAMES = ["张伟","王芳","李娜","刘洋","陈静","杨磊","赵敏","黄强","周杰","吴婷",
"徐明","孙丽","马超","朱琳","胡涛","林燕","郭峰","何萍","高翔","罗静"]
DOMAINS = ["example.com","test.cn","mock.io","demo.org"]
PROVINCES = ["北京","上海","广东","浙江","江苏","四川","湖北","陕西"]
def fake_value(key, idx):
k = key.lower()
if k == "id" or k.endswith("_id") or k.endswith("id"):
return idx
if "email" in k or k.endswith("mail"):
return f"user{idx}{random.randint(0,99)}@{random.choice(DOMAINS)}"
if "phone" in k or "mobile" in k:
return f"1{random.choice('35789')}{random.randint(100000000,999999999)}"
if "name" in k:
return random.choice(NAMES)
if k == "age" or "age" in k:
return random.randint(18, 65)
if any(w in k for w in ("price","amount","salary","money","fee")):
return round(random.uniform(10, 9999), 2)
if "count" in k or "num" in k or k.startswith("qty"):
return random.randint(1, 1000)
if any(w in k for w in ("date","time","at")) or k.endswith("_ts"):
return (datetime.date(2024,1,1) + datetime.timedelta(days=random.randint(0,500))).isoformat()
if any(w in k for w in ("province","city","region")):
return random.choice(PROVINCES)
if "url" in k or "link" in k:
return f"https://{random.choice(DOMAINS)}/{key}/{idx}"
if k.startswith("is_") or k.startswith("has_") or "bool" in k:
return random.choice([True, False])
return None # 回退到按样例类型推断
def gen_rows(sample, count):
rows = []
for i in range(1, count + 1):
row = {}
for key, val in sample.items():
v = fake_value(key, i)
if v is None:
if isinstance(val, bool): v = random.choice([True, False])
elif isinstance(val, int): v = random.randint(0, 1000)
elif isinstance(val, float):v = round(random.uniform(0, 1000), 2)
elif isinstance(val, str): v = f"{key}_{i}"
else: v = val
row[key] = v
rows.append(row)
return rows
def to_sql(rows, table):
cols = list(rows[0].keys())
out = [f"CREATE TABLE IF NOT EXISTS {table} ({', '.join(cols)});"]
for r in rows:
vals = ["'%s'" % v if isinstance(v, str) else str(v) for v in r.values()]
out.append(f"INSERT INTO {table} ({', '.join(cols)}) VALUES ({', '.join(vals)});")
return "\n".join(out)
def to_csv(rows):
buf = io.StringIO()
w = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
return buf.getvalue()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--template", required=True)
ap.add_argument("--count", type=int, default=10)
ap.add_argument("--format", default="json", choices=["json","csv","sql"])
ap.add_argument("--table", default="mock_data")
ap.add_argument("--out", default=None)
a = ap.parse_args()
with open(a.template, encoding="utf-8") as f:
sample = json.load(f)
if not isinstance(sample, dict):
sys.exit("template 必须是 JSON 对象(字段 -> 样例值)")
random.seed(42) # 可复现
rows = gen_rows(sample, a.count)
if a.format == "json": text = json.dumps(rows, ensure_ascii=False, indent=2)
elif a.format == "csv": text = to_csv(rows)
else: text = to_sql(rows, a.table)
if a.out:
open(a.out, "w", encoding="utf-8").write(text)
print(f"wrote {a.count} rows -> {a.out}", file=sys.stderr)
else:
print(text)
if __name__ == "__main__":
main()
在任意项目里 claude,然后:
帮我造 20 条用户数据,JSON 格式,字段像这样:{"id":0,"name":"","email":"","phone":"","age":0,"province":"","created_at":""}
Claude 会:解析出 count=20、format=json → 写模板 → 执行 python3 ${CLAUDE_SKILL_DIR}/scripts/generate.py --template /tmp/mock-tpl.json --count 20 --format json → 用 ``json 块返回 20 条语义合理的数据(手机号是 1 开头的 11 位、邮箱形如 [email protected]、created_at 是 ISO 日期)。换一句"再要 200 条 SQL,表名 users"即可拿到INSERT INTO users ...` 语句。
若担心大批量生成把主会话上下文撑爆,加两行让技能在只读子代理里跑,结果汇总回主会话:
---
name: mock-data-factory
description: ...
context: fork
agent: general-purpose
allowed-tools: Bash(python3 *)
---
注意:context: fork 的正文会成为子代理的唯一任务,拿不到你的对话历史,所以正文必须自包含(这正是我们把字段名、count、format 都从 $ARGUMENTS/样例里取的原因)。
"看到技能被触发"≠"它做对了"。官方推荐装 skill-creator 跑对比评测:
/plugin install skill-creator@claude-plugins-official
/reload-plugins
装好后说 evaluate my mock-data-factory skill with skill-creator。它会:在 evals/evals.json 存测试用例 → 每个用例起一个干净子代理 → 对比"有技能 vs 无技能"的通过率、token、耗时 → 产出 benchmark.json,还能做两版技能的盲测 A/B、自动调 description 的触发命中率。
常见问题
${CLAUDE_SKILL_DIR} 而非写死的 ~ 路径;用 /skills 菜单确认技能已加载。generate.py 已显式 encoding="utf-8";若在 Windows powershell,可加 frontmatter shell: powershell 并设 CLAUDE_CODE_USE_POWERSHELL_TOOL=1。generate.py 里 random.seed(42) 已保证可复现,改种子即换一套数据。references/data-conventions.md 追加规则——这正是第三层渐进披露的价值。flowchart TD
U["用户提问:造 20 条用户 JSON<br/>或 /mock-data-factory 20 json"] --> M{"命中触发?"}
M -->|"description/when_to_use 命中<br/>或手动 /name"| L1["第一层 frontmatter 常驻"]
M -->|"未命中且非手动"| X["不加载,零成本"]
L1 --> L2["第二层:SKILL.md 正文进会话"]
L2 --> SUB["$ARGUMENTS / ${CLAUDE_SKILL_DIR} 字符串替换"]
SUB --> INJ["动态注入 !`git/python` 执行并内联输出"]
INJ --> L3{"正文引用支撑文件?"}
L3 -->|"是"| REF["第三层:按需读 references/*.md"]
L3 -->|"否"| RUN["直接进入指令"]
REF --> RUN
RUN --> SCRIPT["执行 scripts/generate.py"]
SCRIPT --> OUT["输出 JSON/CSV/SQL"]
OUT --> KEEP["正文常驻至会话结束;fork 模式则只回传摘要"]
style L1 fill:#e8f5e9
style L2 fill:#fff3e0
style REF fill:#e3f2fd
agentskills.io/skill-creation/evaluating-skills,evals/evals.json 的格式与"有/无技能"对照实验方法论。platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices,正文简洁性、何时该拆成子代理。subagents 文档的 Preload skills 一节),与本教程的 context: fork 互为反向用法。frontmatter 的 hooks 字段可为技能挂载生命周期钩子,实现"技能激活即自动收集上下文"。skill-creator 还能帮你做版本对比;企业级可用 managed settings 全量下发。| 维度 | 评估 |
|---|---|
| 适合的训练营类型 | AI 工程化 / 提示词工程 / DevOps 自动化训练营;也适合"AI 原生开发"通识课的进阶单元 |
| 学员基础 | 会用终端、写过简单脚本;不需要会 Python 进阶(脚本可直接复制) |
| 教学时长 | 建议 2 课时(约 90 分钟):第 1 课时讲三层披露 + frontmatter + 最小例子;第 2 课时手把手跑通 mock-data-factory 并做 skill-creator 评测 |
| 可裁剪点 | 时间紧可跳过 context: fork 与 skill-creator 章节;时间宽裕可加"把技能发布成插件给团队用"扩展 |
| 学员产出 | 每人带走一个自己的 Skill(不必是数据工厂,鼓励按岗位造:测试造用例、运维造巡检、前端造 mock) |
| 难度阶梯 | 本篇属 Round 2"从零造 Skill",承接安装入门、为后续 MCP 服务器(Round 3)、Agent 自动化(Round 4)打底 |
disable-model-invocation、paths、hooks 等)的逐项图解与取舍建议。[!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 等场景全覆盖。
