<!-- title: 造错误码字典Skill：枚举秒归一 -->

# Claude Code Skills 开发实战：从零造一个错误码字典 Skill

## 概述

本教程手把手带你从零构建一个真实可用的 Claude Code Skill——**错误码字典生成器**（`error-code-catalog`）。它能把散落在代码各处的错误码定义自动扫描出来，归一成一张统一的目录表，并顺手揪出"同码不同文案"的冲突和重复定义。通过这一个例子，你将吃透 Skill 的核心机制：`SKILL.md` 结构、YAML frontmatter、渐进式披露、脚本封装、参数与动态上下文注入、调用方控制、工具预批准，以及触发调优。适合已经装好 Claude Code、想从"用别人 Skill"跨到"造自己 Skill"的开发者。

## 前置条件

- 已安装 Claude Code v2.1.x（本教程涉及 `disable-model-invocation`、`${CLAUDE_PROJECT_DIR}`、`/skills` 菜单等 2.1 能力）。
- 一台装了 Python 3 的机器（脚本仅用标准库，无需 `pip install`）。
- 一个带错误码定义的真实/示例工程（哪怕只有几个 `10001: "用户不存在"`）。
- 对 Markdown 和 YAML 有基本了解。

---

## 第 0 步：先想清楚——这事儿到底该不该造 Skill

很多人刚学 Skills 就上瘾，什么都想包成 Skill。先冷静三秒，问自己三个问题：

1. **这件事我是不是要反复做？** 整理错误码、写提交信息、生成测试样板——这些重复劳动才配得上 Skill。
2. **它的指令是不是已经长得像个"流程"而不是"事实"？** 官方文档里有一句很关键的判据：**当 `CLAUDE.md` 里的某一段从"事实"长成了"多步流程"时，就该拆成 Skill**。因为 `CLAUDE.md` 常驻上下文、按字符收费；而 Skill 的正文只在被调用时才加载。
3. **它能不能用一句话描述触发条件？** 如果连你自己都说不清"啥时候该用它"，模型更说不清。

错误码字典这件事，三条全中：后端工程几乎每个仓库都要做、流程明确（扫→聚合→比对→出表）、触发场景清晰（"帮我整理一下错误码"）。**结论：值得造。**

## 第 1 步：理解 Skill 的三层心智模型

动手前，先把 Skill 拆成三个可独立设计的部件，后续每一步都在围绕它们打转：

| 部件 | 作用 | 你要回答的问题 |
|---|---|---|
| **触发（Trigger）** | 模型凭什么知道"现在该用它" | 一句 `description` + 可选 `when_to_use` |
| **行为（Behavior）** | 被调用后它到底干什么 | 正文里的步骤 + 脚本 |
| **产物（Artifact）** | 它产出什么、谁来用 | 文档表格 / 代码 / 报告 |

> 重要事实：Skill 遵循开放的 **Agent Skills 标准**（agentskills.io），同一份 `SKILL.md` 在 Claude Code、Codex CLI 等多个工具里通用。Claude Code 在标准之上又加了调用控制、子代理执行、动态上下文注入等扩展。所以你造的 Skill 是"可携带资产"，不是 Claude Code 专有。

## 第 2 步：决定放哪——个人 vs 项目

Skill 的存放位置决定了它的作用域：

| 位置 | 路径 | 作用范围 |
|---|---|---|
| 个人 | `~/.claude/skills/<name>/SKILL.md` | 你的所有项目 |
| 项目 | `.claude/skills/<name>/SKILL.md` | 仅当前项目 |
| 插件 | `<plugin>/skills/<name>/SKILL.md` | 启用插件处 |

优先级是 **enterprise > personal > project**，同名时高优先级覆盖低优先级。错误码字典这种"每个仓库规则可能不同"的工具，**建议放项目级**——因为不同项目的错误码约定（码段、文案语言、冲突处理策略）差别很大，规则随代码走才合理。

错误码字典显然是带脚本的，所以建一个带子目录的骨架：

