首页 › tutorials › 造错误码字典Skill:枚举秒归一

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

概述

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

前置条件


第 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,同名时高优先级覆盖低优先级。错误码字典这种"每个仓库规则可能不同"的工具,建议放项目级——因为不同项目的错误码约定(码段、文案语言、冲突处理策略)差别很大,规则随代码走才合理。

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

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)放进上下文做匹配,正文是按需加载的——这就是"渐进式披露"。描述写不准,要么该触发时不触发,要么不该触发时乱触发。两个硬约束记住:

先写一版能用的:

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

要点拆解:

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

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

错误码字典的正文:

# 错误码字典生成

扫描当前仓库里的错误码定义,生成统一目录。脚本支持两种写法约定:
- **映射式**:`"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` 给了子目录,把扫描根换成该路径再扫一遍

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

这里出现两个关键变量:

第 5 步:写扫描脚本 scripts/scanner.py

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

#!/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 定义冲突的仓库里跑):

✅ 已写入 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 |

常见问题:

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

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

## 参考资料
- 码段分配与命名规范见 [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 步:动态上下文注入——让数据先进来

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

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

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

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

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

触发调优清单:

用 skill-creator 做基准对比(这是从"能用"到"好用"的关键一跃):

/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 目录最终长这样:

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

工作流程图

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

延伸阅读

训练营适用性评估

维度 评估
适合的训练营类型 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 官方文档 — Skills 创建、frontmatter 全字段、动态上下文注入、子代理执行、触发调优的权威来源。
  2. Agent Skills 开放标准(SKILL.md 可移植规范) — SKILL.md 的 name/description frontmatter 与跨工具(Claude Code / Codex CLI)通用性说明。
  3. Claude Code Skill Frontmatter: Every YAML Option Explained — 逐字段讲解 frontmatter(含 disable-model-invocation、allowed-tools、hooks 等),适合作为字段速查表。
  4. How to Create a SKILL.md File from Scratch (2026 Tutorial) — 从零创建 SKILL.md 的端到端教程,覆盖测试与分享环节。
  5. Evaluating skill output quality — agentskills.io — 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学通信知识星球海报-扫码加入