Hoi hoi! 👋

我是 @nyaomaru,一位目前正在打造 AI 輔助 CHANGELOG 產生器,並且在實驗 Jev 的前端工程師。😸

最近,我一直在思考一個問題。

我們要怎麼從 Pull Request 中抽取出這段內容

## Why

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

並把它放進 CHANGELOG 裡?

### Fixed

- Fix cache invalidation

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

取得 WHAT 相對容易。
取得 WHY 就困難得多。
而且我不想讓 AI 只是憑空編出一個看起來不錯的理由。

讓我們看看 changelog-bot 目前是怎麼處理這件事的!👀

https://github.com/nyaomaru/changelog-bot


📝 WHAT 很容易,WHY 很難。

假設某個 PR 包含以下內容。

## 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。

但這一句就不一樣了

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

這句就更接近 WHY。

我們當然可以直接把整個 PR 丟給 LLM,然後問:

Why was this change made?

但模型可能會總結、改寫,或推斷出一個原文根本沒有明說的理由。

對 CHANGELOG 來說,我想要的是更嚴格的做法。

也許 WHY extraction 根本不是一個生成問題。

也許它其實是一個 選擇問題。


✂️ 先找出可能的 WHY 候選句

在呼叫模型之前,changelog-bot 會先預處理 PR 說明內容。

它會尋找像這些訊號:

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;

它也會移除像核取方塊、佔位符、以及無關欄位這類模板雜訊。

接著它會建立一些小型候選片段

const candidates = buildWhyCandidateSnippets(
  extracted.sections,
  body,
  options.maxCharsPerPr,
  !extracted.hasTargetSection,
);

所以與其把整個大型 PR 內容送進去,我們可能只會拿到

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

這樣一來,問題就小很多了。

哪一個候選句才是真正的 WHY?


🧠 用 Jev 問兩個問題

這就是我開始實驗 Jev 的地方。

我不會請 Jev 直接寫出一個 WHY 句子。

相反地,每個候選句都會用兩個問題來評估。

第一個是

question: "Does the target candidate explicitly state why this changelog change was made?";

基本上就是在問:

這真的是一個理由嗎?

但這還不夠。

想像一下

We changed the cache invalidation logic.

The README was updated because the previous explanation was confusing.

第二句確實包含一個理由。

但它解釋的是 README 的修改,不是 cache invalidation 的修改。

所以我們還會再問

question: "Does the target candidate state a reason that applies to the identified changelog change?";

現在每個候選句就有兩個訊號

{
  probability: explicitWhyProbability,
  relevanceProbability: changeRelevanceProbability,
}

所以真正的問題就變成:

這是一個理由嗎?

而且

這是這個變更的理由嗎?

🎯 選出候選句

changelog-bot 會比較各個候選句的機率

const bestCandidate = candidateProbabilities.reduce((best, candidate) =>
  combinedWhyCandidateProbability(candidate) >
  combinedWhyCandidateProbability(best)
    ? candidate
    : best,
);

接著兩個最低門檻都必須通過

const accepted =
  selectedCandidate !== undefined &&
  bestCandidate.probability >= JEV_MIN_EXPLICIT_WHY_PROBABILITY &&
  bestCandidate.relevanceProbability >= JEV_MIN_CHANGE_RELEVANCE_PROBABILITY;

如果候選句沒通過

if (!accepted) continue;

就不會加入 WHY。

而我認為這很重要。

有時候正確答案是

There is no reliable WHY in this PR.

這比憑空編造一個理由要好得多。


🔒 保留原始 PR 文字

這大概是我最喜歡的部分。

當某個候選句被接受時

items.push({
  prNumber: item.prNumber,
  why: selectedCandidate,
  confidence: mappedConfidence,
});

注意這裡

why: selectedCandidate;

最終的 WHY 是原始 PR 文字。

不是新生成的文句。

所以整個流程就變成

PR description
      ↓
candidate extraction
      ↓
Jev evaluates candidates
      ↓
select candidate
      ↓
original source text
      ↓
CHANGELOG

Jev 負責判斷。

而 PR 作者仍然提供那句話。😸


🧱 WHY 在 v1 流程裡的位置在哪裡?

