Skip to content
Published on

AI로 블로그 글 파이프라인 만들기 — 초안이 아니라 검증이 병목입니다

공유하기
Authors

들어가며 — 초안은 3분, 검증은 90분

AI로 글을 쓰기 시작하면 대부분 같은 착각을 한 번 거칩니다. 초안이 3분 만에 나오니까 글 한 편에 드는 시간이 10분의 1로 줄 것이라는 기대입니다. 실제로 해 보면 초안은 정말 3분에 나오고, 그 뒤가 90분입니다. 숫자가 맞는지 확인하고, 링크가 살아 있는지 눌러 보고, 인용문이 원문에 실제로 있는 문장인지 대조하고, 같은 말을 세 번 다르게 반복한 문단을 걷어내는 데 그만큼 걸립니다.

그래서 파이프라인을 설계할 때 초안 생성을 중심에 놓으면 잘못 만들게 됩니다. 병목이 아닌 곳을 자동화한 것이기 때문입니다. 실제로 시간을 잡아먹는 단계는 검증이고, 개선의 여지가 가장 큰 곳도 여기입니다. 초안 속도를 두 배로 올리면 3분이 1분 반이 되지만, 검증을 절반으로 줄이면 90분이 45분이 됩니다.

이 글은 프롬프트 모음이 아닙니다. 주제 수집부터 성과 확인까지 일곱 단계를 실제로 돌려 보고, 각 단계에서 모델이 무엇을 잘하고 무엇을 맡기면 사고가 나는지를 구분한 기록입니다. AI가 쓴 글이 무너지는 개별 실패 유형과 각각의 탐지법은 다음 글에서 따로 다뤘고, 여기서는 그것들을 걸러내는 흐름 자체를 봅니다.

파이프라인의 일곱 단계와 각 단계의 실패 비용

먼저 전체 지도를 놓고 시작하겠습니다. 각 단계에서 AI가 실제로 값을 만드는 지점과, 넘기는 순간 되돌리기 어려워지는 지점을 갈라 놓은 표입니다.

단계AI가 실제로 잘하는 것사람이 반드시 하는 것넘겼을 때의 실패 비용
1. 주제 수집후보를 폭넓게 벌리고, 이미 쓴 글과의 중복을 표시독자에게 값이 있는지 판단하고 후보를 버림낮음. 나쁜 주제는 다음 단계에서 걸러짐
2. 조사와 출처 확보검색어 확장, 원문 요약, 상충하는 주장 정리1차 출처인지 확인하고 직접 열어 봄높음. 여기서 들어온 오류는 끝까지 살아남음
3. 초안구조 제안, 빈 화면 제거, 설명 문단의 첫 버전논지와 순서 결정, 무엇을 뺄지 결정중간. 티가 나므로 편집에서 잡힘
4. 사실 검증주장 목록 추출, 인용문 위치 대조 보조숫자·날짜·인용·링크의 최종 확인최고. 발행 후에 드러나면 신뢰가 깨짐
5. 편집과 목소리 통일중복 문단 탐지, 길이 압축, 표현 후보 제시목소리 기준 자체를 정의하고 최종 판단중간. 읽는 사람이 바로 알아챔
6. 발행메타데이터 생성, 링크·빌드 검사 자동화발행 여부 결정과 되돌릴 준비중간. 되돌릴 수 있으면 낮아짐
7. 성과 확인로그 집계, 지표 계산, 이상값 지적지표를 다음 글의 결정으로 바꾸는 해석낮음. 다만 안 하면 개선이 멈춤

표에서 읽어야 할 것은 개별 칸이 아니라 비용의 분포입니다. 2단계와 4단계에 비용이 몰려 있습니다. 이 두 곳은 자동화하되 사람의 승인 없이 통과하지 못하게 막아야 하고, 나머지는 과감하게 넘겨도 됩니다. 파이프라인 설계의 대부분은 이 배분을 지키는 일입니다.

주제와 조사 — AI는 후보를 늘리고, 사람은 후보를 버립니다

주제 수집에서 모델은 발산에 강합니다. 하나의 씨앗에서 스무 개의 각도를 뽑아내는 일은 사람보다 빠르고, 지치지도 않습니다. 반대로 수렴은 못 합니다. "이 중 뭐가 제일 좋아?"라고 물으면 그럴듯한 근거와 함께 하나를 고르지만, 그 판단에는 여러분의 독자가 지난달에 무엇을 읽고 실망했는지가 들어 있지 않습니다. 후보를 늘리는 데 쓰고, 버리는 일은 직접 하세요.

