2026.02.17 - [개발] - 사주 앱 개발 4편 - 룰 엔진 + AI 해석 하이브리드 설계
사주 룰 + AI 해석 하이브리드 설계과정에 대한 좀 상세한 분석이다
설계가 아무리 잘 돼 있어도 실제로 돌리면 예상 못 한 곳에서 터진다.
이 편은 운영 중 실제로 마주친 케이스들이다. 대부분 "이 정도는 괜찮겠지"라고 넘어갔다가 나중에 문제가 된 것들이다. 비슷한 구조를 만드는 사람이라면 미리 알면 시간을 아낄 수 있다.
문제 1: 제약을 뚫고 나오는 할루시네이션
시스템 프롬프트에 "데이터에 없는 내용을 추론하지 말 것"을 명시했다. 그런데 특정 케이스에서 이 제약이 무너졌다.
재현 조건은 오행이 극단적으로 편중된 사주였다. 火가 70% 이상, 水가 0%인 구조. 이 데이터를 넘겼을 때 AI가 "壬水 대운이 오면 균형이 맞춰집니다"라는 문장을 생성했다. 프롬프트에 대운 데이터를 넣지 않았는데 AI가 임의로 추론해서 넣은 것이다.
원인은 명확했다. 데이터가 극단적일수록 AI가 "이건 설명이 필요하다"고 판단하고 없는 정보를 채워 넣으려 한다. 제약 조건이 있어도 강한 패턴 완성 욕구가 이긴다.
해결: 두 가지를 동시에 적용했다.
[시스템 프롬프트 강화]
기존: "데이터에 없는 내용을 추론하지 말 것"
변경: "데이터에 없는 내용을 추론하거나 언급하지 말 것.
대운, 세운, 월운 등 제공되지 않은 시기 정보는
절대 포함하지 말 것. 위반 시 해석 전체가 무효화됨."
// 응답 후처리 검증
function validateResponse(response: string, context: SajuContext): ValidationResult {
const forbiddenPatterns = [
/대운이\s*오면/,
/세운에서/,
/\d{4}년에는/,
/앞으로\s*\d+년/
]
const violations = forbiddenPatterns.filter(pattern =>
pattern.test(response)
)
if (violations.length > 0) {
return { valid: false, reason: '미제공 시기 정보 포함' }
}
return { valid: true }
}
제약을 강하게 쓰는 것만으로 완전히 막기 어렵다. 후처리 검증으로 2차 방어를 추가했다. 검증 실패 시 해당 카테고리만 재호출한다.
문제 2: 캐시가 오히려 독이 된 상황
캐싱을 적용하고 나서 며칠 후 프롬프트를 수정했다. 운명론적 표현을 좀 더 강하게 제한하는 방향으로. 그런데 기존 사용자들은 여전히 수정 전 캐시를 받고 있었다.
더 심각한 건 이걸 한동안 몰랐다는 것이다. 캐시 만료 전까지 API 호출이 없으니 문제를 확인할 방법이 없었다.
해결: 캐시 키에 프롬프트 버전을 포함시켰다.
const PROMPT_VERSION = 'v3' // 프롬프트 수정 시 버전 올림
function generateCacheKey(params: InterpretationParams): string {
return `saju:${PROMPT_VERSION}:${params.birthDate}:${params.birthTime}:${params.gender}:${params.category}`
}
프롬프트를 수정할 때마다 PROMPT_VERSION을 올리면 기존 캐시가 자동으로 무효화된다. 구버전 키로 저장된 데이터는 만료되면 사라진다. 버전 관리와 캐시 무효화를 동시에 해결하는 가장 단순한 방법이다.
문제 3: 스트리밍 도중 연결 끊김
모바일에서 특히 자주 발생했다. 스트리밍 도중 네트워크가 잠깐 끊기면 텍스트가 중간에 잘린 채로 멈춰버렸다. 사용자 입장에서는 "로딩 중"인지 "완료"인지 알 수 없는 상태가 됐다.
// 문제가 있던 초기 구현
const response = await anthropic.messages.stream(...)
for await (const chunk of response) {
onChunk(chunk.delta.text)
}
// 연결 끊기면 여기서 그냥 멈춤
해결: 타임아웃과 재시도 로직 추가.
async function streamWithRetry(
params: StreamParams,
onChunk: (text: string) => void,
maxRetries = 2
): Promise<void> {
let attempt = 0
let accumulatedText = ''
while (attempt <= maxRetries) {
try {
const timeoutId = setTimeout(() => {
throw new Error('Stream timeout')
}, 30000) // 30초 타임아웃
const response = await anthropic.messages.stream({
...params,
// 재시도 시 이어받기를 위한 컨텍스트 추가
system: attempt > 0
? `${params.system}\n이미 생성된 내용: ${accumulatedText}\n위 내용에 이어서 계속 작성하세요.`
: params.system
})
for await (const chunk of response) {
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
accumulatedText += chunk.delta.text
onChunk(chunk.delta.text)
}
}
clearTimeout(timeoutId)
return // 성공 시 종료
} catch (error) {
attempt++
if (attempt > maxRetries) {
onError('해석 생성 중 오류가 발생했습니다. 다시 시도해주세요.')
return
}
await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
}
}
}
이어받기 컨텍스트를 넣으면 재시도 시 이미 생성된 텍스트를 중복으로 출력하지 않는다. 실제로 중간에 끊겼다가 재연결됐을 때 사용자가 눈치채지 못하는 경우가 대부분이었다.
문제 4: 특정 일간에서 반복되는 패턴
庚金 일간 사주를 여러 개 테스트하다가 발견했다. 성격 해석에서 "강인하고 결단력 있는"이라는 표현이 거의 매번 나왔다. 월지가 달라도, 십신 구조가 달라도.
庚金의 전형적인 이미지가 AI 학습 데이터에 너무 강하게 박혀 있어서 궁통보감 데이터가 있어도 그 이미지를 먼저 꺼내는 경향이 있었다.
해결: 지시문에 명시적 경고 추가.
[카테고리 지시문 추가]
일간의 일반적인 이미지나 전형적인 설명 대신,
제공된 오행 분포와 십신 배치의 구체적인 조합을 기반으로
이 사람만의 특성을 설명할 것.
"庚金은 일반적으로..." 같은 표현은 사용하지 말 것.
완전히 없애진 못했다. 하지만 빈도가 눈에 띄게 줄었다. 일반론을 막는 것보다 구체적 데이터를 활용하도록 유도하는 방향이 더 효과적이었다.
문제 5: 음력 입력 처리 엣지 케이스
음력 윤달 처리가 문제였다. 음력 2월 30일이 존재하는 해가 있고 없는 해가 있다. 사용자가 "음력 2월 30일"을 입력했을 때 해당 연도에 그 날짜가 없으면 룰 엔진이 잘못된 기둥을 계산했다.
더 골치 아팠던 건 이 오류가 조용히 넘어간다는 것이다. 에러가 발생하는 게 아니라 엉뚱한 날짜로 계산이 됐다.
function validateLunarDate(year: number, month: number, day: number, isLeapMonth: boolean): boolean {
const lunarMonthDays = getLunarMonthDays(year, month, isLeapMonth)
if (!lunarMonthDays) {
// 윤달이 없는 달을 윤달로 입력한 경우
throw new Error(`${year}년 ${month}월은 윤달이 없습니다`)
}
if (day > lunarMonthDays) {
throw new Error(`${year}년 음력 ${month}월은 ${lunarMonthDays}일까지입니다`)
}
return true
}
입력 단계에서 유효성 검사를 강화하는 것이 유일한 해결책이었다. 룰 엔진에서 막으려면 너무 늦다.
트러블슈팅 요약
| 제약 뚫는 할루시네이션 | 극단적 데이터에서 패턴 완성 욕구 | 프롬프트 강화 + 후처리 검증 |
| 캐시 프롬프트 불일치 | 수정 후 기존 캐시 유지 | 캐시 키에 프롬프트 버전 포함 |
| 스트리밍 연결 끊김 | 모바일 네트워크 불안정 | 타임아웃 + 이어받기 재시도 |
| 일간 전형 패턴 반복 | AI 학습 데이터 편향 | 일반론 금지 + 구체 데이터 활용 지시 |
| 음력 윤달 엣지 케이스 | 입력 유효성 미검증 | 입력 단계 강화 |
공통점이 있다. 설계 단계에서 "이 정도면 됐겠지"하고 넘어간 것들이다. 할루시네이션은 제약 조건 하나면 막을 줄 알았고, 캐시는 만료 시간만 있으면 되는 줄 알았다. 실제 운영은 항상 설계보다 한 겹 더 깊은 곳에서 터진다.
다음 편이 마지막이다. 이 설계가 사주를 벗어나 어디까지 쓸 수 있는지 — 복잡한 도메인 지식과 LLM을 조합하는 구조의 확장 가능성을 정리한다.
'개발 > AI' 카테고리의 다른 글
| 사주 룰 + AI 해석 하이브리드: 이 설계, 사주 말고 어디까지 쓸 수 있나 (0) | 2026.03.04 |
|---|---|
| 사주 룰 + AI 해석 하이브리드: 비용과 품질의 균형점 찾기 (0) | 2026.03.04 |
| 사주 룰 + AI 해석 하이브리드: 룰 결과를 AI에게 넘기는 프롬프트 설계 (0) | 2026.03.04 |
| 사주 룰 + AI 해석 하이브리드: 어디서 나눌 것인가 — 경계 설계 (0) | 2026.03.04 |
| 사주 룰 + AI 해석 하이브리드: AI 단독 해석이 실패하는 지점 (0) | 2026.03.04 |
