나의 커밋 메시지 생성기가 스스로 작업에 서명하는 문제. 이를 막는 것이 해결책이 아니었다.
요약
커밋 메시지 생성 스크립트가 AI의 출처 표기(Co-Authored-By)를 자동으로 포함하는 문제를 겪었습니다. 단순히 프롬프트를 강화하는 대신, 모델 출력에 대한 결정론적 필터링 레이어를 추가하여 이 문제를 해결했습니다.
핵심 포인트
- 자연어 지침은 자문적이며, 출력을 제한할 수 없습니다.
- LLM의 품질 게이트는 개선보다 외부의 결정론적 라우팅이 효과적입니다.
- 모델 출력 후처리 단계에서 특정 키워드를 제거하는 필터를 적용했습니다.
저는 리포지토리 중 하나에 git_commit.py라는 스크립트를 가지고 있습니다. 이 스크립트는 스테이징된 diff를 가지고 claude -p로 외부 호출을 하고, Conventional Commit 메시지를 받아와서 출력합니다. 이것은 prepare-commit-msg git hook에 연결되어 있어서 커밋할 때마다 자동으로 채워진 메시지를 얻게 해줍니다. 작고, 단순하며, 유용합니다.
첫 번째 버전에서는 시스템 프롬프트에 한 가지 지침만 있었습니다: "커밋 메시지만 출력하세요 — 설명도, 마크다운도, 따옴표도 없이." 그게 전부였습니다. 한동안은 잘 작동했지만, 어느 날 커밋 기록에 제가 요청한 적도 없고 개인 리포지토리의 히스토리에 절대 원하지 않았던 Co-Authored-By: Claude <noreply@anthropic.com> 라인이 붙어 있는 것을 발견했습니다.
저는 가장 당연한 조치를 먼저 취했습니다. 프롬프트를 더 구체적으로 만든 것입니다.
SYSTEM = (
"You are a git commit message generator. "
"Output ONLY the commit message — one line, no explanation, no markdown, no quotes,"
...
이것은 제가 어디서나 보는 패턴과 같습니다: 같은 리포지토리의 CLAUDE.md 파일에 굵은 글씨로 "절대 Co-Authored-By: 또는 Claude/AI 참조를 커밋 메시지에 추가하지 마세요."라는 라인이 있습니다. 저는 다른 사람들의 프롬프트 파일에서도 똑같은 패턴을 열두 개나 보았습니다 — X가 한 번 발생할 때마다, 그에 대응하여 붙여진 "절대 X 하지 마라"는 지침 목록이 늘어나고 있는 것입니다.
도움은 되었습니다. 하지만 해결되지는 않았습니다. 모델 호출은 확정된 반환 타입을 가진 함수가 아니라 분포(distribution)의 표본일 뿐입니다. 단 한 번의 생성이라도 지침을 무시할 수 있습니다 — 더 긴 diff, 다른 날짜, 요청 문구의 미묘한 차이만 있어도 똑같은 "절대" 라인은 작동하지 않습니다. 저는 실제로 어떤 누락에 대한 메커니즘인지 알지 못하며, 그럴 필요도 없습니다. 요점은 다음과 같습니다: 자연어 지침은 자문적(advisory)입니다. 그것은 확률 질량(probability mass)을 이동시킬 뿐, 제한(clamp)하지 않습니다.
dev.to에서 읽은 한 기사가 이 내용을 재구성하여 저에게 하나의 깨달음을 주었습니다. LLM의 '품질 게이트(quality gate)'에 대한 대안은 더 잘 쓰인 LLM 품질 게이트가 아니라, 그 출력을 이후 결정론적인(deterministic) 무언가를 통해 라우팅하는 것입니다. 모델에게 '더 어렵게 요청하라'는 것이 아니라, '모델의 출력을 최종 결과물로 신뢰하지 말고, 모델과 그것이 건드리려는 대상 사이에 실제 필터를 두라'는 의미입니다.
커밋 메시지의 경우, 이는 결정론적 게이트를 구축하는 것만큼이나 위험도가 낮고 구현하기 쉽습니다. 그래서 저는 프롬프트를 강화하는 것만 하는 대신, 그 아래에 두 번째의 지루한 레이어를 추가했습니다:
_STRIP = ("co-author", "co-authored", "generated by", "claude", "anthropic", "openai", "llm", "ai:")
raw = subprocess.check_output(
...
이것이 전체 수정 사항입니다. 14줄에 불과합니다. 이 코드는 커밋 메시지를 이해하지 못하고, 모델을 다시 호출하지 않으며, 아무것도 영리하게 처리하지 않습니다. 단지 모델이 생성한 각 줄을 살펴보고, 소수의 하위 문자열 중 하나를 포함하는 모든 줄을 버릴 뿐입니다. 모델이 '사실 이번에는 출처 표기를 추가해야겠다'고 결정하더라도, 이 루프와 접촉하면 살아남을 수 없습니다. 왜냐하면 이 검사는 모델의 허가를 묻지 않고 — 모델 작업이 끝난 후, 루프 외부에서 실행되기 때문입니다.
저는 이 동일한 생성기(generator)를 도구(generate_commit_message)로 감싸는 MCP 서버에서도 같은 작업을 수행했습니다. 왜냐하면 도구 호출은 단지 중요한 무언가로 나가는 과정에 모델 출력을 생성하는 또 다른 코드 경로이기 때문입니다:
_STRIP = ("co-author", "co-authored", "generated by", "claude", "anthropic", "openai", "llm", "ai:")
def _claude(prompt: str, system: str = None) -> str:
...
동일한 필터, 동일한 추론입니다. 이 함수를 호출하는 것이 무엇이든 — 훅(hook), MCP 클라이언트, 제가 아직 작성하지 않은 미래의 스크립트 — 이미 게이트를 통과한 출력을 받게 됩니다. 보장(guarantee)은 모델 내부에 재확인되는 것이 아니라, 외부의 한 곳에 존재하게 된 것입니다.
이것의 일반화된 버전은 저에게는 이렇습니다. 만약 'X를 절대 하지 마라'와 같은 강력한 제약 조건(hard constraint)이 있다면, 그것을 X를 할 수도 있는 것에 대한 지침(instruction)으로만 인코딩하지 마십시오. X를 할 수도 있는 것의 출력에 대한 검사(check)로 인코딩하십시오. 지침 자체는 여전히 유지할 가치가 있습니다. 필터가 실제로 무언가를 잡아내야 하는 빈도를 줄여주고, 문자열 매칭보다 더 미묘한 내용일 경우 유일하게 사용할 수 있는 수단이기 때문입니다. 하지만 기계적으로 검증할 수 있는 제약 조건들—금지된 부분 문자열(banned substring), 필수 필드(required field), 범위 내의 값(value in range), JSON 형태(JSON shape)—에 대해서는 검증기(verifier)를 작성하십시오. 생각보다 저렴하며, 모델을 변경하거나 프롬프트를 업그레이드할 때 표류하지 않고, 모델과 달리 컨디션이 나쁜 날(off day)이 없습니다.
언제 필요한지 알 수 있는 징후는 이렇습니다. X가 계속 발생했기 때문에 'X를 절대 하지 마라'라는 문장을 프롬프트에 한 번 이상, 여러 곳에 추가했다면, 그것이 바로 게이트웨이가 아니라고 알려주는 신호입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기