1,500개의 AI 생성 디렉토리 항목에서 지속적인 보일러플레이트를 잡아낸 3가지 린트(lint) 규칙
요약
AI 생성 콘텐츠에서 반복되는 보일러플레이트 패턴을 탐지하고 제거하기 위한 린트(lint) 규칙과 자동화 방법을 소개합니다. 프롬프트 템플릿과 폴백 콘텐츠로 인해 발생하는 텍스트 중복 문제를 해결하기 위해 문장 클러스터링 기법을 활용합니다.
핵심 포인트
- AI 모델 생성 텍스트에서 특정 문구가 반복되는 패턴 발생
- 프롬프트 템플릿과 폴백 콘텐츠가 보일러플레이트의 주요 원인
- 구절 빈도 및 문장 클러스터링을 통한 린트 규칙 적용
- 숫자와 경로를 정규화하여 문장 중복을 효과적으로 탐지
Claude Haiku를 사용하여 1,500개의 디렉토리 항목에 대한 콘텐츠를 생성할 때, 출력물은 완전히 균일한 보일러플레이트(boilerplate)는 아니지만, 그렇다고 완전히 고유하지도 않습니다. 모델에는 패턴이 있습니다. 특정 문구들이 HuggingFace pipeline tag나 카테고리를 공유하는 항목들 사이에서 반복적으로 나타납니다. 10개의 항목을 나란히 놓고 검토하기 전까지는 알아차리기 어렵지만, 모든 항목이 "It follows chat template conventions and supports system-level role instructions."로 시작한다는 사실을 깨닫게 됩니다.
해당 문구는 87개의 항목에서 토씨 하나 틀리지 않고 그대로 나타났습니다.
템플릿 텍스트가 유입되는 방식
이 설정에서 보일러플레이트가 유입되는 경로는 두 가지입니다.
첫째, 프롬프트 템플릿(prompt template)입니다. AI 모델 항목을 위한 저의 시스템 프롬프트는 "2~3문장의 기술적 요약"을 요구합니다. 모델은 텍스트 생성 모델(text-generation model)이 무엇을 하는지 알고 있으며, 그 지식을 동일한 방식으로 표현하는 경향이 있습니다. "Handles instruction prompts, multi-turn dialogue, and open-ended text generation"은 모든 채팅 모델에 대한 사실적으로 정확한 설명입니다. 이는 곧 모든 채팅 모델의 설명에 이 문구가 등장함을 의미합니다.
둘째, 폴백 콘텐츠(fallback content)입니다. Claude API를 사용할 수 없는 경우(API 키가 없는 CI 환경 또는 첫 번째 보강 실행 전의 새로운 항목들), 저는 템플릿 콘텐츠로 항목을 초기화(seed)합니다. 3단계 품질 사다리(three-tier quality ladder)에서 계층화에 대해 다룹니다. 문제는 Claude가 항목에 실행된 후에도, 생성된 콘텐츠가 때때로 폴백 문구와 매우 유사하게 울리는 경우가 있다는 점입니다. 모델이 유사한 프롬프트가 포함된 유사한 항목들을 보았고, 유사한 출력값으로 수렴한 것입니다.
저는 항목별로 프롬프트를 더 구체적으로 만들거나, "이 문구들을 사용하지 마시오"라는 명시적인 지침을 추가함으로써 이를 해결할 수 있었습니다. 시드 변형 생성(seeded-variant-generation) 접근 방식에서 보여주듯 저도 그렇게 했습니다. 하지만 프롬프트가 완벽해야 한다는 것에 의존하지 않고도 퇴보(regression)를 잡아낼 수 있는 게이트(gate)도 원했습니다.
린트 패스(lint pass)가 하는 일
scripts/lint-humanization.mjs는 세 개의 데이터셋 JSON 파일(models, saas, games)을 읽어 모든 행에서 텍스트 필드를 추출한 뒤, 다음 두 가지 사항을 확인합니다:
- 구절 빈도 (Phrase frequency): 드물게 나타나거나 아예 나타나지 않아야 하는 특정 문자열
- 문장 클러스터링 (Sentence clustering): 모든 항목에 걸친 정규화된 문장 중복 제거 (normalized sentence deduplication)
문장 클러스터링의 경우, 정규화 (normalization)가 중요합니다. "loads 7 billion parameters"와 "loads 3 billion parameters"를 서로 다른 문장이 아니라 동일한 문장 형태(sentence shape)로 취급해야 하기 때문입니다. 저는 비교하기 전에 숫자는 <num>으로, 리포지토리 경로(repository paths)는 <repo>로 교체하고 문장 부호를 제거합니다:
function normalizeSentence(s) {
return s
.toLowerCase()
...
정규화된 문장이 N개 이상의 항목에서 나타나면 검토 대상이 됩니다.
실제 문제를 드러낸 세 가지 규칙
규칙 1: "The main gap" — max 0, severity error
이 구절은 저의 초기 OSS 대안 항목들에서 나타났습니다. 프롬프트(prompt)가 "비교 노트(comparison notes)"를 요청하자, 모델은 일관되게 "The main gap between SaaS X and its open-source alternatives is..."라는 문구로 시작했습니다. 이 말이 틀린 것은 아닙니다. 실제로 그것이 주요 격차(main gap)인 경우가 많으니까요. 하지만 40개의 항목에서 토씨 하나 틀리지 않고 그대로 나타나는 것은 템플릿(templated)을 사용한 것처럼 보입니다.
max: 0은 이 구절이 나타나면 린트 패스(lint pass)가 경고(warn)가 아닌 에러(error)를 발생시킨다는 의미입니다. 이 규칙을 추가한 후, 저는 비교 노트를 다른 구조로 요청하도록 프롬프트를 다시 작성했습니다. 그 결과 해당 구절은 사라졌습니다.
규칙 2: "vendor lock-in" — max 24, severity warn
"vendor lock-in"을 완전히 금지할 수는 없습니다. 많은 맥락에서 실제로 적절한 용어이기 때문입니다. 하지만 500개의 SaaS 항목 중 24개 이상에서 나타난다는 것은, 모델이 특정 의도를 가지고 선택한 것이 아니라 기본 프레임워크(default framing)로서 이 용어에 의존하고 있음을 의미합니다. max: 24 임계값은 데이터셋의 약 5%에 해당하며, 이 수치를 넘어서면 이는 진정한 사용이 아닌 프롬프트 패턴(prompt pattern)임을 나타냅니다.
이 규칙이 트리거되었을 때, 저는 31개의 사례를 발견했습니다. 저는 해당 항목들을 수동으로 다시 작성하지 않았습니다. 대신, 프롬프트를 수정하여 "OSS 대안을 찾아야 하는 이유" 대신 "데이터 이식성 및 셀프 호스팅의 트레이드오프 (tradeoffs)"를 요청하도록 조정했습니다. 이를 통해 어휘가 벤더 종속성 (vendor lock-in) 프레임에서 벗어나도록 유도했습니다.
규칙 3: "handles instruction prompts, multi-turn dialogue, and open-ended text generation" — max 0, severity warn
이 규칙은 매우 구체적이기 때문에, 심각도 (severity)를 error가 아닌 warn으로 유지하면서도 max 값을 0으로 설정하여 플래그를 지정했습니다. 이는 제 시스템 프롬프트 (system prompt) 예시 출력에서 거의 그대로 가져온 문장이었습니다. 모델에게 좋은 항목이 어떤 모습인지 보여주었더니, 모델이 제 예시의 문구를 암기해 버린 것입니다.
해결 방법은 시스템 프롬프트에서 예시를 제거하고 구조적 가이드라인 (structural guidance)만으로 대체하는 것이었습니다. 첫 실행 시에는 항목의 품질이 약간 저하되었지만 (예시가 없어서 모델이 형식을 덜 명확하게 파악함), 지시 사항 (instruction)과 예시 사이의 적절한 균형을 찾은 후에는 다시 회복되었습니다.
2단계 심각도: error vs warn
8개의 모든 규칙은 severity: "error" 또는 severity: "warn"을 가집니다. 린트 (lint) 스크립트는 error가 발생했을 때만(또는 --strict 옵션 사용 시 warning이 발생했을 때도) 0이 아닌 상태 코드로 종료됩니다.
구분 기준은 다음과 같습니다:
- error: 해당 문구가 템플릿이 통과되었음을 나타내며, 프로덕션 (production) 환경에 존재해서는 안 되는 경우입니다. 배포를 차단합니다.
- warn: 해당 문구가 정당하긴 하지만 너무 과도하게 사용된 경우입니다. 추적은 하되, 배포를 차단하지는 않습니다. 다음 검토 시점에 프롬프트를 수정합니다.
--strict 옵션을 사용하여 실행하는 것은 감사 (auditing)에 유용합니다. 옵션 없이 실행하는 것은 CI (지속적 통합)에 유용합니다. 모델이 "오픈 소스 생태계 (open-source ecosystem)"라는 표현을 약간 과하게 사용한다고 해서 매번 차단되는 것이 아니라, 실제 퇴보 (regressions)를 잡아내고 싶기 때문입니다.
이 방식이 잡아내지 못한 것
린트 패스 (lint pass)는 문구 기반이며 문장 단위로 작동합니다. 따라서 다음 사항들은 잡아내지 못합니다:
- 구조적 반복 (Structural repetition) (모든 항목이 동일한 표면 구조를 가진 4개의 장점과 3개의 단점을 포함하는 경우)
- 사실 관계 오류 (Factual errors) (언어 태그가 영어뿐인데 일본어를 지원한다고 설명된 모델의 경우)
- 구체성 결여 (Missing specificity) (기술적으로는 사실이지만 아무런 정보도 전달하지 않는 유효한 문장들)
이러한 문제들을 해결하기 위해, 저는 콘텐츠 품질 게이트 스크립트(content quality gate scripts)와 수동 스팟 체크(manual spot-checks)에 의존합니다. 린트 패스(lint pass)는 하나의 계층일 뿐, 품질 관리의 전부가 아닙니다.
--dry-run 플래그는 아무것도 수정하지 않고 위반 사항을 보고합니다. 저는 현재 데이터셋을 대상으로 이를 매주 실행합니다. 만약 위반 사항이 급증한다면 — 대규모 배치 ETL 실행 후에 주로 발생합니다 — 다음 콘텐츠 갱신(content refresh)을 진행하기 전에 원인을 조사합니다.
세 개의 AI 큐레이션 디렉토리 사이트를 운영하는 6개월간의 지속적인 실험 중 일부입니다. 여기에 언급된 기술적 주장들은 사실이며, 이 기사는 AI의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기