```bash
mkdir -p .claude/skills/error-code-catalog/scripts
```

> 小贴士：Claude Code 会**热加载** `SKILL.md` 文本变更，新增/编辑/删除 Skill 在当前会话内即时生效，不用重启。但如果你是"本会话启动时还不存在的顶级 skills 目录"，那需要重启一次才能被监听。

## 第 3 步：写 frontmatter——`description` 是灵魂

`SKILL.md` 的顶部是 YAML frontmatter（夹在两个 `---` 之间），下面是 Markdown 正文。frontmatter 字段很多（`name`、`description`、`when_to_use`、`arguments`、`allowed-tools`、`disable-model-invocation`、`user-invocable`、`model`、`effort`、`context`、`agent`、`paths`、`hooks`、`shell`…），**但只有 `description` 是强相关项**，其余都可选。

为什么说 `description` 是灵魂？因为模型在每轮对话里，**只把各 Skill 的 `description`（+`when_to_use`）放进上下文**做匹配，正文是按需加载的——这就是"渐进式披露"。描述写不准，要么该触发时不触发，要么不该触发时乱触发。两个硬约束记住：

- `description` + `when_to_use` 合计上限 **1536 字符**，超出被截断。
- **把最关键的用例写在最前面**，因为描述会被截尾。

先写一版能用的：

```yaml
---
name: error-code-catalog
description: 扫描代码库中散落的错误码定义，生成统一错误码字典（markdown 表格）并标记冲突、重复、缺失文案。Use when the user asks to document/list/audit error codes, build an error code catalog/dictionary, or says "错误码字典/目录", "error code catalog", "整理错误码".
allowed-tools: Bash(python3 *) Read
---
```

要点拆解：

- **第一句讲"做什么"**：扫描 + 生成字典 + 标记问题。三件事说清。
- **第二句讲"何时触发"**：列出用户会自然说出口的关键词（中英双列）。把"整理错误码"这种口语化的触发词写进去，命中率会高很多。
- **`allowed-tools`**：预批准 `python3 *` 和 `Read`，让模型跑脚本、读文件时不再逐次弹权限确认。注意：`allowed-tools` 是"免确认授权"，**不是限制**——其余工具仍可调用，受你的权限设置约束。

## 第 4 步：写正文——精简到能背下来

正文是"被调用时才加载、加载后整个会话常驻"的内容，**每一行都是反复的 token 成本**。所以官方建议：**说"做什么"，少说"为什么/怎么做"**，给 `CLAUDE.md` 用的简洁标准同样适用于这里。大段参考资料请挪到独立文件（下一步的渐进式披露）。

错误码字典的正文：

````markdown
# 错误码字典生成

扫描当前仓库里的错误码定义，生成统一目录。脚本支持两种写法约定：
- **映射式**：`"10001": "用户不存在"` 或 `10001: "用户不存在"`
- **命名+尾注释**：`code = 10001 // 用户不存在`

## 步骤
1. 执行扫描：`python3 ${CLAUDE_SKILL_DIR}/scripts/scanner.py . -o docs/error-codes.md`
2. 读 `docs/error-codes.md`，向用户汇报：码总数 / 冲突数 / 重复数
3. 对"冲突"项（同一码不同文案）逐条列位置，请用户裁决保留哪个
4. 若 `$ARGUMENTS` 给了子目录，把扫描根换成该路径再扫一遍

## 红线
- 只读源码，不修改任何业务文件
- 表格按码升序；冲突单独成节并给 `文件:行`
````

这里出现两个关键变量：

- **`${CLAUDE_SKILL_DIR}`**：本 Skill 所在目录。用它能正确引用脚本，**无论 Skill 装在个人级、项目级还是插件级都能定位**。这是写"可移植 Skill"的命脉，别硬写绝对路径。
- **`$ARGUMENTS`**：调用时传进来的参数。`/error-code-catalog services/` 会让 `$ARGUMENTS` = `services/`，从而只扫这个子目录。

