基于微信读书(WeRead)Skill 源码分析,总结编写高质量 Skill 的最佳实践。
目录
- 什么是 Skill
- 文件结构总览
- SKILL.md 主文件编写规范
- 能力表:用户意图的翻译器
- 接口调用规范:减少 Agent 幻觉的核心
- 接口说明文档(xxxmd)编写规范
- 常见错误与防坑指南
- 输出格式规范
- 进阶技巧
- 写作检查清单
什么是 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"}
上面的错误写法会导致 count、lastSort 未被转发,后端按默认值返回第一页,看起来像分页失效。
**为什么正误对比比文字描述更有效?**
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 当成一个认真但缺乏上下文的新手工程师——你要提供它完成任务所需的全部信息,包括它可能犯错的地方。
具体来说,做到以下几点:
- 诚实面对歧义:字段名有歧义就说清楚,不要让 Agent 猜
- 用代码而非文字描述格式:JSON 示例比"平铺在 body 顶层"更准确
- 预判错误:列出常见错误和后果,比事后调试更高效
- 文档即规范:每个接口文档就是一个可执行的 API 契约
- 版本管理:改动接口必须同步更新 version,避免 Agent 缓存旧版本行为