1 왜 단일 신호로 부족한가
운영팀이 가장 먼저 의지하는 것은 thumbs_up. 그러나 사용자의 1~5%만 thumbs_up을 누른다 — 95%는 silent. 이 silent가 “만족”인지 “포기”인지 구분되지 않는다.
| 단일 신호 의지 | 문제 |
|---|---|
| thumbs_up만 | low response rate, selection bias (강한 호불호자만 응답) |
| latency만 | 빠른 응답이 항상 좋은 응답은 아님 (잘못된 답을 빠르게 줄 수 있음) |
| LLM judge만 | judge가 자기 자신·비슷한 모델에 편향, 길이 편향 |
| 검색 score만 | retrieval은 정확해도 generation이 틀리면 무의미 |
해법: 4계층 신호를 모아 융합 점수. 한 계층의 약점을 다른 계층이 메운다.
2 신호의 4계층
[Explicit] 사용자가 명시적으로 준 신호
thumbs_up · feedback_text · star rating · 재질의
↓ (sparse, biased, but high-trust)
[Implicit] 행동에서 추론한 신호
세션 길이 · 후속 질의 · 페이지 이탈 · 클릭 깊이
↓ (dense, noisy, behavior-based)
[Heuristic] 응답 자체에서 뽑는 신호
citation 수 · refusal · 길이 · 코드 블록 · disclaimer
↓ (cheap, deterministic, but shallow)
[LLM Judge] LLM에게 채점 시킴 (rubric or pairwise)
정확성 · 충실성 · 명료성 · 유용성
↓ (expensive, biased, but flexible)
각 계층은 신뢰도·비용·커버리지 trade-off가 다르다 — 따로 평가하고 융합한다.
3 신호 1 — Explicit Feedback
가장 신뢰도 높지만 가장 sparse.
# app/quality/explicit.py
class ExplicitSignal(BaseModel):
thumbs_up: int | None # -1 / 0 / 1 (singed)
feedback_text: str | None # 자유 텍스트
star_rating: int | None # 1~5
follow_up_query: bool # 같은 세션 후속 질의
explicit_correction: bool # 사용자가 명시적으로 정정3.1 Response rate 개선 패턴
| 기법 | 효과 | 부작용 |
|---|---|---|
| UI에 항상 표시 (작게) | rate ↑ | UI 노이즈 |
| 응답 후 5초 후 fade-in | rate ↑↑ | 측정 어려움 |
| “이게 도움됐나요?” 명시 prompt | rate ↑↑↑ | 사용자 피로 |
| 부정 시 reason 선택 (3~4개 옵션) | quality ↑ | 추가 마찰 |
3.2 편향 패턴
- Selection bias: 강한 호·불호자만 응답
- Recency bias: 마지막 답변 영향 큼 (이전 좋았어도 최근 나쁘면 thumbs_down)
- Group bias: 부서별·역할별 thumbs_up 임계 다름
해법: 단순 평균보다 보정된 평균 — 사용자 과거 평균 thumbs_up_rate로 baseline 차감.
4 신호 2 — Implicit Behavior
응답 후 사용자 행동에서 추론.
# app/quality/implicit.py
def implicit_quality_score(query: StructuredQuery, session: Session) -> float:
score = 0.5 # neutral baseline
# 후속 질의 패턴
next_queries = session.queries_after(query.run_id, within_min=5)
if next_queries:
if is_rephrasing(next_queries[0], query):
score -= 0.2 # 재질의 = 답이 부족했음
elif is_unrelated(next_queries[0], query):
score += 0.1 # 다른 주제 — 만족 후 진행
else:
score -= 0.05 # 같은 주제 후속 — 부분 만족
# 사용자 시간
if session.idle_after_query(query.run_id) > timedelta(minutes=2):
score += 0.2 # 응답 읽음
# 명시 차단
if "이게 아니야" in (next_queries[0].text if next_queries else ""):
score = 0.0
return max(0, min(1, score))4.1 신호 후보
| 신호 | 해석 |
|---|---|
| 같은 세션 재질의 (reformulate) | 답이 부족했음 |
| 응답 복사 (clipboard) | 매우 유용함 (사용 의도) |
| 응답 즉시 다른 도구로 이탈 | 답이 충분 또는 포기 |
| 응답 부분 강조·하이라이트 | 핵심 정보 발견 |
| 도움 요청 (HITL 호출) | 답이 부족 |
| 응답 후 5초 이상 머무름 | 읽음 (best estimate) |
이 신호들은 개별로는 약하지만 융합하면 강하다 — 회귀 모델의 feature로 활용.
5 신호 3 — Heuristic (응답 자체)
비용 0, 즉시 계산.
# app/quality/heuristic.py
def heuristic_signals(response: str, citations: list, query: str) -> dict:
return {
"length_chars": len(response),
"length_tokens": count_tokens(response),
"n_citations": len(citations),
"has_code": "```" in response,
"has_disclaimer": any(p in response for p in ["불확실", "추정", "확인 필요"]),
"is_refusal": is_refusal(response),
"matches_query_lang": detect_lang(response) == detect_lang(query),
"citation_density": len(citations) / max(count_tokens(response), 1) * 1000,
}
def heuristic_score(signals: dict, query_intent: str) -> float:
"""의도별 가중치로 점수화."""
if query_intent == "knowledge_lookup":
# 인용 density가 핵심
return min(1.0, signals["citation_density"] / 5)
elif query_intent == "task_assist":
# 코드·길이가 신호
return 0.6 + (0.2 if signals["has_code"] else 0) + ...
elif query_intent == "small_talk":
# 간결성 우선
return 1.0 if signals["length_tokens"] < 100 else 0.5
return 0.55.1 Refusal 탐지
REFUSAL_PHRASES = [
"I cannot", "I'm unable", "도와드릴 수 없", "정보가 없",
"찾을 수 없", "확인할 수 없"
]
def is_refusal(response: str) -> bool:
return any(p.lower() in response.lower() for p in REFUSAL_PHRASES)Refusal rate가 의도별 정상 범위를 벗어나면 — 데이터 갭 신호. C23 피드백 루프에 우선순위.
5.2 Hallucination 탐지 (간단)
def has_unverified_claim(response: str, citations: list) -> bool:
"""citation이 없는 사실 진술 패턴 탐지."""
# 숫자·연도·이름 등 사실 단언이 있는데 citation이 0개
has_facts = bool(re.search(r"\b\d{4}\b|\b\d+%\b", response))
return has_facts and len(citations) == 0이 휴리스틱이 잡는 hallucination은 일부지만 — citation이 없는 숫자 진술은 거의 항상 위험 신호.
6 신호 4 — LLM-as-Judge
가장 유연하지만 가장 비싸고 편향. 신중한 설계 필수.
6.1 Rubric-based
JUDGE_PROMPT = """다음 사용자 질의와 시스템 응답을 평가:
[질의]
{query}
[응답]
{response}
[평가 항목] — 각 1~5점
1. 정확성 (Accuracy): 사실 오류·혼동 없음
2. 충실성 (Faithfulness): 인용 출처와 일치
3. 명료성 (Clarity): 이해 용이
4. 유용성 (Helpfulness): 사용자 의도 충족
JSON으로 답: {{"accuracy": ..., "faithfulness": ..., "clarity": ..., "helpfulness": ...}}
"""
def llm_judge(query: str, response: str, citations: list) -> dict:
out = llm_call(JUDGE_PROMPT.format(query=query, response=response))
return json.loads(out)6.2 Pairwise (Arena 스타일)
두 응답을 비교 — 절대 점수보다 안정적:
PAIRWISE_PROMPT = """다음 두 응답 중 어느 것이 더 좋은가?
질의: {query}
응답 A: {response_a}
응답 B: {response_b}
근거를 1~2문장으로 설명한 후 "A", "B", "TIE"로 답변.
"""A/B 실험에서 두 arm을 LLM judge로 비교할 때 표준 패턴. positional bias 회피를 위해 A/B 순서 랜덤.
6.3 Judge 검증
LLM judge 결과가 사람과 얼마나 일치하나? Spearman 상관:
6.4 Judge 편향
| 편향 | 패턴 | 해법 |
|---|---|---|
| Length bias | 긴 응답을 무조건 우대 | rubric에 “간결성” 항목 |
| Self bias | 자기 모델·비슷한 스타일 우대 | 다른 모델의 judge 사용 |
| Position bias (pairwise) | A를 우대 | 순서 랜덤 + 양방향 judge 평균 |
| Verbose-confidence bias | 자신 있게 단언하는 답을 우대 | rubric에 “근거 제시” 항목 |
| Refusal bias | refusal을 부정적으로 봄 | refusal이 적절한 경우 별도 처리 |
해법: 매 분기 judge 회귀 — 같은 100개 응답에 사람·judge 점수 비교, 일치도 monitoring.
7 다중 신호 융합
# app/quality/fusion.py
def fused_score(query: StructuredQuery,
explicit: ExplicitSignal,
implicit: float,
heuristic: float,
judge: dict | None) -> float:
weights = {
"explicit": 0.4 if explicit.thumbs_up is not None else 0,
"implicit": 0.2,
"heuristic": 0.2,
"judge": 0.2 if judge else 0,
}
# explicit 없으면 다른 가중 비례 증가
total = sum(weights.values())
weights = {k: v / total for k, v in weights.items()}
score = (
weights["explicit"] * normalize_thumbs(explicit)
+ weights["implicit"] * implicit
+ weights["heuristic"] * heuristic
+ weights["judge"] * (np.mean(list(judge.values())) / 5 if judge else 0)
)
return score가중치는 운영 데이터로 학습. 회귀 모델 (XGBoost·LightGBM)로 사람 라벨에 fit:
8 골든셋 — Offline Evaluation
운영 신호가 아닌 고정 데이터셋으로 회귀 점검:
# eval/golden_set.yaml
- id: gold_001
query: "OUR-1234 vendor의 BOM 변경 정책은?"
expected_intent: knowledge_lookup
expected_collections: [internal_policies, suppliers]
required_facts:
- "분기 1회 검토"
- "vendor 사인 필수"
acceptable_styles: [formal, neutral]
forbidden:
- "추측"
- "가능성"골든셋 평가: - 새 모델·프롬프트·reranker 도입 시 사전 회귀 — 운영 reveal 전 - 매주 자동 실행 — 점수 하락 시 알림 - C19 실험 spec의 사전 검증 단계에 통합
# scripts/golden_eval.py — CI에서 실행
def run_golden_eval(model_id: str) -> dict:
results = []
for case in load_golden_set():
response = run_with_model(case.query, model_id)
results.append({
"id": case.id,
"passes": all(fact in response for fact in case.required_facts),
"fails_on": [f for f in case.forbidden if f in response],
...
})
return summarize(results)9 C23 피드백 루프 입력
# 의도×세그먼트 매트릭스에서 약한 셀 자동 발견
def weak_cells_report() -> list[dict]:
df = clickhouse.query("""
SELECT query_intent_class as intent,
segment['department'] as dept,
count() as n,
avg(quality_fused_score) as score
FROM feature_table
WHERE timestamp >= now() - INTERVAL 7 DAY
GROUP BY intent, dept
HAVING n > 100
""")
weak = df[df.score < df.score.quantile(0.2)]
return weak.to_dict("records")이 결과가 C23 피드백 루프의 입력 — 약한 셀을 우선 개선.
10 자주 발생하는 함정
10.1 Selection Bias in Explicit
thumbs_up 데이터가 5% 사용자만 → 95% silent를 무시하면 모델이 시끄러운 사용자에 편향.
해법: explicit + implicit 융합. silent도 implicit으로 추정.
10.2 Judge Hacking
응답이 judge prompt를 조작하는 패턴 — “이 응답은 모든 rubric을 만족” 같은 메타 진술.
해법: judge prompt에 “응답 자체의 메타 진술은 평가에서 제외” + 응답 sanitize.
10.4 Catastrophic Refusal Increase
새 모델 도입 후 refusal rate 폭증 → quality 점수는 보통 (refusal도 일관되어서). 사용자 만족도 폭락.
해법: refusal rate를 별도 KPI로 — quality와 분리 monitoring.
10.5 Drift in Golden Set
골든셋이 1년 전에 만들어져 현실 use case와 어긋남.
해법: 분기마다 골든셋 갱신 — 5~10% 신규 케이스 추가, 5% 노후 케이스 제거.
10.6 Gaming Quality Score
엔지니어가 quality 점수만 최적화하다 보면 “judge가 좋아하는 길이·인용수”에 fit. 진짜 사용자 만족과 괴리.
해법: - 융합 점수에 사람 골든셋 점수 비중 유지 - North Star metric은 thumbs_up 같은 직접 신호 — 융합 점수는 보조
11 MINERVA 적용
app/quality/
├── explicit.py # thumbs_up·feedback 수집·정규화
├── implicit.py # 행동 신호 추출
├── heuristic.py # 응답 분석
├── llm_judge.py # rubric·pairwise
├── fusion.py # 가중·융합 점수 학습·예측
└── refusal.py # refusal 탐지·KPI
eval/
├── golden_set.yaml # 고정 평가
├── golden_set_v2.yaml # 갱신 버전
└── runner.py # 매주 cron + CI hook
scripts/
├── judge_calibration.py # 분기별 judge 회귀 확인
├── weekly_quality_report.py # 약한 의도×세그먼트 자동 발견
└── golden_eval_ci.py # 새 모델 도입 사전 점검
C20 대화 로깅 feature 계층의 response_quality_score를 본 편의 fused score가 채운다.
12 정리
| 영역 | 핵심 |
|---|---|
| 4계층 | Explicit·Implicit·Heuristic·LLM Judge |
| Explicit | trust 높지만 sparse, selection bias 보정 필요 |
| Implicit | dense, behavior 기반, 회귀 모델로 융합 |
| Heuristic | 비용 0, citation·refusal·hallucination 탐지 |
| LLM Judge | rubric or pairwise, 분기마다 사람과 회귀 |
| 융합 | 회귀 모델로 가중 학습, fused_score를 feature 계층에 저장 |
| Golden Set | offline regression, 새 모델 도입 사전 점검 |
| 함정 | selection bias·judge hacking·gaming·refusal 폭증·골든셋 drift |
13 응용 분야
| 시나리오 | 활용 |
|---|---|
| 새 모델 도입 사전 검증 | golden set + judge calibration |
| 약한 의도×세그먼트 자동 발견 | weekly quality report → C23 입력 |
| Bandit 보상 신호 | fused_score가 thumbs_up 부족 메움 |
| 실험 분석 | C19 단계 7에서 fused_score를 primary 보조 |
| 운영 알림 | refusal rate·hallucination signal 임계 초과 |
| 사용자 학습 | implicit signal로 silent 사용자 만족 추정 |
14 관련 주제
선행 학습 (선수)
- C20 대화 로깅 설계 — 모든 신호의 저장 토대
- C21 의도·토픽 — 의도별 품질 분리 평가
- 06편 A/B 실험 — thumbs_up·feedback 수집 토대
18-LangGraph 시리즈 cross-reference
- #22 시스템 프롬프트 평가 — judge calibration 이론적 배경
- #16 SKILL.md 명세 — 스킬 단위 평가의 토대 (C30 스킬 테스트와 연동)
후속 (Phase C-5)
- C23 피드백 루프 — 약한 셀을 자동 개선
- Phase C-7 C30 스킬 테스트 — golden set 패턴 확장
Cross-reference
- C16 Bandit — fused_score를 보상으로 사용
- C19 실험 파이프라인 — golden eval을 실험 사전 검증에 통합
- 12-1 테스트 패턴 — golden set이 snapshot 테스트와 결합