## 第 5 步：写扫描脚本 `scripts/scanner.py`

复杂活儿交给确定性脚本，不交给模型"猜"。这是脚本型 Skill 相比纯 prompt 型的核心优势：**模型负责编排，脚本负责精确执行**。这个脚本只用标准库，开箱即跑：

```python
#!/usr/bin/env python3
"""错误码字典生成器：扫描错误码定义 → 统一目录 + 标记冲突/重复/缺文案。"""
import argparse, json, re
from collections import defaultdict
from pathlib import Path

IGNORE_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv",
               "dist", "build", ".idea", ".vscode"}
INCLUDE_EXT = {".py", ".go", ".ts", ".tsx", ".js", ".jsx", ".java",
               ".kt", ".rs", ".rb", ".php", ".cs"}

# 约定 A："10001": "用户不存在"  或  10001: "用户不存在"
PAT_MAP = re.compile(r'(\d{3,6})\s*[:=]\s*["\'](.+?)["\']')
# 约定 B：code = 10001 // 用户不存在（尾注释即文案）
PAT_NAMED = re.compile(r'(?:code|err(?:or)?(?:_?code)?|status|errno)\s*[:=]\s*(\d{3,6})\b', re.I)

def iter_files(root: Path):
    for p in root.rglob("*"):
        if not p.is_file() or p.suffix.lower() not in INCLUDE_EXT:
            continue
        if any(part in IGNORE_DIRS for part in p.parts):
            continue
        yield p

def scan_file(path: Path, rel: str):
    out = []
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return out
    for i, line in enumerate(text.splitlines(), 1):
        pairs = PAT_MAP.findall(line)
        for code, msg in pairs:
            out.append((code, msg.strip(), rel, i, "map"))
        if not pairs:  # 没匹配到映射式，再试命名+尾注释
            m = PAT_NAMED.search(line)
            if m:
                cm = re.search(r'(?:#|//|;)\s*(.+)$', line.strip())
                msg = cm.group(1).strip() if cm else "(无文案)"
                out.append((m.group(1), msg, rel, i, "named"))
    return out

def main():
    ap = argparse.ArgumentParser(description="生成错误码字典")
    ap.add_argument("path", nargs="?", default=".")
    ap.add_argument("-o", "--output")
    ap.add_argument("--json", action="store_true")
    args = ap.parse_args()

    root = Path(args.path).resolve()
    entries = []
    for f in iter_files(root):
        entries.extend(scan_file(f, str(f.relative_to(root))))

    by_code = defaultdict(list)
    for code, msg, rel, line, kind in entries:
        by_code[code].append((msg, rel, line, kind))
    codes = sorted(by_code, key=int)

    conflicts, dups = [], []
    for code, hits in by_code.items():
        (conflicts if len({h[0] for h in hits}) > 1 else dups).append((code, hits)) if len(hits) > 1 else None

    L = ["# 错误码字典", "",
         f"> 根目录 `{root}` ｜ 码 **{len(codes)}** ｜ 定义点 **{len(entries)}** ｜ 冲突 **{len(conflicts)}** ｜ 重复 **{len(dups)}**", ""]
    L += ["| 错误码 | 文案 | 文件:行 | 类型 |", "|---|---|---|---|"]
    for code in codes:
        for msg, rel, line, kind in by_code[code]:
            L.append(f"| `{code}` | {msg} | {rel}:{line} | {kind} |")
    if conflicts:
        L += ["", "## ⚠️ 冲突（同码不同文案）", "", "| 码 | 文案 | 位置 |", "|---|---|---|"]
        for code, hits in sorted(conflicts, key=lambda x: int(x[0])):
            for msg, rel, line, _ in hits:
                L.append(f"| `{code}` | {msg} | {rel}:{line} |")
    if dups:
        L += ["", f"### 重复（{len(dups)} 个码多处定义，建议收敛）", ""]
        L.append(", ".join(f"`{c}`" for c, _ in sorted(dups, key=lambda x: int(x[0]))))
    md = "\n".join(L)

    if args.output:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(md, encoding="utf-8")
        print(f"✅ 已写入 {args.output}")
    print(md)
    if args.json:
        jp = Path(args.output).with_suffix(".json") if args.output else Path("catalog.json")
        data = {c: [{"msg": m, "file": r, "line": l} for m, r, l, _ in v] for c, v in by_code.items()}
        jp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        print(f"✅ JSON 已写入 {jp}")

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

**预期输出**（在一个有 `10001: "用户不存在"` 和两处 `10002` 定义冲突的仓库里跑）：

```text
✅ 已写入 docs/error-codes.md
# 错误码字典
> 根目录 `/repo` ｜ 码 2 ｜ 定义点 3 ｜ 冲突 1 ｜ 重复 0

