跳到主内容
快讯直播
AI智模界
教程

给 Claude Code 写第一个 Skill:从 SKILL.md 到自动触发

这篇教程能做出什么

做完之后,你会得到一个叫 chess-review 的技能包。把它放进项目里,接着在 Claude Code 里说一句:

> 帮我看下 games/practice.pgn 这盘棋,我黑棋输在哪?

不需要手动指定任何工具,它会自己认出这是一个"棋局复盘"任务,读取技能说明,运行你写好的统计脚本,再按照你定义的模板输出一份复盘笔记:开局走了什么、中局哪几步丢了子、什么阶段结束的、可以改进的方向。

你要写的东西只有三份:一份 SKILL.md(说明书)、一个 Python 脚本(干活的部分)、一份参考资料(模板)。整个过程不需要写模型相关的代码。

前置条件清单

  • 已安装并能正常运行 Claude Code CLI,具体安装方式以官方文档当前版本为准。
  • 终端里能跑 python3 --version 和 git --version。
  • 一个用来练手的项目目录,例如 ~/projects/chess-lab。
  • 一份 PGN 格式的棋谱文件。没有现成的可以先用下面这份:

```text

[Event "练习局"]

[Site "本地"]

[Date "2025.03.01"]

[White "Alice"]

[Black "Bob"]

[Result "1-0"]

1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5

7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 11. Nbd2 exd4 12. cxd4 1-0

```

存成 games/practice.pgn。注意存的编码用 UTF-8,避免中文注释乱码。

第 1 步:弄清 Skill 的目录约定

Skill 本质上就是一个文件夹,文件夹里有一份 SKILL.md。放置位置通常有两种:

  • 项目级:<项目根>/.claude/skills/<技能名>/SKILL.md,跟着仓库走,团队共享。
  • 个人级:~/.claude/skills/<技能名>/SKILL.md,本机所有项目都能用。

SKILL.md 开头需要一段 YAML frontmatter,至少包含 name 和 description。除此之外还有若干可选字段(比如限制可用工具的字段),具体支持哪些以官方文档当前版本为准。

关键在于加载方式:会话开始时,模型一般只看到每个技能的 name 和 description;只有当你的请求和某个 description 对得上,它才会把那份 SKILL.md 的正文读进上下文;正文里提到的脚本、参考资料,再按需读取。这个机制意味着两件事:

1. description 写得好不好,直接决定它会不会被触发。

2. SKILL.md 正文要短,长的清单、模板、术语表要外置成单独文件。

第 2 步:创建技能目录

```bash

cd ~/projects/chess-lab

mkdir -p .claude/skills/chess-review/scripts

mkdir -p .claude/skills/chess-review/references

mkdir -p games

```

目录长这样:

```text

chess-lab/

├── .claude/

│ └── skills/

│ └── chess-review/

│ ├── SKILL.md

│ ├── scripts/

│ │ └── pgn_stats.py

│ └── references/

│ └── review-template.md

└── games/

└── practice.pgn

```

第 3 步:先写工具脚本,再写说明书

顺序很要紧。先把"干活的东西"跑通,再写文档去描述它。这样调试脚本时不会牵扯到技能是否触发的问题。

新建 .claude/skills/chess-review/scripts/pgn_stats.py:

