<!-- title: 从零造无障碍审查Skill -->

## 概述

本教程带你从零构建一个 Claude Code 自定义 Skill——无障碍（a11y）审查技能，学会一套可复用的 Skill 开发方法论：YAML frontmatter 的精确约束、渐进式披露（progressive disclosure）三层结构、捆绑脚本「执行而非载入」的设计、评估驱动的迭代闭环。适合已能跑通 Claude Code、想把团队规范固化为可复用能力的开发者、技术 Lead 与 DevEx 工程师。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常运行），会用 `/skills` 查看已装技能。
- 一台带 Python 3.8+ 的机器（本教程脚本仅用标准库，零外部依赖）。
- 理解 YAML 与 Markdown 语法，能读懂简单 Python。
- 手边有一个含 HTML/JSX 模板的前端项目用于实战验证（没有也行，文末提供测试样本）。

---

## 一、先搞懂 Skill 到底是什么

官方定义一句话：**Skill 就是一个目录，里面至少有一个 `SKILL.md` 文件，外加可选的脚本与参考资料**。它不是插件、不是 API，而是「写给新员工的入职手册」——把你的程序性知识打包成文件夹，让 Claude 在需要时动态发现并加载。

理解 Skill 的关键是**渐进式披露三层结构**，它决定了你的 Skill 怎么占用上下文窗口：

- **第一层：元数据预载。** 启动时，所有已装 Skill 的 `name` 与 `description` 会被注入系统提示词。这一层永远在线，但极省 token——这是 Claude 判断「要不要触发这个 Skill」的唯一依据。
- **第二层：正文按需加载。** 当 Claude 认为某 Skill 与当前任务相关，它会用 Bash/Read 把整个 `SKILL.md` 读进上下文。
- **第三层：引用文件进一步加载。** `SKILL.md` 里被点名的 `references/`、`scripts/` 等文件，只在真正需要时才被读入或执行。未被读到的文件**消耗零 token**。

这套机制让 Skill 的知识量「事实上无上限」——你可以塞进完整的 API 文档、大段规则库，只要它们被合理地分层，就不会污染每一次对话。

> 关键认知：上下文窗口是「公共资源」。Skill 的元数据和你每次对话共享这块预算，所以 `name`/`description` 必须精准，正文必须精炼（官方建议 **SKILL.md 正文控制在 500 行以内**）。

## 二、规划目录结构：先画图再写代码

我们要造的 `auditing-accessibility` Skill 采用「高层指南 + 域内分文件」模式：核心 `SKILL.md` 保持精简，把规则清单拆到 `references/`，把确定性检查逻辑做成可执行脚本放进 `scripts/`。

```text
auditing-accessibility/
├── SKILL.md              # 主指令，触发后加载（精简）
├── references/
│   └── rules.md          # WCAG 规则目录与修复建议（按需加载）
└── scripts/
    └── scan_a11y.py      # 无障碍扫描器（执行，不载入上下文）
```

这样的分层带来三个好处：扫描脚本被「执行」而非「读入」（只有它的 stdout 输出消耗 token），规则文档只在要解释某条规则时才读，且 `SKILL.md` 永远保持轻量。

## 三、步骤 1：写对 frontmatter（这是触发命中的命根子）

### 操作说明

在 `auditing-accessibility/SKILL.md` 顶部写 YAML frontmatter。**只允许两个字段：`name` 与 `description`**，且约束极严：

- `name`：最长 64 字符，只能含小写字母、数字、连字符；禁用 XML 标签；**禁用保留词 `anthropic`、`claude`**。官方推荐动名词形式（gerund），如 `auditing-accessibility`。
- `description`：最长 1024 字符，非空，禁用 XML 标签，**必须第三人称**写，且同时包含「做什么」和「何时用」。

```yaml
---
name: auditing-accessibility
description: Scans HTML, JSX, and Vue templates for common accessibility (a11y) defects — missing alt text, unlabeled inputs, empty interactive elements, duplicate ids, and invalid tabindex. Use when reviewing frontend code for WCAG compliance, before merging UI changes, or when the user mentions accessibility, a11y, WCAG, or screen-reader compatibility.
---
```

### 预期结果

把目录放进 `.claude/skills/`（项目级）或 `~/.claude/skills/`（用户级），重启会话，`/skills` 能看到该条目，描述被完整注入系统提示词。随后你说「帮我查一下这个登录页有没有无障碍问题」，Claude 会自动触发该 Skill。

### 常见问题与解决

