quality_contract v2 구현 방법 — AI 기사를 근본적으로 감사하는 4가지 필드
요약
AI 생성 기사의 품질을 검증하기 위한 quality_contract v2 프레임워크의 구현 방법을 다룹니다. 단순한 구조적 감사를 넘어 E-E-A-T 원칙을 기반으로 기사의 독창성과 실질적인 가치를 검증하는 4가지 핵심 필드와 버전 관리 전략을 설명합니다.
핵심 포인트
- v1의 한계인 일반적인(generic) 콘텐츠 생성 문제 해결
- E-E-A-T(경험, 전문성, 권위성, 신뢰성) 기반의 긍정적 주장 검증
- 버전 관리를 통한 감사 스크립트의 효율적 운영
- primary_keyword를 통한 콘텐츠 범위(scoping) 강제
이 실험을 시작한 지 3개월이 지났을 때, 저는 측정할 수 없는 문제에 직면했습니다. 모든 기사가 품질 게이트(quality gate)를 통과하고 있었지만, 그중 어느 것도 독특하게 느껴지지 않았습니다. Google의 E-E-A-T 가이드라인은 이를 경험(Experience)과 직접적인 지식(First-hand knowledge)으로 정의합니다. 이는 구조적 감사(structural auditing)만으로는 생성할 수 없는 신호들입니다. 상투적인 표현 검사기는 깨끗했고, 태그 풀(tag pool)은 유효했으며, 단어 수도 범위 내에 있었습니다. 하지만 제가 직접 코드를 조사하여 작성한 기사와 생성 루틴(generation routine)이 특별한 근거 없이 조립해낸 기사 사이에는 아무런 차이가 없었습니다.
그것이 바로 quality_contract v2가 해결하고자 하는 간극입니다. v1이 당신이 하지 않은 것을 확인한다면, v2는 당신이 한 것을 확인합니다. 네 가지 필드. 새로운 기사에 대해서는 실패 시 차단(Fail-closed) 방식을 적용합니다. 과거의 기록을 소급하여 다시 작성하지는 않습니다.
v1이 잘못된 것을 잡아냈던 이유
제가 6월에 설명했던 기존 품질 게이트는 세 가지 특정 실패 모드(failure modes)를 다루었습니다: 상투적인 문구, 허용된 풀을 벗어난 태그, 그리고 목표 범위를 벗어난 단어 수입니다. 그 게이트는 첫 달에 약 12개 정도의 기사를 차단했는데, 대부분 생성 루틴이 어조(voice) 측면에서 게을러진 경우였습니다.
하지만 게이트가 잡아내지 못한 것은 기사가 존재해야 할 이유가 있는지 여부였습니다. 상투적이지 않고, 태그가 유효하며, 단어 수가 정확한 산문이라 할지라도 여전히 일반적인(generic) 산문일 수 있습니다. E-E-A-T — 경험(Experience), 전문성(Expertise), 권위성(Authoritativeness), 신뢰성(Trustworthiness) — 은 긍정적인 주장(affirmative claims)을 요구합니다. v1에는 기사가 긍정적으로 주장하는 바가 무엇인지에 대한 모델이 없었습니다.
저는 이러한 실패 모드를 종합적으로 인지했습니다. 6월에 작성된 기사 10개를 다시 읽어보았을 때, 어떤 것이 직접적인 코드 관찰에 기반한 것인지, 아니면 기억으로부터 재구성된 것인지 쉽게 말할 수 없었습니다. 기사들이 거짓말을 한 것은 아니었습니다. 단지 그 기사 자체의 출처(provenance)에 대해 아무런 신호를 주지 않았을 뿐입니다.
v2의 네 가지 필드와 각 필드의 역할
총 5개의 프론트매터(frontmatter) 키가 있습니다: 하나는 버전 표시용이며, 나머지 네 개는 실제 신호를 담고 있습니다.
**quality_contract: "v2"**는 감사 스크립트(audit script)가 어떤 규칙 세트(rule set)를 적용해야 하는지 알려줍니다. 이는 나중에 v3를 도입해야 할 때 기사 생성물들을 구분하고자 할 때 유용합니다. 저는 과거 파일에 잘못된 검사 항목을 적용하는 감사 스크립트를 디버깅하며 시간을 낭비한 적이 있는데, 버전 표시(version marker)를 남기는 데는 단 한 줄이면 충분합니다.
**primary_keyword**는 이 기사가 목표로 하는 단 하나의 검색 문구입니다. 기사를 쓰기 전에 이를 작성하면, 첫 번째 섹션이 존재하기 전이라는 가장 비용이 적게 드는 시점에 범위(scoping)를 결정하도록 강제합니다. 특정 primary_keyword가 있는 기사는 키워드가 계속해서 초점을 되돌려 놓기 때문에, 불필요한 내용(filler sections)이 포함될 가능성이 낮습니다. primary_keyword: "AI article quality gate frontmatter fields"라고 지정된 기사는 키워드가 지정되지 않은 기사보다 일반론적인 이야기로 흘러가기 어렵습니다.
**search_intent**는 한 문장으로 작성합니다: 누가, 왜 검색하는가에 대한 내용입니다. 저는 이를 "[페르소나(persona)], [특정 사항(specific thing)]을 찾는 중"과 같은 형식으로 작성합니다. 이 제약 조건은 생성(generation) 단계에서 진정으로 유용합니다. "이미 매달 40개의 기사를 배포하고 있으며 수준을 높이고 싶어 하는 개발자"를 위해 작성된 기사는, 첫 번째 파이프라인을 구축 중인 사람을 위해 작성된 동일한 제목의 기사와는 다루는 영역이 다릅니다.
**verified_at**은 날짜입니다: 이 기사의 주장들이 최신 상태임을 제가 확인한 날짜를 의미합니다. 코드 기반 기사의 경우, 관련 파일들을 읽은 날짜가 됩니다. 도구 비교의 경우, 해당 도구를 실행한 날짜가 됩니다. 이 필드가 검증(verification)을 강제하는 것은 아닙니다. 날짜 문자열 자체가 검증 기록인 것은 아니기 때문입니다. 하지만 이는 하나의 약속을 드러냅니다: 바로 그 날짜에, 저는 이 사실들이 정확하다고 주장했다는 것입니다. 이러한 마찰(friction)은 잘못된 날짜를 입력하는 것이 부정직하게 느껴지도록 만들기에 충분하며, 결과적으로 그 마찰이 가장 중요한 역할을 합니다.
**original_evidence**는 채우기 가장 어려운 필드입니다. 이 필드는 해당 프로젝트의 저자만이 제공할 수 있는 1인칭 증거 한 가지를 명시합니다. 예를 들어 측정값, 내가 마주친 에러 메시지(error message), 혹은 내가 작성한 함수 같은 것들입니다. 만약 구체적인 무언가를 명시할 수 없다면, 그 기사는 아마도 존재해야 할 경쟁력 있는 이유가 없을 것입니다. GitHub Actions의 cron 작동 방식을 설명하는 기사는 수천 개가 넘습니다. 하지만 특정 날짜에 나의 일일 Bluesky 파이프라인(pipeline)을 망가뜨렸던 구체적인 cron 타이밍의 엣지 케이스(edge case)에 관한 기사는 재현하기가 더 어렵습니다.
이 기사의 전체 블록은 다음과 같습니다:
quality_contract: "v2"
primary_keyword: "AI article quality gate frontmatter fields"
search_intent: "Developers running AI-assisted article pipelines who want to enforce
...
audit-articles.mjs가 이를 강제하는 방법
이 검사는 날짜로 제한됩니다(date-gated). 파일 이름의 날짜가 2026-07-14 당일이거나 그 이후인 기사에 대해서만 v2 규칙이 적용됩니다. 그보다 오래된 모든 것은 v1 규칙으로만 실행됩니다.
const QUALITY_V2_KEYS = [
"quality_contract",
"primary_keyword",
...
날짜 비교는 문자열 비교(string comparison)로 이루어집니다. ISO 날짜는 사전식(lexicographically)으로 정렬되므로, 별도의 날짜 파서(date parser) 없이도 작동합니다. 오늘 밤 스크립트를 끝낼지 말지 결정해야 하는 밤 11시에는 이런 작은 디테일이 중요합니다.
v2 키가 누락되면 에러를 발생시킵니다 — errors.push(...), 종료 코드(exit code) 1이 발생하며, 이는 게시 파이프라인(publish pipeline)의 드라이 런(dry-run) 단계를 차단합니다. 이러한 실패 시 차단(fail-closed) 동작이 핵심입니다. original_evidence가 누락된 기사는 해당 항목 없이 저장소(repository)를 떠날 수 없습니다.
한 가지 미묘한 차이가 있습니다. 스크립트는 게시 예정지가 대기 중인 기사와 모든 곳에 완전히 게시된 기사를 구분합니다.
const publishedTo = Array.isArray(meta.publish_to) ? meta.publish_to : [];
const publishedUrls = meta.published_urls ?? {};
...
완전히 게시된 기사의 경우, v2 키가 누락된 것은 경고(warning) 사항입니다. 게시 단계를 차단할 정도의 오류는 아닙니다. Bluesky 제외 규칙이 존재하는 이유는 초기 Bluesky 포스트들이 프론트매터(frontmatter)에 URL을 항상 기록하지 않았기 때문이며, 이는 Dev.to와 Hashnode API 비교에서 문서화된 행동적 차이입니다. 이러한 플랫폼의 특이사항들이 3개월간의 실제 게시 과정에서 감사 예외 사항(audit exceptions)으로 쌓이게 됩니다.
백필(backfilling) 없이 130개 이상의 과거 기사들을 소급 적용(Grandfathering)하기
7월 중순에 v2를 도입했을 때, 이미 115개의 기사가 네 가지 새로운 필드 없이 게시된 상태였습니다. 저의 첫 번째 시도는 백필(backfilling)이었습니다. 각 기사를 읽고, 필드를 추가한 뒤, 커밋(commit)하는 방식이었죠. 하지만 약 10개 정도를 처리한 후 중단했습니다.
문제는 이렇습니다. 두 달 전에 게시한 기사에 대해 신뢰할 수 있는 original_evidence를 작성하려면, 제가 그 글을 쓸 당시 실제로 무엇을 보고 있었는지를 재구성해야 합니다. 대부분의 기사는 어느 정도 정확하게 재구성이 가능합니다. 하지만 바로 그 "어느 정도의 정확도"가 문제입니다. original_evidence는 기억에 의존해 재구성된 주장을 방지하기 위한 것이어야지, 재구성된 주장들로 구성되어서는 안 되기 때문입니다.
소급 적용 규칙(grandfathering rule) — 즉, 완전히 게시된 기사에서 누락된 v2 키를 에러(error)가 아닌 경고(warning)로 취급하는 것 — 은 기존의 백로그(backlog)를 정직하게 유지할 수 있게 해줍니다. 경고는 CI 로그에 나타납니다. 동일한 주제로 후속 기사를 작성할 때는 적절한 v2 필드가 포함됩니다. 오래된 기사들은 있는 그대로 유지됩니다.
이는 불편한 절충안입니다. 프로젝트가 초기 입지를 구축하던 시기에 작성된 가장 초기 기사들은 신뢰성 마커(credibility markers)를 갖추지 못했을 가능성이 가장 높습니다. 하지만 대안인 "기억에 의존해 original_evidence를 채워 넣는 것"은 이 필드가 방지하기 위해 설계된 바로 그 저신뢰성 주장들을 만들어낼 뿐입니다. 정직한 공백이 억지로 채워 넣은 노이즈보다 낫습니다.
잘된 점, 안 된 점, 그리고 다르게 했을 점
잘된 점: 생성 시점에 실패하도록 설계(fail-closed)한 것입니다. 다섯 가지 필드는 기사의 프론트매터(frontmatter) 템플릿에 존재하며, 본문이 시작되기 전에 채워집니다. 제가 섹션 3을 작성할 때, verified_at과 original_evidence는 이미 그곳에 있습니다. 이를 잊어버릴 수 없습니다. 올바르게 채우기 전까지는 작업 흐름을 방해하기 때문입니다. 이는 파이프라인 끝단에서 수행하는 체크(end-of-pipeline check)와는 정반대의 방식입니다.
지난주에 다룬 세 가지 린트 규칙 (three lint rules)은 데이터 계층(data layer)에서의 병렬적인 문제, 즉 1,500개의 디렉토리 엔트리 전반에 걸쳐 반복되는 상용구(boilerplate) 문장을 잡아내는 문제를 다룹니다. quality_contract v2 필드는 이를 메타데이터 계층(metadata layer)에서 해결합니다. 이 두 가지를 통해, 저는 기사가 독창적인 내용을 담고 있는지(린트), 그리고 그 주장이 1인칭 경험에 근거하는지(계약/contract)를 감사할 수 있습니다. 이들은 상호 보완적이며, 저는 매 게시 전 두 가지를 모두 실행합니다. 콘텐츠 양보다 품질 게이트(quality gates)가 AdSense 승인을 더 잘 보호할 것이라는 더 큰 가설은 3개월 차인 지금까지도 검증되지 않았지만, v2 계약(contract)은 제가 가장 확신하는 메커니즘입니다.
잘 안 된 점: verified_at은 여전히 신뢰 시스템(honor system)에 의존합니다. 스크립트는 해당 필드가 타당한 ISO 날짜 문자열인지 확인하지만, 제가 실제로 무언가를 확인했는지는 검증하지 않습니다. 실제 코드를 읽기보다 기억을 되살려 내용을 종합하는 과정에서, 가끔 반사적으로 오늘 날짜를 입력하는 것을 발견했습니다. 이 필드는 마찰(friction)을 일으키긴 하지만 증거(proof)를 만들어내지는 못합니다.
잘 안 된 점 (2): 요약 및 큐레이션 기사를 위한 original_evidence는 다소 어색합니다. 이러한 기사들은 설계상 코드 기반이 아닌 관찰 기반입니다. "2026-08-01 기준 HN top-15에서 관찰됨"은 유효한 original_evidence이지만, 필드 라벨은 측정이나 코드를 암시합니다. v3에서는 이를 분리하는 것을 고려 중입니다: 측정되거나 구축된 것을 위한 empirical_evidence (경험적 증거), 그리고 집계 기반 기사를 위한 editorial_provenance (편집 출처)로 나누는 것입니다.
내가 다르게 했을 점: 프로젝트 시작 첫날부터 생성 루틴(generation routine)의 프롬프트 템플릿(prompt template)에 이 필드들을 추가했을 것입니다. 프로젝트 시작 3개월 만에 새로운 프론트매터(frontmatter) 요구 사항을 도입하면서, 기존 데이터에 대한 유예 조치(grandfathering)라는 타협안을 강요받게 되었습니다. 만약 첫 번째 기사부터 primary_keyword (주요 키워드)와 search_intent (검색 의도)를 필수 항목으로 지정했다면, 품질 신호(quality signal)가 더 풍부해졌을 것이고 이중 체계(two-tier system)도 생기지 않았을 것입니다.
아직 자동화하지 못한 관련 분야는 verified_at (검증 일시)의 노후화(staleness) 문제입니다. verified_at: "2026-05-15"로 표시된 5월에 발행된 기사는 90일 전의 코드 상태를 인용하고 있는 셈입니다. verified_at이 현재 날짜보다 90일 이상 뒤처진 기사들을 찾아내는 스윕(sweep) 작업을 수행한다면, 어떤 기사를 새로고침(refresh)해야 할지 우선순위를 정하는 데 도움이 될 것입니다. 아마도 이것이 제가 다음에 연결할 작업이 될 것입니다.
FAQ
Q: primary_keyword가 실제로 작성되는 내용을 바꾸나요, 아니면 단순한 메타데이터인가요?
둘 다입니다. 메타데이터로서 이는 감사 로그(audit logs)와 모든 다운스트림 SEO(SEO) 작업에 유용합니다. 강제 기능(forcing function)으로서, 섹션 1을 작성하기 전에 키워드를 작성하는 것은 기사의 내용을 변화시킵니다. primary_keyword: "astro glob loader pnpm monorepo path resolution"가 설정된 기사는 해당 특정 주제를 유지합니다. 지정된 키워드가 없으면 동일한 기사가 일반론적인 내용으로 확장되는 경향이 있습니다. 이 필드는 제가 작업을 시작하기 전에 범위를 좁혀주는데, 범위를 좁히는 작업은 초기 단계에서 수행할 때 비용이 가장 적게 듭니다.
Q: 왜 v2 체크를 발행 스크립트(publish script) 자체가 아니라 발행 전 드라이 런(pre-publish dry-run)에서 실행하나요?
그 이유는 배포에 정서적으로 몰입하기 전에 문제를 포착하는 것이 목표이기 때문입니다. 생성 단계에서의 오류는 비용이 저렴합니다. 작성 중일 때 수정하면 됩니다. 발행 단계에서의 오류는 제가 자랑스럽게 완성한 결과물을 마주하고, 재작성을 하거나 의도적으로 품질을 건너뛰어야 하는 상황에 직면함을 의미합니다. 잘못된 시점에 배치된 마찰(friction)은 행동을 변화시키지 못하고, 그저 우회될 뿐입니다.
Q: original_evidence (원문 증거)가 "내가 이것을 만들었다"와 같은 모호한 주장으로 채워지는 것을 어떻게 방지하나요?
프로그램 방식으로는 불가능합니다. 게이트(gate)는 존재 여부와 유형을 확인하며, 품질을 확인하지는 않습니다. 의심스러울 정도로 짧은 값(30자 미만)은 에러가 아닌 경고를 받습니다. 제가 의존하는 것은 가시성(visibility)입니다. 해당 필드는 원문 마크다운(raw markdown)에 포함되어 있고, 감사 로그(audit logs)에 나타나며, 구체적인 증거 옆에 모호한 주장이 놓여 있으면 그것이 명백히 빈약하다는 것이 눈에 보입니다. 사회적 계약(social contract)은 다음과 같습니다. 만약 증거 주장이 그토록 빈약하다면, 그 기사는 발행되어서는 안 됩니다. 이는 검증(validation)이 아닌 가시성에 의해 강제됩니다.
Q: 이 필드들을 프로그래밍 방식의 디렉토리 페이지로 이전할 수 있나요?
verified_at은 깔끔하게 이전됩니다. OSS 대안 디렉토리의 각 SaaS 항목에 verified_at 필드가 있다면 최신성 감사(freshness audits)를 간단하게 수행할 수 있습니다. verified_at이 90일 이상 지난 항목을 필터링하고, 해당 항목들을 재생성한 뒤, 필드를 업데이트하면 됩니다. 동일한 패턴이며, 콘텐츠 유형만 다를 뿐입니다. 아직 이를 연결하지는 않았지만, 할 일 목록(list)에는 들어 있습니다.
Q: 실무에서 v1과 v2의 차이점은 무엇인가요?
v1은 위반 사항의 부재(absence of violations)를 확인합니다: 금지된 상투적 표현(clichés) 없음, 유효한 태그, 범위 내의 단어 수 등입니다. v2는 긍정적 주장(affirmative claims)의 존재(presence)를 확인합니다: 명시된 키워드, 진술된 의도, 검증 날짜, 1인칭 증거 등입니다. 이들은 서로 다른 것을 잡아냅니다. 어떤 기사는 v2를 통과하고 v1에서 탈락할 수 있고(올바른 메타데이터, 잘못된 어조), 혹은 v1을 통과하고 v2에서 탈락할 수도 있습니다(깔끔한 문장, 출처 없음). 저는 두 가지를 모두 실행합니다. v1 구현 방식은 original v1 post를 참조하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기