```python

#!/usr/bin/env python3

"""棋局复盘辅助脚本:解析 PGN,输出结构化统计。

用法:

python3 pgn_stats.py <pgn文件路径> [--json]

"""

import argparse

import json

import re

import sys

from pathlib import Path

TAG_RE = re.compile(r'\[(\w+)\s+"([^"]*)"\]')

COMMENT_RE = re.compile(r"\{[^}]*\}")

VARIATION_RE = re.compile(r"\([^)]*\)")

MOVE_NUMBER_RE = re.compile(r"^\d+\.+$")

RESULT_TOKENS = {"1-0", "0-1", "1/2-1/2", "*"}

def parse_pgn(text):

headers, body_lines = {}, []

for raw in text.splitlines():

line = raw.strip()

if not line:

continue

m = TAG_RE.match(line)

if m:

headers[m.group(1)] = m.group(2)

else:

body_lines.append(line)

body = " ".join(body_lines)

body = COMMENT_RE.sub(" ", body)

变着可能嵌套,多清理几轮

prev = None

while prev != body:

prev = body

body = VARIATION_RE.sub(" ", body)

moves = []

for token in body.split():

if token in RESULT_TOKENS or MOVE_NUMBER_RE.match(token):

continue

token = token.rstrip("!?")

if token:

moves.append(token)

return headers, moves

def summarize(headers, moves):

castles = [m for m in moves if m.startswith(("O-O", "0-0"))]

return {

"white": headers.get("White", "?"),

"black": headers.get("Black", "?"),

"result": headers.get("Result", "*"),

"event": headers.get("Event", "?"),

"date": headers.get("Date", "?"),

"ply_count": len(moves),

"full_move_count": (len(moves) + 1) // 2,

"captures": sum(1 for m in moves if "x" in m),

"checks": sum(1 for m in moves if "+" in m or "#" in m),

"castles": len(castles),

"promotions": sum(1 for m in moves if "=" in m),

"first_20_plies": moves[:20],

"last_10_plies": moves[-10:],

}

def main():

ap = argparse.ArgumentParser()

ap.add_argument("pgn", help="PGN 文件路径")

ap.add_argument("--json", action="store_true", help="以 JSON 输出")

args = ap.parse_args()

path = Path(args.pgn)

if not path.exists():

print(f"文件不存在: {path}", file=sys.stderr)

return 1

text = path.read_text(encoding="utf-8", errors="ignore")

headers, moves = parse_pgn(text)

if not moves:

print("没有解析出任何走子,请检查 PGN 格式", file=sys.stderr)

return 1

data = summarize(headers, moves)

if args.json:

print(json.dumps(data, ensure_ascii=False, indent=2))

else:

print(f"{data['white']} vs {data['black']} 结果 {data['result']}")

print(f"回合数: {data['full_move_count']} 半回合数: {data['ply_count']}")

print(f"吃子: {data['captures']} 将军: {data['checks']} 易位: {data['castles']}")

print(f"开局前20步: {' '.join(data['first_20_plies'])}")

print(f"最后10步: {' '.join(data['last_10_plies'])}")

return 0

if __name__ == "__main__":

sys.exit(main())

```

赋执行权限并自己跑一遍:

```bash

chmod +x .claude/skills/chess-review/scripts/pgn_stats.py

python3 .claude/skills/chess-review/scripts/pgn_stats.py games/practice.pgn

python3 .claude/skills/chess-review/scripts/pgn_stats.py games/practice.pgn --json

```

只用了标准库,不装任何依赖。如果后面你想接入棋力引擎做逐手评分,再考虑第三方库,安装方式以对应项目的官方文档为准。

第 4 步:写 SKILL.md,重点是触发描述

新建 .claude/skills/chess-review/SKILL.md:

```markdown

---

name: chess-review

description: 用于国际象棋对局复盘。当用户提供 PGN 棋谱文件路径、或粘贴棋谱文本,并要求分析走子、总结失误、写复盘笔记、点评开局中局残局时使用。常见说法包括"复盘这盘棋""我输在哪""分析这份棋谱""帮我看看这局对弈"。不适合实时对弈陪练、开局库查询、棋力等级评定等场景。

---

棋局复盘

执行步骤

1. 确认用户给出的 PGN 文件路径;若是粘贴的棋谱文本,先写入临时文件

/tmp/chess-review-input.pgn,再继续。

2. 运行统计脚本,拿到结构化数据(必须执行,不要凭记忆估算):

python3 .claude/skills/chess-review/scripts/pgn_stats.py <pgn路径> --json

如果当前工作目录不是项目根目录,先用绝对路径定位脚本。

3. 读取 references/review-template.md,按其中的结构组织输出。

4. 输出复盘笔记,控制在 400 字以内。

输出要求

  • 先给一行结论:这盘棋的胜负手大致出现在哪个阶段。
  • 再按开局、中局、残局三段,每段给 1 到 2 条具体观察,引用具体走子(如 11...exd4)。
  • 最后给 2 条可执行的改进建议,要落到具体动作上。
  • 只根据脚本输出和棋谱本身下结论,不要编造引擎评分。

```