- **不触发 / 误触发：** 八成是 description 的锅。检查两点——是否用了第一/第二人称（`I can help`、`You can use` 必须改成 `Scans... / Use when...`）；是否漏写「何时用」的触发词（用户原话里出现的关键词，如 a11y、WCAG、屏幕阅读器）。
- **`name` 报校验错：** 大写、下划线、空格、点号都不允许；`claude-a11y` 这种带保留词的名字会被拒，改成 `auditing-accessibility`。
- **和别的 Skill 撞描述：** 当系统里 Skill 超过 100 个时，描述必须给足差异化关键词，否则 Claude 选不准。

## 四、步骤 2：写精简的 SKILL.md 正文（高层指南）

### 操作说明

正文是「第二层」，遵循「假设 Claude 已经很聪明」——只写它不知道的东西。我们用模板模式 + 工作流清单，把确定性动作交给脚本。

```markdown
# Auditing Accessibility

## 何时使用
当用户需要审查前端无障碍缺陷、检查 WCAG 合规性、或在 UI 改动合入前做门禁时使用。

## 快速流程（plan → validate → execute）
复制下方清单并在执行中勾选：
\`\`\`
a11y Review Progress:
- [ ] 1. 扫描目标文件，生成 findings.json
- [ ] 2. 汇总缺陷类型与数量
- [ ] 3. 逐条修复
- [ ] 4. 重新扫描，确认 finding 数为 0 或可接受
\`\`\`

## 第 1 步：扫描（执行脚本，不要读它）
对单个文件或目录运行扫描器，输出结构化 JSON：
\`\`\`bash
python scripts/scan_a11y.py path/to/templates --format json > findings.json
\`\`\`
脚本只输出 findings，不消耗上下文 token。

## 第 2 步：解释缺陷
当用户问「这条为什么算缺陷 / 怎么修」时，阅读 [references/rules.md](references/rules.md)
中对应规则段，给出 WCAG 条目编号与修复示例。未问到就别加载。

## 第 3 步：修复后复扫（反馈闭环）
修复完必须重跑第 1 步，对比 findings.json 中 finding 数下降。任然报错就回到修复。

## 输出约定
向用户汇报时使用统一格式：
- 严重度（critical/major/minor）
- 文件:行号
- 规则编号（见 rules.md）
- 一行修复建议
```

### 预期结果

Claude 触发后读入这段正文（不到 50 行），立刻知道「先跑脚本 → 看 JSON → 需要解释再读 rules.md → 修复后复扫」。它**不会**在第一轮就加载 rules.md，省下大量 token。

### 常见问题与解决

- **正文太长被嫌弃：** 超过 500 行就拆。本例把规则全挪进 `references/rules.md` 就是为此。
- **嵌套引用坑：** 别让 rules.md 里再链接到另一个 `details.md`。官方明确警告：多层嵌套时 Claude 可能只 `head -100` 预览，导致信息不全。**所有引用文件直接挂在 SKILL.md 下一层**。
- **「读脚本」还是「执行脚本」要写清：** 这是最易混淆的点。本例明确写「执行脚本，不要读它」。若某脚本逻辑复杂需当文档看，则写「See scan_a11y.py for the heuristic」，让 Claude 读源码而非跑它。

## 五、步骤 3：写「不踢皮球」的捆绑脚本

### 操作说明

把确定性检查交给 Python，只依赖标准库（`html.parser`），这样在任何环境都能跑，零安装。脚本要遵循官方两条铁律：**「解决而非踢皮球」（solve, don't punt）**——文件不存在就报清楚、给出可读错误；**禁用「魔法常数」**——每个阈值都写注释说明理由。

