MINERVA Phase C-5 — 응답 품질 자동 평가 (Explicit·Implicit·Heuristic·LLM-as-Judge)

한 신호로는 진짜 품질을 못 잡는다 — 4계층 신호를 융합하고 골든셋으로 회귀를 막는다

thumbs_up만으로는 품질을 못 잡는다 — 사용자가 클릭을 안 할 뿐. 본 편은 응답 품질의 4계층 신호 (Explicit·Implicit·Heuristic·LLM-as-Judge)를 정의하고, 각 신호의 수집 방법·편향 패턴·신뢰도를 정리한다. 다중 신호 융합 점수, 골든셋 기반 offline eval, C23 피드백 루프와의 연동, 자주 발생하는 selection bias·judge bias·gaming 함정도 다룬다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

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.5

5.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 상관:

from scipy.stats import spearmanr

human_scores = [...]      # 100~500개 사람 라벨
judge_scores = [...]      # 같은 응답에 대한 LLM judge 점수
rho, _ = spearmanr(human_scores, judge_scores)
# rho > 0.7 이면 신뢰 가능

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:

# 학습 — sample 500~2000개에 사람 라벨
X = build_signal_matrix(samples)         # (n, k)
y = human_labels                          # (n,) 0~1
model = LGBMRegressor().fit(X, y)
# 운영에서 model.predict로 융합 점수

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.3 Quality-Latency Trade-off Hidden

LLM judge가 빠른 응답을 무시하고 정확성만 봄 → 운영에서 latency 폭증해도 quality 점수 그대로.

해법: 융합 점수에 latency penalty term 추가:

fused = quality - lambda * max(0, latency_ms - sla) / sla

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 관련 주제

선행 학습 (선수)

18-LangGraph 시리즈 cross-reference

  • #22 시스템 프롬프트 평가 — judge calibration 이론적 배경
  • #16 SKILL.md 명세 — 스킬 단위 평가의 토대 (C30 스킬 테스트와 연동)

후속 (Phase C-5)

Cross-reference

Subscribe

Enjoy this blog? Get notified of new posts by email: