本教程面向已经会写「一个 SKILL.md 文件搞定一切」、但想把技能做得更稳健、更省 token、更易维护的开发者。你将掌握 Claude Code Skills 的三层渐进式披露(Progressive Disclosure)架构,并通过一个完整的、可直接运行的依赖许可证合规扫描 Skill(dep-license-checker),学会何时把逻辑拆成 bundled 脚本、何时放进按需加载的 references、何时用 assets 模板,最终完成「构建 → 安装 → 触发测试 → eval 验证 → 打包分发」的完整生命周期。适合训练营中「进阶工程化」阶段的学员,预计 2.5–3 小时。
bash、会读 YAML 与 Markdown很多入门教程教的是「最小可用技能」:一个目录、一个 SKILL.md、几十行指令。这在原型阶段够用,但一旦技能变复杂(分类规则、评分体系、输出模板、几百行分析逻辑),单一文件会出现三个致命问题:
Claude Code 官方的设计哲学叫渐进式披露:技能按需分层加载,每一层只在前一层不够用时才进入上下文。理解它,是写出「生产级」技能的分水岭。
官方文档把 Skills 的加载明确分为三个阶段,每一层都在为 token 效率服务:
| 阶段 | 触发时机 | 进入上下文的内容 | 典型开销 |
|---|---|---|---|
| 第 1 层 · 元数据扫描 | 会话启动时 | 仅 name + description |
每个技能约 30–50 tokens |
| 第 2 层 · 全文加载 | 模型判定相关后 | SKILL.md 正文(不含子文件) | 通常 < 5000 tokens |
| 第 3 层 · 资源执行 | 实际需要时 | 运行脚本(仅输出进上下文)、按需读 references/assets | 脚本代码本身永不进入上下文 |
关键认知有两条:
references/xxx.md」、且当前任务真的需要时,它们才被加载。这让你可以放很厚的参考文档而不拖累日常请求。注意:会话启动时 Claude 才扫描技能目录,修改任何技能文件后必须重启 Claude Code 才能生效。这是排查「改了不生效」问题的第一站。
flowchart TD
A[会话启动] --> B[第1层: 扫描所有 SKILL.md 元数据<br/>name+description, 各30-50 tokens]
B --> C[用户提问]
C --> D{模型语义匹配}
D -->|相关| E[第2层: 加载本技能 SKILL.md 正文]
D -->|无关| Z[技能保持休眠]
E --> F{需要确定性计算?}
F -->|是| G[第3层: 执行 bundled 脚本<br/>代码不进上下文, 仅输出]
F -->|否| H{需要查细则?}
H -->|是| I[第3层: 按需读 references/*.md]
H -->|否| J[生成结果]
G --> J
I --> J
J --> K[填充 assets 模板输出报告]
我们的目标技能 dep-license-checker:用户问「帮我检查这个项目的依赖许可证合规性」时自动触发——读取 package.json / requirements.txt / go.mod 里的依赖,用确定性脚本分类为宽松型(permissive)/ 左版权型(copyleft)/ 未知型,标记违规项,最后套用模板生成 Markdown 合规报告。
它的目录结构如下,每一层都对应渐进式披露的一个角色:
dep-license-checker/
├── SKILL.md # 第2层入口:指令,<500行
├── scripts/
│ └── classify.py # 第3层:确定性分类器,纯标准库
├── references/
│ ├── license_knowledge.md # 第3层:SPDX 分类细则,按需加载
│ └── policy_default.md # 第3层:默认合规策略,按需加载
└── assets/
└── compliance_report.md # 第3层:报告模板,按需填充
设计原则一句话:「能用代码算的就别让模型猜,细则文档只在要用时才加载,输出靠模板而非现编。」
SKILL.md 由 YAML frontmatter 和 Markdown 正文组成。frontmatter 有两个必填字段,校验很严格——写错会导致技能静默加载失败(不报错,就是不出现在索引里):
name:最长 64 字符,只能含小写字母、数字、连字符;不能含 anthropic、claude 等保留词,不能含 XML 标签。description:最长 1024 字符,非空。这是技能能否被触发的命门——模型靠它做语义匹配。写作诀窍:description 必须同时回答「做什么」和「何时用」,并塞进用户可能脱口而出的触发词。
---
name: dep-license-checker
description: 扫描项目的依赖清单(package.json / requirements.txt / go.mod / Cargo.toml),用确定性脚本将每个依赖的许可证分类为宽松型 / 左版权型 / 未知型,标记违反「仅允许宽松许可证」策略的依赖,并生成 Markdown 合规报告。当用户提到「许可证合规」「license compliance」「检查依赖 license」「GPL/AGPL 风险」「开源许可审查」「引入第三方依赖前检查」时使用。
---
# 依赖许可证合规检查
## 何时触发
用户要把第三方依赖引入项目、或要做开源合规审查、或担心左版权(GPL/AGPL/LGPL/MPL)污染时。
## 执行步骤(务必按顺序)
1. 用 Glob/Grep 找到项目根下的清单文件:`package.json`、`requirements.txt`、`go.mod`、`Cargo.toml`。没有就停止并提示用户。
2. 运行确定性分类器(**首选,代码不进上下文**):
`python3 scripts/classify.py <清单文件路径>`
解析它的 JSON 输出,不要自己用文本去分类。
3. 仅当存在 `copyleft` 或 `unknown` 项、且需要向用户解释某许可证含义时,再去读 `references/license_knowledge.md`。否则跳过以省 token。
4. 仅当用户询问「为什么默认禁止左版权」或要改策略时,才读 `references/policy_default.md`。
5. 把脚本输出填进 `assets/compliance_report.md` 模板,生成最终报告。违规项必须高亮,并给出「替换为宽松许可的替代品」建议。
## 边界
- 不联网。本技能基于本地清单与内置的常见包许可证映射做静态判断。
- 对不在映射表里的包标记为 `unknown`,请用户人工确认,不要臆测许可证。
注意正文里大量出现「仅当……才读……」——这正是手动编排渐进式披露的写法:你明确告诉模型第 3 层资源何时该被唤醒。
把分类逻辑写成纯标准库 Python,保证零依赖、可单跑、输出 JSON。脚本体量可以很大,但只有最终 JSON 进入上下文。下面是可直接保存运行的 scripts/classify.py:
#!/usr/bin/env python3
"""依赖许可证确定性分类器。纯标准库,输出 JSON。
分类口径:
permissive = MIT / Apache-2.0 / BSD-* / ISC
copyleft = GPL / AGPL / LGPL / MPL
unknown = 映射表未覆盖(需人工确认)
退出码:存在 copyleft 违规返回 1,否则 0。
"""
import json, re, sys
from pathlib import Path
# 常见包 -> SPDX 许可证(演示用,可按团队实际扩展)
NPM = {
"react": "MIT", "react-dom": "MIT", "vue": "MIT", "lodash": "MIT",
"express": "MIT", "axios": "MIT", "moment": "MIT", "chalk": "MIT",
"jquery": "MIT", "typescript": "Apache-2.0", "rxjs": "Apache-2.0",
"elasticsearch": "Apache-2.0", "left-pad": "WTFPL",
"chart.js": "MIT", "bluebird": "MIT",
}
PY = {
"requests": "Apache-2.0", "django": "BSD-3-Clause", "flask": "BSD-3-Clause",
"numpy": "BSD-3-Clause", "pandas": "BSD-3-Clause", "fastapi": "MIT",
"celery": "BSD-3-Clause", "scipy": "BSD-3-Clause", "pyyaml": "MIT",
"gunicorn": "MIT", "uvicorn": "BSD-3-Clause",
}
PERMISSIVE = {"MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause",
"ISC", "0BSD", "WTFPL", "Unlicense"}
def category(license_id: str) -> str:
if not license_id:
return "unknown"
lid = license_id.upper()
if lid in {x.upper() for x in PERMISSIVE}:
return "permissive"
if re.search(r"\b(GPL|AGPL|LGPL|MPL)\b", lid):
return "copyleft"
return "unknown"
def parse_package_json(text):
data = json.loads(text)
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
return [(name, NPM.get(name.lower(), "")) for name in deps]
def parse_requirements(text):
out = []
for line in text.splitlines():
line = line.split("#")[0].strip()
m = re.match(r"^([A-Za-z0-9_.\-]+)", line)
if m:
name = m.group(1).lower()
out.append((name, PY.get(name, "")))
return out
def parse_go_mod(text):
out = []
for line in text.splitlines():
m = re.match(r"^\s*(require\s+)?([^\s]+)\s+v[\d.]+", line)
if m:
pkg = m.group(2).lower()
# 粗映射:含 / 的取最后一段
last = pkg.split("/")[-1]
out.append((pkg, NPM.get(last) or PY.get(last) or ""))
return out
PARSERS = {
"package.json": parse_package_json,
"requirements.txt": parse_requirements,
"go.mod": parse_go_mod,
}
def main():
path = Path(sys.argv[1]) if len(sys.argv) > 1 else None
if not path or not path.exists():
print(json.dumps({"error": f"未找到清单文件: {path}"}))
sys.exit(2)
parser = PARSERS.get(path.name)
if not parser:
print(json.dumps({"error": f"不支持的清单类型: {path.name}"}))
sys.exit(2)
result = {"permissive": [], "copyleft": [], "unknown": []}
for name, lic in parser(path.read_text(encoding="utf-8")):
cat = category(lic)
result[cat].append({"name": name, "license": lic or "未声明/未知"})
violations = result["copyleft"]
result["summary"] = {
"total": sum(len(result[k]) for k in ("permissive", "copyleft", "unknown")),
"violations": len(violations),
"policy": "仅允许宽松许可证 (MIT/Apache/BSD/ISC)",
"verdict": "不合规" if violations else "合规",
}
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(1 if violations else 0)
if __name__ == "__main__":
main()
预期结果(针对一个含 react、lodash、elasticsearch 的 package.json):
{
"permissive": [
{"name": "react", "license": "MIT"},
{"name": "lodash", "license": "MIT"},
{"name": "elasticsearch", "license": "Apache-2.0"}
],
"copyleft": [],
"unknown": [],
"summary": {"total": 3, "violations": 0,
"policy": "仅允许宽松许可证 (MIT/Apache/BSD/ISC)",
"verdict": "合规"}
}
一旦混入一个 GPL 依赖,copyleft 列表非空、退出码变 1、verdict 变「不合规」——确定性、可复现、零幻觉。
这两份文件默认休眠,只有 SKILL.md 指引、且任务需要时才被读取。可以写得详尽而不怕拖累日常请求。
references/license_knowledge.md(节选):
# 许可证分类细则
## 宽松型(permissive)——通常可放心引入
- MIT / BSD-2 / BSD-3 / ISC / 0BSD:几乎无义务,保留版权声明即可。
- Apache-2.0:需保留 NOTICE 文件,含专利授权条款,企业友好。
## 左版权型(copyleft)——触发「传染」,需法务介入
- GPL:衍生作品必须同样以 GPL 开源。
- AGPL:网络服务也触发开源义务,SaaS 场景高风险。
- LGPL:可作为库链接而不传染,但修改库本身需开源。
- MPL-2.0:文件级 copyleft,比 GPL 弱。
## 未知型(unknown)
映射表未覆盖。处置:去 npm/PyPI 页面或源码 LICENSE 文件人工核实,
**不要由模型猜测**。
references/policy_default.md(节选):
# 默认合规策略:仅宽松许可证
- 允许:MIT、Apache-2.0、BSD-2/3、ISC、0BSD、Unlicense
- 禁止:GPL、AGPL、LGPL、MPL(除非走例外审批)
- 未知:阻断引入,待人工确认后更新 scripts/classify.py 的映射表
- 若团队策略不同(如允许 LGPL),在此文件修改即可,无需动脚本
assets/compliance_report.md(节选)——让模型「填空」而非「从零写」,输出更稳定一致:
# 依赖许可证合规报告
- 检查范围:{{manifest_file}}
- 依赖总数:{{total}}
- 违规项数:{{violations}}
- 结论:**{{verdict}}**
## 违规依赖(需处理)
{{#each copyleft}}
- ⚠️ `{{name}}` —— 许可证 `{{license}}`,建议替换为等价的宽松许可替代品
{{/each}}
## 合规依赖
{{#each permissive}}
- ✅ `{{name}}` —— `{{license}}`
{{/each}}
## 待人工确认
{{#each unknown}}
- ❓ `{{name}}` —— `{{license}}`
{{/each}}
把整个目录放到技能发现路径之一。三种位置按优先级覆盖(同名时 personal > project > plugin):
# 个人级(跨项目可用)
mkdir -p ~/.claude/skills && cp -r dep-license-checker ~/.claude/skills/
# 或项目级(随 git 共享给团队)
mkdir -p .claude/skills && cp -r dep-license-checker .claude/skills/
重启 Claude Code(发现扫描只发生在启动时),然后在终端对一个小项目说:
「帮我检查一下这个项目依赖的许可证合规性。」
预期:Claude 不需要你输入任何 / 命令——它读到 dep-license-checker 的 description 后自动语义匹配并加载全文,接着调用 scripts/classify.py,最后套模板出报告。这就是「模型调用(model-invoked)」模式:技能是被「想起来」的,不是被「点开」的。
自检要点:若不触发,99% 是 description 不够具体——对照第二条,补足触发词。
生产级技能必须可量化。用内置的 skill-creator 技能为 dep-license-checker 生成评测集:准备 5–8 条「应触发」的正例(如「这个 npm 包能不能引入」「AGPL 的依赖有风险吗」)和 5–8 条「不应触发」的负例(如「帮我写个单元测试」「重构这个函数」),跑触发准确率;再准备带已知许可证的样例工程,跑分类正确率与退出码。把 eval 结果写进技能 README,作为回归基线。每次改 description 或脚本后重跑,确保没有劣化。
.claude/skills/ 提交 git,clone 即得,零额外安装。dep-license-checker 放进 plugin 的 skills/ 目录,用户安装 plugin 即自动发现、卸载即移除。flowchart LR
subgraph 构建
S1[SKILL.md] --- S2[scripts/classify.py]
S2 --- S3[references/]
S3 --- S4[assets/]
end
S4 --> R[重启 Claude Code]
R --> D[启动扫描<br/>仅加载 name+description]
D --> M{用户提问<br/>模型语义匹配}
M -->|命中| L2[加载 SKILL.md 正文]
L2 --> EXE[执行 classify.py<br/>仅 JSON 输出进上下文]
EXE --> REF{需要细则?}
REF -->|是| RD[按需读 references]
REF -->|否| TPL
RD --> TPL[填 assets 模板]
TPL --> OUT[Markdown 合规报告]
EV[skill-creator eval<br/>触发率+分类正确率] -.回归.-> S1
| 现象 | 根因 | 解决 |
|---|---|---|
| 技能装了却不触发 | description 太泛(如「分析数据」) | 补「做什么 + 何时用 + 触发词」,参照步骤 1 |
| 技能完全不出现 | frontmatter YAML 非法 / name 含保留词或大写 | 校验 --- 三连、name 全小写连字符;用 debug 启动看加载错误 |
| 改了文件没生效 | 启动后才扫描 | 重启 Claude Code |
| 分类结果不稳定 | 让模型用文本分类而非跑脚本 | 在 SKILL.md 强制「先跑 classify.py 再解析 JSON」 |
| 上下文莫名变满 | references 被无条件读取 | 把「仅当……才读」写进正文,减少默认加载 |
| 依赖不在映射表 | 设计如此 | 标 unknown 让用户人工确认,并提示扩充映射表 |
| 大团队技能过多变慢 | 超 ~100 个技能,元数据开销显著 | 精简到「少而精」,删不用、禁用不常用 |
policy_default.md 并重跑 eval)约 3.5 小时。license_db.json;② 增加 Cargo.toml 解析器;③ 用 hooks 在每次 git commit 前自动跑该技能做门禁(衔接 Hooks 训练模块)。code.claude.com/docs/en/skillsclaude.com/docs/skills/overview~/.claude/skills/、.claude/skills/、plugin)、管理与共享的官方说明。[!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 等场景全覆盖。