```python
# scripts/scan_a11y.py
#!/usr/bin/env python3
"""Accessibility scanner for HTML/JSX/Vue templates. Stdlib-only."""
import argparse, json, sys, glob, os
from html.parser import HTMLParser

# 仅检测这几类高频、可静态判定的问题，避免假阳性爆炸
RULES = {
    "img-alt":         ("major",   "img 缺少 alt 属性"),
    "input-label":     ("major",   "表单控件缺少关联 label"),
    "empty-interactive": ("critical", "button/a 无可访问文本"),
    "duplicate-id":    ("major",   "id 在同一文档内重复"),
    "bad-tabindex":    ("minor",   "tabindex > 0 破坏自然焦点顺序"),
    "missing-lang":    ("minor",   "html 标签缺少 lang 属性"),
}

class A11yParser(HTMLParser):
    def __init__(self, path):
        super().__init__(convert_charrefs=True)
        self.path = path
        self.findings = []
        self._ids = set()
        self._dup_ids = set()
        self._saw_html = False
        self._html_has_lang = False

    def handle_starttag(self, tag, attrs):
        d = dict(attrs)
        line = self.getpos()[0]

        if tag == "html":
            self._saw_html = True
            if "lang" in d:
                self._html_has_lang = True

        if tag == "img" and "alt" not in d:
            self.findings.append(self._mk("img-alt", line, tag))

        if tag in ("button", "a"):
            # 无文本载体（无 aria-label、无 title、且空内容稍后判断）
            if "aria-label" not in d and "title" not in d:
                # 记录待内容处理时核验，这里先标记可疑
                self.findings.append(self._mk("empty-interactive", line, tag))

        if tag in ("input", "select", "textarea"):
            labelled = ("aria-label" in d or "aria-labelledby" in d or "id" in d)
            if not labelled and d.get("type") != "hidden":
                self.findings.append(self._mk("input-label", line, tag))

        if "tabindex" in d:
            try:
                if int(d["tabindex"]) > 0:
                    self.findings.append(self._mk("bad-tabindex", line, tag))
            except ValueError:
                pass  # 非数字 tabindex 忽略，不抛错中断扫描

        if "id" in d:
            if d["id"] in self._ids:
                self._dup_ids.add(d["id"])
            self._ids.add(d["id"])

    def finalize(self):
        if self._saw_html and not self._html_has_lang:
            self.findings.append(self._mk("missing-lang", 1, "html"))
        for i in self._dup_ids:
            self.findings.append(self._mk("duplicate-id", 0, f"id={i}"))
        return self.findings

    def _mk(self, rule, line, ctx):
        sev, msg = RULES[rule]
        return {"rule": rule, "severity": sev, "line": line,
                "file": self.path, "context": ctx, "message": msg}

def collect(paths):
    files = []
    for p in paths:
        if os.path.isdir(p):
            files += glob.glob(os.path.join(p, "**", "*.html"), recursive=True)
            files += glob.glob(os.path.join(p, "**", "*.jsx"), recursive=True)
            files += glob.glob(os.path.join(p, "**", "*.vue"), recursive=True)
        elif os.path.isfile(p):
            files.append(p)
    # 目录无文件时给清晰提示，而非静默返回空
    if not files:
        sys.stderr.write(f"[warn] no html/jsx/vue files under: {paths}\n")
    return sorted(set(files))

def main():
    ap = argparse.ArgumentParser(description="Static a11y scanner (stdlib only).")
    ap.add_argument("paths", nargs="+")
    ap.add_argument("--format", choices=["json", "text"], default="json")
    args = ap.parse_args()

    all_findings = []
    for f in collect(args.paths):
        try:
            with open(f, encoding="utf-8") as fh:
                src = fh.read()
        except (FileNotFoundError, PermissionError) as e:
            # 解决而非踢皮球：跳过坏文件并记录，不中断整批
            sys.stderr.write(f"[warn] skip {f}: {e}\n")
            continue
        p = A11yParser(f)
        p.feed(src)
        all_findings += p.finalize()

    if args.format == "json":
        print(json.dumps({"total": len(all_findings), "findings": all_findings},
                         ensure_ascii=False, indent=2))
    else:
        for fnd in all_findings:
            print(f"[{fnd['severity']}] {fnd['file']}:{fnd['line']} "
                  f"{fnd['rule']} — {fnd['message']}")

if __name__ == "__main__":
    main()
```

### 预期结果

```bash
python scripts/scan_a11y.py templates/login.html --format text
# [critical] templates/login.html:14 empty-interactive — button/a 无可访问文本
# [major]    templates/login.html:7  input-label — 表单控件缺少关联 label
```

JSON 输出是「可验证的中间产物」——Claude 修复后重跑，能机器判定 finding 数是否下降，形成闭环。

### 常见问题与解决

- **脚本依赖外部库在 API 环境跑不起来：** 官方明确——claude.ai 可联网装包，但 Claude API 运行环境**无网络、不能装包**。所以坚持只用标准库，或把依赖写进 SKILL.md 并标注「需 pip install」。
- **魔法常数让人困惑：** 上例的「规则集合」和「tabindex > 0」都有语义注释。任何阈值都该解释来源（WCAG 2.1 SC 2.4.3 等）。
- **路径用了反斜杠：** 即使在 Windows 上也必须用正斜杠 `scripts/scan_a11y.py`，反斜杠在 Unix 上直接报错。

## 六、步骤 4：写规则参考文件（带目录的第三层）

