· Local LLM· Automation· n8n· Ollama· Quality

글 뽑는 데 4분, 나머지는 전부 검증이었다

RTX 3060 한 장, API 비용 0원으로 글·썸네일·배포를 자동화한 기록 — 그리고 로컬 LLM이 거짓말하는 걸 막느라 만든 장치들

“블로그로 부수입”이라는 말은 흔한데, 그 뒤에 붙는 조건은 잘 안 알려져 있다. 꾸준히, 많이, 그런데 품질을 유지하면서 써야 한다는 것. 나는 이걸 사람이 아니라 파이프라인이 하게 만들고 싶었다. 조건은 하나였다 — 돈을 쓰지 않는다. GPT API도, 미드저니 구독도 없이. 집에 있는 RTX 3060 12GB 한 장으로.

두 달쯤 걸릴 줄 알았는데 이틀 만에 첫 글이 자동으로 발행됐다. 그런데 만들고 나서 시간을 훨씬 많이 쓴 쪽은 생성이 아니라 검증이었다. 이 글은 그 이야기다.

만든 것

자동차 오너십·용품 가이드 사이트다. “이 연식은 피해라”, “이 차에 맞는 트렁크 정리함은 뭐냐” 같은 롱테일 검색어를 노리는 정적 사이트.

영역구성
사이트Astro 5 정적 빌드, Cloudflare 무료 티어 (main 푸시 = 자동 배포)
글 생성n8n 워크플로(19노드) + Ollama의 qwen2.5 14B q4 (9GB)
썸네일ComfyUI + Z-Image Turbo (6B, int8) — 같은 GPU
데이터NHTSA(미국 도로교통안전국) 공개 API의 불만 신고·리콜

글 한 편에 파이프라인 실행 4분. API 비용 0원. 월 고정비는 도메인값뿐이다.

전체 흐름은 이렇다.

dossier(JSON)          ← 사람이 만드는 유일한 사실 출처
      ↓  n8n
① 메타 생성      제목·설명·인트로·FAQ + 썸네일 장면 프롬프트
② 본문 생성      섹션당 LLM 1회 호출
③ MDX 조립       표·제목은 코드가 조립. 제품·가격·URL은 dossier에서 복사
④ QA 게이트      결정론적 검사. 걸리면 커밋 전에 중단
⑤ 레드팀         같은 LLM이 dossier 대조 검수 → PR에 권고로만 첨부
⑥ 썸네일         배경 생성 → 코드가 텍스트 오버레이
⑦ GitHub         브랜치 + MDX + 이미지 커밋 + PR (draft)

사람 검수 → 머지 = 배포

설계 원칙: 모델은 산문만 쓴다

로컬 LLM 자동화의 최대 리스크는 환각이다. 특히 제휴 성격의 글에서 제품 링크나 가격을 지어내면 그건 틀린 글이 아니라 신뢰를 파는 사기가 된다.

그래서 구조에서 환각의 경로를 끊었다. 글마다 사람이 만든 dossier(사실 자료집 JSON)가 있고, 모델은 거기 있는 사실로만 쓴다. 제품명·가격·URL·리콜번호 같은 하드 데이터는 모델이 쓰는 게 아니라 코드가 dossier에서 기계적으로 복사해 프론트매터에 박는다. 모델이 링크를 지어내고 싶어도 링크를 쓸 권한 자체가 없다.

모델이 숫자를 섞는다

초기 버전은 글 한 편을 LLM 호출 한 번으로 뽑았다. 결과물은 그럴듯했는데, 검수하다 소름 돋는 걸 발견했다. dossier에는 “엔진 불만이 전체의 14%“라고 있는데 글에는 35%라고 적혀 있었다. 없는 수리비 $8,000이 실제 수치 옆에 자연스럽게 붙어 있기도 했다.

14B급 양자화 모델은 한 호출에 숫자 서른 개를 옮기게 하면 그중 몇 개를 다른 문단의 숫자와 바꿔치기한다. 문장은 유창해서 안 읽어보면 모른다.

