PR 描述模板:让 reviewer 快速进入上下文
我从一次尴尬的 review 说起
上个月我提了一个 PR,标题写的是「fix: 修复订单状态异常」。Reviewer 花了四十分钟看完 diff,问了三个问题:这个「异常」具体是什么表现?为什么选这种修法而不是另一种?有没有回归风险?
这三个问题其实在我写代码的时候都想清楚了,但我一个字没写进 PR 描述里。Reviewer 只能从几百行 diff 里反推我的意图——这不是审查,这是考古。
后来我给自己定了一个规矩:每次点「Create Pull Request」之前,先花五分钟把上下文写下来。Review 的来回次数明显少了,合并速度也快了不少。
PR 描述不是走流程用的装饰文案,它是 review 的入口。Reviewer 打开一个 PR,最缺的不是代码——代码 diff 摆在那里——最缺的是「为什么要改」和「改了之后会怎样」。好描述的价值,是让对方直接从关键问题开始讨论,而不是从零拼凑背景。
为什么 PR 描述值得认真对待
Google 的工程实践文档里有一条核心原则:代码审查的目标是确保代码库的整体质量随时间持续上升,而不是追求某一次提交的完美1。在这个前提下,PR 描述承担的角色是「降低 reviewer 的认知负荷」——让审查者把有限的注意力花在真正需要判断的地方,而不是消耗在理解意图上。
arXiv 上一项针对 8 万条 GitHub PR 的实证研究给出了更具体的数据:描述中包含「代码改动解释」的 PR,合并概率高出 12%–20%;如果作者明确标注了「需要哪种类型的反馈」,合并概率提升幅度达到 64%–72%,尽管只有 16% 的 PR 做到了这一点2。
这两个数字说明了什么?大多数工程师把 PR 描述当成形式,但数据表明它直接影响协作效率。描述写得好,reviewer 能更快给出有价值的反馈;描述写得差,reviewer 要么在评论里反复追问(浪费双方时间),要么草草 approve(埋下隐患)。
我习惯把 PR 描述分成两个层次来理解:描述性信息(做了什么、为什么做、怎么测的)和交互导向信息(需要什么样的反馈、建议按什么顺序看文件)。前者帮助 reviewer 理解,后者帮助 reviewer 行动。大部分模板只覆盖了前者,但后者才是减少来回沟通的关键。
三种常见场景的模板对比
不同改动类型,描述的侧重点不一样。我把日常遇到的 PR 分成三类,分别给出了推荐结构。
场景一:新功能开发
新功能的信息量最大,reviewer 需要理解需求背景、技术方案和测试覆盖。
## 背景
用户反馈在批量导入 SKU 时,超过 500 条会超时。产品需求是支持异步导入并推送结果通知。
## 改动
- 新增 `ImportTask` 模型和 `POST /api/import-tasks` 接口
- 批量导入改为消息队列异步处理,完成后通过 WebSocket 推送
- 前端导入弹窗增加进度条和结果通知
## 验证
- `pnpm typecheck` 通过
- 本地用 1000 条 SKU 测试,队列消费正常,WebSocket 推送延迟 < 2s
- `/import-tasks` 页面浏览器验收通过
## 风险
- 消息队列目前只有单实例,如果 worker 挂掉任务不会自动重试
- `ImportTask` 表新增了 3 个字段,部署前需要跑 migration
## 截图
[导入进度条截图]场景二:Bug 修复
Bug 修复的核心是让 reviewer 确认「修对了」而不是「修歪了」。重点在于复现路径和根因分析。
## 问题
用户在「我的订单」页点击「取消订单」后,页面显示取消成功,但订单状态仍为「待支付」。
## 根因
`cancelOrder` 接口在乐观更新后没有处理 409 冲突响应,导致 UI 状态和服务端状态不一致。
## 修复
- 移除乐观更新,改为等服务端响应后再更新本地状态
- 409 响应时显示「订单状态已变更,请刷新」提示
## 验证
- 复现步骤:创建订单 → 另一终端修改状态 → 原终端点取消 → 现在显示冲突提示
- `pnpm test -- --filter=order` 通过(新增 2 条用例覆盖冲突场景)
## 回归风险
低。只影响取消订单流程,不涉及支付链路。场景三:重构 / 技术优化
重构类 PR 最容易写不出描述,因为「改动」看起来是「没变」。重点要说清楚为什么要动、行为是否保持一致。
## 动机
`useOrderList` hook 有 420 行,混合了数据请求、分页、筛选和权限判断。后续要加「导出」功能时已经很难往里塞逻辑了。
## 改动
拆分为 4 个 hook,行为完全不变:
- `useOrderFetch` — 请求和缓存
- `useOrderPagination` — 分页状态
- `useOrderFilter` — 筛选条件
- `useOrderPermission` — 权限判断
## 验证
- `pnpm typecheck` 通过
- `pnpm test -- --filter=order` 全部通过(现有用例未改动)
- 本地打开订单列表页,分页、筛选、跳转均正常
## 风险
- 纯内部拆分,无对外接口变化
- 如果其他页面有直接 import `useOrderList` 的内部方法,需要同步调整| 场景 | 核心问题 | 必备段落 | 可选段落 |
|---|---|---|---|
| 新功能 | 做了什么、为什么这么做 | 背景、改动、验证 | 风险、截图、接口示例 |
| Bug 修复 | 怎么复现、根因是什么 | 问题、根因、修复、验证 | 回归风险、影响范围 |
| 重构 | 为什么动、行为变没变 | 动机、改动、验证 | 风险、性能对比 |
一个 PR 从创建到合并的信息流
把 PR 描述放到整个 review 流程里看,它的角色会更清楚。下面这张图展示了一个 PR 的生命周期中,描述在哪些环节发挥作用。
从这张图可以看出,描述不充分并不会阻止 review 进行,但会把「理解意图」的成本从文档转移到评论区,增加至少一轮来回。如果 reviewer 追问的问题恰好是开发者遗漏的风险点,那还好;如果只是问「这个为什么这么改」,那就是纯粹的浪费。
四组对比:同一改动的描述质量差异
光说原则太抽象,我整理了四组真实场景,左边是常见写法,右边是改进后的写法。
对比一:背景描述
| ❌ 常见写法 | ✅ 改进写法 |
|---|---|
| 实现订单导出功能 | 运营反馈每次导出发票数据需要手动筛选 3 个后台页面,耗时约 20 分钟。新增「一键导出本月发票」功能,预期将操作时间降到 1 分钟内 |
左边只说了「做了什么」,右边多了一句「为什么要做」和「预期效果」。Reviewer 看到右边,不用去翻需求文档就能判断优先级是否合理。
对比二:验证描述
| ❌ 常见写法 | ✅ 改进写法 |
|---|---|
| 已测试 | pnpm typecheck 通过;pnpm test -- --filter=export 通过(新增 3 条用例);本地用 5000 条发票数据导出,CSV 生成耗时 4.2s,浏览器下载正常 |
「已测试」三个字没有任何信息量。Reviewer 看到它和没看到一样,只能自己判断测试是否充分。右边的写法让 reviewer 可以直接评估覆盖范围是否足够,不需要额外追问。
对比三:风险描述
| ❌ 常见写法 | ✅ 改进写法 |
|---|---|
| 无 | 涉及共享组件 <DataTable>,其他 3 个页面也在使用。改动是向后兼容的(新 prop 有默认值),但建议 reviewer 检查是否影响其他页面的排序行为 |
写「无风险」和「没想风险」在 reviewer 眼里是一回事。主动标出受影响的共享组件,reviewer 才知道该重点看哪些文件。
对比四:改动描述
| ❌ 常见写法 | ✅ 改进写法 |
|---|---|
| 重构了订单模块 | 将 useOrderList(420 行)拆为 4 个独立 hook:useOrderFetch、useOrderPagination、useOrderFilter、useOrderPermission。行为无变化,现有测试全部通过 |
「重构」是一个黑洞词汇——它不传递任何具体信息。右边列出了拆分后的具体模块和行数变化,reviewer 不用打开 diff 就能对改动规模有判断。
PR 描述的质量检查清单
我每次写 PR 描述之前会过一遍这个清单。不需要每一条都满足,但如果高频项缺失,reviewer 大概率会追问。
- 背景写清楚了:一个不了解上下文的工程师读完后,能理解为什么要做这个改动
- 改动范围明确:列出了主要修改的模块或文件,而不是让 reviewer 自己去翻 diff
- 验证有具体证据:写了运行了什么命令、覆盖了什么场景、结果是什么
- 风险没有藏起来:涉及权限、数据迁移、共享组件、性能变化时主动说明
- 有关联链接:关联了 issue、需求文档或设计稿,reviewer 可以一键跳转
- 截图或录屏已附上:UI 变更有前后对比,接口变更有请求/响应示例
- Breaking change 已标注:如果有不兼容变更,在描述开头用醒目方式标出
- 建议的 review 顺序:如果改动文件多,说明建议从哪个文件开始看
- 需要的反馈类型已说明:是需要「逻辑审查」「安全审查」还是「LGTM 就行」
- 描述长度匹配改动规模:小修不用写小作文,大改不能只写一行
- 没有遗留调试信息:确认没有
console.log、debugger或临时代码进入最终 diff - Commit 信息清晰:每个 commit 有明确的意义,不是满屏的
fix和wip
什么时候可以少写
不是所有 PR 都需要填满上述清单。以下几种情况可以适当精简:
一行 hotfix:线上紧急修复,描述写清楚问题、修复和验证三步即可。但「紧急」不等于「不写验证」——恰恰是紧急修复更需要验证证据,因为回滚成本最高。
纯文案修改:改个错别字、调个间距,不需要写背景和风险分析。把改了什么说清楚就够了。
依赖升级:写清楚从哪个版本升到哪个版本、有没有 breaking change、现有测试是否通过。如果升级跨度大,补一句 changelog 链接。
核心原则是:描述的信息量应该和改动的认知复杂度匹配。改了一行代码但影响面巨大(比如修改权限校验逻辑),描述不能短;改了 50 个文件但都是机械替换(比如全局重命名),描述不需要长。
把模板固化到项目里
如果团队里每个人都在自由发挥 PR 描述的质量,最终一定会退化成「写不写全看心情」。我推荐把模板落到代码仓库里——GitHub 支持在 .github/PULL_REQUEST_TEMPLATE.md 放置默认模板,每次创建 PR 时自动填充。
<!-- .github/PULL_REQUEST_TEMPLATE.md -->
## 背景
<!-- 为什么要做这个改动?解决什么问题? -->
## 改动
<!-- 主要修改了哪些模块或行为? -->
## 验证
<!-- 运行了什么命令?覆盖了什么场景?结果是什么? -->
## 风险
<!-- 可能影响哪些路径?有没有需要关注的边界情况? -->
## 素材
<!-- 截图、录屏、接口示例或日志(UI 变更必须有) -->HackerOne 的博客文章里还提到一个实用技巧:如果 PR 描述太短(比如和模板原文差异不超过 20 个字符),可以用 CI 脚本自动提醒作者补充内容3。这个做法有点激进,但它传递了一个信号——PR 描述不是可选的。
Google 的工程实践里还建议用 HTML 注释(<!-- -->)在模板里嵌入引导提示,让作者在填写时看到具体指引,但最终渲染时不会显示出来1。上面的模板就用了这种方式。
| 固化方式 | 适用场景 | 实施成本 | 约束强度 |
|---|---|---|---|
.github/PULL_REQUEST_TEMPLATE.md | GitHub 项目标配 | 低(5 分钟) | 弱(可删除模板内容) |
| CI 检查描述长度 | 需要强制执行的团队 | 中(需写脚本) | 中(可绕过) |
| PR review 时口头约定 | 小团队起步 | 低 | 弱(依赖习惯) |
| Code Owner 审查时把关 | 核心模块 | 低 | 中(仅覆盖核心路径) |
写在最后
PR 描述本质上是给 reviewer 的一封信。你花五分钟写清楚上下文,reviewer 就省二十分钟猜意图。这不是什么高深的工程方法论,就是一个朴素的道理:别让别人替你思考你已经知道的事情。
我自己的经验是,坚持写两周之后会变成习惯。到了后来,打开 PR 页面不写背景就觉得少了点什么——就像发邮件不写主题一样别扭。