| 错误码 | 文案 | 文件:行 | 类型 |
|---|---|---|---|
| `10001` | 用户不存在 | services/user.py:12 | map |
| `10002` | 参数错误 | api/handler.go:8 | named |
| `10002` | 请求超时 | middleware/timeout.py:30 | map |

## ⚠️ 冲突（同码不同文案）
| 码 | 文案 | 位置 |
|---|---|---|
| `10002` | 参数错误 | api/handler.go:8 |
| `10002` | 请求超时 | middleware/timeout.py:30 |
```

**常见问题：**

- **扫不到任何码** → 八成是你的写法不在两种约定里。要么把定义改成约定 A/B，要么在 `scanner.py` 里加一条 `PAT_*` 正则。
- **误报太多** → 正则太宽（比如把端口号也当码）。把 `INCLUDE_EXT` 收紧，或给 `PAT_MAP` 加上限（如码段必须在 `1xxxx~9xxxx`）。

## 第 6 步：渐进式披露——把"说明书"挪出去

`SKILL.md` 官方建议控制在 **500 行以内**。如果以后你要给错误码字典加"码段命名规范表""各团队码段分配""多语言文案对照"这种大段参考，别塞进 `SKILL.md`，而是单独建文件、在正文里引用：

```markdown
## 参考资料
- 码段分配与命名规范见 [naming.md](naming.md)
- 多语言文案对照见 [i18n.md](i18n.md)
```

模型只会在真正需要时才去读这些文件，平时它们零成本。这就是"渐进式披露"的精髓——**首屏只给索引，细节按需展开**。

## 第 7 步：调用方控制——谁有权按这个按钮

默认你和模型都能触发任何 Skill。但有些 Skill 有副作用（部署、发消息），你不希望模型"看着代码 ready 就自己 deploy 了"。两个开关：

| frontmatter | 你能调 | 模型能调 | 典型用途 |
|---|---|---|---|
| 默认 | ✅ | ✅ | 大多数 |
| `disable-model-invocation: true` | ✅ | ❌ | `/deploy`、`/commit` 等你只想手动控的 |
| `user-invocable: false` | ❌ | ✅ | 后台知识（如遗留系统说明），不该当命令敲 |

错误码字典我建议**保持默认（双方都能调）**：它纯只读、零副作用，让模型在你随口说"整理下错误码"时自动跑起来最顺手。但如果你团队的策略是"生成文档前必须人工确认扫描范围"，那就加 `disable-model-invocation: true`，只允许 `/error-code-catalog` 手动触发。

> 进阶：`/skills` 菜单里把光标移到某 Skill 上按 `Space`，能循环切换四种可见状态（`on`/`name-only`/`user-invocable-only`/`off`），回车保存到 `.claude/settings.local.json` 的 `skillOverrides`——这是不改 `SKILL.md` 本体、也能临时收编别人 Skill 的办法。

## 第 8 步：动态上下文注入——让数据先进来

`!`<code>`命令`</code> 这种语法会在**正文发给模型之前**先执行命令、把输出原位替换进去。模型看到的不是"命令字符串"，而是"真实数据"。这对"先采集后分析"的 Skill 特别有用。如果你想让模型直接基于扫描结果做裁决（而不是先读文件），可以在正文加一段：

````markdown
## 当前扫描结果
```!
python3 ${CLAUDE_SKILL_DIR}/scripts/scanner.py .
```
````

注意：这是**预处理**，不是模型执行；每个 `!`<code>`</code> 只跑一次，输出按纯文本插入，不会被二次扫描。如果公司安全策略禁止 Skill 跑 shell，可在 settings 里设 `"disableSkillShellExecution": true`（个人/项目/插件源会被禁，内置和托管 Skill 不受影响）。

