내 앱의 LLM은 아무것도 결정할 수 없습니다
요약
LLM의 환각 문제를 해결하기 위해 결정론적 규칙 엔진을 활용하는 아키텍처를 제안합니다. LLM은 사실을 생성하는 것이 아니라, 규칙 엔진이 계산한 데이터를 페르소나에 맞춰 전달하는 번역가 역할만 수행해야 합니다.
핵심 포인트
- LLM은 결정론적 엔진이 도출한 사실을 전달하는 역할에 집중해야 함
- 데이터 공백 발생 시 모델의 임의 추론을 막기 위해 불확실성을 명시적으로 처리
- 입력값 누락 시 가능한 모든 경우의 수를 계산하여 교집합 정보만 프롬프트에 제공
- 모델이 정보를 지어내지 않도록 명시적인 가이드라인과 기호 활용
저는 LLM의 진실성(truthfulness) 측면에서 최악의 도메인인 점술 분야의 소프트웨어를 개발하고 있습니다. 바로 BaZi(사주팔자) 운세 앱입니다. 이 모델의 역할은 현명한 스승처럼 말하는 것이며, 경쟁 AI 제품들에 대한 사용자 리뷰는 한 가지 불만으로 수렴합니다. 바로 "완전한 헛소리"라는 점입니다. "사주를 읽어달라"는 요청을 받은 LLM은 존재하지 않는 차트 요소를 환각(hallucinate)하고, 전통에 존재하지 않는 규칙을 지어내며, 이 모든 것을 매우 자신감 넘치는 목소리로 전달합니다. 대조하여 확인할 수 있는 외부적인 정답(ground truth)이 전혀 없는 도메인이기에, 사용자들은 동일한 차트에 대한 두 번의 결과가 서로 모순될 때까지는 이를 알아차릴 수 없습니다.
이 도메인에 대해 어떻게 생각하시든(저는 이전에 이 분야의 정말로 어려운 시간대 수학에 대해 글을 쓴 적이 있습니다), 공학적인 해답은 무언가를 지어내서는 안 되는 모든 LLM 제품에 적용 가능합니다. 규칙은 단 하나입니다:
결정론적 엔진(deterministic engine)이 무엇을 말할지 결정합니다. LLM은 오직 그것을 어떻게 말할지만 결정합니다.
차트, 오행의 강도, 용신(favorable-element) 분석, 모든 파생된 사실 — 이 모든 것은 TypeScript로 작성된 규칙 엔진(rules engine)에 의해 계산되며, 단위 테스트(unit-tested)를 거치고, 상수까지 모두 공개되어 있습니다. 모델은 이러한 사실들을 압축된 블록과 함께 전달받으며, 지시 사항을 받습니다: 주어진 내용만 인용할 것. 모델은 신탁(oracle)이 아니라, 페르소나(persona)를 가진 번역가입니다.
그것이 쉬운 80%입니다. 흥미로운 공학적 과제는 이 규칙이 거의 깨질 뻔했던 세 가지 지점에 있습니다.
1. 까다로운 사례: 입력값의 공백
많은 사용자가 자신의 태어난 시간을 모릅니다. 그리고 시간은 사주(four pillars)의 구성 요소 중 하나입니다. 단순한 선택지들은 둘 다 좋지 않습니다. 사용자를 거절하거나, 모델이 공백을 두고 즉흥적으로 대처하게 두는 것입니다.
두 번째 방식이 위험합니다. 시간을 생략했을 때 모델이 무엇을 하는지 맞춰보세요. 모델은 그 구멍을 채워버립니다. 조용히 말이죠. 구체적이고 그럴듯하게 지어낸 기둥(pillar)으로 말입니다.
해결책은 엔진이 불확실성을 결정론적으로 처리하도록 만드는 것입니다.
알 수 없는 시(hour)는 정확히 12가지의 가능한 사주(charts)를 의미합니다. 저는 이 12가지를 모두 계산한 다음, 그 교집합을 구합니다. 즉, 모든 후보 사주에서 공통적으로 성립하는 사실만이 프롬프트(prompt)에 남게 됩니다. 12개 모두에서 요소의 강도(element strength)가 일치하나요? 그렇다면 명시합니다. 만약 7:5로 나뉜다면? 프롬프트는 다음과 같이 있는 그대로 말합니다.
강도 미결정 (12개 후보 중 7개가 강함으로 기울어짐) — 이를 바탕으로 판단을 내리지 마십시오.
그리고 결정적으로, 공백(hole) 자체를 누락하는 대신 명시적으로 드러냅니다.
사주: 己未 丙寅 庚午 ▢
(시주(hour pillar)를 알 수 없음 — 12개의 후보 사주를 계산함; 모든 사주에서 참인 사실만 나열됨)
저 ▢ 기호는 그 자리에 있을 자격이 있습니다. 침묵하는 공백보다 명시된 공백이 낫습니다. 슬롯을 비워두면 모델은 이를 임의로 채워 넣지만(backfills), 이를 표시하고 지침을 내리면("시주를 언급하지 마십시오; 시주가 관장하는 운의 영역을 논하지 마십시오; 하나를 지어내는 것은 거짓말입니다") 모델은 이를 우회하여 처리합니다.
프롬프트는 모델에게 우아하게 마무리하는 방법까지 알려줍니다. 사용자가 자신의 태어난 시간을 알게 될 경우 무엇을 더 볼 수 있는지 언급하는 한 문장을 추가하는 식입니다. 불확실성이 환각(hallucination)의 영역이 되는 대신, 제품의 기능(feature)이 된 것입니다.
2. 두 번째 관문: 첫 번째 관문을 믿지 않는 것처럼 출력을 검증하라
프롬프트는 정책(policy)이지 강제 집행(enforcement)이 아닙니다. 따라서 생성된 모든 해석은 저장되기 전에 검증기(validator)를 통과해야 합니다.
지어낸 기둥(Invented-pillar) 탐지
알 수 없는 시(hour)를 가진 사주의 경우, 출력물에서 12가지 후보 시주(hour pillars)가 있는지 스캔합니다.
중요한 세부 사항은 단일 글자가 아닌, 두 글자로 이루어진 간지(stem-branch) 쌍 전체를 일치시키는 것입니다. 子(자) 하나만 있으면 孩子(
이것은 단순히 취향의 문제가 아닙니다. 검색 품질 가이드라인, 광고 정책, 결제 프로세서 등 이 수직적 영역(vertical)을 규정하는 모든 플랫폼 정책은 구체적인 파멸적 주장(doom claims)과 해석적 성찰(interpretive reflection) 사이에서 허용/금지 선을 긋습니다. 정규 표현식(regex) 리스트는 그 준수 경계(compliance boundary)를 코드로 변환합니다.
폐쇄형 어휘 체크 (Closed-vocabulary check)
엔진이 절대 계산하지 않는 별(star) 및 신(deity) 관련 용어 리스트입니다. 만약 출력값에 이 용어가 나타난다면, 모델이 학습 데이터로부터 민속학적 요소(folklore)를 가져온 것이며, 해당 읽기 결과는 검토를 위해 플래그(flagged) 처리됩니다.
3. 고백: 나의 가드레일(guardrail)은 죽은 코드였고 나는 그것을 알아채지 못했다
원래 검증기(validator)에는 세 번째의 더 강력한 체크 기능이 있었습니다. 출력되는 모든 간지(stem-branch)와 "십신(ten god)" 용어가 차트 JSON에서 온 것인지 확인하는 화이트리스트 단언(whitelist assertion)이었습니다.
몇 달 후 코드를 리뷰하며, 저는 두 루프(loop) 모두 해당 집합(set)이 동일한 집합에서 가져온 항목을 포함하고 있는지를 묻고 있다는 사실을 발견했습니다. 이는 항상 참(true)인 조건이며, 따라서 결코 실행될 수 없는 체크 로직에 연결되어 있었습니다. 두 개의 보조 배열(supporting arrays)은 전혀 읽히지 않았습니다.
그것은 단 한 번의 위반도 잡아내지 못했습니다. 출력이 깨끗해서가 아니라, 가드레일이 작동할 수 없었기 때문입니다.
저는 그것을 삭제하고 왜 그랬는지 설명하는 주석을 달았습니다. 여기에는 실제 버전이 해결해야 할 문제, 즉 앞서 언급한 것과 동일한 단일 문자 충돌(single-character collision) 문제도 포함했습니다. 제가 이제 모든 곳에 적용하는 두 가지 교훈은 다음과 같습니다:
- 작동할 수 없는 가드레일은 가드레일이 없는 것보다 나쁩니다. 이는 모든 아키텍처 다이어그램과 코드 리뷰에서 "우리는 이것을 검증한다"라고 표시되어, 모두가 더 이상 그것에 대해 생각하지 않게 만듭니다.
- 코드를 테스트하는 방식대로 검증기를 테스트하십시오. 반드시 실패해야 하는 입력값(inputs)을 사용해야 합니다. 실패하는 테스트 케이스가 없는 검증 함수는 게이트(gate)가 아니라 가설(hypothesis)일 뿐입니다.
보너스 전투: 프롬프트 언어의 중력 (prompt-language gravity)
페르소나와 모든 지침은 중국어로 작성되어 있습니다. 앱은 영어 읽기 서비스도 제공합니다. 단 한 줄의 "영어로 응답하라"는 2,000자에 달하는 중국어 프롬프트와의 접촉에서 살아남지 못합니다.
모델은 혼종 문장(hybrid sentences)을 운영 환경(production)에 배포했습니다. 실제 예시는 다음과 같습니다:
Your盘的里,其实事业和财这两条线比性格更有讲头
해당 문구는 페르소나(persona) 내부에 포함된 중국어 예시 문장에서 그대로 가져온 것이었습니다.
해결책은 더 직설적이었습니다. 프롬프트에 인용된 모든 문장은 오직 어조(tone)를 보여주기 위한 데모일 뿐이며, 그 중 어떤 것도 복사하거나 번역해서는 안 되고, 출력물에는 주석 처리된 병음(pinyin)을 제외한 중국어가 포함될 수 없음을 명시하는 전용 단락을 추가하는 것이었습니다. 프롬프트가 이중 언어(bilingual)로 작성될 때, 언어 지침(language directive)은 프롬프트의 나머지 모든 내용보다 더 강력하게 전달되어야 합니다.
정확성을 넘어 왜 이 작업이 필요한가
- 비용. 모델이 차트에 대해 "추론(reasoning)"하는 대신, 섹션당 스타일이 가미된 산문 300단어를 작성합니다. 사고 사슬(chain-of-thought)이 필요 없습니다. 사고 모드(thinking mode)를 끄면 호출 시간은 약 1.5초이며 비용은 1센트의 몇 분의 일 수준입니다.
- 일관성. 동일한 차트를 가진 두 사용자가 동일한 사실에 대해 서로 다른 운명을 받는 것이 아니라, 스타일의 변주만을 얻게 됩니다. 다시 읽어도 내용이 모순되지 않습니다.
- 감사 가능성. 사용자가 "왜 그렇게 말하나요?"라고 물었을 때, 사이트의 방법론(method) 페이지에 게시된 것과 동일한 엔진의 사실을 지목할 수 있습니다.
솔직히 이 패턴은 오래된 것입니다. 사실을 방출하는 컴파일러와 이를 렌더링하는 프리티 프린터(pretty-printer)의 조합과 같습니다. 유일하게 새로운 부분은 프리티 프린터가 미대에 가서, 내버려 두면 사실을 지어낼 수도 있다는 점입니다. 그렇게 두지 마세요. 진실을 계산하고, 빈틈을 표시하며, 출력을 검증하고, 당신의 검증기(validator)가 실제로 실패할 수 있는지 테스트하십시오.
앱: auspiceoracle.com — 엔진의 점수 산정 상수(scoring constants)는 방법론 페이지에 공개되어 있으며, 이는 마케팅에도 적용되는 동일한 "과정을 보여주라(show your work)"는 규칙입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기