여기서 실무적으로 값이 큰 자동화가 하나 있습니다. 이미 쓴 글과의 중복 검사입니다. 새 주제 후보를 기존 글 제목·요약과 대조해서 겹치는 것을 표시하면, 같은 이야기를 각도만 바꿔 다시 쓰는 사고를 막을 수 있습니다. 이건 판단이 아니라 대조라서 기계가 잘합니다.

조사 단계는 성격이 다릅니다. 여기서 들어온 오류는 파이프라인 끝까지 살아남고, 뒤 단계는 그것을 더 그럴듯하게 다듬기만 합니다. 그래서 규칙을 하나만 세운다면 이것입니다 — 모델이 요약한 내용을 조사 결과로 쓰지 않습니다. 모델이 하는 일은 어디를 읽어야 하는지 알려 주는 것까지이고, 읽는 것은 사람이 합니다.

실제로 지키기 쉬운 형태로 바꾸면 이렇게 됩니다.

  • 모델이 내놓은 URL은 전부 직접 엽니다. 열리지 않으면 그 주장은 통째로 버립니다.
  • 2차 출처(블로그 정리글, 뉴스 요약)에서 시작해도 좋지만, 인용은 1차 출처(공식 문서, 논문, 발표문, 릴리스 노트)로 바꿔 답니다.
  • 날짜가 없는 페이지는 출처로 쓰지 않습니다. 언제 맞는 말이었는지 모르는 사실은 검증할 수 없습니다.
  • 상충하는 주장을 만나면 어느 쪽이 맞는지 모델에게 묻지 말고, 상충한다는 사실 자체를 글에 적습니다.

마지막 항목이 실전에서 특히 유용합니다. 이번 글의 자매편을 쓰면서 오피스 단축키를 조사할 때, 마이크로소프트 공식 문서끼리 서로 다른 키를 안내하는 경우를 실제로 만났습니다. 그때 답은 하나를 고르는 것이 아니라 "문서가 갈린다"고 쓰는 것입니다.

초안 — 빈 화면은 없애되 구조는 넘기지 않습니다

초안 단계에서 모델의 값은 명확합니다. 빈 화면 앞에서 첫 문장을 고민하는 시간을 없애 줍니다. 이것만으로도 충분히 쓸모가 있고, 이 단계는 실수해도 비용이 낮습니다. 이상하면 티가 나고, 티가 나면 고쳐집니다.

다만 넘기지 말아야 할 것이 하나 있습니다. 구조입니다. 목차를 모델에게 맡기면 거의 항상 같은 모양이 나옵니다. 정의 → 장점 → 단점 → 사례 → 결론. 이 구조는 틀리지는 않지만 아무것도 주장하지 않습니다. 글에서 독자가 기억하는 것은 대개 구조가 만든 순서인데, 그 순서를 평균값으로 채우면 읽고 나서 남는 게 없습니다.

실무에서는 이렇게 나눕니다. H2 제목과 순서는 직접 정하고, 각 H2 아래 무엇을 쓸지를 두세 줄로 적어 모델에게 넘깁니다. 그러면 모델은 문단을 채우는 일을 하고, 논지는 유지됩니다. 반대로 "이 주제로 글 써 줘"라고 하면 논지 없는 글이 나오는데, 그건 모델이 못해서가 아니라 논지를 준 적이 없기 때문입니다.

초안 프롬프트에 넣으면 체감이 달라지는 제약 몇 가지를 적어 둡니다.

  • 모르는 것은 비워 두고 표시하게 합니다. 채워 넣지 말라고 명시하지 않으면 채웁니다.
  • 숫자와 고유명사는 제공한 자료에 있는 것만 쓰게 합니다.
  • 문단 길이 상한을 줍니다. 상한이 없으면 같은 말을 늘려 씁니다.
  • 각 H2마다 "이 절에서 독자가 새로 알게 되는 것 한 줄"을 먼저 쓰게 하고, 그게 앞 절과 겹치면 절을 합칩니다.

사실 검증 — 이 파이프라인의 무게중심

여기가 이 글에서 가장 중요한 절입니다. 앞의 여섯 단계를 대충 해도 이 단계가 살아 있으면 글은 발행할 만하고, 여기가 없으면 나머지를 아무리 잘해도 언젠가 사고가 납니다.

자신 있게 쓴 문장과 확인된 문장은 다릅니다

