从 commit 和 PR 描述里筛出用户能感知的变更,AI 生成的 changelog 才不用人工重写

打开任何一个 AI 编程工具的 changelog 生成功能,把最近 200 条 commit 丢进去,你会得到一份看起来像那么回事、但实际没法直接发布的东西。里面混着“refactor: extract helper function”“fix typo in comment”“update lockfile”这类用户根本感知不到的条目,真正值得写进 release notes 的改动反而被稀释得找不到。

问题不在 AI 能力不够,在于输入源本身没有做面向用户的语义筛选。commit message 是写给协作者看的工程记录,PR 描述是写给 reviewer 看的变更说明,两者都不是 release notes。要让 AI 直接产出可用的 changelog,必须先把“用户能感知的变更”从这两类文本里分离出来。

先定义清楚什么算用户可感知

用户可感知的变更,判断标准只有一条:用户在使用产品时,行为、界面、性能、错误处理上是否会发生可观察到的变化

按这个标准,commit 可以分成三类:

类别 典型例子 是否进 changelog
用户可感知 feat: support exporting report as PDFfix: resolve crash when opening settings on iOS 17perf: reduce cold start time by 40%
用户不可感知但重要 refactor: extract database adapter layerchore: migrate CI to GitHub Actionstest: add unit tests for billing module
边界情况 feat: add rate limiting to public APIsecurity: patch prototype pollution in query parser 视发布渠道而定

第三类最容易出问题。API rate limiting 对 API 用户来说是可感知的,对 UI 用户来说不是。安全补丁通常不写细节,但 release notes 里需要一句“修复了一个潜在的安全问题”。这些边界需要额外规则处理,不能靠一刀切。

commit message 的筛选规则:结构化前缀是基础,但不够

如果你的团队遵循 Conventional Commits 规范,AI 筛选的第一步就简单很多:只保留 featfixperf 类型的 commit,refactorstyletestchoredocsci 全部丢弃

但仅靠类型前缀会漏掉不少真实变更。很多开发者写 commit 时不严格遵守规范,或者把用户可感知的改动塞进了 refactor 里。更可靠的策略是加一层语义判断。

我常用的 prompt 片段长这样:

对以下每条 commit message 进行分类:
- P1: 用户可感知的功能新增、行为变更、bug 修复、性能改善
- P2: 纯内部重构、工具链变更、测试补充、文档更新
- P3: 无法从 message 判断,需要查看 diff

分类依据:如果一个普通用户在不阅读代码的情况下,能否从产品行为中观察到这个变化。

关键在 P3 这个分类。AI 对模糊 commit 的诚实标记,比强行二分类有用得多。P3 的 commit 需要进入第二轮处理——结合 diff 或 PR 上下文判断。

实际测试中,一个 150 条 commit 的迭代周期,按前缀过滤后剩 60 条左右,再经过语义分类,最终 P1 大约 20-25 条。这个数量级才是 changelog 该有的体量。

PR 描述才是金矿,commit 只是兜底

如果你的团队用 squash merge,commit 历史本身已经被压成了一条,这时候 PR 描述就变成了主要信息源。即便不用 squash merge,PR 描述里也往往包含了 commit message 里没有的用户视角信息——因为 reviewer 要求写清楚“这个改动对用户有什么影响”。

从 PR 描述提取用户可感知变更的核心技巧,是让 AI 忽略 PR 模板里的所有工程字段,只聚焦“Motivation”和“User Impact”两个段落。

假设你的 PR 模板长这样:

## Summary
## Motivation
## Implementation Details
## Testing
## Screenshots

提取 prompt 应该明确指定:

从以下 PR 描述中提取面向用户的变更。
只关注 Motivation 和 Summary 中描述用户问题、用户需求、
用户可见行为变化的部分。
忽略 Implementation Details、Testing 中的所有内容。
输出格式:一句话描述,使用过去时或现在完成时,
避免使用“现在”“即将”等时间词。

这里有一个容易忽略的细节:PR 描述的时态和语气与 changelog 需要的时态不同。PR 里写“This PR adds the ability to export reports as PDF”,changelog 需要的是“新增:支持将报表导出为 PDF”。让 AI 在提取时直接做语气转换,比提取后再统一改写效果好很多。

过滤噪音的完整工作流

把上面的规则串起来,一个可落地的流程是这样:

第一步:数据准备。 收集目标版本范围内的所有 commit message 和已合并 PR 的标题与描述。如果 commit 和 PR 有关联关系(比如 commit message 里带 PR 编号),保留这个映射。

第二步:commit 初筛。 用前缀规则过滤掉明确的非用户变更,然后用语义分类 prompt 对剩余 commit 做 P1/P2/P3 标注。

第三步:PR 提取。 对所有已合并 PR 的描述执行提取 prompt,得到“PR 级用户变更”列表。

第四步:合并去重。 将 P1 commit 和 PR 级变更合并。这里需要处理一对多和多对一的关系:一个 PR 可能包含多个 commit,多个 commit 可能对应同一个用户变更。让 AI 按“变更内容描述”做语义去重,而不是按 commit 或 PR 编号去重。

第五步:生成 changelog。 将去重后的变更列表按 feat/fix/perf 分组,交给 AI 生成最终格式。

第四步的去重是整个流程里最容易被低估的环节。一个典型的迭代里,“fix pagination bug”可能在 commit message、PR 标题、PR 描述里各出现一次,措辞略有不同。如果不去重,changelog 里会冒出三条描述同一个修复的条目,读者一眼就能看出是机器生成的。