## 第 9 步：实测、调优、用 eval 打磨

**会触发 ≠ 用对了。** 看到 Skill 被调用，只说明模型"找到"了它，不代表产出符合预期。验证要分两件事独立看：① 该触发时触发了没；② 触发后输出对不对。

**触发调优清单：**

- **不触发** → `description` 关键词不够口语化；`description`/`when_to_use` 超过 1536 字符被截；装的 Skill 太多、描述预算挤爆（用 `/doctor` 和 `/context` 查"被截短/丢弃的描述"）。解法：关键词前置、把低频 Skill 在 `skillOverrides` 里设 `name-only` 省预算。
- **乱触发** → 描述太宽。加 `disable-model-invocation: true` 收成纯手动，或把描述收窄到具体场景。

**用 skill-creator 做基准对比**（这是从"能用"到"好用"的关键一跃）：

```text
/plugin install skill-creator@claude-plugins-official
# 装不上就先刷新市场：
/plugin marketplace update claude-plugins-official
/reload-plugins
```

然后让 Claude 帮你做评测：`evaluate my error-code-catalog skill with skill-creator`。它会：写一批测试用例存到 `evals/evals.json` → 每条用例起一个干净子代理跑（避免你作者上下文"作弊"）→ 打分写 `grading.json` → 把"开 Skill vs 关 Skill"的通过率、耗时、token 汇总进 `benchmark.json` → 还能盲测 A/B 两个版本，确认你这次改动是真改进还是退步。**改完描述务必跑一轮对比再提交**，凭感觉调触发率是最常见的返工源。

## 实战案例：从零到产物的完整跑通

把一个新仓库 `demo-svc/` 接进来，里面有：`10001: "用户不存在"`、Go 侧 `code = 10002 // 参数错误`、以及一处冲突 `10002: "请求超时"`。

1. **建 Skill**（项目级）：`mkdir -p .claude/skills/error-code-catalog/scripts`，写入第 3/4/5 步的 `SKILL.md` 与 `scanner.py`。
2. **自动触发**：在仓库里起 Claude Code，直接说"帮我整理下错误码"。模型命中 `description`、自动加载正文、跑脚本。
3. **或手动触发带参**：`/error-code-catalog services/` 只扫子目录。
4. **模型汇报**："共 2 个码、1 处冲突——`10002` 在 `api/handler.go:8` 是'参数错误'、在 `middleware/timeout.py:30` 是'请求超时'，请裁决保留哪个。"
5. **裁决闭环**：你指定保留"参数错误"，模型改掉另一处的文案（或改码），重跑脚本，冲突归零，`docs/error-codes.md` 同步更新。

整套 Skill 目录最终长这样：

```text
.claude/skills/error-code-catalog/
├── SKILL.md            # 触发 + 步骤（精简，<60 行）
├── scripts/
│   └── scanner.py      # 确定性扫描逻辑（模型只编排，不猜）
└── naming.md           # 可选：码段规范（按需加载，平时零成本）
```

## 工作流程图

