首页 › tutorials › 从零造无障碍审查Skill

概述

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

前置条件


一、先搞懂 Skill 到底是什么

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

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

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

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

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

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

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: 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。

常见问题与解决

四、步骤 2:写精简的 SKILL.md 正文(高层指南)

操作说明

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

# 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。

常见问题与解决

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

操作说明

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

# 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()

预期结果

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 数是否下降,形成闭环。

常见问题与解决

六、步骤 4:写规则参考文件(带目录的第三层)

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

# 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。
{
  "skills": ["auditing-accessibility"],
  "query": "检查 templates/login.html 的无障碍问题",
  "files": ["templates/login.html"],
  "expected_behavior": [
    "运行 scan_a11y.py 生成 findings.json",
    "识别出登录按钮缺少可访问文本(empty-interactive)",
    "识别出密码输入框缺少 label(input-label)",
    "修复后重新扫描,finding 数下降"
  ]
}
  1. 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、修复后主动复扫并报告下降数。

常见问题与解决

八、步骤 6:安装与验证

# 项目级(推荐,随仓库共享给团队)
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 个缺陷):

<!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」。预期它执行:

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 的真实表现。

工作流总览

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 脚本。

延伸阅读

训练营适用性评估

参考来源

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

[!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学通信知识星球海报-扫码加入