本教程面向已经摸过 Claude Code、想把自己的工程经验固化成可复用能力的开发者。我们将以"从零构建一个 Dockerfile 智能审计 Skill"为贯穿案例,完整走通 Skills 开发的全流程:理解三层渐进式披露架构、编写符合规范的 SKILL.md frontmatter、用动态上下文注入把实时文件内容喂给模型、用 references/ 沉淀知识库、用 scripts/ 封装确定性脚本、最后用 skill-creator 跑盲测 eval 量化效果。读完即可独立交付一个生产可用的 Skill。
/run 系列内建 Skill 与部分 frontmatter 字段依赖该版本)python3(Skill 内置审计脚本用纯标准库,无需额外依赖)动手之前必须想清楚一件事:Skill 的本质不是"一段提示词",而是"一本可按需翻阅的入职手册"。Anthropic 在工程博客里把它的加载机制描述为三个层级,这是整个设计的灵魂,写不好 Skill 几乎都是因为没理解它。
flowchart TB
A["启动会话"] --> B["第1层: 元数据<br/>name + description<br/>~100 词, 始终驻留"]
B --> C{"Claude 判断<br/>是否相关?"}
C -- 是 --> D["第2层: SKILL.md 正文<br/>触发时加载<br/>建议 1500-2000 词"]
C -- 否 --> E["不加载, 零成本"]
D --> F{"遇到细分场景?"}
F -- 是 --> G["第3层: 捆绑资源<br/>references/ scripts/ assets/<br/>按需加载, 容量近乎无限"]
F -- 否 --> H["直接执行任务"]
G --> H
三个层级对应三种成本:
| 层级 | 内容 | 加载时机 | 体积建议 |
|---|---|---|---|
| 第 1 层 | name + description |
会话启动即驻留系统提示词 | 控制在约 100 词,且与 when_to_use 合计不超过 1536 字符 |
| 第 2 层 | SKILL.md 正文 |
模型判定相关、或用户手动 /skill-name 触发时 |
1500–2000 词为佳,硬上限 500 行 |
| 第 3 层 | references/、scripts/、assets/ |
模型按需读取,脚本可不进上下文直接执行 | 单文件可 2000–5000+ 词,总量无上限 |
预期结果:理解这一点后你会明白——把所有规则塞进 SKILL.md 是反模式。SKILL.md 是目录和导航,不是百科全书。
常见问题:
- 为什么我写的 50 行规则模型经常不遵守? 多半是 description 没写好,模型压根没触发第 2 层,规则从未被加载。先解决触发,再谈内容。
- Skill 之间会互相挤压吗? 会。所有 Skill 的 description 共享一个"描述预算",默认是模型上下文窗口的 1%。装得太多时,用得少的会被截断甚至只剩名字。跑 /doctor 可以看到哪些被截断了。
官方 skill-development 模板强调,写 Skill 之前先收集 3–5 个真实用例,再反推需要哪些资源。我们围绕 Dockerfile 审计列举用例并分析:
| 真实用例 | 反推出的可复用资源 |
|---|---|
| "帮我审计这个 Dockerfile" | 需要一个确定性静态分析脚本 scripts/audit_dockerfile.py |
| "镜像太大怎么瘦身" | 需要最佳实践知识库 references/best-practices.md |
| "检查有没有用 root 跑" | 脚本里内置 USER/根用户检测规则 |
| "这个 Dockerfile 安全吗" | 需要 secrets、基础镜像漏洞的检测 + 知识库章节 |
| "按公司规范生成修复版" | 需要一份模板资产,但本期先聚焦审计,模板留作迭代 |
预期结果:得到一个清晰的资源清单——1 个脚本 + 1 个知识库文件 + 1 个精简的 SKILL.md。这决定了第三步的目录结构。
常见问题:不知道该写多少规则进知识库? 别预先穷举。先让 Skill 在真实 Dockerfile 上跑几轮,把模型反复"重新发现"的规则沉淀进 references/——这是"迭代沉淀"而非"预先设计"。
Skill 的命令名由它所在的目录决定(frontmatter 的 name 只影响列表里的显示名,不影响你敲的命令)。我们把它放在个人级目录,所有项目都能用:
mkdir -p ~/.claude/skills/container-lint/{references,scripts}
最终结构:
container-lint/
├── SKILL.md # 入口,必需
├── references/
│ └── best-practices.md # 详尽规则,按需加载
└── scripts/
└── audit_dockerfile.py # 确定性静态分析脚本
预期结果:目录建好后,Claude Code 的"实时变更检测"会立即发现它——在已运行的会话里新建/修改 ~/.claude/skills/ 下的 Skill 无需重启即可生效(仅对 SKILL.md 文本生效;若该目录同时是 plugin,hooks/MCP 变更需 /reload-plugins)。
常见问题:该放个人级还是项目级? 个人级(~/.claude/skills/)跨项目可用;项目级(.claude/skills/)随仓库走、可提交版本控制、团队共享。本例是通用工具,放个人级;如果是公司专有镜像规范,放项目级并提交 Git。
frontmatter 是第 1 层,写不好后面全白搭。三个铁律:
description + when_to_use 合计在 Skill 列表里被截断到 1536 字符,关键词靠前才不会被切掉。disable-model-invocation: true 只允许手动触发。---
name: container-lint
description: 审计 Dockerfile 与容器镜像构建配置,定位层冗余、缺失多阶段构建、root 运行、硬编码密钥、过大基础镜像等问题并给出可执行优化建议。当用户提到"审计 Dockerfile"、"优化镜像构建"、"镜像瘦身"、"检查容器最佳实践"、"分析 Dockerfile 安全"、"减小镜像体积"时触发。
when_to_use: 用于任何涉及 Dockerfile、Containerfile、镜像构建、CI 构建优化的请求。
allowed-tools: Read Glob Bash(python3 *)
argument-hint: [Dockerfile路径,留空则审计当前目录]
---
预期结果:开一个新会话问"帮我看看这个 Dockerfile 有没有问题",模型应自动命中本 Skill。也可以直接 /container-lint 手动调用,或 /container-lint ./api/Dockerfile 带参数。
常见问题:
- 模型总是不触发? 三个排查方向:①description 关键词和用户原话对不上;②确认它出现在"What skills are available?"里;③预算被挤占,跑 /doctor 看是否被截断,必要时把低优先级 Skill 在 skillOverrides 里设成 name-only。
- 触发得太频繁? 把 description 写得更窄,或直接 disable-model-invocation: true 改为纯手动。
正文(第 2 层)有两个杀手锏:动态上下文注入和字符串替换。
动态注入用 !`命令` 语法,Claude Code 会在把正文交给模型之前先执行命令、用输出替换占位符。这意味着模型看到的不是"去读文件",而是已经内联好的真实文件内容——决策更准、幻觉更少。
## 待审计内容
当前仓库根目录的 Dockerfile:
!`cat Dockerfile 2>/dev/null || echo '(未找到 Dockerfile)'`
参数指定:$ARGUMENTS
## 执行步骤
1. 调用审计脚本,对上述 Dockerfile 做确定性静态分析:
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/audit_dockerfile.py "$0"
```
2. 结合脚本输出,按 `references/best-practices.md` 中的规则逐条解读。
3. 输出三段:问题清单(按严重度排序)、每条的修复 diff、瘦身后预计体积下降。
4. 若发现 secrets 或 root 运行,标记为【高危】,必须修复。
5. 若用户要求"修复",直接产出改写后的 Dockerfile 并用 Edit 落盘。
关键细节:
${CLAUDE_SKILL_DIR} 会展开为本 Skill 目录的绝对路径,无论 Skill 装在个人级、项目级还是插件里都能正确定位脚本。$ARGUMENTS / $0 拿到命令参数;$0 是第一个参数(这里是 Dockerfile 路径)。预期结果:模型收到正文时,!cat Dockerfile` 已经被替换成真实内容,$0` 已被替换成用户传入的路径,它直接拿到了"带数据的任务书"。
常见问题:!`cmd` 没生效、被当字面量? 规则——! 必须出现在行首或紧跟空白符之后。写成 KEY=!cmd(`!` 前是等号)不会被识别。多行命令改用 ```! 围栏代码块。另外,注入只跑一遍,命令输出里再含 ! 不会被二次展开。
把"为什么这么修"的详尽规则放进 references/best-practices.md,让 SKILL.md 保持精简。这是第 3 层,模型只在需要时才读。
# Dockerfile 构建最佳实践
## 镜像体积
- 优先多阶段构建:编译期镜像与运行期镜像分离,最终镜像不含编译器/源码。
- 运行期选 slim/alpine 变体(如 python:3.12-slim),避免用 :latest。
- 合并 RUN 指令减少层数;同一 RUN 内用 `&&` 串联并清理 apt 缓存。
## 安全
- 必须出现非 root 的 `USER` 指令;避免以 root 运行。
- 禁止 `ADD` 远程 URL(用 curl + 校验替代);优先 COPY。
- 检测 ENV/ARG 中硬编码的密钥、token。
## 可复现性
- 固定基础镜像 tag 与版本;避免 latest。
- package 安装后 pin 版本。
## 缓存友好
- 先 COPY 依赖清单(package.json/requirements.txt)再 COPY 源码,最大化利用层缓存。
SKILL.md 里只留一句指针:"详细规则见 references/best-practices.md"。信息要么在 SKILL.md,要么在 references/,不要两边重复。
预期结果:SKILL.md 正文控制在约 1500 词内,模型触发后上下文清爽;需要解释某条规则时才去读知识库。
把"扫描逻辑"交给 Python 而非模型的 token 生成——确定性、可复现、零幻觉,且脚本可不进上下文直接执行,省 token。下面是审计脚本核心(纯标准库,可直接复制):
#!/usr/bin/env python3
"""Dockerfile 静态审计:输出问题清单,机器可读。"""
import re, sys, json
from pathlib import Path
RULES = {
"latest_tag": ("warn", "使用 :latest,不可复现", "固定具体版本 tag"),
"no_multistage": ("warn", "未见多阶段构建,镜像偏大", "拆分为 builder + runtime 两阶段"),
"root_user": ("high", "未声明非 root USER,以 root 运行", "添加 USER 指令切换非特权用户"),
"uses_add": ("warn", "使用 ADD(可能拉远程或解压)", "改用 COPY 显式控制"),
"hardcoded_secret":("high", "疑似硬编码密钥/Token", "改用运行时注入或构建 secret"),
"apt_no_clean": ("info", "apt install 未在同一层清理缓存", "结尾追加 && rm -rf /var/lib/apt/lists/*"),
}
SECRET_PAT = re.compile(r"(?i)(password|passwd|secret|token|api[_-]?key)\s*[:=]\s*\S+")
def audit(text: str):
lines = text.splitlines()
findings = []
has_user = any(l.strip().upper().startswith("USER") for l in lines)
has_from_as = bool(re.search(r"^FROM\s+\S+\s+AS\s+\S+", text, re.I | re.M))
for i, l in enumerate(lines, 1):
s = l.strip()
if re.match(r"FROM\s+\S+:latest", s, re.I): findings.append((i, "latest_tag"))
if re.match(r"^ADD\s", s, re.I): findings.append((i, "uses_add"))
if re.match(r"^RUN\s+apt", s, re.I) and "rm -rf" not in s:
findings.append((i, "apt_no_clean"))
if SECRET_PAT.search(s): findings.append((i, "hardcoded_secret"))
if not has_user: findings.append((0, "root_user"))
if not has_from_as: findings.append((0, "no_multistage"))
return [
{"line": ln, "severity": RULES[k][0], "issue": RULES[k][1], "fix": RULES[k][2]}
for ln, k in findings
]
if __name__ == "__main__":
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("Dockerfile")
text = target.read_text(encoding="utf-8") if target.exists() else ""
if not text:
print(json.dumps({"error": f"找不到 {target}"}, ensure_ascii=False)); sys.exit(1)
print(json.dumps({"file": str(target), "findings": audit(text)}, ensure_ascii=False, indent=2))
预期结果:对一个故意写坏的 Dockerfile,脚本稳定输出 JSON 问题清单,每次结果一致——这就是"确定性"。模型只需解读这份结构化结果,而非自己逐行猜。
常见问题:脚本要不要让模型读进上下文? 默认不读、直接执行最省 token;只在需要"按环境改脚本"时才让模型读取并打补丁。
allowed-tools 在 Skill 激活期间预批准列出的工具,免去每次确认。本例需要读文件和跑 python:
allowed-tools: Read Glob Bash(python3 *)
注意它"预批准"而非"限制"——未列出的工具仍可调用,只是会照常走权限流程。要真正禁用某工具,用 disallowed-tools。提醒:项目级 Skill 的 allowed-tools 在你信任该工作区后才生效,克隆陌生仓库前务必先审一眼 .claude/skills/,因为 Skill 能给自己授予较宽的工具权限。
会触发 ≠ 好用。官方文档反复强调要用对照基线验证两点:①该触发的请求是否触发了;②触发后输出是否达标。
手动验证(三步):
/container-lint ./Dockerfile,看参数 $0 是否正确传入。skillOverrides 设为 off 禁用"的新会话里跑,对比问题发现率。用 skill-creator 跑量化 eval:
/plugin install skill-creator@claude-plugins-official
装好后让 Claude 跑评估:evaluate my container-lint skill with skill-creator。它会:
evals/evals.json 里存测试用例(prompt + 输入文件 + 期望行为)grading.json 的断言判 pass/failbenchmark.json,对比"启用 vs 禁用"的通过率提升与开销预期结果:你拿到一份量化报告——比如"启用后高危问题检出率从 40% 升到 95%,单次 token 增加 2k",有数据才敢说 Skill 真的好。
常见问题:Skill 用着用着"失效"了? 大概率是上下文压缩导致的——压缩后会重新挂载每个 Skill 最近一次调用(合计 25000 token 预算),调用太多时较早的可能被整体丢弃。压缩后重新 /container-lint 一次即可恢复全文。
把上述产物拼起来。下面是一个典型"坏 Dockerfile"和本 Skill 的完整交付体验。
输入 ./Dockerfile:
FROM python:latest
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
RUN apt-get update && apt-get install -y curl
ENV API_KEY=sk-1234567890abcdef
CMD ["python", "main.py"]
用户敲 /container-lint ./Dockerfile,脚本输出(节选):
{
"findings": [
{"line": 1, "severity": "warn", "issue": "使用 :latest,不可复现", "fix": "固定具体版本 tag"},
{"line": 2, "severity": "warn", "issue": "使用 ADD", "fix": "改用 COPY"},
{"line": 6, "severity": "high", "issue": "疑似硬编码密钥/Token", "fix": "改用运行时注入或构建 secret"},
{"line": 0, "severity": "high", "issue": "未声明非 root USER,以 root 运行", "fix": "添加 USER 指令"},
{"line": 0, "severity": "warn", "issue": "未见多阶段构建", "fix": "拆分为 builder + runtime"}
]
}
模型结合知识库给出修复版:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim AS runtime
RUN useradd -m appuser
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
USER appuser
CMD ["python", "main.py"]
修复点命中:tag 固定、多阶段构建、COPY 替代 ADD、非 root 用户、移除硬编码密钥。预估体积从 ~1GB 降到 ~150MB。
这套 Skill 为何能成立,可以用一张架构图说清:
flowchart LR
U["用户: /container-lint Dockerfile"] --> S["SKILL.md 触发"]
S -->|"!cat Dockerfile 注入"| D["真实 Dockerfile 内容"]
S -->|"\$0 = 路径"| P["audit_dockerfile.py 执行"]
P --> O["结构化问题清单 JSON"]
D --> M["模型解读"]
O --> M
M -->|"查规则"| R["references/best-practices.md<br/>按需加载"]
M --> OUT["问题清单 + 修复 diff + 预估瘦身"]
code.claude.com/docs/en/skills —— frontmatter 字段全集、动态注入、subagent 执行、权限控制的最权威出处。anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills —— 三层渐进式披露的设计动机,含 PDF Skill 实例。agentskills.io —— 跨工具可移植的规范与 eval 工作流。github.com/anthropics/claude-code 仓库 plugins/plugin-dev/skills/skill-development/SKILL.md —— 6 步创建流程与写作风格约束。本文由「51学通信」原创整理,站长爱卫生(微信:gprshome201101)持续输出通信与 AI 工程实战内容,欢迎交流。
[!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 等场景全覆盖。
