如何用 Jev 从 PR 中提取 “Why” 并写入 CHANGELOG?🤔

Hoi hoi! 👋

我是 @nyaomaru,一名前端工程师,目前正在开发一个 AI 辅助的 CHANGELOG 生成器,并尝试使用 Jev。😸

最近,我一直在思考一个问题。

我们要怎么从 Pull Request 中提取这样的内容:

md 复制代码
## Why

The previous implementation could leave stale release data after an update.

然后把它放进 CHANGELOG?

md 复制代码
### Fixed

- Fix cache invalidation

  Why: The previous implementation could leave stale release data after an update.

提取 WHAT 相对容易。 提取 WHY 就难得多。 而且我不希望 AI 只是"编出一句看起来很合理的原因"。

来看看 changelog-bot 目前是怎么处理这个问题的吧!👀

github.com/nyaomaru/ch...


📝 WHAT 很容易,WHY 很难

假设一个 PR 里写着:

md 复制代码
## Description

Extract shared runtime configuration into ProviderBase.

Update OpenAI, Anthropic, and Gemini adapters to use the shared implementation.

This reduces duplicated setup logic and makes adding new providers easier.

前两句话主要在描述 WHAT 和 HOW。

但这一句不一样:

text 复制代码
This reduces duplicated setup logic and makes adding new providers easier.

它更接近 WHY。

我们当然可以把整个 PR 丢给 LLM,然后问:

text 复制代码
Why was this change made?

但这样一来,模型可能会总结、改写,甚至推断出一个原文里从来没有明确写出的理由。

对于 CHANGELOG,我希望规则更严格一些。

也许 WHY extraction 并不是一个 generation problem。

也许它其实是一个 selection problem。


✂️ 先找出可能的 WHY 候选

在调用模型之前,changelog-bot 会先预处理 PR description。

它会寻找这样的信号:

ts 复制代码
const STRONG_CANONICAL_SECTION_NAMES = new Set([
  "why",
  "reason",
  "because",
  "motivation",
  "context",
  "background",
  "problem",
  "rationale",
]);

const RATIONALE_MARKER_RE =
  /\b(why|because|so that|in order to|reason|rationale|motivation|to avoid|to prevent|context|problem)\b|,\s*so\b/i;

它还会移除模板噪音,比如 checkbox、placeholder 和无关字段。

然后生成较短的 candidate snippets:

ts 复制代码
const candidates = buildWhyCandidateSnippets(
  extracted.sections,
  body,
  options.maxCharsPerPr,
  !extracted.hasTargetSection,
);

这样,我们就不需要把一大段 PR body 全部发给模型,而可能得到:

ts 复制代码
[
  "Extract shared runtime configuration into ProviderBase.",
  "This reduces duplicated setup logic and makes adding new providers easier.",
];

现在问题小多了。

到底哪个 candidate 才是真正的 WHY?


🧠 向 Jev 问两个问题

这就是我开始尝试 Jev 的地方。

我不会让 Jev 自己写一句 WHY。

相反,每个 candidate 都会被两个问题分别评估。

第一个:

ts 复制代码
question: "Does the target candidate explicitly state why this changelog change was made?";

简单来说就是:

这句话本身真的是一个理由吗?

但只有这个还不够。

例如:

text 复制代码
We changed the cache invalidation logic.

The README was updated because the previous explanation was confusing.

第二句话显然包含一个理由。

但它解释的是 README 为什么被修改,而不是 cache invalidation 为什么被修改。

所以我们还会问:

ts 复制代码
question: "Does the target candidate state a reason that applies to the identified changelog change?";

于是每个 candidate 都会得到两个信号:

ts 复制代码
{
  probability: explicitWhyProbability,
  relevanceProbability: changeRelevanceProbability,
}

真正的问题就变成了:

text 复制代码
这是一个理由吗?

AND

这是这次 change 的理由吗?

🎯 选择 candidate

changelog-bot 会比较 candidate 的 probability:

