如何写好一个 Skill 教程

5 阅读14分钟

基于微信读书(WeRead)Skill 源码分析,总结编写高质量 Skill 的最佳实践。


目录

  1. 什么是 Skill
  2. 文件结构总览
  3. SKILL.md 主文件编写规范
  4. 能力表:用户意图的翻译器
  5. 接口调用规范:减少 Agent 幻觉的核心
  6. 接口说明文档(xxxmd)编写规范
  7. 常见错误与防坑指南
  8. 输出格式规范
  9. 进阶技巧
  10. 写作检查清单

什么是 Skill

Skill 是 AI Agent 调用外部能力(通常是 HTTP API)的说明书。它告诉 AI:

  • 何时调用这个能力(用户说了什么话)
  • 如何调用(请求格式、参数)
  • 如何理解返回结果(字段含义、计量单位)
  • 如何组织回复(输出格式、用户体验)

一个好的 Skill 能让 Agent 准确、可靠地完成用户请求;一个差的 Skill 会让 Agent 产生幻觉、乱调接口、给出错误答案。


文件结构总览

一个典型的 Skill 项目结构如下:

weread-skills/
├── SKILL.md          # 主入口文件:元数据 + 能力表 + 通用规范
├── search.md         # 各能力对应的接口说明文档
├── book.md
├── shelf.md
├── readdata.md
├── notes.md
├── review.md
├── discover.md
└── profile.md

设计原则:SKILL.md 只放元数据和通用规范,每个接口文档独立成文件,按需引用。这样既保证了主文件的简洁,又便于维护和更新。


SKILL.md 主文件编写规范

1. YAML Frontmatter 元数据

---
name: 微信读书
description: 微信读书助手  搜索书籍、管理书架、查看笔记划线、浏览书评、阅读统计、发现推荐好书
version: 1.0.3
---
  • name:技能名称,应简洁、明确、符合用户认知
  • description:一句话描述,同时列出所有支持的能力(让用户一眼看出能做什么)
  • version:语义化版本号,每次修改接口文档必须同步更新

最佳实践:description 中列举的能力名应与能力表中的名称保持一致,形成用户可见名称到内部文档的精确映射。


2. 能力表:用户意图的翻译器

能力表是 SKILL.md 最核心的部分,它将用户的自然语言意图映射到具体的接口和文档。

## 支持的能力

| 能力 | 说明 | 用户示例 | 详细说明 |
|------|------|----------|----------|
| 搜索书籍 | 在书城搜索 | "帮我搜一下三体" | `search.md` |
| 书籍信息 | 查看书籍详情、章节目录、阅读进度 | "这本书有多少章" "我读到哪了" | `book.md` |
| 书架管理 | 查看书架 | "看看我的书架" | `shelf.md` |
| 阅读统计 | 阅读时长、天数、偏好分析、阅读统计摘要 | "我这个月读了多久" | `readdata.md` |

能力表设计要点:

要点说明
能力名称要用户友好用用户能说出口的话命名,如"阅读统计"而不是"readdata API"
用户示例要真实模拟真实对话场景,包括模糊提问("我读到哪了")和精确提问
关联到具体文档每个能力对应一个 .md 文件,便于按需查阅
按用户场景分组先列出高频能力(搜索、书架),再列出低频能力(推荐、统计)

3. 接口调用规范

这是减少 Agent 幻觉最关键的部分。必须清晰定义:

3.1 统一入口

## 接口调用规范

### 统一入口
POST https://i.weread.qq.com/api/agent/gateway

统一声明 API 端点,避免每个接口文档重复写基础 URL。

3.2 鉴权方式

### 鉴权
- Header:`Authorization: Bearer $WEREAD_API_KEY`
- `WEREAD_API_KEY` 从环境变量获取,格式 `wrk-xxxxxxxx`
- 若未设置,提示用户:`export WEREAD_API_KEY=<你的apikey>`

最佳实践:明确说明环境变量名称和格式,以及缺失时的用户引导话术。

3.3 请求格式规范

### 请求格式
- **Method**:POST
- **Content-Type**:application/json
- **Body**:JSON,`api_name` 指定接口,其余为接口参数,**每次请求必须带 `skill_version`**

