1. 핵심 원칙 — "점수는 룰, 멘트는 LLM"
❌ 잘못된 접근
모든 자연어를 코드로 생성
if(diversity < 50) text = `다양성 부족 — ${missing.join('·')}이 비어요`;
if(texture_puree_pct > 60) text += `죽 비중 ${puree_pct}%로 핑거푸드 부족`;
if(texture_puree_pct > 60) text += `죽 비중 ${puree_pct}%로 핑거푸드 부족`;
결과: 부자연스러운 템플릿 문장. 부모 맥락·뉘앙스 X. 매번 똑같은 표현 → 식상.
✅ 우리 방식
룰로 데이터, LLM으로 자연어
룰: diversity = {pct: 62.5, missing: ['콩','유제품','과일']}
LLM (Haiku): "이번 주 콩·유제품·과일을 못 만났어요. 두부 한 컵 + 우유 한 잔이면 +25점 가능해요."
LLM (Haiku): "이번 주 콩·유제품·과일을 못 만났어요. 두부 한 컵 + 우유 한 잔이면 +25점 가능해요."
결과: 정확한 숫자(룰) + 따뜻한 자연어(LLM). 매번 다른 표현. 부모 맥락 반영.
왜 이 분리가 중요한가
- 학술 정합성 — 점수·등급은 결정론적 Rule로 재현·감사 가능 (RCT 데이터 신뢰성)
- 비용 통제 — 모든 화면을 LLM으로 채우면 월 ₩수십M. 룰로 95% 처리하고 LLM은 자연어·정성에만
- 속도 — 룰은 p95 50ms · LLM은 800ms~2.5s. 핵심 화면은 룰
- 자연스러움 — 부모는 같은 문구 반복 안 좋아함. LLM이 매번 다른 표현 + 맥락 반영
- 오프라인 작동 — 룰 기능은 LLM 장애에도 작동 (도감·신호등·점수)
2. 3 카테고리 분류
🟢 A · PURE RULE
코드만 (LLM 0%)
정형 데이터 + 결정론적 로직. 자연어 생성 X. 점수·등급·매칭·카운트.
22개 기능 · p95 50ms · 비용 ₩0
- 도감·레시피 정적 조회
- WHO MDD 다양성 카운트
- KDRI 36 영양 신호등
- BMI·점수·등급 계산
- 마일리지·챌린지·룰렛
🟡 B · RULE + LLM
룰로 데이터, LLM으로 자연어 표현
점수·통계는 룰. 그 결과를 사람한테 설명하는 자연어 멘트만 LLM (Haiku).
11개 기능 · Haiku 95% · 비용 ₩4/회
- 각 축 진단 멘트 ("죽 65%...")
- 식단표 5문장 자연어 가이드
- 주간 리포트·카톡 알림톡
- 도감 식재료 친해지기 팁
- 결핍 영양 보충 제안 멘트
🟣 C · LLM-PRIMARY
LLM 코어 (룰은 검증·가드만)
자유 텍스트 이해·생성·맥락 추론. 정성 시계열 분석. 양방향 체이닝.
8개 기능 · Haiku+Sonnet · 비용 ₩4~₩50/회
- 자유 텍스트 → MealLog 전처리
- 정성 노트 시계열 분석
- 다음 입력 시 역질문 생성
- 편지·동적 질문 회전
- 식단표 OCR + 메뉴 매핑
3. 결정 트리 — 신규 기능 어디에 넣을까
flowchart TD
START{새 기능 요구}
Q1{입력이 정형 데이터?
(숫자·enum·매핑)} Q2{출력이 정형 데이터?
(점수·등급·매칭 결과)} Q3{출력에 자연어
인사이트·멘트 필요?} Q4{입력이 자유 텍스트
또는 사진?} Q5{시계열 맥락
또는 정성 분석 필요?} A[🟢 A · Pure Rule
코드로만] B[🟡 B · Rule + LLM Narration
룰 계산 → Haiku 자연어] C[🟣 C · LLM-Primary
Haiku/Sonnet 코어] START --> Q1 Q1 -->|"YES"| Q2 Q1 -->|"NO (자유 텍스트·사진)"| Q4 Q2 -->|"YES"| Q3 Q2 -->|"NO"| C Q3 -->|"NO (숫자만)"| A Q3 -->|"YES"| B Q4 -->|"단순 정규화 필요"| C Q4 -->|"시계열 분석·체이닝"| Q5 Q5 -->|"YES"| C classDef start fill:#FFE8D0,stroke:#C45A00,color:#1a2b4a classDef qst fill:#FFFBF5,stroke:#FF8A47,color:#1a2b4a classDef cata fill:#E8F5E9,stroke:#16A085,color:#1a2b4a,font-weight:bold classDef catb fill:#FFF8E1,stroke:#F9A825,color:#1a2b4a,font-weight:bold classDef catc fill:#F3E5F5,stroke:#9C27B0,color:#1a2b4a,font-weight:bold class START start class Q1,Q2,Q3,Q4,Q5 qst class A cata class B catb class C catc
(숫자·enum·매핑)} Q2{출력이 정형 데이터?
(점수·등급·매칭 결과)} Q3{출력에 자연어
인사이트·멘트 필요?} Q4{입력이 자유 텍스트
또는 사진?} Q5{시계열 맥락
또는 정성 분석 필요?} A[🟢 A · Pure Rule
코드로만] B[🟡 B · Rule + LLM Narration
룰 계산 → Haiku 자연어] C[🟣 C · LLM-Primary
Haiku/Sonnet 코어] START --> Q1 Q1 -->|"YES"| Q2 Q1 -->|"NO (자유 텍스트·사진)"| Q4 Q2 -->|"YES"| Q3 Q2 -->|"NO"| C Q3 -->|"NO (숫자만)"| A Q3 -->|"YES"| B Q4 -->|"단순 정규화 필요"| C Q4 -->|"시계열 분석·체이닝"| Q5 Q5 -->|"YES"| C classDef start fill:#FFE8D0,stroke:#C45A00,color:#1a2b4a classDef qst fill:#FFFBF5,stroke:#FF8A47,color:#1a2b4a classDef cata fill:#E8F5E9,stroke:#16A085,color:#1a2b4a,font-weight:bold classDef catb fill:#FFF8E1,stroke:#F9A825,color:#1a2b4a,font-weight:bold classDef catc fill:#F3E5F5,stroke:#9C27B0,color:#1a2b4a,font-weight:bold class START start class Q1,Q2,Q3,Q4,Q5 qst class A cata class B catb class C catc
판단 예시 (3가지)
| 요구 | 판단 | 분류 |
|---|---|---|
| "이번 주 다양성 8/8 채우면 +12점" | Q1=YES → Q2=YES → Q3=NO (숫자만) | 🟢 A |
| "콩·유제품 비어있어요. 두부 한 컵..." 안내 문장 | Q1=YES (룰 결과) → Q2=YES → Q3=YES | 🟡 B |
| "오늘 시금치 한 입 먹고 거부, 평소보다 덜 화냄" 노트 분석 | Q1=NO → Q4=YES → Q5=YES | 🟣 C |
4. 41개 핵심 기능 매트릭스
| # | 기능 | 분류 | LLM 모델 | 이유 |
|---|---|---|---|---|
| 1 | 식재료 도감 정적 조회 (147→650) | A | — | DB 조회 |
| 2 | 레시피 카드 표시·검색 | A | — | DB 매칭·필터 |
| 3 | 도감 식재료 친해지기 팁 (자녀 맥락) | B | Haiku 4.5 | 자녀 거부도 + 자연어 |
| 4 | SOS 6단계 표시 (정적 가이드) | A | — | 정적 콘텐츠 |
| 5 | 제철 식재료 매핑 (12개월) | A | — | 월별 룩업 |
| 6 | 자유 텍스트 → MealLog 정규화 | C | Haiku → Sonnet | 자유 입력 해석 |
| 7 | 식재료 fuzzy 매칭 (147 풀) | A | — | Levenshtein 룰 |
| 8 | 미매칭 식재료 → ai_estimated 추론 | C | Haiku | 새 식재료 분류 |
| 9 | 사진 → 식사 분석 (Vision) | C | Claude Vision | 이미지 이해 |
| 10 | meal_chip·시간·자율성 룰 검증 | A | — | schema validation |
| 10b | ★ served vs consumed 자유 텍스트 분리 ("소고기 골라냄") | C | Haiku | 거부 표현 인식 + state 분류 |
| 10c | ★ 식재료 chip [✓ 먹음] / [✗ 남김] 토글 UI | A | — | UI 상태 토글 (룰) |
| 11 | WHO MDD 다양성 카운트 (consumed/partial만) | A | — | set 카운트 + state 필터 |
| 12 | 14 sub-카테고리 깊이 카운트 (consumed/partial만) | A | — | set 카운트 + state 필터 |
| 13 | KDRI 36 영양소 빈도 추정 (partial=50% 환산) | A | — | state 가중 frequency |
| 13b | ★ 시도 vs 섭취 진전도 (Toomey SOS 단계 자동 산출) | A | — | successCount / attemptCount + 임계치 |
| 14 | 영양 신호등 색상 결정 (green/orange/red) | A | — | 임계치 룰 |
| 15 | BMI·백분위 계산 (성장 곡선) | A | — | WHO 차트 룩업 |
| 16 | 8축 점수·총점·등급 산출 | A | — | 가중 평균 + 매핑 |
| 17 | 메뉴 반복도 (4주 동일 카운트) | A | — | group by + 카운트 |
| 18 | NOVA 가공식품 비율 | A | — | NOVA 1-4 카운트 |
| 19 | 알레르겐 표시 검증 | A | — | text grep |
| 20 | 각 축 진단 멘트 ("죽 65% — 핑거푸드 부족") | B | Haiku 4.5 | 점수 → 자연어 설명 |
| 21 | 식단표 5문장 자연어 가이드 | B | Haiku 4.5 | 여러 축 통합 자연어 |
| 22 | 결핍 영양 보충 제안 멘트 | B | Haiku 4.5 | 자녀 베이스 + 자연어 |
| 23 | 주간 리포트 자연어 | B | Haiku 4.5 | 7일 통계 → 한 문단 |
| 24 | 이번 주 추천 식재료 Top 5 (룰 필터) | A | — | 4 원칙 + 빈도 룰 |
| 25 | 레시피 후보 1차 필터 (Phase 0-1) | A | — | 알레르겐·식감·식재료 룰 |
| 26 | 레시피 LLM 정제 (Phase 2, "이유" 생성) | B | Sonnet 4.7 | 자녀 맥락 + 자연어 이유 |
| 27 | Food Chaining 기회 추출 | C | Sonnet 4.7 | 정성 + 정량 결합 추론 |
| 28 | 이미지 전처리·해시 (sharp) | A | — | 이미지 처리 룰 |
| 29 | OCR 식단표 → 텍스트 | C | Claude Vision | 이미지 → 텍스트 |
| 30 | OCR 메뉴 → recipe_id fallback | C | Haiku | fuzzy 실패 시 LLM |
| 31 | 정성 raw_note 시계열 분석 | C | Sonnet 4.7 | 맥락·뉘앙스·진전 추론 |
| 32 | 다음 입력 시 역질문 생성 | C | Haiku 4.5 | thread 기반 동적 질문 |
| 33 | 주간 편지 답장 | C | Sonnet 4.7 | raw_text 인용 + 자연어 |
| 34 | 동적 질문 회전 (7 차원) | B | Haiku 4.5 | 차원 룰 + 자연어 표현 |
| 35 | 감정 추정 (parent_emotion) | C | Sonnet 4.7 | 자유 텍스트 감정 추론 |
| 36 | 마일리지 적립·반감기·일일 한도 | A | — | Stage 룰 + 검증 |
| 37 | 90일 챌린지 완주 판정 | A | — | 일수 카운트 |
| 38 | 친구 초대·룰렛 (IP·fingerprint·확률) | A | — | 검증 + 분포 룰 |
| 39 | 카톡 알림톡 메시지 personalization | B | Haiku 4.5 | 템플릿 + 자연어 채움 |
| 40 | 매일 +50종 도감 enrich (분류·메타) | C | Haiku 4.5 | 새 식재료 분류·메타 생성 |
| 41 | 도감 댓글 모더레이션 (광고·욕설 필터) | C | Haiku 4.5 | 자연어 분류 |
🟢 A (Pure Rule) 24개 · 🟡 B (Rule + LLM) 11개 · 🟣 C (LLM-Primary) 9개 · 합 44개
5. 🟢 Pure Rule (22개) — 결정론·재현·빠름
왜 룰만으로 충분한가
- 입출력 모두 정형: 식재료 ID·점수·등급·일수 등 숫자/enum
- 학술 정합성: KDRI·WHO·NOVA는 명확한 룩업 테이블 + 카운트 룰
- 재현 가능: 같은 입력 → 같은 출력 (RCT 검증 가능)
- 빠름·무료: p95 50ms · 비용 ₩0
- 오프라인 작동: LLM 장애에도 핵심 기능 유지
대표 알고리즘
// WHO MDD 다양성 카운트 (식품군 1-2단계)
function axisDiversity(logs) {
const groups = new Set();
logs.forEach(log => log.ingredients.forEach(i => groups.add(i.foodGroup)));
const mddSatisfied = groups.size >= 5; // WHO 기준 8 중 5 이상
// 14 sub 깊이 (1번 축 2단계)
const subCats = new Set();
logs.forEach(log => log.ingredients.forEach(i => subCats.add(i.subCategory)));
const subDepth = subCats.size;
return {
pct: !mddSatisfied ? 60 : subDepth >= 10 ? 95 : subDepth >= 6 ? 90 : 85,
grade: !mddSatisfied ? 'C' : subDepth >= 10 ? 'A+' : subDepth >= 6 ? 'A' : 'B+',
raw: { mddGroups: groups.size, subDepth }
};
}
이 카테고리에 추가 안 되는 것
- ❌ "다양성이 부족해요" 같은 자연어 — 카테고리 B로 (Haiku)
- ❌ 자녀별 맥락 추론 — 카테고리 C로 (Sonnet)
- ❌ 부모 노트 해석 — 카테고리 C로 (Sonnet)
6. 🟡 Rule + LLM Narration (11개) — 점수는 룰, 멘트는 LLM
패턴 — 두 단계 호출
async function axisNarrative(axisResult, childContext) {
// 1단계: 룰로 계산 (Cat A) — 이미 끝남
// axisResult = { axisId:'texture', pct:55, grade:'C',
// raw:{pureeChunkPct:65, fingerCount:2, ageMonths:14} }
// 2단계: 캐시 확인 (같은 raw + ageBand → 자연어 재사용)
const cacheKey = sha256(JSON.stringify(axisResult.raw) + childContext.ageBand);
const cached = await redis.get(`narr:${cacheKey}`);
if (cached) return cached;
// 3단계: Haiku로 자연어 생성 (5-7일 캐시)
const narrative = await haiku.generate(NARRATIVE_PROMPT, {
axis: axisResult,
childAge: childContext.ageMonths,
childName: childContext.nickname,
tonePreference: childContext.parentTone || 'friendly'
});
await redis.setex(`narr:${cacheKey}`, 604800, narrative);
return narrative; // "지우 어머니, 죽이 65%네요. 14개월이면..."
}
왜 LLM이 필요한가
- 매번 다른 표현 — 같은 데이터도 부모마다·날마다 다르게
- 자녀 맥락 반영 — 닉네임·연령·잘먹는 베이스 등 결합
- 학술 용어 → 자연어 번역 — "RNI 65%" → "주 3-4회 만남"
- 톤 일관성 — design-spec §10 (권유·긍정) 자동 적용
비용 통제 — 캐시가 핵심
- (raw 데이터 + ageBand) hash 키로 7일 캐시
- 비슷한 식습관의 다른 부모는 캐시 재사용 (수치 동일하면 멘트 동일 OK)
- 예상 cache hit rate: 60%+
- 실 호출 평균: 사용자당 일 1-2회 × ₩4 = 월 ₩120 / 사용자
이 카테고리 11개 기능 한 줄 요약
| 기능 | 룰 출력 (정형) | LLM 출력 (자연어) |
|---|---|---|
| 각 축 진단 멘트 | {axis, pct, raw} | "죽이 65%네요. 14개월이면 핑거푸드도 시도해볼 때예요." |
| 5문장 식단표 가이드 | 8축 점수 + 결핍 영양 목록 | "이 식단은 한국 평균 +14점, 상위 25%... 한식 위주라..." |
| 결핍 영양 보충 제안 | {nutrient: 'choline', missing: 0.6} | "콜린이 부족해요. 노른자죽이나 두부찜 한 끼면 OK." |
| 도감 식재료 친해지기 팁 | {ingredient, childRejection: 0.8} | "시금치 거부 강하네요. 계란찜에 살짝 섞어보세요." |
| 주간 리포트 자연어 | 7일 통계 dict | "이번 주 신호등 25→28 (+3). 잡곡 늘린 게 효과예요." |
| 카톡 알림톡 personalization | {template, vars} | "지우 어머니, 90일 챌린지 7일째예요!" (템플릿 변수) |
| 레시피 LLM "이유" (Phase 2) | {recipe, childMatch: 0.85} | "카레라이스는 지우가 잘 먹는 베이스 + 시금치 도전 매치예요." |
| 동적 질문 회전 표현 | {dimension: 'cebq', day: 'mon'} | "오늘 거부 반응 어땠어요? (한 입도 X / 골라냄 / ...)" |
7. 🟣 LLM-Primary (8개) — LLM 코어, 룰은 검증·가드
왜 LLM이 코어인가
- 입력이 자유 텍스트·이미지: 정형 데이터로 환원 불가능 (의도·뉘앙스 손실)
- 시계열 맥락 추론: 3주차 raw_note들을 연결해 진전 곡선 추출 — 룰로 불가능
- 창의적 생성: 편지·동적 질문은 매번 새로운 문장 필요
- cross-domain 매칭: Food Chaining 같이 식재료+거부도+선호도 결합
패턴 — LLM 출력은 항상 검증된 schema로
async function llmPrimary(input) {
// 1. 입력 sanitize (prompt injection 가드)
const safe = sanitizeUserInput(input);
if (detectInjection(safe)) return { error: 'rejected' };
// 2. LLM 호출 (Sonnet — 복잡 추론용)
const raw = await sonnet.generate(SYSTEM_PROMPT, safe);
// 3. zod schema 검증 (LLM 출력은 항상 정형으로 환원)
const parsed = OutputSchema.safeParse(extractJSON(raw));
if (!parsed.success) return { error: 'schema_fail', retry: true };
// 4. 룰로 추가 검증 (학술 정합성 가드)
if (parsed.data.sosStage > 6) return { error: 'invalid_sos' };
if (parsed.data.confidence < 0.3) return { ...parsed.data, lowConfidence: true };
return parsed.data;
}
이 카테고리 8개 기능 한 줄 요약
| 기능 | 입력 | LLM | 출력 |
|---|---|---|---|
| 자유 텍스트 → MealLog | rawText + 시간 | Haiku → Sonnet | MealLog 객체 |
| 식단표 OCR | 이미지 | Claude Vision | 메뉴 텍스트 + recipe_id 매핑 |
| OCR 메뉴 fallback | fuzzy 실패 텍스트 | Haiku | recipe_id 추정 |
| 정성 노트 시계열 분석 | raw_note thread | Sonnet | {진전 곡선, SOS 단계, 다음 질문} |
| 다음 입력 시 역질문 | 최근 5 notes | Haiku | "지난번 X 잘 먹었던데..." 문장 |
| 주간 편지 답장 | 1주 thread 요약 | Sonnet | raw_text 인용한 편지 본문 |
| parent_emotion 추정 | raw_note 시계열 | Sonnet | joy/worry/frustration |
| Food Chaining 기회 | 좋아하는 + 거부 식재료 | Sonnet | 우회 식재료·레시피 제안 |
| 매일 +50종 enrich | 식재료명 | Haiku | 카테고리·KDRI·SOS·이모지 메타 |
| 댓글 모더레이션 | 댓글 텍스트 | Haiku | safe/review/delete |
8. 비용 분석 (MAU 1만 기준)
| 카테고리 | 기능 수 | 호출 빈도 | 모델 | 건당 | 월 비용 |
|---|---|---|---|---|---|
| 🟢 A · Pure Rule | 22개 | 무제한 | — | ₩0 | ₩0 |
| 🟡 B · Rule + LLM 각 축 멘트·5문장 가이드·주간 리포트 등 | 11개 | 사용자당 일 2-3회 (캐시 hit 60%) | Haiku 4.5 | ₩4 | ₩400k |
| 🟣 C · LLM-Primary 전처리·OCR·정성 분석·편지 | 8개 | 일 1-2 Haiku + 주 1 Sonnet | Haiku·Sonnet | ₩4-50 | ₩800k |
| 일회성·매일 cron enrich 50종·댓글 모더레이션 | — | 일 1회 cron | Haiku | ₩200/일 | ₩6k |
| 카톡 알림톡 SENS | — | 월 8건/사용자 | — | ₩8/건 | ₩640k |
| 합계 (MAU 1만) | ~₩1.85M / 월 | ||||
| 참고 — 매출 (월 ₩35M 보수 / ₩140M 낙관) | ROI 19~76배 | ||||
왜 룰 분리가 핵심인가 (비용은 부수적)
사실 도감·레시피 같은 정적 콘텐츠는 LLM으로 만들어도 한 번 생성 후 영구 캐시되므로 추가 비용 ≈ 0. 비용보다 진짜 이유는 따로 있다.
| 이유 | 룰 (Cat A) | LLM 사용 시 | 왜 중요 |
|---|---|---|---|
| 재현성 | 같은 입력 → 항상 같은 출력 | 같은 입력에도 매번 다른 표현 | RCT 학술 데이터 검증 가능성 |
| 응답 속도 | p95 50ms | p95 800ms~2.5s | 홈 진입·신호등은 즉시 떠야 |
| 오프라인 | 지하철·비행기 OK | 네트워크 필수 | 식사 기록은 어디서나 |
| 장애 내성 | Anthropic 장애에도 작동 | API 다운 시 기능 중단 | 핵심 22개 기능 항상 유지 |
| 감사·디버그 | 코드 추적 명확 | black box | 잘못된 점수 원인 파악 |
| 비용 | ₩0 | 캐시 가능 시 ≈ ₩0 | (비용 차이는 크지 않음 — 부수적) |
→ 룰 분리의 진짜 이유는 재현성·속도·오프라인·장애 내성. 비용 차이는 미미하지만, 위 4가지는 학술·UX·신뢰성에 결정적.
9. 가드레일 — LLM이 학술 정합성 깨지 않도록
⚠️ Category B·C 위험 — LLM이 룰 결과를 왜곡하면?
예: 룰이 "비타민D 결핍 위험"이라고 했는데 LLM이 "괜찮아요"라고 자연어 생성. 학술 신뢰성·법적 책임 문제.
가드레일 5종
- ① 출력 schema 강제 — LLM 응답은 zod schema 통과 필수. 자유 텍스트만 허용되는 필드(narrative)와 정형 필드(score·grade·status) 분리
- ② 핵심 사실 룰 검증 — narrative 내 인용한 숫자는 룰 출력과 정확 일치 확인 (regex). 불일치 시 폐기 + Sonnet 재호출
- ③ 톤 가드 — design-spec §10 금지 표현(엄마가/정상/조속히 등) 자동 필터. 적발 시 재생성
- ④ 의료 자문 가드 — "진단·치료·약" 표현 차단. 항상 disclaimer 첨부
- ⑤ Confidence threshold — LLM confidence < 0.7 시 narrative 생략하고 룰 결과만 표시 (안전 fallback)
실패 시 fallback (모든 Cat B·C)
- LLM 호출 실패·timeout → 룰 결과 + 표준 템플릿 멘트 ("{축} {등급} 등급이에요")
- 일일 LLM 비용 초과 ($50/day) → Haiku만 유지, Sonnet 차단 → 자연어 품질 약간 ↓ but 작동 유지
- schema 검증 3회 실패 → Cat A 결과만 표시, 사용자에게 "잠시 후 다시 시도해주세요" 안내
- Anthropic API 장애 → Cat A 22개 기능 100% 정상 작동 (도감·신호등·점수·키트)
모니터링 지표
- 일일 LLM 호출 수 (Haiku/Sonnet 분리)
- cache hit rate (목표 60%+)
- schema 검증 실패율 (≤ 3%)
- 학술 가드 차단율 (≤ 1%)
- 일일 비용 (목표 $30/day → MAU 1만 시)