`references/rules.md` 超过 100 行，按官方要求**顶部放目录**，让 Claude 即使 `head` 预览也能看到全貌：

```markdown
# Accessibility Rules Reference

## 目录
- A. 图片与媒体（img-alt）
- B. 表单可访问性（input-label）
- C. 可交互元素文本（empty-interactive）
- D. 焦点顺序（bad-tabindex）
- E. 文档语言（missing-lang）
- F. 唯一标识（duplicate-id）

## A. 图片与媒体（img-alt）— WCAG 1.1.1
所有 <img> 必须有 alt。装饰性图片用 alt=""。
示例： <img src="logo.svg" alt="ACME 首页">  ✅
       <img src="spacer.gif" alt="">          ✅ 装饰性
       <img src="banner.jpg">                 ❌ 缺 alt
...（B~F 同结构展开）
```

注意它只被 `SKILL.md` 引用一次（一层深），且只在「解释/修复」场景才加载。

## 七、步骤 5：评估驱动的迭代（Claude A / Claude B）

官方反复强调：**先写评估，再写文档**，别为想象中的需求造功能。

### 操作说明

1. **建基线：** 拿 3 个真实 HTML 页面，在**不装 Skill** 的情况下让 Claude 做 a11y 审查，记录它漏掉了哪些缺陷（这暴露了真实能力缺口）。
2. **写评估场景：** 每个场景包含 `query`、`files`、`expected_behavior`。

```json
{
  "skills": ["auditing-accessibility"],
  "query": "检查 templates/login.html 的无障碍问题",
  "files": ["templates/login.html"],
  "expected_behavior": [
    "运行 scan_a11y.py 生成 findings.json",
    "识别出登录按钮缺少可访问文本（empty-interactive）",
    "识别出密码输入框缺少 label（input-label）",
    "修复后重新扫描，finding 数下降"
  ]
}
```

3. **Claude A / Claude B 双开迭代：** 用一个会话（Claude A）写/改 Skill；另开一个干净会话（Claude B）只装这个 Skill 跑真实任务。观察 B 的实际行为——它有没有按 plan→validate→execute 走？有没有提前加载 rules.md 浪费 token？把观察带回 A 改进（比如把「修复后必须复扫」从普通陈述改成 `MUST` 重语气）。

### 预期结果

经过 2~3 轮迭代，Claude B 能稳定地：先执行脚本而非自己写检查代码、只在被问及修复细节时加载 rules.md、修复后主动复扫并报告下降数。

### 常见问题与解决

- **Claude 偷懒自己写 Python 而非用脚本：** 在 SKILL.md 用更强的祈使语气——「**ALWAYS** 运行 `scripts/scan_a11y.py`，不要自行编写扫描逻辑」。
- **Claude 从不读 rules.md：** 说明正文里指向它的「触发条件」不够显眼。把它放进工作流清单的显式步骤里。
- **只在 Opus 上效果好、Haiku 上不行：** 不同模型需要不同指令密度。给 Haiku 用更具体的步骤、更少自由度（低自由度模式）。

## 八、步骤 6：安装与验证

```bash
# 项目级（推荐，随仓库共享给团队）
mkdir -p .claude/skills/auditing-accessibility/{references,scripts}
# 把上面三份文件分别放进去，然后：
chmod +x .claude/skills/auditing-accessibility/scripts/scan_a11y.py

# 用户级（你自己全局可用）
mkdir -p ~/.claude/skills/auditing-accessibility
```

重启会话后，先跑 `/skills` 确认条目出现；再丢一句自然语言「这个组件有没有无障碍问题」验证自动触发。可用 skill-creator 技能或 `/skill` 命令辅助生成骨架，但理解了上述结构后手写更可控。

## 九、实战案例：给一个登录页做完整 a11y 闭环

准备 `templates/login.html`（故意埋 4 个缺陷）：

```html
<!DOCTYPE html>
<html>                              <!-- ❌ missing-lang -->
<body>
  <img src="/logo.png">             <!-- ❌ img-alt -->
  <input type="text" placeholder="用户名">   <!-- ❌ input-label -->
  <button></button>                 <!-- ❌ empty-interactive -->
</body>
</html>
```

对 Claude 说：「用无障碍技能审查 templates/login.html」。预期它执行：

```bash
python scripts/scan_a11y.py templates/login.html --format json > findings.json
# total: 4
```