3.4 请求 Few-Shot:正误对比

这是最有价值的部分。通过对比正确写法错误写法,Agent 能准确理解接口契约。

### 请求 few-shot

**正确:业务参数平铺在 body 顶层。**
```json
{"api_name":"/user/notebooks","count":100,"skill_version":"1.0.5"}

正确:下一页继续平铺 lastSort

{"api_name":"/user/notebooks","count":100,"lastSort":1516907353,"skill_version":"1.0.5"}

错误:不要把业务参数包在 params 内。

{"api_name":"/user/notebooks","params":{"count":100,"lastSort":1516907353},"skill_version":"1.0.5"}

上面的错误写法会导致 countlastSort 未被转发,后端按默认值返回第一页,看起来像分页失效。


**为什么正误对比比文字描述更有效?**

1. **具体可执行**:Agent 可以直接复制正确格式
2. **错误后果清晰**:每种错误写法都附带说明"会导致什么问题"
3. **消除歧义**:文字描述容易产生多种理解,代码示例只有一个正确答案

#### 3.5 通用规则

通用规则是接口文档无法覆盖、但 Agent 容易犯错的行为规范。

```markdown
### 通用规则

1. **版本上报**:每次请求 body 必须包含 `"skill_version": "1.0.3"`
2. **参数平铺**:业务参数必须和 `api_name`、`skill_version` 放在同一层
3. **能力文档预检**:调用接口前必须阅读对应说明文件,确认接口参数、字段含义
4. **字段解释优先级**:以说明文件中的字段说明为准;如果回包字段名和直觉含义冲突,必须服从说明文件
5. **bookId 解析**:用户输入书名时,先调搜索接口获取 bookId,再执行后续操作

4. 深度链接(URL Schema)

如果 API 背后有 App 跳转能力,应在主文件中统一说明:

## 深度链接(URL Schema)

### 打开书籍
weread://reading?bId={bookId}

### 跳转到指定章节
weread://reading?bId={bookId}&chapterUid={chapterUid}

### 跳转到划线/想法所在位置
weread://bestbookmark?bookId={bookId}&chapterUid={chapterUid}&rangeStart={rangeStart}&rangeEnd={rangeEnd}&userVid={userVid}

格式规范:用表格明确每个参数的名字、说明、来源(哪个字段),让 Agent 准确构造链接。


接口说明文档(xxxmd)编写规范

每个接口文档应包含以下章节(视接口复杂度可选):

1. 接口概述(一句话说明这是什么)
2. 重要概念(容易混淆的概念先解释清楚)
3. 接口定义(请求参数表)
4. 回包字段说明(响应字段表)
5. 概念理清(常见误判/误读)
6. Few-Shot 示例(正误对比)
7. 工作流(调用顺序和逻辑分支)
8. 输出格式(回复的组织方式)

1. 重要概念先行

在最前面解释容易混淆的概念,不要让 Agent 通过字段名去猜:

## 重要概念

**专辑 = 有声书**,两者是同一概念。微信读书中,有声书/听书内容以"专辑"形式存在,存放在书架的 `albums` 字段中,与 `books`(电子书)完全独立。

**书架里的"书"包含电子书和专辑/有声书。** 当用户问"我的书架里有多少本书"时,不能只数 `books[]`,必须同时计入 `albums[]`。

常见错误:
- ⚠️ **不要**通过遍历 `books` 逐个调 `/book/info` 检查 `format` 来判断有声书——效率极低
- ⚠️ 书架数量必须用实际返回数组计算,且必须包含 `albums[]`

写作技巧:用 ⚠️ 符号突出警告,用粗体强调关键词,用代码高亮字段名。

2. 字段说明表要包含"陷阱"信息

普通字段说明只需列出名称和含义,但遇到容易误判的字段,必须在说明中写清楚:

| 字段 | 说明 |
|------|------|
| `totalReadTime` | 当前请求周期的总阅读/收听时长(**秒**)。统计总时长时优先使用该字段,**禁止误当成分钟或小时** |
| `dayAverageReadTime` | 日均阅读/收听时长(秒),分母是当前周期已过去的自然日数或历史完整周期自然日数,不是 `readDays` |
| `preferTime` | 24 小时阅读时段分布数组,值为秒数。注意输出顺序从 6 点开始,不是从 0 点开始 |