해법은 모델을 키우는 게 아니라 일을 줄이는 것이었다. 글을 섹션 단위로 쪼개서 섹션당 호출 한 번, 각 호출에는 그 섹션의 데이터 조각만 준다. 옮길 숫자가 적으니 섞을 숫자도 없다. 부수 효과로 분량이 421단어에서 1,083단어로 올라왔다 — 짧게 쓰던 이유가 능력 부족이 아니라 컨텍스트 과부하였던 거다.

차단권은 코드에게만

생성이 끝나면 결정론적 QA 게이트가 돈다. 여기 걸리면 커밋 자체가 안 된다.

  • 가짜 실사용 표현 — “we tested”, “hands-on” 같은 것들. 로컬 LLM이 제일 잘 지어내는 거짓말이 “우리가 직접 써봤는데”다
  • 모델이 쓴 링크 — dossier에 없는 URL이 본문에 있으면 무조건 환각
  • 숫자 교차검증 — 본문의 모든 퍼센트·달러·리콜번호를 dossier와 대조

세 번째에서 하나 배웠다. 허용 숫자 집합을 만들 때 리콜번호(22V838000) 같은 ID형 토큰은 빼야 한다. 안 그러면 지어낸 $8,000이 저 번호 안의 8000과 매칭돼서 통과해 버린다.

LLM에게도 검수를 시킨다(레드팀 패스). 그런데 이건 차단권 없이 권고로만 쓴다. 이유는 스코어보드가 말해줬다. 어떤 실행에선 지적 4건이 전부 오탐이었고(자기가 받은 dossier의 판정을 “근거 없음”이라 했다), 다음 실행에선 3건 전부 진짜였다(전문가 평가를 “오너들이 말하길”로 바꿔치기한 걸 잡아냈다). 못 믿을 심판에게 휘슬은 줘도 퇴장권은 주면 안 된다.

검수자를 검수하다

레드팀 오탐율이 궁금해서 실행 일곱 번에서 나온 지적 22건을 전부 dossier와 대조해 라벨을 붙였다. 오탐 64%(14/22). 열 건 지적하면 여섯 건이 헛다리라는 뜻이다. 이 상태로는 지적을 읽는 것 자체가 일이 된다.

라벨을 붙이고 나니 패턴이 보였다. 오탐율이 실행마다 갈리는데, 기준이 프롬프트 길이였다.

구분오탐율
프롬프트가 8192 토큰을 넘은 실행78%
넘지 않은 실행40%

num_ctx가 8192였고, 그걸 넘는 프롬프트를 Ollama가 조용히 잘라내고 있었다. 에러도 경고도 없다. 그리고 하필 dossier가 프롬프트 앞쪽에 있어서, 잘릴 때 근거 자료의 머리부터 날아간다. 검수자가 “근거가 없다”고 지적한 게 절반은 맞는 말이었다 — 자기가 받은 프롬프트에는 정말로 없었으니까.

실패한 시도: 판정 기준을 프롬프트로 가르치기

먼저 시도한 건 프롬프트 개선이었다. 판정 루브릭을 주고, 지적할 때 dossier 원문을 인용하게 강제했다. 결과는 오탐 78%로 악화였고, 그보다 나쁜 건 재현율이 무너진 것이다. 진짜였던 CR-V 귀속 조작 3건이 전부 통과했고, 지어낸 미래 날짜도 통과했다.

압권은 이거였다. 모델이 240k를 근거 없다고 지적하면서, 그 지적의 인용문으로 240k가 적힌 dossier 줄을 붙여왔다. 자기가 인용한 문장 안에 답이 있는데 못 본다.

여기서 결론을 내렸다. 14B 양자화 모델은 판정 루브릭을 실행하지 못한다. 심판을 프롬프트로 똑똑하게 만드는 건 이 크기에서 막다른 길이고, 레버리지는 심판 주변을 결정론으로 감싸는 쪽에 있다.

그래서 코드로 걸렀다

