본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 06. 00:57

거짓말하지 않는 AI 페르소나 구축하기: 아무도 신경 쓰지 않는 부분들

요약

단순한 API 호출을 넘어, 신뢰할 수 있는 AI 페르소나를 구축하기 위한 정교한 파이프라인 설계 과정을 다룹니다. 큐레이션, 보강, 검증, 승인 단계를 포함한 다단계 워크플로우를 통해 AI의 환각 문제를 해결하는 방법을 설명합니다.

핵심 포인트

  • 단순 생성 모델을 넘어선 다단계 파이프라인 설계의 중요성
  • 로컬 LLM과 이미지 모델을 활용한 프라이버시 중심 아키텍처
  • 인간의 승인(Human-in-the-loop) 단계를 통한 최종 품질 검증
  • 뉴스 소스 큐레이션부터 플랫폼별 게시까지의 자동화 워크플로우

대부분의 "소셜 미디어에 게시물을 올리는 AI 봇을 만들었습니다"라는 프로젝트들은 약 80줄의 코드로 이루어져 있습니다. 뉴스 API를 호출하고, 결과를 프롬프트(Prompt)에 넣고, LLM(대규모 언어 모델)을 호출한 뒤, 결과가 나오는 대로 게시하는 식이죠. 제 프로젝트도 처음에는 그렇게 시작했습니다. 문제는 이런 단순한 버전이 정확히 예상 가능한 대로 작동한다는 점입니다. 유창하고 자신감 있게 말하지만, 기술적인 세부 사항을 끊임없이 틀리게 말합니다.

AlexaPavlova는 베를린의 시니어 개발자로서 Mastodon과 Bluesky에 기술 뉴스 및 오픈 소스에 대한 건조한 견해를 게시합니다. 이 모든 과정은 제 집에 있는 하드웨어에서 실행됩니다. 텍스트를 위한 로컬 LLM(Local LLM), 이미지를 위한 로컬 이미지 모델(Local Image Model)을 사용하며, 클라우드 API(Cloud API)로 나가는 것은 아무것도 없습니다. 코드는 현재 MIT 라이선스로 공개되어 있습니다.

제가 실제로 신경 쓰는 부분은 게시물이 올라가는 것 자체가 아닙니다. "여기에 뉴스 기사가 있습니다"와 "여기에 게시할 가치가 있는 무언가가 있습니다" 사이에서 얼마나 많은 일이 일어나야 하는가 하는 점입니다. 그 간극을 메우는 데 수개월이 걸렸습니다.

[

A published post on Bluesky: the persona's text plus a generated portrait
]
실제로 게시되는 내용: 페르소나의 견해와 이미지 모델에서 생성된 초상화.

파이프라인이 곧 제품이다

단 하나의 게시물도 제가 눈으로 확인하기 전에 다음과 같은 과정을 모두 거칩니다:

소스(sources) → 큐레이션(curate) → 보강(enrich) → 생성(generate) → 정화(sanitize) → 검증(validate) → 재생성?(regen?) → 승인(approval) → 게시(publish)
  HN        선택     가져오기 +   로컬      ~10      결정적-    LLM 근거(grounding)  한 번    Telegram   Mastodon
  Lobsters  슬롯     요약       LLM       론적      검증,     재시도    사진 +    + Bluesky,
...

[

Telegram approval card: a generated post shown with its image and separate Mastodon and Bluesky versions, above approve / regenerate / cancel buttons
]
승인 단계는 단순히 제 휴대폰입니다. 각 게시물은 이미지와 각 플랫폼에 맞게 조정된 버전이 포함된 Telegram 카드로 도착하며, 제가 승인하기 전까지는 아무것도 게시되지 않습니다.

그리고 이것은 생성 경로(generation path)일 뿐입니다. 답글(Replies)은 별도의 장기 실행 프로세스(long-running process)입니다. 이 프로세스는 30분에서 2시간 사이의 무작위 간격으로 게시된 포스트를 폴링(poll)하며, 단순히 최상위 답글만 가져오는 것이 아니라 전체 답글 트리(reply tree)를 가져옵니다. 그 다음 자신의 목소리로 답변 초안을 작성하고, Telegram의 동일한 승인 단계를 거쳐 전송합니다. 저는 콜백 처리(callback handling)를 포스트 승인 리스너(post-approval listener)와 의도적으로 분리했는데, 두 개의 폴러(poller)가 동일한 Telegram 업데이트 스트림에 접근하면 서로 충돌(stepping on each other)하게 되기 때문입니다.