随后它读 findings.json，按 `critical→major→minor` 汇总，给出逐条修复（补 `lang="zh"`、补 `alt`、给 input 加 `<label for>`、给按钮加文本）。修复后它**主动复扫**，findings.json 归零即闭环完成。整个过程中 `references/rules.md` 只在你追问「WCAG 编号是多少」时才被加载——这就是渐进式披露省 token 的真实表现。

## 工作流总览

```mermaid
flowchart TD
    A[启动会话] --> B[系统提示词预载所有 Skill 的 name+description<br/>第一层 · 永远在线]
    B --> C{用户提到 a11y / WCAG / 无障碍?}
    C -- 命中 --> D[Claude 读取 SKILL.md 全文<br/>第二层 · 按需]
    D --> E[执行 scripts/scan_a11y.py<br/>第三层 · 只取 stdout]
    E --> F[findings.json 可验证中间产物]
    F --> G{需要解释/修复细节?}
    G -- 是 --> H[按需读取 references/rules.md<br/>第三层 · 按需]
    G -- 否 --> I[逐条修复]
    H --> I
    I --> J[复扫 → 对比 finding 数下降]
    J --> K{finding 归零?}
    K -- 否 --> I
    K -- 是 --> L[输出汇报]
    C -- 不命中 --> M[该 Skill 消耗 0 额外 token]
    style B fill:#e8f5e9
    style F fill:#fff3e0
    style M fill:#fce4ec
```

## 安全须知

Skill 能带来新能力，也能带来新风险——恶意 Skill 可能执行危险代码或诱导 Claude 外泄数据。**只从可信来源安装**；装来路不明的 Skill 前，先逐文件审读，重点看 `scripts/` 里的代码依赖、网络请求、以及指令里是否要求 Claude 连接不可信的外部地址。团队共享 Skill 时建议走代码评审流程，像对待业务代码一样对待 Skill 脚本。

## 延伸阅读

- **渐进式披露深度篇：** 把 SKILL.md 想象成一本带目录的手册——先目录、再章节、再附录，逐层加载。理解了它就理解了 Skill 的 token 经济学。
- **MCP 与 Skill 的分工：** MCP 服务器负责「连外部工具/数据」，Skill 负责「教 Claude 复杂的工作流」。两者互补——在 SKILL.md 里引用 MCP 工具时务必用全限定名 `ServerName:tool_name`，否则多服务器并存时 Claude 会找不到工具。
- **自由度匹配：** 高自由度用文字指令（如代码评审），低自由度用固定脚本（如本例扫描器，强调一致性）。给错自由度要么浪费 token、要么失去灵活性。
- **跨模型测试：** Opus 能理解抽象指令，Haiku 需要更具体步骤。若你的 Skill 要在多模型上用，按 Haiku 的标准来写，Opus 自然兼容。

## 训练营适用性评估

- **适合类型：** 前端工程训练营、DevEx/平台工程训练营、AI 辅助研发训练营的中段进阶课（第 2~3 天，学员已能跑通 Claude Code）。
- **教学时长：** 约 2.5~3 小时（讲解 45 分钟 + 跟着造 Skill 90 分钟 + 评估迭代实战 45 分钟）。
- **前置衔接：** 建议放在「Claude Code 入门」之后、「MCP 服务器开发」之前——Skill 是两者之间最自然的桥梁，学员能立刻把团队规范固化为可复用能力。
- **可拓展练习：** 让学员把扫描器扩展两条规则（如检测 `aria-hidden` 误用、检测对比度 via 内联样式），并独立完成一轮 Claude A/B 迭代评估。

## 参考来源

1. [Equipping agents for the real world with Agent Skills — Anthropic Engineering](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills) — Skill 官方定义、渐进式披露三层结构、代码执行与安全考量的权威出处。
2. [Skill authoring best practices — Claude Platform Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — frontmatter 校验规则（64/1024 字符、保留词）、自由度分级、评估驱动开发、反模式清单。
3. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — Claude Code 内 Skill 的创建、管理与 `/skills` 用法。
4. [AI Agent Skills Guide 2026: SKILL.md, Claude Code, Codex & Security — The Prompt Index](https://www.thepromptindex.com/how-to-use-ai-agent-skills-the-complete-guide.html) — 跨平台（Claude/Codex）Skill 生态与安全视角的独立评测。
5. [Claude Code Skills 2026 — Progressive Disclosure — anhtu.dev](https://anhtu.dev/claude-code-skills-2026-progressive-disclosure-and-how-to-standardise-workflow-for-engineering-teams-1062) — 工程团队如何用三层渐进式披露标准化工作流的实战拆解。

> [!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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
