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 目前是怎么处理这个问题的吧!👀

📝 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 设计可以在这里看到 👇
🧪 那 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。
我很期待看看这条路线接下来会发展到哪里!
感谢阅读!
下一篇文章再见!👋😸