컨텍스트를 16384로 올려 잘림을 없앴고(오탐 78%→70%, 덤으로 지어낸 날짜 하나를 새로 잡았다) — 이 숫자는 나중에 도로 내려야 했는데 그 얘기는 뒤에서 한다 — 그다음 지적을 PR에 올리기 전에 코드가 한 번 더 판정하게 했다. 중복, 판정문 되풀이, dossier에 그대로 있는 내용, dossier 안의 캠페인 번호 같은 것들을 규칙으로 기각한다.

구분필터 전필터 후
남은 오탐142
남은 진짜87
오탐율64%22%

진짜 하나를 잃었다. 다만 그건 상투구(“known for its”) 지적이었고 결정론적 게이트가 독립적으로 이미 잡는 항목이라, 시스템 전체로는 재현율 손실이 0이다. 기각된 지적도 PR 본문에 사유와 함께 전부 표시된다. 사람에게서 숨기는 건 없다.

그 필터로 과거 글을 다시 감사했다

여기서 질문이 하나 남았다. 이미 발행한 글은 어떡하나? 오탐율이 높았다는 건 검수자가 트집을 많이 잡았다는 뜻이지 발행 글에 오류가 많다는 뜻은 아니다. 논리적으로는 그렇다. 그런데 논리로 안심하는 것과 확인하는 건 다르다.

그래서 파이프라인이 만든 발행 글 네 편 전부에 개선된 레드팀을 다시 돌렸다.

  • 지적 13건 → 진짜 4건 / 오탐 9건
  • 수치 오류는 0건. 진짜 4건은 전부 표현·귀속 수준이었다

네 건은 이랬다. 지어낸 미래 날짜(“as of early 2027”), 걸렸어야 할 상투구 하나, 전문가 평가를 “owners consistently cite”로 바꿔치기한 귀속 조작, 그리고 내가 예전 검수에서 진짜라고 판정해놓고 수정을 빠뜨린 것 하나.

마지막 건이 뜨끔했다. 감사가 모델의 실수만 잡은 게 아니라 내 실수를 잡았다.

게이트에도 버그가 있었다