이 중 화려한 것은 하나도 없으며, 이 모든 과정은 고된 시행착오를 통해 자리를 잡았습니다. 여기서 중요한 레이어(layer)들을 소개합니다.

레이어 1:

기사를 아예 읽는 것 자체가 나중에 추가된 변경 사항이었으며, 이는 제가 작성한 그 어떤 프롬프트 (prompt)보다 더 중요했습니다. 첫 번째 버전은 헤드라인과 소스에서 반환한 스니펫 (snippet)에만 의존해 작동했는데, 그 결과가 고스란히 드러났습니다. 모델이 실제 내용이 아닌 제목에 반응했기 때문에, 의견은 일반적이거나 은밀하게 지어낸 것들이었습니다. 페이지를 가져오고, 본문을 추출하고, 이를 요약하는 과정이 비로소 의견의 설득력을 실어주었습니다. 또한 이 과정은 시스템을 더 까다롭게 만들었으며, 이는 수치상으로도 나타납니다. 데이터 보강 (enrichment) 단계에서 깔끔하게 읽을 수 없는 것을 버리고, 검증기 (validator) 단계에서 근거를 찾을 수 없는 것을 버리다 보니, 8개의 후보로 시작한 배치 (batch)가 이제는 23개만 발행되는 경향이 있습니다. 예전에는 78개였습니다. 이러한 감소는 시스템이 고장 난 것이 아닙니다. 품질 기준 (quality bar)이 제 역할을 하고 있는 것이며, 저는 차라리 이 상태가 유지되기를 바랍니다.

'지어내느니 차라리 버려라'라는 이 규칙은 전체 시스템의 중추입니다. 그 이후의 모든 것은 동일한 원칙을 더 강력하게 강제하기 위해 존재합니다.

레이어 2: LLM의 판단 이전에 수행되는 결정론적 정제 (Deterministic sanitization)

비용이 많이 드는 검증 (validation)을 거치기 전에, 가공되지 않은 출력물은 약 10단계의 결정론적 (deterministic) 정제 과정을 거칩니다. 모델을 사용하지 않는 일반 함수 (plain functions)를 사용하므로 비용이 들지 않습니다. 이 함수들은 LLM이 예측 가능한 방식으로 틀리는 요소들을 제거합니다: 금지된 해시태그, 유출된 내부 슬롯 ID (slot IDs), 끝에 서명하길 좋아하는 바이라인 (byline), 고립된 문장 시작 부분, 반복되는 동일한 도입부 습관, 별표 (*)를 이용한 자기 검열, XML 형태의 태그로 둘러싸인 이모지 등이 대상입니다. 각 페르소나 (persona)는 자신의 아이덴티티 파일 (identity file)에 고유한 해시태그 허용 목록 (allowlist)과 금지 문구 목록을 가지고 있습니다.

원칙은 간단합니다. 정규 표현식 (regex)이 할 수 있는 일에 모델을 투입하지 마십시오.

이것이 중요한 데에는 더 깊은 이유가 있으며, 저도 이를 깨닫기까지 시간이 좀 걸렸습니다. 게시물이 잘못 나올 때마다 저의 첫 번째 본능은 프롬프트 (prompt)에 규칙을 하나 더 추가하는 것이었습니다. 하지만 모든 규칙은 모델의 주의력 (attention)을 두고 경쟁하며, 일정 수준을 넘어서면 모델은 몇 가지 제약 조건만 충족하고 나머지는 놓아버리는데, 대개 전체적인 일관성 (coherence)부터 무너지기 시작합니다. 작은 양자화 모델 (quantized models)의 경우 이러한 한계점에 매우 빠르게 도달합니다. 프롬프트에서 떼어내어 결정론적 코드 (deterministic code)로 옮길 수 있는 모든 행동은, 제가 글쓰기 자체에 다시 돌려줄 수 있는 주의력이었습니다. 정제 레이어 (cleanup layer)는 단순히 속도만을 위한 것이 아닙니다. 모델이 여전히 목소리 (voice)를 유지할 수 있도록 프롬프트를 충분히 짧게 유지하기 위한 것입니다.

레이어 3: LLM 검증기 앞단의 정규 표현식 (regex) 사전 스크리닝