```mermaid
flowchart TD
    U[用户:'整理下错误码'] --> M{模型}
    M -->|每轮只在上下文里放 description| D[description 匹配?]
    D -->|命中| L[按需加载 SKILL.md 正文]
    D -->|手动| SL["/error-code-catalog<br/>子目录可选作 \$ARGUMENTS"]
    SL --> L
    L --> V{展开变量}
    V --> SD["\${CLAUDE_SKILL_DIR}<br/>定位脚本(跨个人/项目/插件)"]
    V --> AR["\$ARGUMENTS<br/>传入扫描根/子目录"]
    L --> INJ["可选 !`命令`<br/>动态注入扫描结果"]
    SD --> RUN[执行 scanner.py]
    INJ --> RUN
    RUN --> OUT["docs/error-codes.md<br/>统一表 + 冲突/重复清单"]
    OUT --> RPT[模型汇报码数/冲突/重复]
    RPT --> FIX[用户裁决冲突 → 改文案/改码]
    FIX --> RUN
    L -.渐进式披露.-> REF[naming.md / i18n.md 按需读取]
    style D fill:#fff3cd
    style RUN fill:#d1ecf1
    style OUT fill:#d4edda
```

## 延伸阅读

- **Agent Skills 开放标准**：`https://agentskills.io` —— 同一份 `SKILL.md` 跨工具通用，含评测文件格式与迭代工作流。
- **Skill 编写最佳实践**：`https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices` —— 跨 Claude 产品通用的写作指南。
- **skill-creator 插件**：`https://github.com/anthropics/claude-plugins-official/tree/main/plugins/skill-creator` —— 自动化评测/基准/A/B 对比的官方工具。
- **Claude Code 官方 Skills 文档**：`https://code.claude.com/docs/en/skills` —— frontmatter 全字段、动态注入、子代理执行的权威说明。
- **子代理（Subagents）与 Plugins**：把 Skill 升级成带 hooks、MCP、agents 的可分发插件包。

## 训练营适用性评估

| 维度 | 评估 |
|---|---|
| **适合的训练营类型** | AI 编程实战营、研发效能/DevOps 训练营、后端工程师进阶营。强调"AI 编排 + 确定性脚本"的混合范式，对有真实项目经验者收益最大。 |
| **不适合** | 零基础纯入门营（需要先理解 Claude Code 基础操作与 YAML）。建议作为 Round 2~3 的进阶内容。 |
| **建议教学时长** | 授课 + 跟练 **90~120 分钟**（含完整跑通 + 一次 eval 对比）。 |
| **可裁剪方案** | 去掉"动态注入"和"skill-creator eval"两节，可压缩到 60 分钟的精简版。 |
| **验收标准** | 学员能独立造出一个**本项目规则**的错误码字典 Skill，跑出含冲突标记的 `error-codes.md`，并能用 `description` 调参解释触发/不触发现象。 |

## 参考来源

1. [Extend Claude with skills — Claude Code 官方文档](https://code.claude.com/docs/en/skills) — Skills 创建、frontmatter 全字段、动态上下文注入、子代理执行、触发调优的权威来源。
2. [Agent Skills 开放标准（SKILL.md 可移植规范）](https://codex.danielvaughan.com/2026/05/05/agent-skills-open-standard-portable-skills-codex-cli-cross-agent/) — SKILL.md 的 name/description frontmatter 与跨工具（Claude Code / Codex CLI）通用性说明。
3. [Claude Code Skill Frontmatter: Every YAML Option Explained](https://allahabadi.dev/blogs/ai/claude-code-skills-frontmatter-complete-guide/) — 逐字段讲解 frontmatter（含 disable-model-invocation、allowed-tools、hooks 等），适合作为字段速查表。
4. [How to Create a SKILL.md File from Scratch (2026 Tutorial)](https://www.agensi.io/learn/create-skill-md-from-scratch) — 从零创建 SKILL.md 的端到端教程，覆盖测试与分享环节。
5. [Evaluating skill output quality — agentskills.io](https://agentskills.io/skill-creation/evaluating-skills) — eval 文件格式与基准对比迭代工作流，配合 skill-creator 使用。

---

> 本文由「51学通信」与站长爱卫生（微信：gprshome201101）整理分享。学 5G/6G 核心网、协议与 AI 工程化实战，关注公众号「51学通信」获取更多深度内容。

> [!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)