对比写法:在字段说明中直接写"不是 X"比单独列 FAQ 更有效,因为 Agent 是逐字段处理的。

3. 口径与计算公式必须明确

当接口返回的数据需要组合计算时,必须给出精确公式:

## 数量口径

| 用户问题/指标 | 正确计算方式 | 说明 |
|------|------|------|
| 书架界面有多少本/多少条目 | `books.length + albums.length + (mp 非空 ? 1 : 0)` | 默认回答这个口径 |
| 电子书数 | `bookCount``books.length` | 仅 `books[]`,不含专辑 |
| 有声书/专辑数 | `albums.length` | 专辑按有声书管理 |

4. 工作流:分步骤说明调用逻辑

工作流不是简单的接口列表,而是描述"在什么条件下调用什么接口":

## 工作流

1. **默认**:调 `/readdata/detail`,不传参数使用默认 `mode=monthly` 展示本月阅读数据。
2. **用户问本周/今年/总共**:对应传 `mode=weekly`/`annually`/`overall`3. **用户问历史数据**:如"上个月读了多少",将上月某天的时间戳作为 `baseTime` 传入。
4. **用户问跨年区间**:必须按自然年逐年查询:从起始年份到当前年份分别调用 `mode=annually`5. **用户问任意起止日期区间**:先判断是否能拆成完整自然年/月/周;完整周期使用 `totalReadTime` 累加。

5. 分页规则要写成可执行代码

不要只写"用 lastSort 翻页",要把正确的请求格式写出来:

#### 分页规则
- `/user/notebooks` 使用基于时间排序值的游标分页,不支持 `offset`/`limit` 分页。
- 第一次请求只传 `count`;如果 `hasMore` 为 1,取本页 `books` 最后一项的 `sort`,下一次作为 `lastSort` 传入。
- **所有业务参数必须平铺在 JSON body 顶层**

#### 分页 few-shot

正确:首页请求,参数平铺。
```json
{"api_name":"/user/notebooks","count":20,"skill_version":"1.0.5"}

正确:下一页请求,lastSort 取上一页 books 最后一项的 sort

{"api_name":"/user/notebooks","count":20,"lastSort":1778312777,"skill_version":"1.0.5"}

错误:不要使用 params 包裹业务参数。

{"api_name":"/user/notebooks","params":{"count":20,"lastSort":1778312777},"skill_version":"1.0.5"}

错误:不要使用 offset/limit,这些字段不是本接口分页参数。


---

## 常见错误与防坑指南

### 1. 字段名误读

**问题**:字段名与实际含义不符(如 `noteCount` 其实是划线数,不是笔记总数)。

**解决方案**- 在字段表中直接写清楚实际含义
- 在文档顶部加"概念解释"章节
- 在能力表中加"⚠️注意"提示

### 2. 参数嵌套

**问题**:Agent 习惯性地把参数包在 `params``data` 对象里,导致后端收不到。

**解决方案**:提供多个正误对比的 JSON 示例,错误示例要附上后果说明。

### 3. 分页参数混淆

**问题**:不同接口用不同的分页方式(游标 vs offset/limit),Agent 混用。

**解决方案**:每个接口单独写分页规则,包括正确字段名和错误字段名。

### 4. 单位混淆

**问题**:时长字段有秒/分钟/小时多种单位,Agent 搞混。

**解决方案**- 在每个时长字段旁标注单位
- 在输出格式规范中统一转换规则(如"所有时长字段均按秒处理,秒 → 'x 小时 y 分钟'格式")

### 5. 版本忘记上报

**问题**:Agent 调用接口时漏掉 `skill_version`,导致服务端无法追踪。

**解决方案**- 在主文件的通用规则中强调
- 在每个接口文档的示例中包含 `skill_version`

---

## 输出格式规范

输出格式规定了 Agent 最终如何组织回复。良好的格式规范能显著提升用户体验。

### 核心原则

| 原则 | 说明 | 示例 |
|------|------|------|
| 编号列表 | 便于用户通过数字选择 | `1. 书名`, `2. 书名` |
| 重点信息前置 | 关键数据放在最前面 | 书名、作者、评分优先于简介 |
| 单位必须转换 | 原始数字不能直接展示 | Unix 时间戳 → `YYYY-MM-DD` |
| 禁止直译字段 | 不要按字段名字面意思翻译 | `noteCount` 不能翻译成"笔记数" |
| 上下文衔接 | 记住已获取的 bookId 等状态 | 后续操作无需用户重复提供 |

### 示例

```markdown
## 输出格式
- 书架列表用编号展示,支持通过编号选择查看详情
- 无参数时显示书架全览,第一句给出可见书架条目数:`books.length + albums.length + (mp 非空 ? 1 : 0)`
- 公开/私密阅读数量必须展示为遍历 `books[]`、`albums[]` 后得到的分组计数
- 涉及有声书/专辑/听书的问题,直接使用 `albums` 字段回答

