RicoCheese기술 뉴스와 기록
← 목록으로
뉴스2026-10-1013분

PR 속 '왜'를 CHANGELOG로 옮기는 법: changelog-bot과 Jev 실험기

프런트엔드 엔지니어 nyaomaru가 PR 설명에서 변경 이유(WHY)를 골라 CHANGELOG에 넣는 changelog-bot의 접근법을 소개했습니다. AI에게 이유를 지어내게 하지 않고, PR에 이미 적힌 문장 중 하나를 Jev가 고르게 하는 방식입니다.

#ai#github#opensource#productivity#news
How Do We Extract the “Why” from a PR into a CHANGELOG with Jev? 🤔

개요 #

CHANGELOG에 "무엇이 바뀌었나"를 적기는 쉽습니다. 어려운 건 "왜 바꿨나"입니다. AI 기반 CHANGELOG 생성기 changelog-bot을 만들고 있는 프런트엔드 엔지니어 nyaomaru가 이 문제를 풀어 온 과정을 공유했습니다.

핵심 아이디어는 간단합니다. 변경 이유를 새로 쓰는(generation) 대신, PR 설명에 이미 적혀 있는 문장 가운데 하나를 고르는(selection) 문제로 바꾼 것입니다. 후보 문장을 평가하는 일은 Jev가 맡고, CHANGELOG에는 PR 작성자가 쓴 원문이 그대로 들어갑니다.

목표는 아래 같은 PR 설명을

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.

GitHub에서 changelog-bot 보기 →

왜 '생성'이 아니라 '선택'인가 #

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) 바꿨는지 설명합니다. 마지막 문장인 "중복된 설정 로직을 줄이고 새 프로바이더 추가를 쉽게 만든다"가 WHY에 가깝습니다.

PR 전체를 LLM에 넘기고 "이 변경은 왜 했나?"라고 물을 수도 있습니다. 그런데 이렇게 하면 모델이 내용을 요약하거나 다른 말로 바꾸고, 심하면 어디에도 적혀 있지 않은 이유를 추론해 냅니다. 작성자는 그럴듯한 이유를 AI가 지어내는 상황을 피하고 싶었다고 말합니다. CHANGELOG라면 기준이 더 엄격해야 한다는 판단입니다.

1단계: WHY 후보 추리기 #

changelog-bot은 모델을 부르기 전에 PR 설명부터 전처리합니다. 먼저 이유를 암시하는 섹션 이름과 표현을 찾습니다.

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;

체크박스, 플레이스홀더, 관련 없는 항목 같은 템플릿 잡음도 지웁니다. 그다음 짧은 후보 문장 목록을 만듭니다.

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

긴 PR 본문 대신 아래처럼 정리된 후보만 남습니다.

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

이제 남은 질문은 하나입니다. 이 중 어느 문장이 진짜 WHY인가?

2단계: Jev에게 두 가지를 묻는다 #

작성자는 Jev에게 WHY 문장을 써 달라고 하지 않습니다. 대신 후보마다 두 가지 질문을 던집니다.

첫 번째 질문은 "이 문장이 변경 이유를 명시적으로 밝히고 있는가"입니다.

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 수정에 대한 이유입니다. 그래서 두 번째 질문으로 "그 이유가 지금 다루는 변경에 해당하는가"를 묻습니다.

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

결과적으로 후보마다 두 개의 확률값이 붙습니다.

ts
{
  probability: explicitWhyProbability,
  relevanceProbability: changeRelevanceProbability,
}

한마디로 "이게 이유인가?"와 "이 변경의 이유인가?"를 둘 다 통과해야 합니다.

3단계: 후보 고르기, 그리고 '없음'도 정답 #

changelog-bot은 두 확률을 합친 점수로 가장 높은 후보를 고릅니다.

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

그다음 두 값이 각각 최소 기준을 넘는지 확인합니다.

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

기준을 넘지 못하면 WHY를 아예 붙이지 않습니다.

ts
if (!accepted) continue;

작성자는 이 부분을 중요하게 봅니다. "이 PR에는 믿을 만한 WHY가 없다"가 정답일 때도 있고, 그럴 땐 이유를 지어내는 것보다 비워 두는 편이 낫다는 것입니다.

4단계: PR 원문을 그대로 남긴다 #

작성자가 가장 좋아한다고 밝힌 부분입니다. 후보가 채택되면 다음처럼 저장합니다.

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

why에 들어가는 값은 selectedCandidate, 즉 PR에 적혀 있던 원래 문장입니다. 모델이 새로 쓴 문장이 아닙니다. 판단은 Jev가 하고, 문장은 여전히 PR 작성자의 것입니다.

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