그다음은 근거 확인 (grounding check) 단계로, 게시물의 주장이 실제 소스 자료에 의해 뒷받침되는지 LLM을 통해 확인하는 과정입니다. 이 방식은 효과적이지만, 한 번의 왕복 (round trip) 비용이 발생하며, 이 왕복 과정은 느리고 때로는 그 자체로 틀리기도 합니다.

비용을 낮출 수 있었던 비결은 가장 흔한 환각 (hallucinations)들이 특정한 형태를 띠고 있다는 점을 발견한 것이었습니다. 지어낸 파일 경로, file:line 참조, v2.3.1과 같은 버전 문자열, 에러 코드, CVE 번호 등이 그것입니다. 이 모든 것들은 정규 표현식 (regex)과 일치합니다. 따라서 사전 스크리닝 (pre-screen)이 먼저 실행되며, 만약 실제 존재하지 않는 곳을 가리키는 core/handler.py:88 토큰을 발견하면, LLM 호출이 일어나기 전에 게시물에 플래그 (flag)가 지정됩니다. 저렴한 결정론적 확인 (deterministic check)이 일반적인 사례를 즉시 잡아내고, 모델은 그 과정을 통과한 것들만 검토하게 됩니다.

실패한 게시물은 한 번의 재생성 (regeneration) 시도를 거치며, 이때 실패한 특정 주장들을 프롬프트에 다시 입력하여 재시도 시 모델이 다시 주사위를 던지는 대신 무엇을 피해야 하는지 알 수 있도록 합니다.

레이어 4: 생성기와 검증기는 같은 것을 원해야 한다

이 부분이 저에게 가장 많은 시간을 소모하게 만든 지점이었습니다. 페르소나 프롬프트 (persona prompt)의 초기 버전들은 검증기 (validator)가 바로 폐기해 버릴 내용을 정확히 요구하고 있었습니다. 프롬프트는 "특정 기술적 세부 사항을 참조할 것"이라고 명시했지만, 모델은 이를 "그럴듯한 파일 경로를 지어낼 것"으로 해석했습니다. 그 후 근거 확인 (grounding check) 단계에서 지어낸 경로가 적발되었고, 게시물은 폐기되었습니다. 저는 제 자신의 품질 게이트 (quality gate)를 통과하지 못하도록 설계된 게시물을 생성하는 데 컴퓨팅 자원 (compute)을 낭비하고 있었고, 그 게시물들이 실패했다는 것을 감지하는 데 또 다른 컴퓨팅 자원을 낭비하고 있었습니다.

해결책은 양쪽 끝을 모두 수정해야 했습니다. 이제 프롬프트는 파일 경로, 줄 번호, 버전 번호, 함수 이름 또는 API 엔드포인트 (API endpoints)를 지어내지 말라고 명시적으로 말하며, 검증기 또한 동일한 규칙을 강제합니다. 만약 여러분의 생성 후 검증 (generate-then-validate) 루프에서 거절되는 내용이 많다면, 문제는 대개 모델에 있지 않습니다. 프롬프트가 검증기가 처벌하도록 설계된 무언가를 요구하고 있는 것이 문제입니다.

메모리 (Memory) 기능에서도 동일한 유형의 버그가 있었습니다. 저는 연속성을 위해 이전 게시물의 본문 전체를 컨텍스트 (context)에 입력하곤 했고, 모델은 이를 패턴 매칭 (pattern-matching)했습니다. 만약 지난주 게시물에 file:line 참조가 있었다면, 이번 주 게시물은 새로운 것을 지어내게 됩니다. 몇 가지 예시를 통해 "이런 식의 게시물에는 구체적인 내용이 포함된다"는 것을 학습했기 때문입니다. 이를 "이미 다뤄진 주제들이니 반복하지 마시오"라는 프레임으로 구성된 제목만 사용하는 방식으로 전환하자, 해당 카테고리의 문제가 완전히 사라졌습니다. 컨텍스트 내의 예시들은 여러분이 의도했든 아니든 하나의 지시 사항 (instructions)으로 작용합니다.

이것이 종합되었을 때

나머지는 배관 작업 (plumbing)입니다. 각 페르소나는 고유의 정체성 파일, 프롬프트, 그리고 SQLite 데이터베이스와 함께 각자의 디렉토리에 격리되어 있으므로, 새로운 페르소나를 추가하는 것은 디렉토리를 복사하는 것과 같습니다. 처음부터 끝까지 UTC를 유지하는 스케줄러 (scheduler), 디스패처 루프 (dispatcher loop), 두 개의 별도 텔레그램 (Telegram) 승인 인터페이스, 그리고 각 계정의 자격 증명 (credentials)을 사용하여 Mastodon과 Bluesky 양쪽에 게시하는 기능이 포함됩니다. 이때 각 플랫폼은 자신의 길이 제한에 맞게 생성된 텍스트 버전을 받게 됩니다. 이 모든 것을 유지하기 위해 약 312개의 테스트가 모두 모킹 (mocked)된 상태로 작동하고 있습니다.

