LLM을 사용하여 Git push를 변경 사항(changelog) 초안으로 변환했습니다. 프롬프트는 쉬운 부분이었습니다.
요약
Git push 내역을 Changelog 초안으로 변환하는 기능을 구현하며 겪은 실무적 설계 경험을 공유합니다. 프롬프트 엔지니어링보다 데이터 수집 주기, 출력 세분도, 빈 응답 처리와 같은 시스템 설계가 더 중요함을 강조합니다.
핵심 포인트
- 모든 push마다 생성하지 말고 버퍼링을 통해 적절한 주기로 실행할 것
- 기존 데이터베이스 스키마의 세분도에 맞춰 모델의 출력 형식을 설계할 것
- 의미 있는 변경 사항이 없을 경우 모델이 빈 응답을 반환하도록 명시할 것
- 수집(Ingestion)과 생성(Generation) 단계를 분리하여 CI 지연을 방지할 것
기존 제품에 LLM 기능을 추가하고 있다면, 프롬프트(prompt)를 작성하는 데 일주일을 소비한 뒤 프롬프트가 전혀 위험한 부분이 아니었다는 사실을 깨닫게 될 것입니다. 진짜 위험한 부분은 모델에 무엇을 입력하는지, 얼마나 자주 호출하는지, 잘못된 응답에 어떻게 대처하는지, 그리고 결과물을 어떻게 보관하는지입니다.
저는 방금 Git 저장소로의 push를 변경 사항(changelog) 초안 항목으로 변환하는 기능을 출시했습니다. 프롬프트 엔지니어링(prompt engineering)이 아닌, 실제로 작동 여부를 결정했던 다섯 가지 결정 사항을 소개합니다.
1. 모든 이벤트마다 생성하지 마세요
뻔한 설계는 이렇습니다: push가 도착하면, 항목을 생성하고, 끝냅니다. 하지만 이는 두 가지 이유로 잘못된 방식입니다.
첫 번째는 양(volume)입니다. 일반적인 업무일에는 "fix typo", "wip", "actually fix it"와 같은 push가 발생합니다. push마다 항목을 하나씩 만들면 하루에 20개의 초안이 쌓이게 되고, 아무도 열어보지 않는 검토 대기열이 됩니다.
두 번째는 더 심각합니다. 하나의 기능은 보통 여러 번의 push에 걸쳐 반영됩니다. push마다 생성하게 되면 모델이 전체 변경 사항을 볼 수 없으므로, 하나의 사안에 대해 하나의 설명을 쓰는 대신 하나의 사안을 세 개의 반쪽짜리 설명으로 나누어 쓰게 됩니다.
따라서 push는 버퍼(buffer)에 저장되고, 생성 작업은 일정한 주기(cadence)에 따라 실행됩니다. 각 실행은 작업의 일정 범위를 가져가며 그 전체적인 형태를 볼 수 있습니다. 주기는 사용자가 선택합니다 (하루 종일 배포가 일어나는 저장소라면 시간 단위로, 사이드 프로젝트라면 주 단위로). 그리고 스케줄러(scheduler)는 가장 짧은 주기보다 더 자주 작동하여 "시간 단위"가 몰래 "최대 1시간 지연"이 되지 않도록 합니다.
버퍼링(Buffering)은 또한 두 부분을 정확히 적절한 경계에서 나누어 줍니다. 수집(Ingestion)은 빨라야 합니다. CI 단계가 HTTP 응답을 기다리고 있기 때문입니다. 생성(Generation)은 몇 분이 걸립니다. 이 둘을 동일한 요청에 넣으면 언어 모델(language model) 때문에 멈춰버리는 CI 작업이 발생하게 됩니다.
2. 프롬프트를 작성하기 전에 출력의 세분도(granularity)를 결정하세요
적절한 단위는 이미 데이터베이스 스키마(database schema)에 정의되어 있었습니다. 각 변경 사항(changelog) 항목은 하나의 유형(feature, fix, improvement, security)을 가지며, 하나의 배지(badge)가 달린 하나의 항목으로 렌더링됩니다. 따라서 계약(contract)은 사용자에게 보이는 변경 사항 하나당 하나의 항목으로 N개의 항목을 만드는 것이며, 모델에는 관련 커밋(commit)들을 하나의 항목으로 클러스터링(cluster)하도록 지시합니다.
일반적인 교훈: 기존에 가지고 있는 스키마는 보통 사용자가 생각하는 세분성(granularity)을 이미 인코딩하고 있습니다. 그것과 일치시키세요. 모델의 출력 형태와 저장소의 형태가 불일치하면, 입력 과정에서 출력을 훼손하거나 필요하지 않은 두 번째 검토 화면을 구축하게 될 것입니다.
3. 빈 응답도 성공이다 (An empty response is a success)
이것은 잘못하기 쉽고, 잘못 남겨두면 비용이 많이 드는 부분입니다.
의존성 업데이트(dependency bumps), 리팩토링(refactors), CI 수정 및 테스트 변경 사항으로 이루어진 일주일 동안 사용자가 볼 수 있는 내용은 아무것도 없을 수 있습니다. 그 주의 올바른 출력은 아예 아무것도 없는 것입니다.
만약 이를 명시적으로 지시하지 않으면, 모델은 무언가를 꾸며낼 것입니다. 모델에게 처리해야 할 작업 덩어리를 주고 변경 로그 항목을 요청했는데, 아무것도 반환하는 것이 실패처럼 느껴지기 때문에
따라서 이 규칙은 검증기 (validator) 역할도 수행합니다. 모든 제목과 본문이 확인되며, 금지된 문자가 포함된 응답은 전체가 거부됩니다. 그 후 거부 사유를 다시 피드백으로 제공하여 재시도 (retry)를 한 번 수행함으로써, 재시도 시 무엇을 수정해야 하는지 알 수 있게 합니다. 만약 다시 실패하면, 작업은 실패하며 아무것도 생성하지 않습니다.
이 설계에서 가져올 만한 가치가 있는 세 가지 속성이 있습니다:
잘못된 항목만 수정하지 말고, 응답 전체를 거부하세요. 네 개의 초안 중 세 개만 유지하면, 사람은 어떤 것이 왜 누락되었는지 파악해야 합니다. 모든 것을 거부하는 것은 한 번의 재시도 비용이 들지만 상태를 단순하게 유지해 줍니다.
이유를 피드백으로 제공하세요. "이전 답변이 거부되었습니다: 제목에 엠 대시 (em dash)가 포함되어 있습니다. 이를 수정하여 다시 답변해 주세요."라고 하는 것이 동일한 프롬프트를 두 번 보내는 것보다 훨씬 더 나은 재시도 방식입니다.
위반 사항을 배포하는 것보다 실행을 실패하는 것이 낫습니다. 귀하의 규칙이 무엇이든 (가격 주장 금지, 고객 이름 포함 금지, 미출시 기능에 대한 약속 금지 등), 어느 쪽에서 실패할 것인지 미리 결정하십시오. 규칙을 코드로 작성하세요. 프롬프트에만 존재하는 규칙은 선호 사항 (preference)일 뿐이기 때문입니다.
그리고 '찾기 및 바꾸기 (find and replace)'로 수정하지 마세요. 엠 대시 (em dash)를 쉼표로 바꾸는 것은 동일한 리듬을 남기며, 이것이 실제 식별 포인트가 됩니다. 다시 작성하도록 요청하거나 응답을 버리십시오.
5. 작업이 완료되는 즉시 입력을 삭제하세요
"fix stuff"와 같은 커밋 메시지로부터 제대로 된 항목을 작성하려면, 모델에는 디프 (diff)가 필요합니다. 이는 해당 기능이 사용자들에게 소스 코드를 제3자에게 보내도록 요청한다는 것을 의미하며, 이를 어떻게 처리하느냐는 개인정보 보호정책의 한 단락이 아니라 실제적인 문제입니다.
제가 결정한 설계는 다음과 같습니다:
- 제외 사항 (Exclusions)은 저장소 (repo)에서 무엇인가가 나가기 전, CI 단계에서 적용됩니다. 따라서 제외된 경로가 전송된 후 버려지는 것이 아니라, 아예 전송되지 않습니다.
- 디프 (diff)는 실행 (run)이 이를 소비할 때까지만 저장되며, 그 후 초안을 작성하는 동일한 트랜잭션 (transaction) 내에서 null 처리됩니다.
- 실행이 도달하지 못한 모든 항목에 대해 스윕 (sweep)을 수행하여 디프 (diff)를 null 처리하므로, 생성 (generation)이 계속 실패하더라도 소스 코드가 남아 있지 않습니다.
- 커밋 메시지와 통계는 유지됩니다. 이는 초안이 어디에서 왔는지에 대한 감사 추적 (audit trail) 역할을 하며, 소스 코드를 포함하지 않기 때문입니다.
여기에는 실제적인 비용이 따르며, 이를 숨기기보다는 명시하는 것이 가치가 있습니다. 즉, diff(차이점)가 사라지면 초안을 다시 생성할 수 없다는 점입니다. 해당 실행(run)에서 생성된 초안을 그대로 사용하거나, 아니면 직접 작성해야 합니다. 저는 "코드를 삭제합니다"라는 말이 문자 그대로 사실일 때만 의미가 있다고 판단했기에, 이러한 트레이드오프 (trade-off)를 수용했습니다.
그 이면에 있는 패턴
이 모든 사례는 동일한 형태를 띠고 있습니다. 모델은 계약 (contract)을 가진 하나의 컴포넌트 (component)입니다. 즉, 정의된 입력 (input), 정의된 출력 (output), 그리고 정의된 실패 (failure)가 존재합니다. 프롬프트 (prompt)는 이 계약을 설명하고, 코드는 이를 강제하며, 보존 정책 (retention policy)은 해당 컴포넌트가 무엇을 기억할 수 있는지 결정합니다.
프롬프트를 기능 (feature) 그 자체로 취급하는 팀은 결국 동작 방식이 의견에 따라 달라지는 시스템을 갖게 됩니다. 반면 프롬프트를 계약의 한 부분으로 취급하는 팀은 실제로 제품을 출시할 수 있습니다.
저는 소규모 SaaS를 위한 changelog 위젯인 Patchlog를 만들고 있으며, 이 기능은 그곳에 구현되어 있습니다 (현재 Pro 플랜에서 제공되며, GitHub 전용이고, 프로젝트당 하나의 리포지토리, push 트리거만 지원합니다). 위의 모든 내용은 이 기능이 어떻게 구축되었는지에 대한 설명이므로, 제품을 가져가지 않고도 설계 방식은 가져가실 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기