目录概要
- 为什么拿 release notes 当例子
- 系统全景:四个文件,三层架构
- 跑起来是什么样
- 四个文件逐个拆解
- 完整执行流程时序图
- 两种 Skill 模式对比
- 实验:拆掉重来看看
- 踩过的坑
1. 为什么拿 release notes 当例子
上一篇画了决策树、讲了选型逻辑------但那些都是抽象的。到了这一篇,终于可以看真代码了。
选个贴真实工程的例子------Release Notes 生成器 。输入一个起始 git tag、一个目标版本号,输出一份按 feat / fix / chore / breaking 分桶好的 RELEASE_NOTES_v1.2.md。每个团队都干过这件事,手写到第 N 版会想"这活能不能自动化"------那就自动化给它看。需求本身不复杂,但拿来演示三层编排正好------既不是 hello world,又不会复杂到注意力被业务细节带偏。
更关键的是,它把我们在 02 篇讨论的每一个东西都用上了:Command 协调入口、Agent 独立上下文里扫 commit、两种 Skill 模式分工(读 git log 的知识预加载、排版写文件的流程直接调用)、模型分层省钱、memory 跨会话记住"上次处理到哪个 sha"------一个不落。
这一篇的源码是为教学清晰度构造的最小闭环。你要亲手建就照下面的 frontmatter + 正文敲四个文件;只想理解架构抽象,跟着拆就够了。
2. 系统全景:四个文件,三层架构
整个生成器由 4 个文件构成。先给你一张对照表,等会儿逐个看源码:
| 组件 | 类型 | 文件位置 | 职责 |
|---|---|---|---|
release-notes-crafter |
Command | .claude/commands/release-notes-crafter.md |
入口,问用户要 since-tag 和目标版本号 |
release-notes-agent |
Subagent | .claude/agents/release-notes-agent.md |
执行,独立上下文里扫 commit、分类 |
git-log-reader |
Skill(预加载) | .claude/skills/git-log-reader/SKILL.md |
知识,告诉 agent 怎么读 git log、怎么识别 conventional commits |
release-notes-formatter |
Skill(直接调用) | .claude/skills/release-notes-formatter/SKILL.md |
输出,按模板生成 RELEASE_NOTES_<version>.md |
架构总览:
model: haiku
负责编排] end subgraph "执行层(Agent,独立上下文)" AGT[release-notes-agent
model: sonnet
maxTurns: 8] AGT -.预加载.-> PS[git-log-reader Skill
commit 解析知识] end subgraph "输出层(Skill)" SK[release-notes-formatter
Markdown 排版] end CMD -->|Step 1 AskUserQuestion| Unit[since-tag? version? tone?] Unit --> CMD CMD -->|Step 2 Agent 工具| AGT AGT -->|Bash git log| GIT[(本地 git 仓库)] GIT -->|commit 列表| AGT AGT -->|返回分桶数据| CMD CMD -->|Step 3 Skill 工具| SK SK -->|写文件| FS[(RELEASE_NOTES_v1.2.md
release-notes/output.md)] style CMD fill:#afa style AGT fill:#faa style PS fill:#fcc style SK fill:#aaf
颜色编码:绿 = Command ,红 = Agent 链 ,蓝 = 输出 Skill。这一套颜色后面都会用。
3. 跑起来是什么样
假设四个文件都建好了,在 Claude Code 里这样触发:
bash
cd /你的项目
claude
arduino
/release-notes-crafter
接下来 Claude 会:
- 弹出结构化单选菜单,分三步确认:
- "起始 tag?"(默认上一个 tag)
- "目标版本号?"(例如
v1.2.0) - "风格:terse / detailed?"
- 派遣
release-notes-agent去扫 commit(独立上下文里) - Agent 跑
git log <since>..HEAD,按 conventional commits 分类 - Agent 返回结构化数据:"feat × 7, fix × 4, breaking × 1, chore × 12"
- Claude 调用
release-notes-formatter按模板排版 - 在
release-notes/目录下看到:RELEASE_NOTES_v1.2.0.mdoutput.md(一段 summary)
跑完大约一分钟。主对话从头到尾清清爽爽------你不会在主窗口里看到 50 条 git log 刷屏,因为那部分全在 agent 的独立上下文里发生。
4. 四个文件逐个拆解
4.1 Command:release-notes-crafter
源码(关键部分):
yaml
---
description: Craft release notes for a given version --- fetches commits between tags, categorizes them, writes a polished RELEASE_NOTES.md
argument-hint: [since-tag] [target-version]
model: haiku
---
# Release Notes Crafter Command
## Workflow
### Step 1: Collect Parameters
Use the AskUserQuestion tool to confirm:
- Starting tag (defaults to most recent tag if not given)
- Target version (e.g., v1.2.0)
- Tone: terse / detailed
### Step 2: Analyze Commits
Use the Agent tool to invoke the release-notes-agent:
- subagent_type: release-notes-agent
- prompt: Analyze commits from <since>..HEAD, return structured JSON
### Step 3: Write Release Notes
Use the Skill tool to invoke the release-notes-formatter skill with the categorized data.
这里有三个设计决策值得留意。
决策一:为什么 model: haiku?
Command 只负责"协调",不做重活。让便宜的 haiku 处理入口交互性价比最高。真正的分析判断在 agent 里用 sonnet 干。这是个很有用的小技巧------不同层用不同模型,总账上省不少钱。
反问一下------既然 haiku 够用,为什么整个会话不全用 haiku?因为 haiku 分类 conventional commits 的时候经常掉链子(特别是遇到
refactor(api)!:这种带 scope + breaking marker 的组合),分错一次你改一次,省下来的钱全吐回去。入口用便宜的、干活用贵的,这个配比得自己踩几次才有感觉,不是拍脑袋定的。
决策二:为什么用 AskUserQuestion 工具而不是直接"问用户"?
直接写"请问用户要从哪个 tag 开始"------Claude 大概率会用对话形式问你,用户回答"呃上一个 tag"还要 Claude 再推断一次。AskUserQuestion 会弹出结构化单选菜单(或者预填默认值),返回值规范,还能一次问三个问题不乱序。
'问用户要 since-tag'] --> B[Claude 用对话问] B --> C[用户打字回答
可能说 '上一个' 'v1.1' 'HEAD~20'...] D[AskUserQuestion 工具] --> E[弹出结构化菜单
可预填默认] E --> F[用户点选或确认
返回值规范] style A fill:#fcc style D fill:#cfc
决策三:Agent 用 Agent 工具、Skill 用 Skill 工具
官方一度把 Task 重命名为 Agent(v2.1.63),但旧名 Task 保留为 alias。你在别处看到 Task(subagent_type=...) 和 Agent(subagent_type=...) 两种写法,都对,等价。
绝对不要用 bash 命令去调用 agent 或 skill------那不是设计路径。必须用 Agent 工具和 Skill 工具。
4.2 Agent:release-notes-agent
这是整个系统里信息密度最高的文件,几乎把 agent frontmatter 能用的字段都用上了:
yaml
---
name: release-notes-agent
description: Use this agent PROACTIVELY when you need to draft release notes
from a git commit range. This agent reads git log, categorizes commits using
its preloaded git-log-reader skill, and returns structured data for a
formatter skill to consume.
allowedTools:
- "Bash(git log*)"
- "Bash(git tag*)"
- "Bash(git show*)"
- "Read"
model: sonnet
color: blue
maxTurns: 8
permissionMode: acceptEdits
memory: project
skills:
- git-log-reader
hooks:
PreToolUse:
- matcher: "Bash(git *)"
hooks:
- type: command
command: python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/hooks.py --agent=voice-hook-agent
timeout: 5000
async: true
---
# Release Notes Agent
## Workflow
1. Run `git log <since-ref>..HEAD` per the preloaded git-log-reader skill instructions
2. Classify each commit (feat / fix / chore / refactor / docs / breaking)
3. For breaking changes, read commit body via `git show <sha>`
4. Memory: Update your agent memory with the commit range processed for historical tracking
5. Return structured JSON: { feat: [...], fix: [...], chore: [...], breaking: [...] }
逐字段看一遍:
| 字段 | 作用 | 这里的选择 |
|---|---|---|
description |
Claude 自动匹配的依据,必写 PROACTIVELY | 明确触发条件:draft release notes from a git range |
allowedTools |
工具白名单 | 只给 Bash(git *) 子集和 Read,不给 Write(输出交给 skill 做) |
model: sonnet |
比入口重一级的模型 | 分类 conventional commits、判断 breaking change 要上下文理解 |
maxTurns: 8 |
最多执行 8 轮就停 | 一般 50 条 commit 3--5 轮搞定,8 轮留余量防死循环 |
permissionMode: acceptEdits |
自动接受文件编辑 | agent 不写文件,其实不强需要,但留着省事 |
memory: project |
记忆存到项目级文件 | 下次跑记得上一次处理到哪个 sha,避免重复分析 |
skills: [git-log-reader] |
预加载 skill | 这是关键,看下一节 |
hooks.PreToolUse |
每次调 git 命令前播音效 | 声音反馈,防止 agent 后台跑完你不知道 |
重点:这个 agent 的 Markdown 正文非常短------它只说"按照你预加载的 skill 执行"。真正"git log 怎么 parse、conventional commits 的前缀表是什么、breaking change 怎么识别"的细节全部在 skill 里。
这一点最容易被新手搞反------agent 正文写得越来越长、越来越全,把 git 命令、正则、错误处理全塞进去,写完自己都懒得读。正确的做法反过来:agent 只留骨架,知识全在 skill 里。
这就是职责分离:
- Agent 管"我要做什么"(工作流骨架)
- Skill 管"具体怎么做"(领域知识)
4.3 Skill(预加载):git-log-reader
yaml
---
name: git-log-reader
description: Instructions for reading git commits between two refs and
classifying them as conventional commits (feat/fix/chore/refactor/docs/breaking).
user-invocable: false
---
## Instructions
1. Fetch commits:
git log --pretty=format:"%H|%s|%an|%ad" --date=short ..HEAD
markdown
Parse each line into `{ sha, subject, author, date }`.
2. Classify by conventional commit prefix:
- `feat:` / `feat(scope):` → **feat**
- `fix:` / `fix(scope):` → **fix**
- `chore:` / `refactor:` / `docs:` / `test:` / `style:` → accordingly
- No recognized prefix → **uncategorized**(flag for human review)
3. Detect breaking changes:
- Subject contains `!:` (e.g., `feat!: drop Node 14`)
- OR commit body contains `BREAKING CHANGE:` (fetch via `git show <sha>`)
4. Return per-category lists, preserving commit sha so the formatter can link back.
user-invocable: false 是什么意思?
这个字段让 skill 不出现在 / 菜单里 ------用户不能主动 /git-log-reader 触发它。它只作为 agent 的"私人手册"存在。对这种"不自洽的半成品指令"(单独跑毫无意义),隐藏是对的。
为什么把 commit 解析规则放在 skill 里而不是 agent 里?
对比两种写法:
300+ 行
git 命令 正则 前缀表 全在这] end subgraph "写法 B 当前实现" B1[release-notes-agent.md
10 行工作流骨架] B2[git-log-reader/SKILL.md
commit 解析规则] B1 -.预加载.-> B2 end style A1 fill:#fcc style B1 fill:#cfc style B2 fill:#cfc
写法 B 的好处:
- Skill 可以被多个 agent 复用------以后做"月度活跃贡献者榜" agent,直接预加载同一个 skill
- Agent frontmatter 保持干净------骨架和知识解耦
- 便于演进------公司内部从 conventional commits 切到 gitmoji 约定,只改 skill 一个文件
4.4 Skill(直接调用):release-notes-formatter
yaml
---
name: release-notes-formatter
description: Formats categorized commit data into a polished
RELEASE_NOTES_<version>.md following the project's house style. Writes
to release-notes/RELEASE_NOTES_<version>.md and release-notes/output.md.
---
## Instructions
1. Read the Markdown template from [reference.md](reference.md)
2. Render each category section (skip empty categories so空分桶不留空 heading)
3. Prepend breaking changes at the top with ⚠ marker --- breaking 最显眼
4. Append compare link: `https://github.com/<repo>/compare/<since-tag>...<target-version>`
5. Write to `release-notes/RELEASE_NOTES_<target-version>.md`
6. Write a one-paragraph summary to `release-notes/output.md`
## Additional resources
- For template, category headers, tone examples, see [reference.md]
- For 10 real release notes across OSS projects as style reference, see [examples.md]
两个值得注意的细节:
1. 没有 user-invocable: false
对比上一个 skill,这个可以被用户 /release-notes-formatter 直接触发 ,也会出现在 / 菜单------只要当前上下文里已经有分桶好的 commit 数据,比如你手工贴一段 JSON 进去,skill 就能直接跑。它是独立可复用的操作,不绑死在 agent 上。
2. 渐进式揭露(Progressive Disclosure)
SKILL.md 正文只有几行,但引用了 reference.md 和 examples.md:
入口 简述 何时用
总是加载] -->|链接到| B[reference.md
Markdown 模板 类别头 语气
用到才加载] A -->|链接到| C[examples.md
10 份真实 OSS release notes
用到才加载] style A fill:#ff9 style B fill:#9ff style C fill:#9ff
核心理由:省 token 。把 500 行 Markdown 模板和 10 份 release notes 样本直接塞进 SKILL.md,每次 skill 被激活都会消耗这上千行;现在 Claude 只在真正要生成 release notes 时才去读 reference.md。不这么做的代价你自己算------每次触发多 1000 行上下文,一天跑十次就是 1 万行白烧。
这个设计模式不只是 release-notes-formatter 一个 skill 在用,它是 Skill 生态的一条基本设计原则。后面专门讲 Skills 的篇章还会再讲。
5. 完整执行流程时序图
(独立上下文) participant Reader as git-log-reader
(预加载) participant Git as 本地 git 仓库 participant Fmt as release-notes-formatter participant FS as 文件系统 User->>Main: /release-notes-crafter Main->>Cmd: 加载 command 内容为提示词 Cmd->>Q: 调用 AskUserQuestion Q->>User: 弹出三步单选菜单 User->>Q: since=v1.1.0, target=v1.2.0, tone=terse Q-->>Cmd: 参数回传 Note over Main,Agent: Agent 启动
独立上下文 Cmd->>Agent: Agent 工具调用
subagent_type=release-notes-agent Note over Agent,Reader: skills: [git-log-reader]
已注入 system prompt Agent->>Reader: 按预加载指令执行 Reader-->>Agent: 提供命令 + 分类规则 Agent->>Git: git log v1.1.0..HEAD Git-->>Agent: 48 条 commit Agent->>Agent: 逐条分类 Agent->>Git: git show
这张图里藏着两个精妙之处。
精妙一:Agent 上下文是真正隔离的
release-notes-agent 工作时,它看不到主对话的历史,也看不到 command 里的所有提示词。它只看到:
- 自己的 frontmatter
- 预加载的
git-log-reader内容 - Command 传给它的 prompt
执行完毕只有返回值 流回主对话。主对话不会被 48 条 git log 输出、breaking change 的 commit body、各种 git show 的中间结果污染------这一点在 commit 数多的大版本里价值爆炸。不信你试试不走 agent、在主对话里直接拉 300 条 commit 做分类,上下文窗口直接满。
精妙二:两种 skill 调用并存
git-log-reader是预加载 (skills:字段注入 agent 启动时的 system prompt)release-notes-formatter是运行时调用 (通过Skill工具显式调用)
同一个 skill 概念,两种使用模式------下一节专门对比。
6. 两种 Skill 模式对比
| Agent Skill(预加载) | Skill(直接调用) | |
|---|---|---|
| 例子 | git-log-reader |
release-notes-formatter |
| 何时加载 | Agent 启动时注入 system prompt | 被调用时执行 |
| 如何触发 | 通过 agent frontmatter 的 skills: 字段 |
通过 Skill 工具 或 /skill-name |
| 作用 | 给 agent 注入领域知识 | 在当前上下文执行一个流程 |
| 用户可见 | 通常 user-invocable: false 隐藏 |
可以出现在 / 菜单 |
| Token 成本 | Agent 每轮都带着 skill 内容 | 只在调用时消耗 |
一条经验法则:
- 如果 skill 是 agent 完成任务必需的知识 → 预加载
- 如果 skill 是一个独立可复用的操作 → 直接调用
git-log-reader 是"没有它 agent 不知道怎么 parse commit"------必需的知识,所以预加载。 release-notes-formatter 是"把结构化数据排版成 Markdown 文件"这件事------独立操作,给什么数据都能跑,所以运行时调用。
7. 实验:拆掉重来看看
看完源码不如动手。试试下面三个变体:
实验 1:不用 Command,直接对话
arduino
> 帮我根据 v1.1.0..HEAD 起一份 release notes 草稿
观察:Claude 会自动识别到 release-notes-agent(因为 description 里写了 PROACTIVELY),直接派遣它。你会省掉 AskUserQuestion 这一步,代价是参数需要在自然语言里说清楚。
实验 2:直接调用 skill
shell
> /release-notes-formatter
观察:skill 会提示它需要分桶好的 commit 数据,因为当前上下文没有这些数据。它不会"自己去跑 git log"------它只负责排版,不负责分析。职责分离在这里看得最清楚。
实验 3:对话式触发排版
css
> 已知 feat 有 X/Y/Z、fix 有 P/Q、breaking 有 W,给我排一份 v1.2.0 release notes
观察:Claude 会自动匹配 release-notes-formatter(description 匹配),把上下文里的分桶数据作为输入。这种用法适合你已经自己手工整理好 commit 列表、只想借个模板的场景。
小结
| 触发方式 | 路径 | 覆盖机制 |
|---|---|---|
/release-notes-crafter |
Command 主动触发 | Command |
| "帮我起份 release notes" | Claude 匹配 agent description | Agent 自动匹配 |
/release-notes-formatter |
Skill 主动触发 | Skill 主动调用 |
| "给我排一份 release notes" | Claude 匹配 skill description | Skill 自动匹配 |
三种扩展机制的"触发面"被四个实验全部覆盖。上一篇的决策树里讲的"谁触发",到这里就有了具体感觉。
8. 踩过的坑
一些在真实项目里踩过的坑,列在这里防你踩。
坑 1:Task 还是 Agent?
Command 源码里写 Task tool 还是 Agent tool?v2.1.63 之后官方重命名为 Agent tool,旧名 Task 保留为 alias。看到新老文档混用别困惑------两个都跑得通。
坑 2:Agent 里 allowedTools 忘声明 git 命令族
Agent 的 allowedTools 是白名单。如果只写 Bash(*),权限太大,review 过不去;如果只写 Bash(git log*),忘了 git show*------agent 跑到检查 breaking change 那步直接报"无工具可用"。新建 agent 最容易踩这个。正确做法:按需最小化,但覆盖全工作流,写清楚每条子命令。
坑 3:user-invocable: false 搞反
想让 skill 给 agent 预加载,要写 user-invocable: false(不让用户看见)。很多人会写成 true------结果 skill 同时出现在 / 菜单,用户点了一脸懵(因为它单独跑毫无意义)。
坑 4:memory: project 不会自动总结
Agent 的 memory 是有的,但需要你在 agent 正文里明确指示 "更新你的 memory 记录这次处理到哪个 sha"。不写的话 agent 不会主动维护 memory------这个默认行为谁拍的板我不知道,反正第一次用的人基本都会栽一次。release-notes-agent 正文里就有这一步:
sql
4. Memory: Update your agent memory with the commit range processed for historical tracking
memory 字段本身的 user / project / local 三档差异、以及和 CLAUDE.md 的关系,后面 08 篇"Memory 与配置层级"会专门展开,这里先知道"需要手动触发"就够了。
坑 5:Skill 引用 reference.md 的路径
release-notes-formatter/SKILL.md 里写的是 [reference.md](reference.md)------相对路径。Skill 的相对路径是相对于 skill 目录本身的,不是工作目录。容易搞错,尤其从别的 skill 例子里 copy 过来不改的时候。
拆完看全景
把整个 release-notes 生成器剖开之后,前两篇的抽象概念应该都落到了具体的字段和执行路径上:
- Command 用 haiku 干协调、Agent 用 sonnet 干活 ------ 分层用模型,省钱又合理
- Agent 正文短、Skill 存细节 ------ 职责分离,骨架与知识解耦
- Skill 分预加载和直接调用两种用法 ------ 同一个概念、两种使用模式
- SKILL.md 写简述 + 引用其他文件 ------ 渐进式揭露,省 token
- Agent 独立上下文隔离 ------ 主对话不被 50 条
git log和中间推理过程污染
下一篇会把 Commands 单独拎出来细讲------官方内置命令有哪些、哪些值得背下来、以及"什么时候该自己封装 command"的判断标准。