B-0 도메인 라우팅(자동 분류) — 평가 하네스 배선
3.2절 도메인 라우팅을 자동 분류로 확정(수동 선택 안 함) — 근거·설계:
_project/decisions/007-도메인-라우팅-자동분류-확정.md.
w2-domain-routing와 범위가 다르다 — 그 티켓은 A안(수동) vs B안(자동) 결정 자체(완료)이고, 이 티켓은 그 결정을 실제로 구현하는 작업 (하네스 배선 완료, 분류기 구현은 미착수)이다. 슬러그가 비슷해 보이지만 중복이 아니다.
끝낸 것: 평가 하네스에 B-0 배선 완료. services/core/eval/metrics/domain_routing.py
(정확도 + 오분류 행렬, 규칙 기반), harness.py에 DomainPredictor Protocol 추가(아직
None — “측정 불가 — 모듈 미구현”으로 정직하게 보고), 골든셋 domain 필드를 정답
라벨로 재사용. 테스트(test_domain_routing_metrics.py, test_harness.py 배선 검증)
포함 pytest services/core 33개 통과. 6.1절에 목표(정확도 ≥0.95) 반영.
남은 것: 실제 KcELECTRA 계열 분류기 구현·학습은 미착수 — 골든셋 7건(F-2 케이스 제외)만으로는 분류기를 학습시킬 수 없다. 2주차 이후 골든셋이 50개로 늘어나면 착수. 신뢰도 낮을 때 폴백(4개 인덱스 전체 검색)은 B-2 하이브리드 검색이 먼저 있어야 구현 가능 — 아직 그 모듈도 없음.
w2-domain-routing 과의 관계
이름이 겹쳐 보드에 두 장으로 보이지만 다른 일이다. 세션 종료 검사(③ 중복 티켓)가 슬러그로 경고하는 것이라 합치지 않는다.
| 티켓 | 무엇 | 상태 |
|---|---|---|
w2-domain-routing |
방식 결정 — 수동 선택(A) vs 자동 분류(B) | done (B안 확정, decisions/007) |
| 이 티켓 | 구현 — 하네스 배선 + 분류기 학습 | in-progress (배선만 끝남) |
2026-08-26 갱신: 전제였던 골든셋 50건은 확보됐다(golden-set/v1-50.json, 도메인
라벨 있는 채점 표본 34건 — F-2 16건은 발화 텍스트가 없어 제외). 남은 전제는 B-2 검색이며,
그건 ES 인덱스 분할 결정이 나야 시작할 수 있다.
2026-08-27 — v1 구현·측정 (류준). 아직 in-progress 인 이유
분류기를 만들지 못했다. 학습 데이터가 없다. 골든셋은 평가 세트다 — 그걸로 학습시키고 그걸로 채점하면 정확도 1.0 이 나오는데 아무것도 측정하지 않은 숫자다.
대신 decisions/007 설계의 ② 폴백(4개 도메인 전체 검색)을 1차 경로로 먼저 만들었다.
새 도구를 들이지 않고(3.1절 투입자원 원칙) 학습 데이터 없이 지금 잴 수 있다.
ai/apps/retrieval/adapter/outbound/search_domain_router.py — 발화로 전 도메인을 검색해
상위 문서의 도메인에 1/rank 로 표를 준다. 신뢰도는 1등과 2등의 표 차이다.
점수가 아니라 순위를 쓰는 이유는 RRF 와 같다 — BM25 점수는 질의마다 스케일이 달라
임계값을 세울 수 없다.
실측 정확도 0.647 (22/34, 목표 ≥0.95). 오분류가 전부 finance 로 쏠린다 —
문서 수 불균형(34/27/21/20)이 그대로 편향이 됐다. 자세한 혼동 행렬은
w2-baseline 참고.
done 이 아닌 이유: 목표(≥0.95)에 못 미치고, 설계의 ①(KcELECTRA 분류기)이 아직 없다.
이 v1 은 그 분류기가 넘어야 할 기준선이다. 분류기 학습에는 골든셋이 아닌 별도
학습 데이터가 필요하다 — AI Hub 민원 데이터셋에 도메인 라벨이 있으므로 그쪽을 봐야 한다.
2026-08-27 (이어서) — AI Hub 데이터로 분류기를 학습했다. 그런데 v1 보다 낫지 않다
data/raw/aihub-minwon-qa 에 도메인 라벨이 붙은 실제 콜센터 전사가 있다. 골든셋을 쓰지
않고 학습할 수 있는 데이터다.
| 원본 | 레코드 221,155건 (고객 111,559 / 상담사 109,596) |
| 고객 질문만·4자 이상·중복 제거 | 36,146건 (finance 8,798 · dasan 5,822 · shopping 9,522 · health 12,004) |
| 학습 / 검증 | 32,533 / 3,613 (도메인 비율 유지 분할, seed 42) |
| 베이스 | models/kcelectra-base (decisions/010) |
.venv/bin/python scripts/train_domain_classifier.py # 2 epoch, 약 20분 (M5 MPS)
AI Hub 검증 정확도 0.8148 (epoch 1 0.7789 → epoch 2 0.8148). 도메인별 dasan 0.572 · finance 0.793 · health 0.850 · shopping 0.939.
⚠ 골든셋에서는 v1 보다 낮다
| 채점 대상 | 검색 기반 v1 | 분류기 |
|---|---|---|
| B(검색) 14건 | 0.857 | 0.786 |
| 전체 34건 | 0.647 | 0.588 |
학습이 안 된 게 아니라 분포가 다르다. AI Hub 는 실제 콜센터 전사이고(“카드를 잃어버렸어요”), 골든셋 발화는 우리가 시나리오로 쓴 문장이다(“카드를 잃어버렸는데 신고하기 전에 이미 누가 써버린 돈은 저는 못 돌려받는 거예요?”). 길이도 어투도 다르다.
분류기를 기본으로 켜지 않았다. --domain-router auto 는 모델이 있으면 쓰지만, 지금 수치로는
v1 이 더 낫다. 어느 쪽으로 쟀는지는 실행할 때 출력에 찍힌다.
채점 방법 자체가 틀려 있었다 (같이 고침)
처음엔 도메인 라벨이 있는 34건 전체로 쟀다. 그런데 그중 20건이 C·C-5 항목이고 그 발화에는 도메인 단서가 없다 — “본인 확인을 위해서 주민등록번호를 불러주시겠어요?” 가 어느 도메인인지 텍스트만 보고는 알 수 없다. 실제로 그 20건에서는 두 방식 다 0.45~0.50, 사실상 찍기였다.
B-0 은 decisions/007 대로 통화 초반 고객 발화로 판정하는 것이므로 harness.py 의
채점 대상을 B 항목으로 좁혔다. 측정할 수 없는 것을 섞어 재고 있었다(절대 원칙 10).
⚠ n=14 로는 “≥0.95” 를 판정할 수 없다
14건에서 0.95 를 넘으려면 14/14 여야 한다(13/14 = 0.929). 만점 말고는 통과가 없고, 두 방식의 차이(12/14 vs 11/14)도 한 건이라 의미를 두기 어렵다. 3주차 골든셋 150건 확장 때 도메인 판정용 표본을 따로 늘려야 이 목표를 잴 수 있다.
다음에 해볼 것 (아직 안 함)
- 골든셋 문체로 도메인 적응 — AI Hub 로 사전학습한 뒤 골든셋 문체에 맞춰 추가 학습. 다만 골든셋을 학습에 쓰면 평가가 무의미해지므로, 평가용과 분리된 시나리오 문장이 필요하다
- epoch 을 더 늘리기 — 2 epoch 에서 여전히 오르는 중이었다(0.779 → 0.815)
- dasan 이 두 방식 다 가장 약하다(AI Hub 0.572). 표본이 제일 적다(5,822건)
done 이 아닌 이유: 목표 미달이고, 그보다 목표를 판정할 표본이 없다.
2026-08-27 (이어서 2) — epoch 4 + 초반턴 증강. 골든셋은 한 자리도 안 움직였다
앞선 결과(2 epoch, 증강 없음)에서 “epoch 을 더 늘려볼 것 / 골든셋 문체에 적응시킬 것”을 남겼다. 둘 다 해봤다.
골든셋으로 학습하지 않았다 — 평가 세트라 학습에 쓰면 그 라벨로 다시 잴 수 없다.
대신 실제 데이터를 골든셋 문체에 가깝게 만들었다: AI Hub 고객 발화는 짧은 턴으로
쪼개져 있고("카드를 잃어버렸어요") 골든셋은 완결된 한 문장이라, 한 대화의 앞쪽 고객
턴을 이어 붙였다(load_opening_samples). 지어낸 문장이 아니라 실제 전사를 잇는 것이고,
B-0 의 실제 입력(통화 초반 발화)과도 더 맞는다. +13,204건.
| 방식 | AI Hub 검증 | 골든셋 (n=14) |
|---|---|---|
| 검색 기반 v1 | — | 0.857 (12/14) |
| 분류기 2 epoch · 증강 없음 | 0.815 | 0.786 (11/14) |
| 분류기 4 epoch · 증강 포함 | 0.879 | 0.786 (11/14) |
AI Hub 는 +6.4%p 올랐는데 골든셋은 혼동 행렬까지 동일하다. 틀린 3건이 완전히 같다.
틀린 3건 — 확신을 갖고 틀린다
GS-016 dasan → finance (0.771) 「제가 직접 못 가서 그런데 가족이 대신 등본을 발급받을 수 있나요」
GS-018 shopping → finance (0.965) 「상자를 열어보니까 물건이 배송 중에 깨져서 왔던데 어떻게 처리되나요」
GS-021 health → finance (0.819) 「이 정도 진료를 받으면 대략 비용이 얼마나 나오는지 미리 알 수 있나요」
전부 finance 로 가고 신뢰도가 0.77~0.97 이다. 헷갈리는 게 아니라 확신하며 틀린다.
발급·처리·비용 이 AI Hub 금융 데이터에 압도적으로 많은 탓으로 보이고,
데이터를 더 넣고 더 오래 학습할수록 이 편향은 오히려 강해진다. 실제로 그렇게 됐다.
두 방식이 서로 다른 데서 틀린다
v1 은 위 3건을 다 맞히고 대신 GS-002(저상버스 도착 정보)·GS-019(사이즈 교환)를 틀린다.
지식베이스에 등본 발급·배송 파손·진료비 조항이 실제로 있어 문서를 직접 찾아가는
방식이 문체 차이에 둔감하다.
겹치지 않는다 → 합치면 둘 다 넘길 여지가 있다. RRF 병합 함수는 이미 있다(decisions/021).
dasan 은 개선됐다 (그래도 최하위)
AI Hub 검증 도메인별: dasan 0.572 → 0.746, finance 0.871, health 0.887, shopping 0.953. 초반턴 증강이 dasan 에 +1,778건 들어간 효과로 보인다. 여전히 최하위다.
dasan 보강 — 데이터로 확인한 원인 세 가지
- 표본이 절반뿐이다 — dasan 7,600건 = health(16,237)의 47%
- 어휘 겹침이 모든 쌍 중 가장 크다 — 상위 400단어 기준 dasan↔finance 0.47
(health 0.39 · shopping 0.29).
수도요금·납부·지원금·요금이 금융 어휘와 정면으로 겹친다 - 고유어는 있는데 드물다 —
시내버스·지하철·대중교통·서울시는 뚜렷하지만, dasan 상위 60단어 중 남과 공유하는 것은 전부 기능어(있나요·어떻게·되나요·제가…)다. 신호 단어가 드물게 나타나므로 표본이 적으면 충분히 못 본다 — ①과 ②가 곱해진다
해법 순서
| 방법 | 비용 | 비고 | |
|---|---|---|---|
| 1 | 클래스 가중치 — dasan 손실에 ~2배 | 몇 줄 | 오버샘플링보다 낫다(복제하면 그 문장에 과적합). 표본 부족을 가리는 것이지 없애는 게 아니다 |
| 2 | 다산콜DB 6,614건 전사 ⭐ | 정성윤 님 w2-stt-batch 의존 |
data/raw/seoul-minwon-audio/민원상담_음성/다산콜DB/ 에 wav 만 있고 전사가 없다. 전사하면 dasan 이 7,600 → 14,000+ 로 불균형이 사라진다. 덤: 전사본은 STT 오류 특성을 그대로 가져 지금의 “깨끗한 텍스트 vs 실제 입력” 간극까지 메운다 — 5주차 오류 내성 실험과 이어진다 |
| 3 | 요금/납부 문맥 분리 |
— | 1·2 뒤에도 남으면. 지금 규칙으로 손대면 실측 없이 튜닝하는 것 |
⚠ 방법론 결함 하나 (기록해 둔다)
이번 학습은 epoch 2→4 와 초반턴 증강을 한꺼번에 바꿨다. 그래서 골든셋이 그대로인 것은
확실하지만 AI Hub 상승분이 어느 쪽 공인지는 말할 수 없다. --no-opening-turns 플래그를
넣어 뒀으니 분리 측정은 가능하다. 애초에 하나씩 갔어야 맞다.
기본값은 v1 이다
run_eval.py --domain-router auto(기본)는 분류기가 있어도 v1 을 쓴다. 더 나쁜 쪽이
조용히 끼어들면 안 된다. 분류기를 보려면 --domain-router model 로 명시한다.
done 이 아닌 이유: 목표(0.95) 미달이고, n=14 로는 그 목표를 판정할 수도 없다.
⚠ 2026-08-28 — 완료가 아니라 취소로 닫는다 (류준)
도메인이 다산 하나가 되면서(_project/decisions/201) 분류할 도메인이 없어졌다.
칸반 상태 어휘가 todo|in-progress|done 셋뿐이라 done 으로 옮겼지만,
이 티켓의 완료 조건은 달성되지 않았다 — 아래를 그대로 남긴다(절대 원칙 8).
| 달성한 것 | 검색 기반 v1 라우터 · KcELECTRA 분류기 학습 · 하네스 배선 · 실측 비교 |
| 미달 | 목표였던 분류 정확도 ≥0.95 를 넘지 못했다. 최종 실측 0.857(B 케이스 14건) |
| 지운 것 | ai/apps/training/ 전체 · search_domain_router.py · scripts/train_domain_classifier.py |
| 남긴 것 | hub 의 DomainRoutingPort·DomainClassificationDTO (계약, 구현체 없음 — 장민석 소관) |
이 실험에서 남는 사실: AI Hub 전사 데이터로 학습한 분류기(0.879)가 골든셋 문체에 전혀 전이되지 않았다 — 혼동 행렬이 검색 기반 v1 과 동일했다. 분포 차이가 그만큼 컸다. A-3·C-6·D 에서 모델을 학습할 때 같은 함정을 밟지 않도록 이 기록을 남긴다.