去重 prompt 的核心是要求 AI 输出“合并后的变更描述 + 原始来源列表”,这样你可以人工抽查合并是否合理:

以下是从 commit 和 PR 中提取的用户可感知变更列表。
请将描述同一变更的条目合并为一条。
合并规则:如果两条描述指向同一个用户可见的功能或修复,
即使措辞不同,也应合并。
输出格式:合并后的描述 | 原始条目编号列表

边界情况的处理规则

实际项目里总有一些 commit 和 PR 掉在分类的灰色地带。根据我的经验,这些边界可以预先定义好规则,不需要 AI 临场发挥:

安全修复:永远进 changelog,但措辞需要特殊处理。不要写漏洞细节,用“修复了一个潜在的安全问题”或“提升了 X 模块的安全性”这类措辞。如果项目有安全公告渠道,changelog 里只放一句引用。

依赖升级:只有当升级带来用户可感知的变化时才进。比如升级某个 UI 库修复了一个渲染 bug,那应该写成 bug fix 而不是“升级 xxx 到 v2.3.1”。纯 patch 版本升级不进。

废弃与移除:deprecation 和 removal 都是用户可感知的,必须进 changelog。这类变更在 commit 里经常被写成 refactor: remove legacy API,按前缀过滤会被误杀。需要在语义分类规则里明确:包含“deprecate”“remove”“drop support”等关键词的 commit,即使前缀是 refactor,也标记为 P1。

实验性功能:如果藏在 feature flag 后面且默认关闭,不进 changelog。如果默认开启或用户可选择开启,进。

把这些规则写进 prompt 的 few-shot examples 里,比抽象地要求 AI“判断用户是否可感知”有效得多。给 AI 三到五个典型边界例子并标注你的决策,它就能在大部分情况下做出一致的判断。

实际效果和一个可复用的 prompt 模板

我在一个 40 人团队、双周迭代的项目上跑通了这套流程。每次发布前,从 200-300 条 commit 和 80-100 个 PR 里,最终产出 15-25 条 changelog 条目。人工审核时间从原来的 2-3 小时降到 15 分钟以内,主要工作是检查去重是否合理、措辞是否需要微调。

下面是可以直接拿来用的完整 prompt 模板:

你是一个 release notes 编辑。你的任务是从 commit message 和 PR 描述中
提取用户可感知的变更,生成 changelog 草稿。

## 输入数据
[粘贴 commit message 列表和 PR 描述列表]

## 分类规则
1. 用户可感知(P1):功能新增、行为变更、bug 修复、性能改善、安全修复、
   废弃或移除功能、依赖升级带来的用户可见变化
2. 用户不可感知(P2):纯重构、代码风格调整、测试补充、CI/CD 变更、
   文档更新、纯 patch 版本依赖升级
3. 无法判断(P3):从现有信息无法确定是否需要用户知晓

## 特殊规则
- 安全修复始终标记为 P1,措辞使用“修复了一个潜在的安全问题”
- 包含 deprecate、remove、drop support 的变更始终标记为 P1
- feature flag 默认关闭的功能不标记为 P1
- 纯依赖版本号升级不标记为 P1,除非描述中说明了用户可见的影响

## 输出要求
1. 先输出 P3 列表,逐条说明需要补充什么信息才能判断
2. 再输出 P1 列表,每条包含:
   - 变更描述(中文,一句话,直接可发布)
   - 变更类型(feat/fix/perf/security/deprecate)
   - 来源(commit hash 或 PR 编号)
3. 对描述同一变更的 P1 条目进行合并去重
4. 最终按变更类型分组输出 changelog 草稿

模板里要求先输出 P3 再输出 P1,是有意为之。如果让 AI 直接输出最终 changelog,P3 的 commit 会被静默丢弃或强行归类。先暴露不确定项,你才能决定是去查 diff 还是直接忽略。

常见问题

Q:commit message 写得很烂,没有规范前缀,这套流程还能用吗?

能,但效果打折扣。没有前缀的情况下,第一步的机械过滤就失效了,所有 commit 都要走语义分类。语义分类对清晰的中文或英文 commit message 准确率不错,但对“fix stuff”“update”这类无信息量的 message 无能为力。建议先跑一遍分类,把 P3 比例统计出来——如果 P3 超过 30%,说明 commit 质量本身是瓶颈,先解决规范问题比优化 prompt 更紧迫。

Q:AI 生成的 changelog 条目和人工写的差距在哪里?

最大的差距在语感和信息密度。AI 倾向于把 commit message 原样翻译成中文,保留大量工程语境。比如 fix: resolve race condition in websocket connection pool 会被生成成“修复了 WebSocket 连接池中的竞态条件”,但人工会写成“修复了偶发的连接断开问题”。解决方式是在提取 prompt 里加一条输出要求:描述用户遇到的问题或获得的能力,而不是描述代码层面的改动。

Q:每次发布都要重新跑一遍完整流程吗?

不用。把流程做成 CI 里的一个 job,每次合并 PR 时就对单个 PR 做提取和分类,把结果存下来。发布时只需要做聚合、去重和格式化,耗时从分钟级降到秒级。单个 PR 的提取结果还可以作为 PR review 的一部分,让 reviewer 顺便确认“这个变更用户会怎么感知”,从源头保证数据质量。