真实案例带你理解mcp skill command- claude_0x03

17 阅读13分钟

目录概要

  1. 为什么拿 release notes 当例子
  2. 系统全景:四个文件,三层架构
  3. 跑起来是什么样
  4. 四个文件逐个拆解
  5. 完整执行流程时序图
  6. 两种 Skill 模式对比
  7. 实验:拆掉重来看看
  8. 踩过的坑

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-crafterCommand.claude/commands/release-notes-crafter.md入口,问用户要 since-tag 和目标版本号
release-notes-agentSubagent.claude/agents/release-notes-agent.md执行,独立上下文里扫 commit、分类
git-log-readerSkill(预加载).claude/skills/git-log-reader/SKILL.md知识,告诉 agent 怎么读 git log、怎么识别 conventional commits
release-notes-formatterSkill(直接调用).claude/skills/release-notes-formatter/SKILL.md输出,按模板生成 RELEASE_NOTES_<version>.md

架构总览:

graph TB
    User([用户]) -->|/release-notes-crafter| CMD

    subgraph "入口层(Command)"
        CMD[release-notes-crafter<br/>model: haiku<br/>负责编排]
    end

    subgraph "执行层(Agent,独立上下文)"
        AGT[release-notes-agent<br/>model: sonnet<br/>maxTurns: 8]
        AGT -.预加载.-> PS[git-log-reader Skill<br/>commit 解析知识]
    end

    subgraph "输出层(Skill)"
        SK[release-notes-formatter<br/>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<br/>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 里这样触发:

cd /你的项目
claude
/release-notes-crafter

接下来 Claude 会:

  1. 弹出结构化单选菜单,分三步确认:
    • "起始 tag?"(默认上一个 tag)
    • "目标版本号?"(例如 v1.2.0
    • "风格:terse / detailed?"
  2. 派遣 release-notes-agent 去扫 commit(独立上下文里)
  3. Agent 跑 git log <since>..HEAD,按 conventional commits 分类
  4. Agent 返回结构化数据:"feat × 7, fix × 4, breaking × 1, chore × 12"
  5. Claude 调用 release-notes-formatter 按模板排版
  6. release-notes/ 目录下看到:
    • RELEASE_NOTES_v1.2.0.md
    • output.md(一段 summary)

跑完大约一分钟。主对话从头到尾清清爽爽——你不会在主窗口里看到 50 条 git log 刷屏,因为那部分全在 agent 的独立上下文里发生。


4. 四个文件逐个拆解

4.1 Command:release-notes-crafter

源码(关键部分):

---
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弹出结构化单选菜单(或者预填默认值),返回值规范,还能一次问三个问题不乱序。

graph LR
    A[普通提示词<br/>'问用户要 since-tag'] --> B[Claude 用对话问]
    B --> C[用户打字回答<br/>可能说 '上一个' 'v1.1' 'HEAD~20'...]

    D[AskUserQuestion 工具] --> E[弹出结构化菜单<br/>可预填默认]
    E --> F[用户点选或确认<br/>返回值规范]

    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 能用的字段都用上了:

---
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: [...] }

逐字段看一遍:

字段作用这里的选择
descriptionClaude 自动匹配的依据,必写 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

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

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

对比两种写法:

graph TB
    subgraph "写法 A 全塞在 agent 里"
        A1[release-notes-agent.md<br/>300+ 行<br/>git 命令 正则 前缀表 全在这]
    end

    subgraph "写法 B 当前实现"
        B1[release-notes-agent.md<br/>10 行工作流骨架]
        B2[git-log-reader/SKILL.md<br/>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

---
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.mdexamples.md

graph TD
    A[SKILL.md<br/>入口 简述 何时用<br/>总是加载] -->|链接到| B[reference.md<br/>Markdown 模板 类别头 语气<br/>用到才加载]
    A -->|链接到| C[examples.md<br/>10 份真实 OSS release notes<br/>用到才加载]

    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. 完整执行流程时序图

sequenceDiagram
    actor User as 用户
    participant Main as 主对话 Claude
    participant Cmd as release-notes-crafter
    participant Q as AskUserQuestion
    participant Agent as release-notes-agent<br/>(独立上下文)
    participant Reader as git-log-reader<br/>(预加载)
    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 启动<br/>独立上下文
    Cmd->>Agent: Agent 工具调用<br/>subagent_type=release-notes-agent
    Note over Agent,Reader: skills: [git-log-reader]<br/>已注入 system prompt
    Agent->>Reader: 按预加载指令执行
    Reader-->>Agent: 提供命令 + 分类规则
    Agent->>Git: git log v1.1.0..HEAD
    Git-->>Agent: 48 条 commit
    Agent->>Agent: 逐条分类
    Agent->>Git: git show <sha> (检查 breaking)
    Git-->>Agent: commit body
    Agent-->>Cmd: { feat: [...], fix: [...], breaking: [...] }

    Note over Main,Fmt: 回到主上下文
    Cmd->>Fmt: Skill 工具调用 + 传入结构化数据
    Fmt->>Fmt: 读 reference.md 拿模板
    Fmt->>FS: Write RELEASE_NOTES_v1.2.0.md
    Fmt->>FS: Write release-notes/output.md
    Fmt-->>Cmd: 完成
    Cmd->>User: 展示摘要 + 文件路径

这张图里藏着两个精妙之处。

精妙一: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-readerrelease-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,直接对话

> 帮我根据 v1.1.0..HEAD 起一份 release notes 草稿

观察:Claude 会自动识别到 release-notes-agent(因为 description 里写了 PROACTIVELY),直接派遣它。你会省掉 AskUserQuestion 这一步,代价是参数需要在自然语言里说清楚。

实验 2:直接调用 skill

> /release-notes-formatter

观察:skill 会提示它需要分桶好的 commit 数据,因为当前上下文没有这些数据。它不会"自己去跑 git log"——它只负责排版,不负责分析。职责分离在这里看得最清楚。

实验 3:对话式触发排版

> 已知 feat 有 X/Y/Z、fix 有 P/Q、breaking 有 W,给我排一份 v1.2.0 release notes

观察:Claude 会自动匹配 release-notes-formatter(description 匹配),把上下文里的分桶数据作为输入。这种用法适合你已经自己手工整理好 commit 列表、只想借个模板的场景。

小结

触发方式路径覆盖机制
/release-notes-crafterCommand 主动触发Command
"帮我起份 release notes"Claude 匹配 agent descriptionAgent 自动匹配
/release-notes-formatterSkill 主动触发Skill 主动调用
"给我排一份 release notes"Claude 匹配 skill descriptionSkill 自动匹配

三种扩展机制的"触发面"被四个实验全部覆盖。上一篇的决策树里讲的"谁触发",到这里就有了具体感觉。


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 正文里就有这一步:

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"的判断标准。


Footnotes

外部链接