v1 설계: 결정론적 코드가 먼저, AI는 보조 #

WHY 추출 작업과 함께 작성자는 changelog-bot의 v1 파이프라인도 다시 설계하고 있습니다. 원칙은 결정론 우선(deterministic-first) 입니다. AI를 꺼 두거나 AI 호출이 실패해도 완전한 CHANGELOG는 나와야 한다는 것입니다. AI는 결과를 보강할 뿐, 결과를 책임지지 않습니다.

목표로 하는 v1 파이프라인은 대략 이렇습니다.

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

여기서 WHY 추출은 독립된 선택 단계입니다. 지금은 생성된 마크다운에서 대상 위치를 찾아 WHY를 뒤늦게 끼워 넣는 부분이 남아 있는데, v1에서는 이 경계를 없앨 계획입니다. WHY 엔진은 prNumber, why, confidence를 구조화된 릴리스 데이터로 먼저 넘기고, 화면에 어떻게 보일지는 결정론적 렌더러 하나가 정합니다.

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

역할도 깔끔하게 나뉩니다. WHY 엔진은 근거를 찾고, 렌더러는 그 근거를 형식에 맞춰 출력합니다. 작성자는 v1 마이그레이션 전체에 "CHANGELOG 구조는 결정론적 코드가 소유하고, AI는 보강만 한다"는 규칙을 적용하고 있다고 설명합니다. 마이그레이션 1~4단계는 이미 구현을 마쳤고, 구조화된 렌더링과 보강 경계 정리는 다음 단계에 들어갑니다.

v1 설계 문서 보기 →

임계값은 감이 아니라 측정으로 #

A white paper in a vintage typewriter Photo by Markus Winkler on Pexels

확률을 쓰기 시작하면 다음 질문이 따라옵니다. 기준을 0.5로 할지, 0.6으로 할지, 0.8로 할지. 작성자는 그럴듯해 보인다는 이유만으로 숫자를 정하고 싶지 않아서, 14개 사례로 시작하는 평가 하네스를 만들었습니다.

정답 후보가 있는 사례는 이런 식입니다.

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

반대로 구현 내용은 많지만 명시적인 이유가 없는 사례도 넣었습니다. 이때 정답은 null입니다.

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

컨트리뷰터 합류로 50개 사례로 확장 #

실험이 진행되던 중 @johnnylemonny가 컨트리뷰터로 합류했습니다.

johnnylemonny GitHub 프로필 보기 →

그는 작은 벤치마크를 약 50개 사례로 늘리자고 제안했고, 다음 네 범주를 다루도록 했습니다.

  • 명시적 이유가 있는 경우 (Explicit Rationale)
  • 구현 내용만 있는 경우 (Implementation-Only)
  • 템플릿·잡음 (Template & Noise)
  • 다국어 PR (Multilingual PRs)

작성자는 제안을 받아들이면서 한 가지 원칙을 지켰습니다. 정답 데이터는 현재 Jev 임계값과 무관하게 정해야 한다는 것입니다. 후보가 실제로 유효한지를 먼저 정의하고, 여러 임계값은 그다음에 평가합니다.

확장된 코퍼스는 PR #212로 들어왔습니다. 사례는 14개에서 50개로 늘었고, 까다로운 부정 사례도 포함됐습니다. 임계값 탐색 구간도 더 촘촘해졌습니다.

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

이제 "0.8이면 안전해 보인다" 대신 정밀도(precision), 재현율(recall), F1을 비교해 기준을 고를 수 있습니다.

현재 파이프라인 전체 모습 #

지금의 WHY 추출 흐름은 이렇습니다.

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: 한 줄을 위해 꽤 많은 단계를 거치는데, 작성자는 바로 그 점이 이 문제를 재미있게 만들었다고 말합니다.

작성자가 얻은 결론 #

처음에 작성자가 붙잡은 질문은 "AI가 좋은 이유를 쓰게 하려면 어떻게 해야 하나"였습니다. 지금은 "이유가 이미 존재한다는 근거를 어떻게 찾을까"가 더 흥미롭다고 합니다. 현재 접근법은 세 줄이면 됩니다.

  • 근거는 결정론적 코드로 찾는다.
  • 그 근거를 Jev로 평가한다.
  • 출력에는 원래 근거를 그대로 남긴다.

changelog-bot은 계속 발전 중이고, Jev 지원은 아직 실험 단계입니다.

changelog-bot 저장소 보기 →


이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.

댓글GitHub Discussions