在處理 WHY extraction 的同時,我也在重新設計 changelog-bot 的 v1 流程。

核心理念是 先確定性,再 AI。

即使 AI 被停用或失敗,完整的 CHANGELOG 也應該能夠產生。

所以 AI 應該是增強結果,而不是主導結果。

目標中的 v1 流程大致如下

source changes
      ↓
ReleaseChange[]
      ↓
deterministic classification
      ↓
complete ReleaseDraft
      ↓
optional editorial enrichment
      +
optional WHY enrichment
      ↓
ReleaseResult
      ↓
deterministic renderer
      ↓
CHANGELOG

因此,WHY extraction 是一個獨立、可選的增強階段。

概念上可以看成

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

這裡有一個很重要的改變:WHY 引擎不需要控制最終的 Markdown。

目前,部分流程仍然是先從產生出的 Markdown 中找目標,再把 WHY 文字補上去。

對 v1 來說,我想把這個邊界拿掉。

改成

{
  prNumber,
  why: selectedCandidate,
  confidence,
}

先成為結構化的 release 資料。

然後再由一個確定性的 renderer 決定它最後長什麼樣子

- Fix cache invalidation
  - Why: The previous implementation could leave stale release data after an update.

這樣責任就更清楚了

WHY 引擎
→ 找出證據

Renderer
→ 格式化證據

這也是我在 v1 遷移中一直遵守的一個更大的原則的一部分:

確定性程式碼負責 CHANGELOG 結構。AI 只負責增強內容。

這個遷移的第 1 到第 4 階段已經實作完成,而結構化渲染與增強邊界則屬於下一階段。

你可以在這裡看到完整的 v1 設計 👇

https://github.com/nyaomaru/changelog-bot/blob/main/v1-design.md


🧪 但門檻值該設多少?

當然,一旦我們有了機率,另一個問題就出現了。

0.50?
0.60?
0.80?

我不想只因為「感覺合理」就隨便選一個。

所以我建立了一個評估架構,先用 14 個案例組成的小型 corpus。

例如

{
  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,
}

我們也需要負面案例

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

這裡有很多實作層面的資訊。

但沒有明確、強而有力的 WHY。

所以 null 才是正確結果。


🤝 後來 John 加入了這個專案

當我在做這個實驗時,@johnnylemonny 作為貢獻者加入了這個專案。🎉

https://github.com/johnnylemonny

John 提議把這個小型 benchmark 擴充到大約 50 個案例,涵蓋

Explicit Rationale
Implementation-Only
Template & Noise
Multilingual PRs

我很喜歡這個想法,但我希望保留一個明確的規則:

Ground truth 應該要獨立於當前的 Jev 門檻值。

所以 benchmark 會先定義某個候選句到底是不是真正有效。

然後我們再去評估不同的門檻值。

John 在 PR #212 中實作了擴充後的語料庫。🙌

benchmark 從 14 個案例增加到 50 個,包含了很多棘手的負面案例。

他也加入了更細的門檻掃描

[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 感覺很安全!👍

那就實際測量吧。😸


🧩 目前的流程

今天,WHY extraction 大致長這樣

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:,步驟還真不少。😂

但也正是這點讓這個問題變得有趣。


🎯 我的學習心得

當我開始做這個功能時,我以為核心問題是

How can AI write a good reason?

現在我認為,更有趣的問題是

How can we find evidence that the reason already exists?

所以我目前的方法很簡單

Use deterministic code to find evidence.

Use Jev to evaluate the evidence.

Keep the original evidence as the output.

而且多虧了 John 的貢獻,我們現在有更好的方法來測試這種做法到底有沒有效。

當有人看了你的實驗,然後說:

要不要我們把這個正式測試看看?

接著還真的跟你一起把它做出來,開源才會好玩。🙌

changelog-bot 仍在持續演進,而 Jev 支援也還是實驗性功能。

我很好奇這種做法接下來會走到哪裡!

https://github.com/nyaomaru/changelog-bot

感謝閱讀!

我們下一篇文章見!👋😸


原文出處:https://dev.to/nyaomaru/how-do-we-extract-the-why-from-a-pr-into-a-changelog-with-jev-52pk


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝44   💬1  
630
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登