ts 复制代码
const bestCandidate = candidateProbabilities.reduce((best, candidate) =>
  combinedWhyCandidateProbability(candidate) >
  combinedWhyCandidateProbability(best)
    ? candidate
    : best,
);

然后两个最低 threshold 都必须通过:

ts 复制代码
const accepted =
  selectedCandidate !== undefined &&
  bestCandidate.probability >= JEV_MIN_EXPLICIT_WHY_PROBABILITY &&
  bestCandidate.relevanceProbability >= JEV_MIN_CHANGE_RELEVANCE_PROBABILITY;

如果 candidate 没有通过:

ts 复制代码
if (!accepted) continue;

那就不添加 WHY。

我觉得这一点非常重要。

有时候正确答案就是:

text 复制代码
There is no reliable WHY in this PR.

这比编一个理由出来更好。


🔒 保留 PR 原文

这大概是我最喜欢的一部分。

当某个 candidate 被接受时:

ts 复制代码
items.push({
  prNumber: item.prNumber,
  why: selectedCandidate,
  confidence: mappedConfidence,
});

注意这里:

ts 复制代码
why: selectedCandidate;

最终的 WHY 就是 PR 里的原始文本。

不是重新生成的一段文字。

所以整个 pipeline 会变成:

text 复制代码
PR description
      ↓
candidate extraction
      ↓
Jev evaluates candidates
      ↓
select candidate
      ↓
original source text
      ↓
CHANGELOG

Jev 负责判断。

但真正进入 CHANGELOG 的句子,仍然来自 PR 作者。😸


🧱 WHY 在 v1 pipeline 的什么位置?

在做 WHY extraction 的同时,我也在重新设计 changelog-bot 的 v1 pipeline。

核心思想是 deterministic-first。

即使 AI 被关闭或者失败,也应该能生成完整的 CHANGELOG。

所以 AI 应该 enrich 结果,而不是掌控结果。

目标中的 v1 pipeline 大致如下:

text 复制代码
source changes
      ↓
ReleaseChange[]
      ↓
deterministic classification
      ↓
complete ReleaseDraft
      ↓
optional editorial enrichment
      +
optional WHY enrichment
      ↓
ReleaseResult
      ↓
deterministic renderer
      ↓
CHANGELOG

因此,WHY extraction 是一个独立、可选的 enrichment stage。

从概念上看:

text 复制代码
ReleaseDraft / ReleaseChange[]
      ↓
find eligible WHY targets
      ↓
collect PR rationale candidates
      ↓
LLM or Jev
      ↓
trust / confidence validation
      ↓
attach structured WHY notes
      ↓
ReleaseResult

这里有一个很重要的变化:WHY engine 不需要控制最终的 Markdown。

现在,pipeline 里还有一部分逻辑会从已经生成的 Markdown 中找到 target,然后再插入 WHY 文本。

到了 v1,我想移除这层边界。

取而代之的是:

ts 复制代码
{
  prNumber,
  why: selectedCandidate,
  confidence,
}

先变成 structured release data。

然后由一个 deterministic renderer 决定它最终如何显示:

md 复制代码
- Fix cache invalidation
  - Why: The previous implementation could leave stale release data after an update.

这样职责会清晰很多:

text 复制代码
WHY engine
→ 找证据

Renderer
→ 格式化证据

这也是我在 v1 migration 中采用的一条更大的原则:

CHANGELOG 的结构由 deterministic code 决定。AI 只负责 enrichment。

这次 migration 的 Phase 1--4 已经实现,structured rendering 和 enrichment boundary 会进入下一阶段。

完整的 v1 设计可以在这里看到 👇

github.com/nyaomaru/ch...


🧪 那 threshold 应该设多少?

当然,只要有 probability,就会马上出现另一个问题:

text 复制代码
0.50?
0.60?
0.80?

我不想仅仅因为某个值"感觉合理"就选它。

所以我做了一个 evaluation harness,并准备了最初的 14-case corpus。

例如:

ts 复制代码
{
  candidates: [
    "Separate changelog execution into input-resolution and output-finalization phases.",
    "This simplifies the main orchestration flow and runs independent GitHub lookups concurrently.",
  ],
  expectedSelectedCandidateIndex: 1,
}

