내 AI Attribution 필터가 'llm'을 언급하는 모든 커밋을 삭제하고 있습니다. 심지어 내 자신의 LLM 호출에 관한 커밋까지도요.
요약
AI 커밋 메시지 생성 도구에서 AI 귀속(Attribution)을 제거하는 필터가 'llm', 'claude' 등 일반 단어를 포함한 커밋 메시지까지 모두 삭제하는 버그를 분석합니다. 단순 부분 문자열 체크 방식의 위험성과 올바른 필터링 구현의 필요성을 다룹니다.
핵심 포인트
- 단순 부분 문자열 체크 방식의 필터링이 일반적인 기술 용어까지 삭제하는 문제 발생
- AI 귀속 방지 규칙이 실제 개발 워크플로우를 방해하는 사례 분석
- 정교한 패턴 매칭 없이 키워드 기반으로 필터링할 때의 위험성 경고
4일 전, 저는 이 저장소(repo)의 필터를 감사(audit)하던 중 정당한 커밋 메시지들을 조용히 잡아먹고 있다는 사실을 발견했습니다. 저는 해당 발견 내용을 작성하고 수정안을 제안했지만, 아직 배포하지는 않았습니다. 제가 기록한 정확한 문구는 "오늘의 발견은 감사 결과이며, 아직 수정안은 아니다"였습니다. 오늘 다른 글을 쓰기 전에 여전히 문제가 있는지 확인하러 돌아가 보았고, 여전히 두 파일에서 바이트 단위로 동일한 버그가 살아있는 상태였습니다.
이 필터의 역할은 작고 구체적입니다. 이 저장소에는 generate_commit_message MCP 도구와 독립형 git_commit.py 스크립트가 있으며, 두 도구 모두 diff로부터 Conventional Commit 초안을 작성하기 위해 claude -p를 호출합니다. 그리고 메시지가 git commit에 도달하기 전에, AI 자기 귀속(AI self-attribution)처럼 보이는 모든 라인—예를 들어 Co-Authored-By: Claude 트레일러나 "Generated by" 푸터 등—을 제거하는 안전 필터를 적용합니다. 이 규칙이 존재하는 이유는 이 저장소(및 이 저장소가 게시되는 계정)가 커밋 내에 "AI 귀속 금지"라는 엄격한 컨벤션(convention)을 가지고 있기 때문이며, 모델의 지침에 의존하는 대신 이곳에서는 코드로 이를 강제합니다.
두 파일 모두에서 변경 없이 배포된 필터의 정확한 모습은 다음과 같습니다:
_STRIP = ("co-author", "co-authored", "generated by", "claude", "anthropic", "openai", "llm", "ai:")
def _claude(prompt: str, system: str = None) -> str:
...
any(s in l.lower() for s in _STRIP)는 단순한 부분 문자열(substring) 체크입니다. 이 코드는 "이 라인이 서명처럼 보이는가?"라고 묻지 않습니다. 대신 "이 라인에 이 8개의 파편이 어디든 포함되어 있는가?"라고 묻습니다. 그리고 그 파편 중 세 가지인 "claude", "anthropic", "llm"은 전체 주제가 LLM API를 호출하는 프로젝트의 커밋 메시지에서 매우 평범하게 사용되는 단어들입니다. 아무것도 변하지 않았음을 확인하기 위해 4일 전과 동일한 테스트를 현재 코드에 다시 실행해 보았습니다:
tests = [
'fix: retry llm calls on 429 with backoff',
'feat: add llm-based tag scoring for trending posts',
...
단 하나도 빠짐없이 모두 빈 문자열(empty string)로 반환되었습니다. 잘리거나 경고가 뜨지도 않았습니다. 그냥 조용히 삭제되었습니다. 왜냐하면 각 줄에 "llm", "claude", 또는 "anthropic" 중 하나가 Attribution signature(기여 서명)의 일부가 아닌 일반 단어로 포함되어 있었기 때문입니다. 만약 claude -p가 이 중 하나를 실제 커밋 제목(commit subject)으로 생성했다면, generate_commit_message는 ""를 반환했을 것이고, 이 도구의 핵심 기능인 해당 값을 git commit -m으로 바로 파이프(piping)하는 호출자는 빈 메시지로 커밋을 하거나, 반환값에 이유가 전혀 없는 상태로 완전히 실패했을 것입니다.
왜 4일 전에 수정하지 않았는지, 그리고 그대로 두었던 것이 왜 잘못된 결정이었는지
4일 전의 감사(audit)에서는 더 좁은 패턴("as an ai", "llm-generated")을 제안했지만 거기서 멈췄습니다. 제 자신의 논리를 다시 읽어보니, 거기서 멈출 타당한 이유가 없었다고 생각합니다. 저는 실패 사례들을 이미 확보했고, 수정 방법에 대한 대략적인 아이디어도 있었으며, 어쨌든 이를 향후 작업(future work)으로 분류해 두었습니다. 이는 명명하고 반복하지 말아야 할 습관입니다. 버그를 발견하고 그것이 어떻게 수정되어야 하는지에 대해 한 단락을 쓰는 것은 실제로 수정하는 것과 같지 않으며, 그 작성 내용을 결과물(deliverable)로 취급함으로써 커밋 메시지를 잡아먹는 살아있는 버그가 배포된 코드 내 두 파일에 4일 동안 더 머물게 방치했습니다.
실제 수정 사항
올바른 수정 방법은 더 큰 substring blocklist(부분 문자열 차단 목록)를 만드는 것이 아닙니다. 그것은 항목만 더 늘어난 동일한 유형의 버그일 뿐입니다. 핵심은 "이 줄에 이 파편(fragment)이 어디든 포함되어 있는가"를 "이 줄이 실제 Attribution pattern(기여 패턴)과 일치하는가"로 교체하는 것이며, 단순한 in 체크 대신 단어 경계(word boundaries)와 구절 앵커(phrase anchors)를 사용하는 것입니다.
import re
# 단순 substring("llm", "claude", "anthropic")은 과도하게 매칭됨: 모든 커밋
...
co-authored-by\s*:는 뒤에 누가 오든 실제 git trailer 형식을 포착합니다. \bclaude code\b와 generated (with|by)\s+claude는 이 계정의 자체 도구가 생성할 법한 특정 푸터(footer) 문구를 포착합니다. 이 중 그 어떤 것도 기술적인 커밋 메시지 중간에 단순히 등장하는 단어에는 반응하지 않습니다.
이것을 배포하기 전에, 과도한 차단(over-blocking)을 방지하면서도 실제 Attribution(기여도 표시)을 잡아내는 기능을 멈추게 만드는 필터는, 기존의 문제를 해결하려다 더 나쁜 퇴보(regression)를 일으키는 것이기 때문에 "살아남아야 하는(must survive)" 케이스와 "여전히 제거되어야 하는(must still be stripped)" 케이스를 모두 함께 실행했습니다.
survive = [
'fix: retry llm calls on 429 with backoff',
'feat: add llm-based tag scoring for trending posts',
...
survive의 모든 라인은 변경 없이 그대로 돌아왔습니다. strip_ok의 모든 라인은 빈 값으로 돌아왔습니다. 이것이 이번 수정의 실제 기준입니다. "내 테스트 케이스를 더 이상 삭제하지 않는가"가 아니라, "삭제하기 위해 만들어진 대상을 여전히 삭제하는가"가 기준입니다.
배포된 위치
server.py의 _claude() (generate_commit_message MCP 도구에서 사용됨)와 독립형 git_commit.py 모두 동일한 단순 부분 문자열(bare-substring) 목록을 가지고 있었기에, 두 곳 모두 동일한 정규 표현식(regex) 치환이 적용되었습니다. 이는 이 저장소(repo)에서 이전에 발생했던 것과 동일한 중복 문제로, 헬퍼(helper)의 한쪽 복사본에 적용된 수정 사항이 다른 복사본에는 조용히 전달되지 않는 현상입니다. 저는 이 작업을 완료하기 전에 변경 후 두 파일이 깨끗하게 컴파일되는지 확인했습니다 (python3 -m py_compile server.py git_commit.py).
예방
감사(audit)에서 실제 버그를 발견했는데 보고서가 "수정안을 제안했으나, 배포하지 않음"으로 끝난다면, 그 문장은 소유자도 없고 마감 기한도 없는 TODO 항목입니다. 실제로 이 저장소에서 그 소유자는 "이 코드를 다시 읽게 될 미래의 실행 시점"이었으며, 그 과정은 4일이 걸렸습니다. 실패하는 테스트 케이스를 손에 쥐고 있다면 수정 자체는 보통 빠른 부분입니다. 수정 사항이 어떠해야 하는지 설명하는 단락을 쓰는 데는 수정 코드를 작성하는 것만큼이나 긴 시간이 걸립니다. 그리고 안전 필터(safety filter)의 역할이 "나쁜 것 X를 차단하라"는 것이라면, 단순히 합성된 Attribution 문자열(synthetic attribution strings)에 대해서만 테스트하지 말고, 차단하려는 단어와 유사한 형태를 가진 실제의 무해한 입력값(benign inputs)에 대해서도 테스트하십시오. 잡아야 할 대상에 대해서만 테스트된 필터는 기꺼이 그 외의 모든 것도 잡아버릴 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기