콘텐츠 품질 점수 산정 시스템 구축: PostAll이 출력 표준을 보장하는 방법
요약
AI 생성 콘텐츠의 품질을 보장하기 위해 고유성, 가독성, SEO 준수 여부를 통합적으로 평가하는 점수 산정 파이프라인 구축 방법을 소개합니다. 단순한 검사기를 넘어 콘텐츠의 구조적 유사성을 잡아내고 배포 여부를 결정하는 시스템 구현 과정을 다룹니다.
핵심 포인트
- 단일 지표가 아닌 고유성, 가독성, SEO를 결합한 복합 점수 시스템 구축
- 구조적 유사성(Uniqueness Drift)을 감지하여 로봇 같은 콘텐츠 방지
- Python의 textstat 라이브러리를 활용한 가독성 점수 산정
- Node.js와 Python을 연동한 실전적인 품질 게이트 파이프라인 설계
PostAll은 기사를 빠르게 생성합니다. 속도는 결코 어려운 부분이 아니었습니다.
대량 생성 기능을 출시한 지 3주 후, 한 베타 고객이 생성된 200개의 기사 중 40개가 정확히 동일한 구조적 골격(동일한 도입부 후크, 동일한 3개 섹션 구성, 동일한 한 줄 결론, 명사만 교체됨)을 따르고 있다고 문제를 제기했습니다. 모든 기사는 기술적으로는 고유했습니다. 하지만 그중 어떤 것도 200개의 서로 다른 콘텐츠처럼 느껴지지 않았습니다.
그날 저는 생성과 전달 사이에 위치하는 품질 게이트(quality gate)를 구축하기 시작했습니다. 사후에 덧붙여진 가독성 검사기가 아니라, 단 하나의 기사가 고객의 CMS에 도달하기 전에 고유성 드리프트(uniqueness drift), 가독성 불일치, 그리고 기본적인 SEO 실패를 잡아내는 실제 점수 산정 파이프라인(scoring pipeline)입니다.
이것이 바로 그 시스템이며, 현재 프로덕션(production)에서 실행 중인 버전과 작동하지 않았던 이전 두 가지 버전입니다.
이것이 보기보다 어려운 이유
가독성 검사기(Hemingway App은 아주 오래전부터 존재했습니다)는 존재합니다. 표절 검사기도 존재합니다. SEO 체크리스트도 존재합니다. 어려운 점은 이 중 어느 하나를 구축하는 것이 아니라, 그 어떤 것도 단독으로는 "이번 주에 생성한 다른 모든 기사와 똑같이 들린다"는 점을 잡아내지 못한다는 것입니다.
고유성(Uniqueness), 가독성(readability), 그리고 SEO 준수(SEO compliance)는 서로 다른 방향으로 작용합니다. 콘텐츠는 Flesch 읽기 점수(Flesch reading score)에서 만점을 받을 수 있지만 여전히 로봇처럼 들릴 수 있습니다. 표절 검사(복사된 문장 없음)를 통과할 수 있지만 여전히 지난 50개의 기사와 구조적으로 동일할 수 있습니다. 세 가지 신호(signals)를 하나의 숫자로 결합하고, 그 숫자가 너무 낮을 때 무엇을 할지 결정하는 것이 실제 문제라는 사실이 밝혀졌습니다.
당신이 구축하게 될 것
이 글을 마칠 때쯤이면, 생성된 콘텐츠를 입력받아 다음과 같은 결과를 반환하는 점수 산정 파이프라인(scoring pipeline)을 갖게 될 것입니다:
{
"composite_score": 81.4,
"passed": true,
...
하나의 결정으로 가중치가 부여된 세 가지 독립적인 검사: 그대로 배포할 것인가, 아니면 재생성(regeneration)을 위해 다시 보낼 것인가.
선수 학습 요구 사항 (Prerequisites)
- Node.js 18+ (네이티브
fetch사용을 위해, 별도의 HTTP 클라이언트 불필요) - Python 3.10+
- 서비스 간 HTTP 요청(HTTP requests)에 대한 기본적인 이해
- 유료 API 키 불필요 — 여기서 수행되는 모든 검사는 로컬에서 실행됩니다.
설정 (The setup)
pip install fastapi uvicorn textstat
Python 측 설정은 이것으로 끝입니다. Node 측은 이 작업을 위해 의존성이 전혀 없습니다. 내장된 fetch를 사용하기 때문입니다.
1단계: 가독성 점수 산정 (Readability scoring)
저는 직접 Flesch-Kincaid 구현체를 작성하는 대신 textstat을 사용했습니다. 1948년에 만들어진 가독성 공식을 다시 만드는 것은 이득이 거의 없기 때문입니다.
간과하기 쉬운 부분은
패러프레이징(paraphrasing, 문장 바꿔쓰기) 단계에서 문제가 발생했습니다. PostAll의 생성 모델은 문장을 완전히 재구성하여—의미는 같지만 문자 그대로의 중복은 전혀 없는 상태로—만들 수 있었고, 이 경우 shingle(싱글) 체크는 이를 100% 고유한 것으로 점수를 매겼습니다. 반면, 밀접하게 연관되어 있지만 실제로 다른 제품(예: "무선 이어폰" vs "무선 헤드폰")에 관한 두 기사는 사람이 보기에는 전혀 혼동될 일이 없음에도 불구하고, 공통된 구절을 충분히 공유한다는 이유로 유사 중복(near-duplicates)으로 분류되었습니다.
해결책은 비교 방식을 문자 그대로의 텍스트 중복에서 의미적 유사성(semantic similarity)으로 전환하고, shingle 대신 문장 임베딩(sentence embeddings)을 사용하는 것이었습니다:
# 실제로 중요했던 부분 — shingle을 임베딩으로 교체
from sentence_transformers import SentenceTransformer
import numpy as np
...
전체적인 구조는 이전과 동일합니다. 새로운 텍스트를 임베딩하고, 마지막 N개의 shingle 세트 대신 마지막 N개의 임베딩과 비교하여 최대 유사도를 구합니다. 검사 방식이 "이 문장들이 단어를 공유하는가"에서 "이 문장들이 같은 의미를 갖는가"로 바뀌었으며, 이것이 우리가 실제로 중요하게 생각했던 지점이었습니다.
3단계: SEO 점수 산정
이 부분은 Node 레이어에 존재합니다. 렌더링된 HTML이 전달되기 전에 이미 해당 레이어에 존재하기 때문입니다.
// scorer/seo-check.js
function scoreSEO(html, targetKeyword) {
const checks = {
...
특별한 기술이 들어간 것은 아닙니다. 렌더링된 HTML에 대해 정규 표현식(regex)을 사용하는 방식입니다. 이 로직이 Python이 아닌 Node에 있는 유일한 이유는 HTML이 이미 메모리에 있는 레이어이기 때문입니다. 이를 Python으로 다시 전달(round-tripping)하는 것은 이득 없이 지연 시간(latency)만 추가할 뿐입니다.
4단계: 종합 점수 (The composite score)
이 부분이 실제로 콘텐츠의 발행 여부를 결정하는 단계입니다.
// scorer/orchestrator.js
const { scoreSEO } = require('./seo-check');
...
Python 서비스에 두 개의 병렬 HTTP 호출을 보내고, 로컬 SEO 체크를 수행한 뒤, 가중 평균을 내어 임계값 게이트(threshold gate)를 통과시킵니다. 이것이 결정 레이어의 전부입니다.
발생할 수 있는 문제점
Flesch 점수는 단순히 나쁜 글쓰기가 아니라 기술적 어휘(technical vocabulary)에 대해 감점을 부여합니다. Kubernetes에 대해 올바르게 작성된 기사는 단순히 단어 길이 때문에 요리에 관한 기사보다 "읽기 어렵다"는 점수를 받게 됩니다. 다양한 기술 도메인에 걸쳐 콘텐츠를 생성하는 경우, 하나의 전역 밴드(global band) 대신 카테고리별 타겟 밴드(per-category target bands)를 사용하십시오. 초보자를 위한 JavaScript 튜토리얼에서 "읽기 쉬운" 기준은 데이터베이스 내부 구조(database internals)에 대한 심층 분석(deep-dive)의 기준과 동일하지 않습니다.
임베딩 기반의 고유성 검사(Embedding-based uniqueness checks)는 실제 지연 시간(latency)을 추가합니다. all-MiniLM-L6-v2를 로드하고 CPU에서 추론(inference)을 실행하면 네트워크 왕복 시간(round-trip) 외에 검사당 약 150-200ms가 추가됩니다. 처리량이 적을 때는 눈에 띄지 않지만, 시간당 500개의 기사를 처리할 때는 파이프라인 전체 시간에서 의미 있는 비중을 차지하게 됩니다. 여러 콘텐츠를 동시에 검사하는 경우, 기사당 한 번씩 요청하는 대신 임베딩 호출을 배치(batch) 처리하십시오.
실패 시 자동 재생성(Automatic regeneration)은 걷잡을 수 없이 커질 수 있습니다. 실패한 콘텐츠가 자동으로 재생성 시도를 트리거하고, 재생성된 콘텐츠마저 다시 실패하며, 재시도 횟수를 제한하지 않는다면 해결되지 않는 루프 속에서 API 비용을 낭비할 수 있습니다. 재시도는 두 번으로 제한하고, 무한히 재시도하는 대신 사람의 검토(human review) 단계로 넘기십시오.
통과 임계값(pass threshold)은 데이터를 확보하기 전까지는 추측일 뿐입니다. 저는 아무것도 측정하지 않은 상태에서 합리적이라고 느껴졌기 때문에 75로 시작했습니다. 첫날 선택한 수치를 신뢰하기 전에, 최소 몇 주 동안은 위양성률(false-positive rate, 좋은 콘텐츠가 거부되는 비율)을 추적하십시오.
현재 상태
이 게이트(gate)는 PostAll이 생성한 모든 기사가 고객에게 도달하기 전에 실행됩니다. 콘텐츠의 약 9%가 첫 번째 통과 단계에서 플래그(flagged) 처리되는데, 대다수의 경우 가독성이 아닌 고유성(uniqueness)이 그 원인입니다. 실패한 콘텐츠는 더 높은 온도(temperature) 설정으로 한 번의 자동 재생성 시도를 거치며, 두 번째 실패 시에는 조용히 배포되는 대신 사람의 검토 큐(human review queue)로 전달됩니다.
임베딩 (embedding) 왕복 과정은 기사당 약 340ms의 p95 지연 시간 (latency)을 추가합니다. 현재 우리의 물량(volume)에서는 수용 가능한 트레이드오프 (tradeoff)입니다. 만약 이것이 수용 불가능한 지점에 도달한다면, 임베딩 호출을 배치 (batching) 처리하는 것이 다음으로 명확한 해결책이 될 것입니다.
제가 예상하지 못했던 부분은 가독성 (readability) 문제는 거의 발생하지 않았다는 점입니다. 대규모 (scale) 환경에서 실제로 나타나는 실패 모드 (failure mode)는 고유성 드리프트 (uniqueness drift)이며, 이는 대량 생성 (bulk generation) 시 무엇이 가장 먼저 저하되는지에 대해 시사하는 바가 있습니다.
만약 여러분도 유사한 시스템을 구축하고 있다면 — 정당하게 연관된 콘텐츠를 차단하지 않으면서도 "내용이 똑같다"는 것을 잡아내기 위해 임베딩 유사도 (embedding similarity)보다 더 나은 신호 (signal)를 찾으셨나요? 제가 현재 사용 중인 임계값 (threshold)이 아직 적절한지 완전히 확신할 수 없기에, 여러분에게 무엇이 효과적이었는지 진심으로 알고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기