进阶技巧

1. 周期组合规则

当接口只支持固定周期查询,但用户问了任意区间时,需要告诉 Agent 如何组合:

## 周期特点与区间组合

`/readdata/detail` 只支持按固定自然周期查询,不支持直接传任意起止日期。

| mode | 周期粒度 | 适合用途 |
|------|----------|----------|
| `weekly` | 自然周 | 本周、某历史周 |
| `monthly` | 自然月 | 本月、某历史月 |
| `annually` | 自然年 | 某年全年、今年至今 |
| `overall` | 全部历史 | 总计 |

**组合原则:**
1. 优先用较大周期减少调用次数
2. 跨年区间按自然年拆分
3. 不完整边界周期使用 `dailyReadTimes` 日级精确扣减

2. 升级处理

当服务端返回 upgrade_info 字段时,Agent 必须知道如何处理:

> **⚠️ 使用前必须阅读本文件字段说明。** 阅读统计字段容易因字段名产生误判,调用 `/readdata/detail` 前必须先确认本文件中的参数、字段单位和统计口径;禁止凭字段名或数值大小推断单位。

3. 错误处理规范

- `errcode` 非 0 时表示错误,给出中文提示
- 发送 `{"api_name": "/_list"}` 可查看所有可用接口及参数定义
- 如果回包中出现 `upgrade_info` 字段,必须立即暂停当前操作,按照 `upgrade_info.message` 中的指引完成升级

写作检查清单

写完一个 Skill 后,用以下清单自检:

SKILL.md 主文件

  • Frontmatter 元数据完整(name, description, version)
  • 能力表覆盖所有用户意图,包含真实用户示例
  • 统一入口和鉴权方式已声明
  • 每次请求的 skill_version 要求已强调
  • 至少包含 3 个正误对比的 Few-Shot 示例
  • 深度链接格式已声明(如适用)
  • 通用规则覆盖了字段解释、单位转换、上下文衔接等

接口文档(xxx.md)

  • 重要概念在文件开头解释清楚
  • 请求参数表包含所有参数(含可选参数和默认值)
  • 回包字段表包含所有字段,含单位说明和陷阱提示
  • 分页规则写成可执行的 JSON 示例
  • 工作流覆盖了所有用户场景分支
  • 输出格式规定了回复的组织方式
  • 常见错误(至少 3 种)已列出并给出解决方案

整体一致性

  • 所有文档的 skill_version 与 SKILL.md 顶部一致
  • 能力表中的文档文件名与实际文件一一对应
  • 通用规则中提到的字段名与接口文档完全一致
  • URL Schema 中的参数名与回包字段名完全对应

总结

写好一个 Skill 的核心心法只有一句话:

把 Agent 当成一个认真但缺乏上下文的新手工程师——你要提供它完成任务所需的全部信息,包括它可能犯错的地方。

具体来说,做到以下几点:

  1. 诚实面对歧义:字段名有歧义就说清楚,不要让 Agent 猜
  2. 用代码而非文字描述格式:JSON 示例比"平铺在 body 顶层"更准确
  3. 预判错误:列出常见错误和后果,比事后调试更高效
  4. 文档即规范:每个接口文档就是一个可执行的 API 契约
  5. 版本管理:改动接口必须同步更新 version,避免 Agent 缓存旧版本行为