상투구 하나가 왜 게이트를 통과했는지 파보니 원인이 어이없었다. 모델은 곱슬따옴표(it’s)를 쓰는데 정규식은 직선따옴표(it's)만 매칭하고 있었다. 눈으로는 같아 보이는 두 글자가 다른 코드포인트다. 따옴표를 정규화하고 description 필드까지 스캔 범위를 넓혔다.

필터도 못 믿는다

필터를 만들었으면 필터도 검증해야 한다. 세 가지를 고쳤다.

미탐 리스크. 판정 문장 안에 조작된 숫자가 숨어 있으면 문장째 기각될 수 있었다. 숫자 백스톱을 넣었다 — dossier에 없는 숫자를 언급한 지적은 어떤 규칙으로도 기각되지 않는다. 적대적 사례로 단위 테스트를 짰고 라벨셋 성능은 그대로였다.

필터 크래시가 곧 실행 사망. 필터가 예외를 던지면 파이프라인 전체가 죽었다. try/catch로 감싸서 실패 시 “무필터 원본 표시”로 강등되게 했다. 필터는 편의지 관문이 아니다.

정직성. 64%→22%는 규칙을 튜닝한 그 라벨셋으로 잰 in-sample 수치다. 새 데이터에서도 그럴 거라는 보장이 없다. eval 문서에 그렇게 적어두고, 다음 다섯 편 뒤 재라벨링한다는 조건을 남겼다.

근거의 근거

여기까지 오면 다음 질문이 나온다. dossier는 확실한가? 모델이 dossier만 보고 쓰게 만들어놨는데, 그 dossier가 틀렸으면 전부 무너진다.

정직하게 계층을 나누면 이렇다.

데이터출처신뢰도
NHTSA 수치 (불만 건수·점유율·리콜번호)코드가 정부 API에서 기계 복사, LLM 무개입재검증 가능
웹 출처 사실 (가격·마력·평판)검색 결과에서 손으로 옮김미검증

두 번째 계층이 약점이다. 그래서 기계적으로 검증 가능한 것들을 실제로 재조회해봤다. NHTSA는 API에서 다시 받아 dossier와 대조했고 전부 일치했다. 연비는 EPA 공식 사이트(fueleconomy.gov)와 대조했는데 이것도 일치했다.

일치했다는 것보다 중요한 건, 연비가 원래 “손으로 옮긴” 계층에 있었는데 알고 보니 공식 API가 있었다는 사실이다. 그래서 수집기를 만들어 기계 복사 계층으로 올렸다.

node pipeline/gather/epa.mjs 2026 Mazda CX-5

신뢰 등급도 official > verified > listed > inferred 네 단계로 정리했다. 연비는 이제 항상 최상위다.

여기도 함정이 하나 있었다. EPA는 차 이름을 CR-V가 아니라 CR-V AWD처럼 등록해둔다. 그래서 CX-5를 검색하면 다른 차인 CX-50이 딸려 나온다. 부분 문자열 매칭을 그대로 두면 조용히 틀린 차의 연비가 들어간다.

수집기를 처음 실제로 쓴 글에서 dossier의 등급 분포는 official 5건, listed 11건이었다. 등급을 나눴다고 자료가 갑자기 단단해지는 게 아니다. 어디가 무른지가 보이게 됐을 뿐이고, 아직 대부분이 무른 쪽이다.

가격과 마력은 여전히 listed 등급이다. 공식 무료 API가 없어서 한계고, 글에서도 단정이 아니라 인용 톤으로 쓴다. 검증 안 된 걸 검증된 것처럼 쓰지 않는 게 지금 할 수 있는 최선이다.

썸네일: 다섯 번 뒤집은 이야기

글보다 썸네일이 더 오래 걸렸다.

  1. 브랜드 SVG 템플릿 → “컨설팅 보고서 표지 같다” (맞는 말이었다)
  2. 다크 버전, 기울인 버전 → 골격이 같아서 여전히 밋밋
  3. 하이브리드: AI가 배경만 생성, 글자는 코드가 얹기 → AI 이미지의 깨진 글자 문제를 원천 차단하면서 사진의 분위기는 가져온다
  4. 무드 장면 풀(눈길, 해안도로…) → “예쁜데 글 내용과 무관하잖아”
  5. 콘텐츠 파생 (최종) → 메타 생성 단계에서 모델이 dossier를 읽고 그 글의 핵심 주제로 장면을 뽑는다. 오일 소모 글이면 리프트에 올라간 차와 오일팬, 연료펌프 시동꺼짐 글이면 갓길에 비상등 켠 차
연료·오일 계통 문제를 다룬 글의 썸네일 배경. 장면(리프트 위의 차, 오일팬)은 dossier에서 파생됐고 차량은 모델이 그렸다.
연료·오일 계통 문제를 다룬 글의 썸네일 배경. 장면(리프트 위의 차, 오일팬)은 dossier에서 파생됐고 차량은 모델이 그렸다.

여기서 내 가정 하나가 깨졌다. 다만 절반만 깨졌다.

처음엔 “AI가 그린 차는 실차와 달라서 신뢰를 깎는다”며 차량 묘사를 금지했다. 실측해보니 Z-Image Turbo는 인기 차종을 세대 단위로 알아볼 만하게 그렸다. 차체 비율, 헤드램프 형상, 그릴 구조가 그 세대로 읽힌다. 기사 자체가 세대 단위로 쓰이니 그 정확도면 충분했고, 규칙을 폐기했다.

대신 발행 전 사람 눈 검수는 유지했는데, 이 검수가 실제로 Model 3 실내에 아날로그 계기판을 그려 넣은 이미지를 잡아냈다. Model 3엔 계기판이 없다. 규칙은 실측으로 바꾸되 안전망은 남긴다.

문제는 그다음이다. 실루엣이 그럴듯하게 나오는 걸 보고 엠블럼도 당연히 깨끗하게 나올 줄 알았다. 이 글을 쓰면서 확대해보고서야 아니라는 걸 알았다. 위 이미지의 스바루 배지를 네 배로 당기면 여섯 개 별 배치가 뭉개져 하나의 큰 별처럼 되어 있고, 옆에 붙은 글자는 판독이 안 된다. 표시 크기에서 브랜드로 읽히는 것과 정확하게 그려진 것은 다른 얘기다.

규칙을 실측으로 바꾼 건 맞다. 그런데 내가 실측한 건 실루엣이었지 엠블럼이 아니었다. 잰 범위를 넘어서 결론을 넓혀버린 거다.

다른 브랜드도 확대해봤더니 결과가 갈렸다. 토요타의 타원 세 개, 혼다의 H, 테슬라의 T는 형태가 제대로 나온다. 뭉개진 건 스바루뿐이었다. 별 여섯 개가 타원 안에 배열된 패턴이라 그 크기에서 디테일이 살아남지 못한다. 모델이 엠블럼을 못 그리는 게 아니라 표식이 얼마나 잘게 쪼개져 있느냐가 갈림길이었다.

알고 나니 고치는 건 간단했다. 정면 그릴이 잡히던 스바루 장면 다섯 개를 후면·측면 구도로 다시 짰다. 배지가 프레임 밖으로 나가면 뭉갤 것도 없다. 그리고 안전망으로, 복잡한 엠블럼 브랜드에는 렌더러가 “배지가 보이지 않게” 문구를 프롬프트 끝에 자동으로 붙이게 했다. 마쓰다는 확인 전이지만 곡선이 복잡해서 같은 목록에 미리 넣어뒀다.

그 뒤 실제로 마쓰다 글이 나왔을 때 썸네일은 측면 프로필로 뽑혔다. 규칙이 먼저 작동한 것이다. 덕분에 배지는 안 나왔고, 마쓰다 엠블럼이 정말 뭉개지는지는 아직도 모른다. 예방은 검증을 대신하지 않는다.

이미지 열 장을 다시 뽑고 사람 눈으로 확인했다. 위 이미지는 재생성 전 버전이다 — 뭉개진 배지가 증거라서 그대로 뒀다.

고친 것보다 이 순서가 남는다. 처음엔 “AI가 그린 차는 못 쓴다”는 금지였고, 실측하고 풀었고, 확대해서 한계를 찾았고, 그 한계가 브랜드가 아니라 표식의 복잡도에서 온다는 걸 알고 나서야 제대로 된 규칙이 나왔다. 금지에서 시작해 규칙까지 오는 데 네 단계가 걸렸고, 각 단계를 넘긴 건 전부 실측이었다.

12GB에 모델 두 개 살기

글 모델(9GB)과 이미지 모델(약 7GB)은 12GB에 동시에 못 산다. 처음엔 몰랐다가 파이프라인이 CUDA out of memory로 죽고 나서 알았다. 이미지 작업이 끝난 뒤에도 ComfyUI가 VRAM을 물고 있으면 다음 실행의 첫 LLM 호출이 죽는 구조였다.

해법은 시간 분할이다. 렌더러가 이미지 생성을 마치는 즉시 ComfyUI에 메모리 해제를 명령한다. 다음 이미지 생성 때 15초쯤 재로딩하는 게 임대료다. 한 장짜리 GPU로 멀티모달 파이프라인을 돌리려면 누군가는 계속 이사를 다녀야 한다.

같은 천장이 뒤에서 한 번 더 걸렸다. 앞에서 오탐을 줄이려고 컨텍스트를 16384로 올렸는데, 12288을 넘는 순간 모델과 KV 캐시가 VRAM을 초과했다. 그러면 죽는 게 아니라 느려진다. 윈도우가 부족분을 시스템 메모리로 페이징하면서 생성 속도가 초당 19토큰에서 3.5토큰으로 떨어졌고, 호출이 너무 길어져 도커 프록시의 유휴 한도를 넘기고 “connection aborted”로 끊겼다. 로그만 보면 네트워크 문제처럼 보인다.

결국 10240으로 내리고, flash attention과 q8_0 KV 캐시를 켜서 그 크기가 GPU에 온전히 들어가게 맞췄다. 프롬프트 예산은 9k 토큰 아래로 묶였다. 검수 품질을 컨텍스트로 사려던 시도가 VRAM 한 장에 다시 부딪힌 셈이다.

그 밖의 삽질들

각각 한 시간씩은 먹은 것들이다.

  • Windows 방화벽 팝업의 “허용”은 함정이다. 그 순간 생기는 규칙은 해당 프로그램의 전 포트를 전 네트워크에 연다. 조건을 붙이려 하면 거부된다. 규칙을 끄고 포트 지정 규칙을 새로 파야 한다
  • resvg는 폰트를 못 찾으면 네모(□)를 그리는 게 아니라 텍스트를 조용히 생략한다. 배경만 있는 멀쩡해 보이는 썸네일이 나온다. 출력이 12KB 미만이면 서버가 스스로 에러를 던지게 해서 막았다
  • n8n의 바이너리 필드에는 base64 대신 filesystem-v2라는 문자열이 들어올 수 있다. 그걸 GitHub에 커밋하려다 422를 맞고 알았다. 이후로 모든 이미지는 커밋 전에 파일 시그니처를 검증한다
  • NHTSA API는 빈 결과를 HTTP 400과 함께 준다. 본문은 멀쩡한 JSON이다. 상태코드를 믿지 말고 본문을 파싱해야 한다
  • ComfyUI 포터블 기본판이 기동 즉시 죽었다. CUDA 13.0 빌드인데 드라이버가 12.6까지였다. cu126 변형판으로 교체
  • n8n 워크플로를 UI에서 재임포트하면 credential이 증발한다. 연결이 초기화된다. DB에서 노드 parameters만 이식하고 같은 id로 다시 넣었다

숫자로 정리

항목
구축 기간이틀 (파이프라인 + 배포 + 발행 13편)
본문 섹션 호출약 35초/섹션 (5개 기준 4분)
글 1편 생산파이프라인 4분 + 사람 리서치·검수 약 10분
글당 API 비용0원
월 고정비도메인값이 전부
하드웨어RTX 3060 12GB 한 장

회고

병목은 생성이 아니라 검증이다. 글을 뽑는 건 4분이면 된다. 가치는 그 글이 거짓말을 안 한다는 보증에서 나오고, 그 보증은 결정론적 게이트와 사람 검수에서 나온다. “AI로 블로그 자동화”의 성패는 프롬프트가 아니라 검증 설계에 달렸다.

LLM의 판정에 차단권을 주지 마라. 스코어보드가 증명했다. LLM 검수는 사람 검수의 어시스트로는 훌륭하지만 심판으로는 실격이다.

검증 도구도 검증 대상이다. 오탐 필터를 만들었더니 필터의 미탐 리스크가 나왔고, 과거 글을 재감사했더니 게이트의 정규식 버그가 나왔고, 내가 놓친 수정도 거기서 나왔다. 검수 체계를 한 번 만들고 끝내면 그 체계가 조용히 썩는다.

제약이 설계를 만든다. 12GB 제한이 시간 분할 구조를, 숫자 섞임이 섹션별 생성을, 0원 제약이 로컬 스택 전체를 만들었다. 제약을 치우는 것보다 제약에 맞는 구조를 찾는 게 대부분 더 빨랐다.

규칙은 실측으로 갱신하라. 다만 잰 범위까지만. “AI가 그린 차는 못 쓴다”는 확신은 테스트 한 번에 깨졌다. 그런데 그때 확인한 건 차체 실루엣이었고, 엠블럼까지 괜찮을 거라고 넘겨짚은 건 나였다. 측정은 결론의 범위도 같이 정한다.