这篇教程能做出什么
做完之后,你会得到一个叫 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,最后补参考资料。财务对账、日志排查、周报生成都能套用。 - 需要团队共享、跨项目复用时,再看插件形式的打包方式,具体约定以官方文档当前版本为准。