모델의 출력에는 확신의 정도가 표시되지 않습니다. 학습 데이터에서 수천 번 본 사실과, 그럴듯해서 방금 만들어낸 문장이 똑같은 어조로 나옵니다. 사람의 글에서는 확신이 없을 때 문장이 흔들리기 때문에 우리는 무의식적으로 그 신호를 읽는데, 모델의 글에는 그 신호가 없습니다. 그래서 "읽어 보니 자연스럽다"는 검증이 되지 못합니다. 오히려 자연스러울수록 위험합니다.

특히 조심할 항목은 정해져 있습니다. 숫자, 날짜, 버전, 가격, 인용문, 사람 이름과 소속, 그리고 URL입니다. 이것들은 모델이 가장 그럴듯하게 지어내고, 독자가 가장 쉽게 확인할 수 있는 항목이기도 합니다. 나머지 문장이 다 맞아도 이 중 하나가 틀리면 글 전체의 신뢰가 같이 떨어집니다.

지식 컷오프도 같은 자리에서 문제를 일으킵니다. 모델은 자기가 모르는 최신 정보를 "모른다"고 말하는 대신 마지막으로 아는 상태를 현재형으로 말합니다. 가격, 요금제, 제품명, API 파라미터, 정책 문구처럼 자주 바뀌는 것은 모델의 답을 출발점으로만 쓰고 반드시 현재 문서에서 다시 확인해야 합니다.

모델이 실제로 읽은 것만 인용하게 만드는 법

"출처를 붙여 줘"라고 요청하면 출처가 붙습니다. 문제는 그 출처가 읽은 곳이 아니라 그럴듯한 곳일 수 있다는 점입니다. URL 형식은 학습하기 쉬워서, 존재하지 않는 문서 번호나 죽은 앵커가 자연스러운 모양으로 생성됩니다.

이걸 막는 방법은 요청을 바꾸는 것이 아니라 검증 가능한 형태로 출력을 강제하는 것입니다. 핵심은 축자 인용입니다. 요약이 아니라 원문의 글자를 그대로 옮기게 하면, 그 문자열이 원문에 실제로 있는지를 기계가 대조할 수 있습니다.

주장 하나마다 이 네 가지를 함께 내놓게 합니다.

  1. 주장 문장 자체
  2. 출처 URL
  3. 그 URL의 본문에서 글자 그대로 가져온 한 문장 이상의 인용
  4. 그 인용이 주장을 뒷받침한다고 보는 이유 한 줄

3번이 자동 검증의 열쇠입니다. 인용문이 페이지에 없으면 그 주장은 근거 없이 만들어진 것이고, 사람이 읽어 보기 전에 스크립트가 먼저 잡아낼 수 있습니다. 아래는 그 대조를 하는 최소 구현입니다.

// verify-claims.mjs — 주장 원장(claims.jsonl)의 축자 인용이 실제 원문에 있는지 대조합니다.
// 각 줄 형식: {"claim": "...", "url": "https://...", "quote": "...원문 그대로...", "why": "..."}
import { readFileSync } from 'node:fs'

