VOC를 KPI 우선순위로
환산하는 가중치 시스템
공개 리뷰를 KPI 노드에 매핑하고 플랫폼별 가중치로 우선순위 계산
도서·오디오북 구독 3사(밀리의서재·리디셀렉트·Audible)의 공개 VOC 1,079건을 카테고리, KPI 노드, Pillar, NSM까지 하나의 트리로 연결하는 4-Layer LLM 파이프라인입니다. 핵심은 분류가 아니라 그 위에 얹은 Eval · Observability · Guardrails 운영 레이어입니다.
골든셋 143건 · 합격선 75%
목표 <0.1 달성
자동 분류율
가중치 매트릭스
01 · PROBLEM빈도 집계만으로 드러나지 않는 플랫폼별 차이
구독 서비스에는 매주 수백~수천 건의 리뷰·CS가 쏟아집니다. 문제는 이게 보통 '불만 빈도순 리스트'로만 쌓인다는 것. 빈도만으로는 "지금 우리 핵심 KPI가 어떤 불만으로 위협받고 있는지", "그 불만을 어떻게 풀어야 KPI 성과로 이어지는지"를 알 수 없습니다.
VOC 우선순위를 KPI Tree 노드와 연결해 가중치로 환산하지 않음. 그래서 구독 갱신을 직접 좌우하는 기능에 불만이 쌓이고 있어도, 건수가 적으면 개선 순위에서 뒤로 밀림. 가장 먼저 고쳐야 할 문제가 목록 아래쪽에 묻히는 구조
02 · SYSTEM4-Layer 아키텍처
[Layer 1 · 입력] 빌드용 3사 공개 VOC 오프라인 스크래핑 (App Store / Google Play) 사용자용 엑셀·CSV 업로드 + 라이브 샘플 붙여넣기 → 본실행 1,079건 (밀리 386 · 리디 361 · Audible 332) → 자동 분류 성공 977 + 미분류 102건은 직접 카테고리·KPI 노드를 매핑해 정답지로 확보 → 원천 스크래핑 3,300건, 대표 표본 1,000 [Layer 2 · 분류] 2계층 매핑 (카테고리에서 KPI 노드로) ① 카테고리 Claude Sonnet · 프롬프트 v3.4 (결정 트리 + few-shot 19개) 13종 중 하나로 분류. 무엇 때문에 불만인가 (관측 언어) ② KPI 노드 고정 매핑 테이블 · LLM 아님 카테고리가 정해지면 노드는 자동 결정. 어떤 지표를 움직이는가 (판단 언어) 같은 카테고리는 항상 같은 노드로 감. "왜 이 노드인가" 설명 가능 (상세 03) JSON 7키 고정 스키마 강제 · temperature 0 → primary_category / secondary_category / kpi_node / node_confidence / sentiment / confidence / key_phrase [Layer 3 · 운영] ★ 이 프로젝트의 핵심 Eval 골든셋 143건 주기 채점 (Top-1 / Top-2 분리) Observability Langfuse Cloud (EU): 비용·지연·확신도 trace Guardrails PII 마스킹 / confidence 0.7 라우팅 / 차단 카테고리 / 02 편향 가드 / surface 미식별 가드 [Layer 4 · 출력] KPI 노드 × 3사 히트맵(드릴다운) / 3사 KPI Tree / 우선순위 백로그 XLSX
03 · KEY DESIGN왜 2계층인가: 관측 언어 vs 판단 언어
VOC를 KPI 노드에 바로 매핑하지 않고, 사이에 카테고리 계층을 한 층 더 뒀습니다. 운영팀 라우팅과 KPI Tree 안정성을 함께 확보하기 위한 선택입니다.
| 카테고리 (앞단) | KPI 노드 (뒷단) | |
|---|---|---|
| 언어 | 고객이 무엇 때문에 불만인가 (관측) | 그게 어떤 사업 지표를 움직이는가 (판단) |
| 성격 | VOC 매핑 체계 수정 가능 | VOC와 별개로 수립 임의 수정 불가 |
| 쓰임새 | 운영팀 라우팅 (어느 팀에 보낼까) | PO 전략 우선순위 (어느 지표에 임팩트) |
| 매핑 주체 | LLM (확률적) | 고정 매핑 테이블 (결정적) |
분리로 얻은 것
- LLM 추론 점프 축소: "1.5배속이 깨져요"에서 곧바로
Reading Completion Rate로 가기엔 도약이 너무 큼. 중간에 관측 가능한04_오디오북_기능을 두면 "분류가 틀렸나 / 매핑이 틀렸나"를 따로 측정·디버깅 가능 - KPI Tree 자산 보호: 실제로 v1 카테고리 정확도 60%가 나왔을 때 KPI Tree는 그대로 두고 카테고리만 수정해 정확도를 75.5%까지 끌어올림
- 노드매핑 고정: 앞단 카테고리는 VOC마다 판단이 들어가지만, 뒷단 노드매핑은 카테고리 값에 따라 자동으로 결정되는 고정 테이블. "왜 이 노드로 갔는지" 항상 설명 가능
- 청중 분리(목적 기준): 카테고리는 운영, 노드는 전략. Root cause 룰도 카테고리 층에서 먼저 걸러야 라우팅과 백로그가 둘 다 망가지지 않음
04 · THE DECISION측정, 분류 정확도를 위한 재설계
· "이상해요" "예전만 못해요"처럼 구체적 증상이 없는 추상 부정도 무조건
02_앱성능/버그로 매핑· "캐시가 30일 후 사라져요"(정책 = 10)를 결제(11)로 매핑
① 증상 키워드 게이트: 오류·튕김·꺼짐·먹통·강제종료·로딩 실패·다운로드 실패·동기화 실패·렉·끊김 같은 명시적 증상 키워드가 하나라도 있을 때만 02로 매핑 허용. 추상 부정만 있으면 불허.
② 06/10/11 결정 트리: 헷갈리는 3형제를 순서대로 묻게 해 사고 순서를 강제.
Q1. 이번 1회의 결제·적립·환불 처리 문제인가? → 11 결제/환불
Q2. 구독 구조·해지·포인트 정책 문제인가? → 10 요금제/해지
Q3. 가격 수준·환산율 비교·결제수단 강제 문제인가? → 06 가격/가성비
05 · EVAL측정 결과: 골든셋 143건
조건: 프롬프트 v3.4 · Claude Sonnet · 정답지 144건 중 예측 확보 143건 채점
| 지표 | 결과 | 95% CI | 합격선 | 판정 |
|---|---|---|---|---|
| Top-1 정확도 | 75.5% (108/143) | [67.9, 81.8] | ≥75% | ✓ 달성 |
| Top-2 정확도 | 88.1% (126/143) | [81.8, 92.4] | ≥90% | △ 1.9%p 미달 |
| 감정 정확도 | 90.9% (130/143) | – | 80% | ✓ |
| KPI 노드 정확도 | 71.3% (102/143) | – | 70% | ✓ |
| └ 카테고리 정답 한정 | 90.7% (98/108) | – | – | ★ |
| ECE (확신 보정오차) | 0.090 | – | <0.1 | ✓ 달성 |
| 99(분류 모호) 비율 | 2.8% | – | <15% | ✓ |
KPI 노드 71.3%의 의미
KPI 노드는 카테고리 하위의 더 세밀한 지표라 정확도가 구조적으로 카테고리를 넘을 수 없습니다. KPI 오답 41건 중 31건(76%)은 카테고리도 함께 틀린 케이스: 카테고리 오답이 흘러내린 것입니다. 카테고리가 맞은 경우만 보면 KPI 정확도는 90.7%.
순수 KPI 혼동(카테고리는 맞고 노드만 틀림)은 10건뿐이고, 전부 의미가 인접한 형제 노드 사이에서 발생 (Session_Reliability ↔ Sync_Offline_Stability, Functional_Quality ↔ UX_Quality). 사람이 라벨링해도 갈리는 경계
ECE 확신도 보정: "90% 확신"이 실제로 90% 맞는지 검증
| confidence 구간 | 건수 | 평균 확신도 | 실제 정답률 | 차이 | ◦ 확신도 · ● 정답률 |
|---|---|---|---|---|---|
| 0.7 ~ 0.8 | 37 | 75% | 57% | 18%p | |
| 0.8 ~ 0.9 | 72 | 84% | 81% | 4%p | |
| 0.9 ~ 1.0 | 30 | 92% | 97% | 5%p |
06 · WEIGHTS가중치 매트릭스: 39칸 (3 플랫폼 × 13 노드)
우선순위 = VOC 빈도 × node_weight[node] (전역 스칼라)문제: 3사가 같은 KPI 노드에 같은 우선순위를 가짐. 플랫폼 트리가 아무 일도 하지 않음.
개선: 가중치를 (노드, 플랫폼) 함수로 승격 →
VOC 빈도 × node_weight[node][platform]
| KPI 노드 | Pillar | 밀리 | 리디 | Audible | 해석 |
|---|---|---|---|---|---|
| Catalog_Coverage | Retention | 0.90 | 0.80 | 0.80 | 3사 공통 高 구독 가치의 전제 |
| Audio_Content_Quality | Retention | 0.30 | 0.30 | 0.95 | Audible 단독 낭독 = 제품 |
| UX_Quality (읽기뷰어) | Retention | 0.70 | 0.90 | 0.40 | 리디 뷰어 강점 |
| Subscription_Renewal | Revenue | 0.80 | 0.70 | 0.80 | 3사 공통 高 NSM 직결 |
| Sync_Offline | Content | 0.75 | 0.75 | 0.75 | v3.0 신설 명시적 가설값 |
| Session_Reliability | Engagement | 0.30 | 0.30 | 0.30 | 위생요인 의도적 하향 |
(13노드 중 6개 발췌)
해석 규정 3가지: 이 점수를 오용하지 않기 위해
- 중요도 ≠ 업무 우선순위: 이 점수는 전략적 중요도(Impact) 신호이지 개발 착수 순서가 아님. 실제 순서는 심각도·Effort·팀 상황이 더해져 각 Pillar 오너가 결정
- 글로벌 랭킹 금지, Pillar별 패널 분리: 부서가 다른 노드를 하나의 큐로 줄세우지 않음. UI로 이 정의를 강제
- 플랫폼 간 절대 비교는 가중 점수로 하지 않음: 3사 비교는 원천 VOC 빈도 히트맵으로 (가중치 분포 차이로 인한 왜곡 방지)
07 · FINDING원시 빈도 1위와 가중 우선순위 1위가 달라짐
밀리 77건 · 리디 53건 · Audible 30건.
이 노드는 KPI 트리에서 위생요인으로 두어 가중치를 0.30으로 설정했습니다.
설정한 가중치를 적용하면 플랫폼별 상위 항목이 달라집니다.
| 플랫폼 | 원시 빈도 1위 | 가중 우선순위 1위 | 성격 |
|---|---|---|---|
| 밀리 | Session_Reliability (77건) → 가중 4위 (23.1) | Catalog_Coverage 50건 × 0.90 = 45.0 · 부정 58% | 라인업 결핍 + 동기화 |
| 리디 | Session_Reliability (53건) → 가중 5위 (15.9) | Discovery_Experience 34건 × 0.60 = 20.4 · 부정 65% | 경험(발견) 이슈 콘텐츠 불만은 부정 11%뿐 |
| Audible | Session_Reliability (30건) → 가중 6위 (9.0) | Sync_Offline_Stability 22건 × 0.75 = 16.5 · 부정 68% | 유일하게 Revenue Pillar가 큼 |
건수는 적지만 가중 우선순위가 올라가는 경우
Audible의 Subscription_Renewal은 18건뿐입니다.
건수로 줄세우면 6위 Session_Reliability(30건)에도 밀리나
18건 중 17건(94%)이 부정이고, 가중치가 0.80 → 가중 우선순위 2위(14.4)로 올라옵니다.
이 점수는 검토할 이슈를 정리하는 데 사용하며, 실제 착수 순서는 심각도와 개발 부담을 함께 고려합니다.
Pillar 프로파일: 유사한 도메인인데 고칠 곳은 저마다 다름
| Pillar | 밀리 | 리디 | Audible |
|---|---|---|---|
| Retention | 88.8 | 58.7 | 36.6 |
| Engagement | 39.9 | 36.3 | 16.7 |
| Content (Sync) | 32.2 | 20.2 | 16.5 |
| Revenue | 13.6 | 15.5 | 29.2 |
| Support | 1.2 | 0.3 | 1.2 |
08 · OPS운영 레이어: Eval · Observability · Guardrails
단일 호출 위에 출력의 신뢰성을 측정·관리하는 3개 레이어를 두었습니다.
Eval: 조건이 바뀌면 이전 점수는 무효
모델이나 프롬프트가 바뀌면 이전 조건에서 나온 정확도는 전부 무효 처리하고, 바뀐 조건으로 골든셋을 다시 채점한 값만 합격/미달 판정에 사용. "테스트에서 쓴 조건과 실행에서 쓴 조건이 다르면 그 테스트 승인은 무효"를 게이트 조건으로 명문화
Observability: Langfuse Cloud (EU)
분류 1건이 얼마 비용으로 · 얼마나 걸려서 · 얼마나 확신하고 돌아갔는지 계기판으로 관측합니다.
Guardrails: 본 실행 1,079건 실측
| 구분 | 건수 | 비율 | 목표 | 판정 |
|---|---|---|---|---|
| 자동 분류 | 963 | 89.2% | ≥50% | ✓ |
| 사람 검토 큐 | 116 | 10.8% | – | – |
Silent Failure: 조용히 실패하는 경우
개발 중, 프롬프트의 {review_text} 자리표시자가 치환되지 않은 채 LLM에 전달되는 경우가 발생. LLM은 정직하게 "분류 불가(99), confidence 0.3"을 반환
진단: 운영에서 조용히 반복되면 모든 리뷰가 99로 빠지면서도 개별 confidence는 낮아, 겉으로는 정상 작동처럼 보이는 상태가 됨. 그래서 방어층을 두 겹으로 설계
- 파이프라인 층: LLM 호출 직전, 치환되지 않은 자리표시자가 남아 있으면 그 자리에서 즉시 중단 (구현: 프롬프트에
{...}잔존 여부를 정규식 1줄로 검사, 호출 차단) - 모니터링 층: 99 비율 15% 초과 시 drift 알람
09 · LESSONS시행착오: 분류 기준을 다시 세운 세 지점
① Root cause 우선 원칙
11_결제/환불로 분류.정정: 본문의 핵심은 '렉·오류'고 환불은 그로 인한 부수 요청. →
02_앱성능/버그가 맞음
정립한 룰: VOC 카테고리는 "사용자가 무엇을 요청하는가(intent)"가 아니라 "무엇 때문에 불만인가(root cause)"를 기준으로 잡는다.
- 결제/환불 노드의 VOC 카운트가 잘못 올라가면 → 백로그 우선순위 자체가 왜곡
- 분류 기준을 표면 현상이 아니라 원인으로 잡아야 같은 원인의 VOC가 한 노드에 모임
② 모호한 분기는 '다축'으로 명문화
UX_Quality와 Functional_Quality의 판단 기준이 모호해 라벨링 정확도 하락 → 4축 기준 도입
| 축 | UX_Quality (인간·맥락) | Functional_Quality (시스템·기술) |
|---|---|---|
| 중심축 | 사용자 인지·감정 | 백엔드·하드웨어 수행 가능성 |
| 평가 척도 | 만족도·인지비용 | Pass / Fail |
| 범위 | 흐름(Flow) | 점(Dot)·독립 스펙 |
| 객관성 | 주관적 | 객관적 |
"가독성 안 좋아요", 인간·만족도·흐름·주관 = UX / "음성 자동연결 사라짐", 시스템·Pass/Fail·점·객관 = Functional.
③ 자동화의 한계를 데이터로 측정
골든셋을 키워드 후보 추출로 보강하면서, 보강안이 실제로 얼마나 채택되는지를 함께 측정했습니다.
| 우선순위 | 대상 | 채택률 |
|---|---|---|
| 1번 (미사용 카테고리) | 32건 | 50% |
| 2번 (한자리 보강) | 32건 | 65% |
| 3번 (분기 케이스) | 3건 | 0% ★ |
10 · SHIP배포 방식: 빌드·서버 의존성 제거
- Next.js에서 자체 완결형 정적 HTML 단일 파일로 교체: 빌드·서버·의존성 0. Netlify에 드래그&드롭만으로 배포. 인터넷이 끊겨도 로컬 파일 더블클릭으로 동일 동작. 데모 중 서버·네트워크 문제로 동작하지 않을 가능성을 줄이려는 선택
- 비용 구조 분리: 본 배치 1,079건은 Claude Code CLI(구독)로 처리해 API 비용 0원. 라이브 데모만 서버리스 함수 + API로 분리. 용도별로 비용 구조를 나눈 판단
- Non-Goal 명시: 실트래픽·파인튜닝·풀스택 SaaS는 하지 않기로 문서에 명시
구현 범위: 공개 리뷰 분류, KPI 노드 매핑, 플랫폼별 가중치 계산, 평가·검토 라우팅을 구현했습니다. 카테고리, KPI 노드, Pillar, NSM까지 연결했습니다.
측정 결과: 골든셋 143건의 Top-1 정확도는 75.5%였고, Top-2는 88.1%로 목표 90%에 미달했습니다. 첫 측정 60% 이후 프롬프트가 아니라 카테고리 체계를 교체해 다시 측정했습니다.
다음 개선 과제: 02(버그) 카테고리의 과잉 분류를 줄이는 것입니다. 골든셋 기준 예측 분포가 27.3%로 임계(25%)를 넘습니다.
실트래픽·파인튜닝·풀스택 SaaS는 Non-Goal로 명시해 범위에서 제외했습니다.
폴백 · 규칙기반 배지 명시). 본 분류 1,079건은 Claude Sonnet 오프라인 배치의 실제 결과이고, 라이브 박스는 흐름 시연용입니다. 실호출 경로는 이미 구현·배포돼 있고 키만 넣으면 전환됩니다.