几个写法上的要点:

  • description 写的是"什么时候该用它",不是"它是什么"。把用户可能脱口而出的说法(复盘、输在哪、分析棋谱)都列进去。
  • 明确写出不适合的场景,能减少误触发。
  • 步骤里点名要跑脚本,并且给出可复制粘贴的完整命令。
  • 加一句"不要编造引擎评分"的约束,这类边界说明比技术细节更有价值。

第 5 步:把参考资料挂进去

新建 .claude/skills/chess-review/references/review-template.md:

```markdown

复盘笔记模板

一句话结论

(一句话,指出胜负手出现的阶段)

开局

  • 开局体系:
  • 双方是否按常规出子:
  • 出现的第一处偏差:

中局

  • 子力交换情况:
  • 关键走子:
  • 王的安全:

残局

  • 进入残局时的子力:
  • 结束方式:

改进建议

1.

2.

```

模板、术语表、常见失误清单这类内容放在 references/ 里,正文只用一行路径指过去。这样 SKILL.md 能保持在几十行的量级,模型每次触发时的上下文开销也可控。

第 6 步:用一次真实任务验证自动触发

重启 Claude Code 会话(或新开一个会话),让技能索引生效,然后在项目目录里发起请求。第一次先用明确的说法:

```text

帮我复盘 games/practice.pgn 这盘棋,重点看开局和中局的走子问题。

```

观察三点:

1. 它是否主动读取了 .claude/skills/chess-review/SKILL.md;

2. 是否执行了 pgn_stats.py,而不是直接凭棋谱文本空口分析;

3. 输出结构是否和 references/review-template.md 对得上。

第二次换成含混的说法,检验触发描述是否真的起作用:

```text

games/practice.pgn 这局我黑棋问题出在哪?

```

如果两次都能触发,说明 description 里的关键词覆盖到位了。想确认它究竟用了哪个技能,直接在会话里问一句"你刚才用了哪个技能",或者查看工具调用记录。不同版本查看已加载技能的方式不同,以官方文档为准。

常见坑与排错

技能不触发。 九成出在 description 上。写成"这是一个用于分析国际象棋棋谱的技能"这种定义式描述,模型很难判断何时该用。改成场景式描述,把用户的说法原样写进去。

YAML 头部报错。 frontmatter 必须用 --- 单独成行包裹,冒号后面要有空格。description 里含冒号或引号时用双引号把整段包起来。

目录名和 name 不一致。 目录叫 chess_review、frontmatter 写 chess-review,容易出现对不上的情况。保持一致更省事。

脚本执行失败。 常见原因是工作目录不对导致相对路径失效。在 SKILL.md 里写清楚脚本相对本文件的路径,并允许模型用绝对路径执行;脚本内部对文件不存在的情况给出明确报错,方便定位。

脚本没有执行权限。 记得 chmod +x,同时命令里显式带 python3,两种方式都留一条路。

SKILL.md 写太长。 超过一两百行后,关键步骤容易被淹没。把清单、模板、示例统统挪到 references/,正文只留流程和约束。

PGN 解析结果为空。 多半是遇到了嵌套变着或非 UTF-8 编码。脚本里已经用 errors="ignore" 兜底;更复杂的棋谱建议换成成熟的棋谱解析库,别自己硬啃。

靠记忆编造分析。 如果输出里出现了脚本没提供的数字(比如"这步损失了 0.8 个兵"),说明约束没写够。在 SKILL.md 里明确禁止无依据的量化结论。

下一步建议

  • 把 .claude/skills/ 一起提交进仓库,队友拉下来就能用同一套复盘流程。
  • 加第二个脚本,把棋谱转成逐手 FEN 列表,方便后续做局面可视化。
  • 在 references/ 下加一份开局对照表,让复盘能点出开局体系的名称。
  • 把这套模板复制到别的领域:先写脚本跑通,再写场景式 description,最后补参考资料。财务对账、日志排查、周报生成都能套用。
  • 需要团队共享、跨项目复用时,再看插件形式的打包方式,具体约定以官方文档当前版本为准。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。