// 공백/따옴표/대소문자 차이는 인용 실패로 보지 않습니다. 내용이 다른 경우만 잡아야 합니다.
const normalize = (s) =>
  s
    .replace(/<[^>]+>/g, ' ')
    .replace(/&[a-z]+;|&#\d+;/gi, ' ')
    .replace(/[‘’“”]/g, "'")
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase()

const rows = readFileSync('claims.jsonl', 'utf8')
  .split('\n')
  .filter(Boolean)
  .map((l) => JSON.parse(l))

const cache = new Map()
let failed = 0

for (const [i, row] of rows.entries()) {
  const label = `[${i + 1}] ${row.claim.slice(0, 50)}`

  if (!row.url || !row.quote) {
    console.error(`MISSING ${label} — url 또는 quote가 비었습니다`)
    failed++
    continue
  }

  if (!cache.has(row.url)) {
    try {
      const res = await fetch(row.url, { redirect: 'follow' })
      // 죽은 링크는 인용 대조 이전에 이미 실패입니다.
      cache.set(row.url, res.ok ? normalize(await res.text()) : null)
    } catch {
      cache.set(row.url, null)
    }
  }

  const page = cache.get(row.url)
  if (page === null) {
    console.error(`DEAD    ${label}${row.url} 를 열 수 없습니다`)
    failed++
  } else if (!page.includes(normalize(row.quote))) {
    // 여기가 이 스크립트의 존재 이유입니다: 그럴듯하지만 원문에 없는 인용.
    console.error(`NOQUOTE ${label} — 인용문이 원문에 없습니다: "${row.quote.slice(0, 60)}"`)
    failed++
  }
}

console.log(`\n${rows.length}건 검사, ${failed}건 실패`)
process.exit(failed ? 1 : 0)

이 스크립트가 잡지 못하는 것도 분명히 해 둘 필요가 있습니다. 인용문이 원문에 존재하더라도 문맥이 잘린 인용이거나, 인용은 맞는데 그것이 주장을 뒷받침하지 않는 경우는 통과합니다. 그래서 4번 항목(이유 한 줄)을 사람이 읽는 절차가 여전히 필요합니다. 다만 기계가 앞에서 걸러 주면 사람이 읽어야 할 양이 크게 줄어듭니다. 실제로 이 대조를 붙이고 나서 사람 검증 시간이 눈에 띄게 줄었는데, 지어낸 인용을 찾느라 원문을 훑는 일이 사라졌기 때문입니다.

발행 직전 체크리스트

아래는 발행 버튼을 누르기 전에 실제로 돌리는 목록입니다. 기계가 할 수 있는 것과 사람이 해야 하는 것을 나눠 놨습니다.

항목확인 방법자동화
모든 외부 링크가 열리는가응답 코드 확인가능
인용문이 원문에 글자 그대로 있는가축자 대조 스크립트가능
내부 링크가 실제 경로를 가리키는가파일 존재 확인가능
숫자와 날짜가 출처와 일치하는가원문과 눈으로 대조불가능
시점에 의존하는 서술이 있는가최신·현재·올해 같은 표현 검색 후 판단부분 가능
인용이 문맥을 왜곡하지 않는가원문 앞뒤 문단 읽기불가능
근거 없이 단정한 문장이 있는가단정 어미 검색 후 근거 확인부분 가능
같은 내용을 반복한 문단이 있는가문단 유사도 측정 후 판단부분 가능
내가 이 글을 설명할 수 있는가읽지 않고 요지를 말해 보기불가능

마지막 항목이 형식적으로 보이지만 실제로 가장 잘 걸러 냅니다. 글을 보지 않고 핵심 주장을 말해 보려는데 막힌다면, 그 부분은 검증한 것이 아니라 통과시킨 것입니다.

편집·발행·성과 확인 — 목소리를 통일하고 되돌릴 수 있게 만듭니다

편집 단계에서 모델에게 맡길 수 있는 것은 탐지이고, 맡길 수 없는 것은 기준입니다. "이 글이 내 목소리인가"를 모델은 판단하지 못합니다. 학습한 평균적인 글맛으로 수렴시키려 하기 때문에, 맡기면 문장은 매끄러워지고 글은 남의 것이 됩니다.

대신 목소리를 규칙으로 적어 두면 기계가 검사할 수 있습니다. 쓰지 않는 단어 목록, 문단 길이 상한, 감탄 표현 금지, 특정 접속사 남용 금지 같은 것들입니다. 이건 취향이 아니라 검사 가능한 조건이라서 자동화가 됩니다. 반대로 "더 자연스럽게 고쳐 줘"는 검사 조건이 아니라 취향의 위임입니다.

발행 단계에서 중요한 것은 속도가 아니라 되돌릴 수 있음입니다. 글은 코드와 달리 발행 후 오류가 드러나도 롤백이 조용하지 않습니다. 이미 읽은 사람이 있고, 인용된 경우도 있습니다. 그래서 발행 자체를 자동화하더라도 되돌리는 경로를 먼저 만들어 두는 편이 낫습니다. 정정 내역을 글 안에 남기는 규칙을 미리 정해 두면, 사고가 났을 때 조용히 고치고 싶은 유혹을 피할 수 있습니다.

성과 확인 단계에서는 지표를 줄이는 것이 요령입니다. 조회수는 다음 글을 바꾸지 못합니다. 실제로 결정을 바꾸는 것은 체류 시간의 분포(첫 화면에서 이탈하는 비율), 검색 유입 질의와 글 제목의 어긋남, 그리고 어느 절에서 스크롤이 멈추는가 정도입니다. 특히 검색 질의와 내용의 어긋남은 다음 글의 주제를 직접 알려 주기 때문에, 하나만 본다면 이것을 권합니다.

여기서 구글의 정책을 한 번 짚고 갈 필요가 있습니다. 구글은 AI로 만들었다는 이유로 콘텐츠에 불이익을 주지 않는다고 명시하고 있으며, 판단 기준은 생성 수단이 아니라 독자에게 값을 주는가라고 검색 센터 문서에서 밝히고 있습니다. 다만 순위를 목적으로 가치 없는 글을 대량 생산하는 행위는 스팸 정책에서 대량 생성 콘텐츠 남용으로 명시적으로 금지합니다. 요약하면, 파이프라인의 목표를 "많이 찍어내기"로 잡는 순간 정책의 반대편에 서게 됩니다.

자동화 스케치 — 멈출 수 있는 지점을 남긴 워크플로

전체를 하나로 묶으면 아래와 같은 모양이 됩니다. 중요한 것은 자동화의 범위가 아니라 어디서 멈추는가입니다. 이 워크플로는 검증 게이트를 통과하지 못하면 발행 단계로 넘어가지 않고, 사람의 승인 없이는 초안이 발행 브랜치에 닿지 않습니다.

# .github/workflows/content-gate.yml
# 초안 PR이 열리면 기계가 할 수 있는 검증을 전부 돌리고, 통과해도 자동 병합하지 않습니다.
name: content-gate

on:
  pull_request:
    paths: ['data/blog/**/*.mdx']

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'

      - name: 빌드 안전성 — MDX가 실제로 컴파일되는지
        run: node scripts/check-posts.mjs

      - name: 내부 링크 — 가리키는 경로가 존재하는지
        run: node scripts/check-links.mjs

      - name: 축자 인용 대조 — 인용문이 원문에 실재하는지
        run: node scripts/verify-claims.mjs

      - name: 시점 의존 표현 — 사람이 판단해야 할 자리를 표시만 합니다
        run: |
          # 실패시키지 않습니다. 검증이 아니라 리뷰 유도용 신호입니다.
          grep -nE '최신|현재|올해|작년|최근|가장 빠른|업계 최초' data/blog/**/*.mdx \
            | tee /tmp/time-sensitive.txt || true
          echo "위 표현은 컷오프 이후 사실일 수 있습니다. 리뷰어가 직접 확인하세요."

      - name: 문단 중복 — 유사도 상위 쌍만 뽑아 보고
        run: node scripts/find-duplicate-paragraphs.mjs --top 5 || true

  # 사람 개입 지점: 위 검증이 모두 초록이어도 여기서 멈춥니다.
  # 승인은 GitHub의 리뷰 승인으로 받고, 워크플로가 대신 승인하지 않습니다.

여기서 의도적으로 하지 않은 것이 두 가지 있습니다.

첫째, 검증 통과를 발행 트리거로 쓰지 않았습니다. 기계 검증은 "발행해도 된다"가 아니라 "명백한 사고는 없다"까지만 말해 줍니다. 자동 병합을 붙이는 순간 체크리스트의 마지막 세 항목(문맥 왜곡, 단정, 설명 가능성)이 파이프라인에서 사라집니다.

둘째, 시점 의존 표현 검사를 실패로 처리하지 않았습니다. 이 검사는 정밀도가 낮아서 실패로 만들면 곧 무시하게 되고, 무시되는 게이트는 없는 게이트보다 나쁩니다. 사람에게 어디를 보라고 알려 주는 역할까지만 시키는 편이 오래갑니다.

마치며 — 자동화할 값어치가 있는 것은 생성이 아니라 판정입니다

AI 콘텐츠 파이프라인을 만들 때 손이 먼저 가는 곳은 생성입니다. 결과가 눈에 보이고, 만들면 즉시 빨라진 느낌이 들기 때문입니다. 하지만 실제로 시간을 돌려주고 사고를 막아 주는 것은 판정 쪽입니다. 인용이 실재하는지, 링크가 살아 있는지, 같은 말을 반복하지 않았는지를 기계가 먼저 걸러 주면, 사람은 기계가 못 하는 일에만 시간을 씁니다.

그리고 기계가 못 하는 일은 생각보다 적고 분명합니다. 인용이 문맥을 왜곡하지 않는지, 이 주제가 독자에게 값이 있는지, 그리고 이 글의 논지가 내 것인지. 이 셋은 끝까지 사람의 몫이고, 파이프라인이 잘 만들어졌다면 이 셋에 쓸 시간이 남아 있어야 합니다.

한 줄로 줄이면 이렇습니다 — 초안을 못 믿는 것이 아니라, 확인하지 않은 초안을 믿을 근거가 없을 뿐입니다.

참고 자료