환각 없는 결정론적 점술 엔진 설계: Rust 상태 기계와 LLM 의미 그래프의 하이브리드 아키텍처
LLM이 명반을 직접 계산하면 변효와 별 위치를 틀립니다. 토큰 샘플링이 이산 불변량을 지키지 못하기 때문입니다. 이 사례 분석은 InsightApp의 운영 설계를 읽습니다. 순수 Rust 커널이 JSON 사실 그래프를 만들고 LLM은 언어만 담당하며, 아직 남은 세 가지 한계도 짚습니다.
이해관계 공개: InsightApp과 sns.style은 같은 운영자가 관리합니다. 아래 코드 인용은 모두 InsightApp 백엔드 commit `22cccf4`에서 가져왔으며 파일 경로를 함께 적었습니다(저장소는 비공개입니다). 독립 평가가 아닌 사례 분석입니다. 전체 버전은 중국어판이며 이 글은 한국어 요약판입니다.
주역 괘나 자미두수 명반을 대형 언어 모델에 바로 맡기면 문장은 그럴듯하지만 검산하면 틀리는 경우가 많습니다. 변효를 잘못 잡거나, 주성이 두 궁에 동시에 나타나거나, 화기가 명반에 없는 별에 붙습니다. 원인은 모델의 지능이 아니라 작업의 형태입니다. 명반 계산은 이산 조합 수학이고, 해석은 자연어입니다.
InsightApp의 주역·자미두수 엔진은 이 둘을 나눕니다. Rust로 작성한 결정론적 커널이 명반을 계산해 구조화된 JSON 사실 그래프를 출력하고, LLM은 그 그래프를 언어로 옮기는 일만 합니다. 제목의 "제로 환각"은 명반 계층에서만 성립하며 해석 계층에서는 성립하지 않습니다.
핵심 설계
명궁은 `mod12(lunar_month + 1 - hour)`, 신궁은 `(lunar_month + 1 + hour) % 12`로 계산되고, 오행국은 5×6 납음표 `NA_YIN_JU`에서 조회합니다(`backend/src/ziwei/calc.rs`). 14개 주성이 12궁에 정확히 한 번씩 나타나야 한다는 불변량은 토큰 단위 샘플링으로는 보장되지 않습니다. 다만 LLM 단독 계산의 오류율은 이 글에서 측정하지 않았습니다.
주역에서 무작위성은 동전을 던지는 한 지점(`backend/src/agent/hexagram.rs`)에만 있습니다. 세 동전의 합이 6, 7, 8, 9가 될 확률은 1/8, 3/8, 3/8, 1/8이고, 각 효가 변효가 될 확률은 1/4, 변효가 하나도 없을 확률은 약 17.8%입니다. 괘 조회에 실패하면 엔진은 기본값 대신 `EngineError`로 멈춥니다. 64괘 조회 페이지에서 확인할 수 있습니다.
자미두수의 `full_chart`는 I/O와 난수가 없는 순수 함수이며, `patterns`와 `feixing` 필드는 서버만 생성합니다. 윤달 규칙은 오픈소스 iztro와 505개 표본을 대조해 차이를 0으로 만든 뒤 추가되었다고 코드 주석에 적혀 있습니다. 화기 연쇄는 깊이 4에서 멈추고 같은 궁으로 돌아오면 `is_loop`를 표시하는 유한 그래프 탐색입니다(비성사화 페이지).
사실 그래프가 언어 모델에 들어가는 방식
자미두수 해석 API는 명반 JSON 전체를 prompt에 넣고, 두 가지 검색 결과를 덧붙입니다. 하나는 명반 내용으로 지식 베이스를 검색한 항목이고, 다른 하나는 명궁 주성과 감지된 격국의 식별자로 고전 문헌에서 찾은 원문 조각입니다. prompt는 모델에게 `patterns` 필드에 감지된 격국이 있으니 해석에 반영하라고 분명히 지시합니다. 주역 해석 prompt도 같은 구조입니다. 본괘, 변효, 변괘의 원문과 풀이를 하나씩 나열하고, 정해진 출력 형식을 주며, 의료 진단이나 투자 조언, 생사 예측을 하지 말라고 적습니다.
이렇게 나누면 모델의 일은 "계산하고 말하기"에서 "읽고 말하기"로 바뀝니다. 모델은 여전히 틀릴 수 있지만, 틀리는 방식이 달라집니다. 별의 위치를 기억에서 만들어 낼 필요가 없습니다. 위치가 눈앞의 데이터에 적혀 있기 때문입니다. 기본 모델은 OpenRouter의 `google/gemini-2.0-flash-001`이고, 샘플링 온도는 0.7입니다. 표현은 달라져도 되지만 명반은 달라지면 안 되기 때문입니다.
다른 분야에도 쓸 수 있는 점검표
세금 계산, 근무표 작성, 화학 계량, 게임 규칙 판정처럼 규칙이 있는 분야에서 LLM 제품을 만든다면 같은 계층 구조가 통합니다. 먼저 반드시 지켜야 할 불변량을 적고, 어떤 단계를 커널에 넣을지 정합니다. 커널은 순수 함수로 만들고 무작위성은 한 입구에 가둡니다. 파생된 사실은 서버만 만들고, 클라이언트가 보낸 같은 이름의 필드는 버립니다. 독립 구현과 표본 단위로 대조해 차이를 구체적인 규칙까지 추적합니다. 표 조회에 실패하면 그럴듯한 기본값을 내지 말고 오류로 멈춥니다. 알려진 근사는 주석과 문서에 적어 둡니다. 마지막으로 모델이 사실을 계산하게 하지 말고 읽게 하며, 생성 뒤에는 결정론적 검증을 한 번 더 거칩니다.
한계
해석 API는 클라이언트가 보낸 명반을 서버에서 다시 계산하지 않고 그대로 prompt에 넣습니다. 유년 천간은 `(year - 4) mod 10` 근사라 입춘 전후 하루 이틀은 틀릴 수 있다고 코드가 스스로 밝힙니다. 생성된 해석을 명반과 대조하는 사후 검증도 없습니다. 진태양시 보정은 Rust가 아니라 프런트엔드에서 합니다.
`STARS_14`와 `PALACES_12` 표에서 168개의 성요입궁 페이지가 생성됩니다. 예: 자미성 명궁. 더 많은 글은 InsightApp 블로그에 있습니다.
Sources
FAQ
왜 LLM이 명반을 직접 계산하지 않나요?
명반 계산은 나머지 연산과 표 조회의 연쇄이며, 14개 주성이 한 번씩만 나타나야 하는 불변량을 지켜야 합니다. 토큰 샘플링은 이를 보장하지 못합니다. 오류율은 측정하지 않았고 구조적 논증입니다.
여기서 '제로 환각'은 무엇을 뜻하나요?
명반 계층만을 뜻합니다. Rust 커널은 순수 함수라 iztro 등과 표본 단위로 대조할 수 있습니다. LLM 해석은 여전히 틀릴 수 있고, 생성 후 대조도 아직 없습니다.
남아 있는 한계는 무엇인가요?
해석 API가 클라이언트 명반을 재계산 없이 사용하는 점, 유년 천간이 입춘 전후로 틀릴 수 있는 근사인 점, 생성 후 검증이 없는 점입니다. 진태양시 보정은 프런트엔드에서 합니다.