제가 계속해서 되돌아오게 되는 지점은, LLM (Large Language Model)이 텍스트를 생성하게 만드는 것은 쉬운 20%에 불과하다는 사실입니다. 모델이 신뢰할 수 있게 거짓말을 하지 않도록 만드는 것이 나머지 80%이며, 그 80%는 큐레이션 (curation), 즉 이야기를 지어내는 대신 버리는 규율, 결정론적 정제 (deterministic cleanup), 계층적 검증 (layered validation), 그리고 거부권을 가진 인간의 영역입니다. 어려운 부분의 거의 전부는 모델이 아닙니다. 거의 모든 것이 모델 주변의 스캐폴딩 (scaffolding, 구조물)입니다.

실제로 작동하는 방식

두 대의 기기가 사용됩니다. Raspberry Pi 4 한 대가 모든 오케스트레이션 (orchestration)을 담당하며 절대 꺼지지 않습니다. 이 기기는 SQLite 데이터베이스를 보유하고, 세 개의 지속적인 프로세스를 systemd 유닛으로 실행하여 재부팅 후에도 다시 시작되도록 하며, cron을 통해 일일 배치 (batch) 작업을 실행합니다. GPU가 장착된 노트북은 실제로 생성할 내용이 있을 때만 무거운 작업 (heavy lifting)을 수행합니다. 두 기기는 LAN을 통해 통신합니다.

네 개의 프로세스가 작업을 수행합니다:

  • main_batch.py — cron에 의해 시작되는 일일 실행 프로세스입니다. 이야기를 가져와 최대 8개의 후보 게시물과 이미지를 구축하고, 강화 (enrichment) 및 검증 (validation)을 통과한 것들을 승인을 위해 Telegram으로 전송한 뒤 종료됩니다. 데몬 (daemon)이 아닌 일회성 실행 방식입니다.
  • main_telegram_listener.py — 각 Telegram 카드에 있는 APPROVE (승인) / REGEN (재생성) / CANCEL (취소) 버튼을 처리하는 지속적인 루프 (loop)입니다.
  • main_dispatcher.py — 승인된 게시물을 예정된 시간에 Mastodon과 Bluesky에 게시하는 지속적인 루프입니다.
  • main_reply_listener.py — 게시된 게시물의 답글을 폴링 (polling)하여 답변 초안을 작성하고, 이를 다시 Telegram을 통해 승인을 받도록 경로를 지정하는 지속적인 루프입니다.

GPU 측은 단순히 Pi가 호출하는 두 개의 로컬 HTTP 서비스로 구성됩니다. 텍스트 모델을 위한 LMStudio와 이미지를 위한 SwarmUI입니다.

유지 비용을 낮게 유지할 수 있는 비결은 하드웨어가 아니라 분리(split)에 있습니다. 항상 켜져 있는 절반은 24시간 내내 몇 와트(watts)만 소비하는 Pi 4입니다. 추론(inference)을 담당하는 절반은 결코 겸손하지 않습니다. 그것은 RTX 5080 (16 GB)을 탑재한 MSI Vector 노트북으로, 진정한 GPU 머신입니다. 핵심은 이 장치가 24시간 내내 돌아갈 필요가 없다는 점입니다. GGUF로 양자화(quantized)된 14B 모델은 16 GB에 여유롭게 들어가며, 하루에 몇 번 게시물을 올리는 하나의 페르소나를 위해 GPU는 일일 배치(batch) 작업과 가끔 발생하는 답장 초안 작성 시에만 실제로 작동하고 나머지 시간에는 유휴(idle) 상태로 있습니다. 항상 켜져 있는 부분은 비용이 거의 들지 않으며, 비용이 발생하는 부분은 거의 켜져 있지 않습니다.

Raspberry Pi 4

잠들지 않는 Pi 4: SQLite, 리스너(listeners), 일일 cron.

the MSI laptop that does inference

추론을 수행하는 MSI — 생성할 내용이 있을 때만 깨어납니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0