我们也需要 negative examples:

ts 复制代码
{
  candidates: [
    "Extract shared provider runtime configuration into ProviderBase.",
    "Refactor OpenAI, Anthropic, and Gemini adapters to inherit it.",
  ],
  expectedSelectedCandidateIndex: null,
}

这里有很多 implementation information。

但没有足够明确的 WHY。

所以 null 才是正确结果。


🤝 然后 John 加入了项目

就在我做这个实验的时候,@johnnylemonny 作为 contributor 加入了项目。🎉

John 提议把这个小 benchmark 扩展到大约 50 个 case,覆盖:

text 复制代码
Explicit Rationale
Implementation-Only
Template & Noise
Multilingual PRs

我很喜欢这个想法,但我想保持一条规则非常清楚:

Ground truth 应该独立于当前的 Jev threshold。

所以 benchmark 首先定义一个 candidate 到底是否有效。

之后再去评估不同的 threshold。

John 在 PR #212 中实现了扩展后的 corpus。🙌

benchmark 从 14 个 case 增长到了 50 个,其中还包含不少 tricky negative cases。

他还加入了更细粒度的 threshold sweep:

ts 复制代码
[0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9];

现在我们可以比较 precision、recall 和 F1,而不是说:

0.8 感觉挺安全!👍

不如直接测。😸


🧩 当前 pipeline

现在,WHY extraction 大致是这样:

text 复制代码
Pull Request
     ↓
Normalize PR body
     ↓
Extract WHY candidates
     ↓
Remove noise
     ↓
Local trust check
     ↓
Jev:
  explicit rationale?
  relevant to this change?
     ↓
Apply threshold
     ↓
Candidate or none
     ↓
Original PR text
     ↓
CHANGELOG

为了最终那一小行 Why:,步骤还真不少。😂

但也正因为这样,这个问题才变得有意思。


🎯 我学到了什么

刚开始做这个功能时,我以为最主要的问题是:

text 复制代码
How can AI write a good reason?

现在我觉得更有意思的问题是:

text 复制代码
How can we find evidence that the reason already exists?

所以我现在的做法很简单:

text 复制代码
Use deterministic code to find evidence.

Use Jev to evaluate the evidence.

Keep the original evidence as the output.

而且多亏了 John 的贡献,我们现在也有了更好的方式去验证这套方法到底有没有真正奏效。

Open source 最有意思的时候,就是有人看到你的实验后说:

要不我们认真测一下?

然后真的和你一起把它做出来。🙌

changelog-bot 还在持续演进,Jev support 目前也仍然是 experimental。

我很期待看看这条路线接下来会发展到哪里!

github.com/nyaomaru/ch...

感谢阅读!

下一篇文章再见!👋😸

相关推荐
Shirley~~1 小时前
从0搭建一个3D立体场景:光影与交互全解析
前端·3d
Dovis(誓平步青云)1 小时前
一年1956亿行:智能电表场景下的时序数据存储实践
运维·前端·自动化·音视频·prometheus
兰亭妙微UI设计公司1 小时前
兰亭妙微分享:高手设计登录页的12种切入点
开发语言·前端·javascript
前端Baymax1 小时前
静默的 86 小时:全链 402 停摆与零告警的故障复盘
开发语言·前端·javascript
要开心吖ZSH1 小时前
定时任务从外部调度搬回本地 @Scheduled:一次连接池被打满的复盘与选型思考
前端·firefox
不停喝水1 小时前
【前端转全栈java速通课】 项目实战④12-14节 拦截器过滤器和全局配置-cors跨域设置-token体系-利用session做验证码
java·开发语言·前端·ai·容器
代码猎人1 小时前
怎么开发一个微信小程序
前端
console.log('npc')1 小时前
第 3 章 工程化与项目初始化
前端·人工智能·大模型
掘金者阿豪1 小时前
数据集成软件不能只看“同步成功”:聊聊金仓 KFS 和订单数据同步这件事
前端·后端