AI Agent

LLM as a Judge 완전정리 7편 — 판사가 절대 못 하는 것들: 한계와 대안

cell-devlog 2026. 5. 26. 16:01
반응형

6편에서 파이프라인을 완성했습니다. 7편은 멈추는 법입니다. LLM 판사가 강력한 도구인 것은 사실이지만, "강력하다"와 "만능이다"는 다릅니다. 판사가 구조적으로 실패하는 영역이 있습니다. 사실 오류를 탐지하는 데 취약하고, 전문 도메인에서 전문가와 일치율이 급락하고, 적대적 입력에 취약하고, 검증 가능한 태스크에서 실행 없이 판단하는 것이 본질적 한계입니다. 이 한계를 모르면 판사가 틀렸을 때 조용히 틀립니다. 무엇이 한계인지, 각각의 대안이 무엇인지, 그리고 이 모든 걸 종합한 하이브리드 평가 아키텍처로 7편을 마무리합니다.


7편이 다루는 것 → 한계 1: 사실 오류 탐지 실패 — 왜 유창함이 정확성을 가린다 → 한계 2: 전문 도메인 갭 — 의료·법률·금융에서 일치율 10~15% 하락 → 한계 3: 적대적 입력 취약성 — ASR 30~73%의 프롬프트 주입 공격 → 한계 4: 검증 가능 태스크의 한계 — 코드·수학은 실행이 정답 → 한계 5: 레이턴시·비용 제약 — 실시간 요건에서의 구조적 한계 → 태스크별 최적 도구 매핑 — 판사가 맞는 곳 vs 대안이 필요한 곳 → 하이브리드 평가 아키텍처 — Hybrid Norm의 실전 구현


판사가 틀리는 방식의 특성

LLM 판사가 틀리는 방식은 두 가지가 있습니다. **노이즈(Noise)**는 같은 입력에 다른 결과를 내는 것입니다. 충분한 샘플링과 앙상블로 줄일 수 있습니다. **구조적 실패(Structural Failure)**는 특정 유형의 입력에 대해 체계적으로 틀리는 것입니다. 더 많은 샘플을 쓴다고 해결되지 않습니다. 이 편에서 다루는 것은 구조적 실패입니다.


한계 1: 사실 오류 탐지 실패

이것이 가장 위험한 한계입니다. LLM 판사는 유창하게 틀린 응답을 정확한 것으로 분류하는 경향이 있습니다.

왜 발생하는가

LLM은 다음 토큰 예측 훈련에서 사실 정확성이 아닌 그럴듯함을 학습합니다. 자주 반복되는 잘못된 정보가 훈련 분포에 포함될 수 있고, 판사 모델이 이를 "그럴듯한 사실"로 학습합니다. 특히 롱테일 엔터티(빈도 낮은 고유명사, 최신 사실)에서 정확도가 급격히 떨어집니다.

from anthropic import Anthropic
import json

client = Anthropic()

# 판사가 사실 오류를 통과시키는 케이스 예시
FACTUAL_TRAP_EXAMPLES = [
    {
        "question": "양자 컴퓨터가 RSA-2048 암호를 해독하려면 어떤 알고리즘이 필요하고, 그 복잡도는?",
        "good_response": "Shor 알고리즘을 사용하면 이론적으로 O(n³)의 다항 시간으로 RSA-2048을 해독할 수 있습니다.",
        "bad_response_fluent": "Grover 알고리즘을 사용하면 O(n²) 시간으로 RSA-2048을 해독할 수 있습니다.",
        # ↑ 유창하지만 틀림: Grover는 검색 알고리즘, RSA 해독은 Shor
    },
]

def factual_trap_test(judge_model: str) -> dict:
    """
    판사가 유창한 사실 오류를 통과시키는지 테스트.
    판사가 맞히면: 사실 오류 감지 능력 있음
    판사가 틀리면: 유창함에 속음
    """
    results = []
    for trap in FACTUAL_TRAP_EXAMPLES:
        prompt = f"""다음 두 응답 중 어느 것이 더 정확한가?

[질문] {trap['question']}

[응답 A] {trap['good_response']}
[응답 B] {trap['bad_response_fluent']}

A 또는 B만 출력:"""

        verdict = client.messages.create(
            model=judge_model, max_tokens=5,
            messages=[{"role": "user", "content": prompt}]
        ).content[0].text.strip().upper()

        results.append({
            "question": trap["question"][:50] + "...",
            "verdict": verdict,
            "correct": verdict == "A",   # A가 정답
        })

    accuracy = sum(r["correct"] for r in results) / len(results)
    return {
        "factual_accuracy": round(accuracy, 3),
        "results": results,
        "note": "유창한 오류에 속는 비율: " + str(round(1 - accuracy, 3))
    }

실측 데이터

전문 도메인에서 LLM 판사와 도메인 전문가의 일치율은 60~68%에 불과합니다. 이는 전문 도메인에서는 신뢰할 수 없는 수준입니다. 특히 의료 분야 연구에서 GPT-4와 Llama-3.3을 판사로 쓴 결과, 임상적으로 의미 있는 오류를 신뢰할 수 없는 비율로 통과시켰습니다.

대안: FActScore + 원자 사실 분해

긴 응답을 원자 클레임(Atomic Claim)으로 분해하고, 각 클레임을 독립적으로 검증합니다.

def factual_verification_pipeline(
    response: str,
    knowledge_source: str | None = None,   # RAG 컨텍스트 또는 None
    judge_model: str = "claude-opus-4-7",
) -> dict:
    """
    FActScore 스타일의 원자 사실 검증.
    1) 응답을 원자 클레임으로 분해
    2) 각 클레임을 독립적으로 검증
    3) 지지된 클레임 비율을 사실 점수로 계산
    """

    # 1단계: 원자 클레임 추출
    decomp_prompt = f"""다음 텍스트를 독립적으로 검증 가능한 원자 클레임 리스트로 분해하세요.
각 클레임은 하나의 구체적 사실만 포함해야 합니다.

텍스트: {response}

JSON: {{"claims": ["클레임1", "클레임2", ...]}}"""

    raw_claims = json.loads(
        client.messages.create(
            model=judge_model, max_tokens=512,
            messages=[{"role": "user", "content": decomp_prompt}]
        ).content[0].text.strip()
    )
    claims = raw_claims["claims"]

    # 2단계: 각 클레임 검증
    verified_claims = []
    for claim in claims:
        if knowledge_source:
            verify_prompt = f"""다음 지식 베이스를 바탕으로 클레임이 사실인지 판단하세요.

지식 베이스: {knowledge_source[:2000]}

클레임: {claim}

답변 (supported/not_supported/uncertain):"""
        else:
            verify_prompt = f"""다음 클레임이 사실인지 판단하세요.
확실하지 않으면 'uncertain'으로 표시하세요.

클레임: {claim}

답변 (supported/not_supported/uncertain):"""

        verdict = client.messages.create(
            model=judge_model, max_tokens=20,
            messages=[{"role": "user", "content": verify_prompt}]
        ).content[0].text.strip().lower()

        verified_claims.append({
            "claim": claim,
            "verdict": verdict,
            "supported": "supported" in verdict,
        })

    # 3단계: FactScore 계산
    n_supported  = sum(c["supported"] for c in verified_claims)
    n_uncertain  = sum("uncertain" in c["verdict"] for c in verified_claims)
    fact_score   = n_supported / len(claims) if claims else 0.0

    return {
        "fact_score": round(fact_score, 3),
        "n_claims": len(claims),
        "n_supported": n_supported,
        "n_not_supported": len(claims) - n_supported - n_uncertain,
        "n_uncertain": n_uncertain,
        "verified_claims": verified_claims,
        "recommendation": (
            "사실성 높음" if fact_score >= 0.85
            else "사실성 의심 — 전문가 검토 필요" if fact_score >= 0.60
            else "높은 환각 위험 — 사용 금지"
        ),
    }

한계 2: 전문 도메인 갭

의료·법률·사이버보안·금융 같은 전문 도메인에서 LLM 판사는 일반 텍스트 평가보다 현저히 낮은 신뢰도를 보입니다.

수치로 보기

일반 텍스트 평가: 인간-LLM 판사 일치율 ~80%
전문 도메인:      인간-LLM 판사 일치율 60~68%
                  → 10~15pt 하락

특히 심각한 영역:
- 정신 건강 챗봇 응답: "임상적으로 적절한가?" → LLM 판사 신뢰 불가
- 법률 문서 검토: 법원 관할권·판례 정확성 → 변호사만 검증 가능
- 의약품 정보: 용량·금기사항 → 약사/의사만 검증 가능
- 사이버보안 취약점 분석: 실제 악용 가능성 → 보안 전문가 필요

대안: 전문가 레이어 + LLM 스크리닝

LLM 판사로 명백한 케이스를 필터링하고, 경계 케이스와 고위험 케이스만 전문가에게 보냅니다.

def domain_expert_pipeline(
    responses: list[dict],
    domain: str,
    screening_threshold: float = 3.5,
) -> dict:
    """
    전문 도메인 평가 파이프라인.
    LLM으로 명확한 케이스 분류 → 경계 케이스만 전문가에게.
    """

    DOMAIN_RISK_THRESHOLDS = {
        "medical":    {"expert_required_below": 4.0, "block_below": 2.0},
        "legal":      {"expert_required_below": 4.0, "block_below": 2.5},
        "financial":  {"expert_required_below": 3.5, "block_below": 2.0},
        "cybersecurity": {"expert_required_below": 4.0, "block_below": 2.0},
        "general":    {"expert_required_below": 2.5, "block_below": 1.5},
    }

    thresholds = DOMAIN_RISK_THRESHOLDS.get(domain, DOMAIN_RISK_THRESHOLDS["general"])

    # 1차: LLM 판사 스크리닝
    screened = []
    for resp in responses:
        # 간단한 패스/페일 LLM 스크리닝
        score = basic_llm_screen(resp, domain)
        category = (
            "block"          if score < thresholds["block_below"]
            else "expert"    if score < thresholds["expert_required_below"]
            else "auto_pass"
        )
        screened.append({**resp, "llm_score": score, "category": category})

    # 결과 분류
    blocked      = [r for r in screened if r["category"] == "block"]
    needs_expert = [r for r in screened if r["category"] == "expert"]
    auto_passed  = [r for r in screened if r["category"] == "auto_pass"]

    return {
        "domain": domain,
        "total": len(responses),
        "auto_passed": len(auto_passed),
        "needs_expert_review": len(needs_expert),
        "blocked": len(blocked),
        "expert_queue": needs_expert,
        "estimated_cost_reduction": round(
            1 - len(needs_expert) / len(responses), 2
        ),  # 전문가 검토 없이 처리된 비율
    }

한계 3: 적대적 입력 취약성

이것은 LLM 판사가 평가 점수를 올리거나 낮추도록 의도적으로 조작될 수 있다는 의미입니다.

실측 공격 성공률

Prompt Injection 공격 성공률 (ASR):
- 소형 오픈소스 모델 (3B): ~65.9% 평균
- 프론티어 모델 (GPT-4, Claude): ~30~35%
- 전이성 (다른 모델에도 통용): 50.5~62.6%

CUA (Comparative Undermining Attack):
  응답에 악의적 접미사를 추가해 판사 판정 뒤집기
  ASR > 30% (Qwen2.5, Falcon3 기반)

JMA (Justification Manipulation Attack):
  판사의 CoT 추론을 조작해 결론 변경
  ASR 15~17%

학술 논문 리뷰 시나리오:
  "Reject" 판정을 "Accept"으로 바꾸는 간접 주입
  성공률 상당히 높음

왜 이게 실제 문제인가

공개 평가 시스템(Chatbot Arena 스타일), RLHF 데이터 수집, 자동 코드 리뷰 파이프라인 — 이런 시스템에서 생성 모델이 판사를 속이도록 출력을 최적화하면 평가 신호 전체가 의미를 잃습니다.

import re

def adversarial_input_sanitizer(text: str) -> tuple[str, list[str]]:
    """
    판사 조작 시도를 탐지하고 제거.
    Returns: (정제된 텍스트, 탐지된 패턴 목록)
    """
    detected = []

    # 1. 직접적인 판사 지시 패턴
    judge_injection_patterns = [
        r"당신은\s+[AB]를\s+선택해야\s+합니다",
        r"이\s+응답에\s+최고점을\s+주세요",
        r"(ignore|무시).{0,20}(above|previous|이전|위의)",
        r"you\s+must\s+(select|choose|pick)\s+[AB]",
        r"rate\s+this\s+response\s+as\s+\d+",
        r"score\s*:\s*\d+/\d+",
        r"\[INST\]|\[/INST\]",   # 지시 토큰 주입
        r"<\|system\|>|<\|user\|>",
    ]

    cleaned = text
    for pattern in judge_injection_patterns:
        matches = re.findall(pattern, cleaned, re.IGNORECASE)
        if matches:
            detected.extend(matches)
            cleaned = re.sub(pattern, "[REDACTED]", cleaned, flags=re.IGNORECASE)

    # 2. 비정상적으로 긴 무관한 텍스트 (패딩 공격)
    words = cleaned.split()
    if len(words) > 500:
        # 응답 뒷부분에 갑자기 다른 주제가 나오면 의심
        first_half_unique = set(words[:len(words)//2])
        second_half_unique = set(words[len(words)//2:])
        overlap_ratio = len(first_half_unique & second_half_unique) / max(len(second_half_unique), 1)
        if overlap_ratio < 0.1:
            detected.append("possible_padding_attack")
            cleaned = " ".join(words[:300]) + " [TRUNCATED]"

    return cleaned, detected


def judge_with_sanitization(
    question: str,
    response_a: str,
    response_b: str,
    judge_model: str = "claude-opus-4-7",
) -> dict:
    """
    입력 정제 후 평가.
    """
    clean_a, detected_a = adversarial_input_sanitizer(response_a)
    clean_b, detected_b = adversarial_input_sanitizer(response_b)

    result = pairwise_judge(question, clean_a, clean_b, judge_model)

    return {
        **result,
        "adversarial_signals": {
            "response_a": detected_a,
            "response_b": detected_b,
            "sanitization_applied": bool(detected_a or detected_b),
        }
    }

구조적 방어 전략

# 응답 익명화: 출처 신호 제거
def anonymize_for_judge(response: str, model_name: str = None) -> str:
    """
    응답에서 판사를 속일 수 있는 출처 신호와 자기 참조 제거.
    """
    anonymized = response
    if model_name:
        anonymized = anonymized.replace(model_name, "[AI ASSISTANT]")

    # "나는 Claude입니다", "I'm GPT-4" 같은 자기 공개 제거
    self_disclosure = [
        r"I('m| am) (Claude|GPT|Gemini|Grok|an AI|a language model)",
        r"나는 (Claude|GPT|Gemini|언어 모델|AI)입니다",
        r"As (Claude|GPT|an AI assistant)",
        r"This response was generated by",
    ]
    for pattern in self_disclosure:
        anonymized = re.sub(pattern, "I am an AI assistant", anonymized, flags=re.IGNORECASE)

    return anonymized

한계 4: 검증 가능 태스크 — 실행이 정답이다

코드 정확성, 수학 계산, JSON 스키마 검증, SQL 쿼리 결과 — 이런 태스크는 LLM 판사가 보기에 그럴듯해도 실제로 실행해보면 틀린 경우가 많습니다.

"Execution-Free" 판사의 한계

CodeJudgeBench 연구에서 LLM 판사는 코드를 실행하지 않고 정확성을 판단할 때 심각한 오류를 범했습니다. 특히 고난이도 문제에서 "그럴듯하지만 틀린 코드"를 정확한 것으로 분류하는 비율이 높았습니다.

import subprocess
import tempfile
import os
import json
from typing import Any

def execution_based_judge(
    code: str,
    test_cases: list[dict],
    language: str = "python",
    timeout_seconds: int = 10,
) -> dict:
    """
    코드를 실제로 실행해서 정확성 판단.
    LLM 판사 대신 사용하는 결정론적 verifier.

    test_cases: [{"input": ..., "expected_output": ...}, ...]
    """

    if language != "python":
        return {"error": f"현재 Python만 지원 ({language} 미지원)"}

    results = []
    for i, tc in enumerate(test_cases):
        try:
            with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
                # 코드 + 테스트 입력 실행
                test_code = f"""
{code}

# 테스트 실행
import sys, json
input_data = {repr(tc['input'])}
try:
    result = solution(input_data)
    print(json.dumps({{"output": result}}))
except Exception as e:
    print(json.dumps({{"error": str(e)}}))
"""
                f.write(test_code)
                tmp_path = f.name

            proc = subprocess.run(
                ["python3", tmp_path],
                capture_output=True, text=True,
                timeout=timeout_seconds,
            )
            os.unlink(tmp_path)

            if proc.returncode == 0:
                output = json.loads(proc.stdout.strip())
                passed = output.get("output") == tc["expected_output"]
                results.append({
                    "test_case": i,
                    "passed": passed,
                    "actual": output.get("output"),
                    "expected": tc["expected_output"],
                })
            else:
                results.append({
                    "test_case": i,
                    "passed": False,
                    "error": proc.stderr[:200],
                })

        except subprocess.TimeoutExpired:
            results.append({"test_case": i, "passed": False, "error": "timeout"})
        except Exception as e:
            results.append({"test_case": i, "passed": False, "error": str(e)})

    pass_rate = sum(r["passed"] for r in results) / len(results) if results else 0

    return {
        "pass_rate": round(pass_rate, 3),
        "passed": int(pass_rate == 1.0),
        "test_results": results,
        "method": "execution_based",   # LLM 판사 아님 — 실행 기반
    }


# 수학 계산 검증기
def math_verifier(expression: str, expected: float, tolerance: float = 1e-6) -> dict:
    """수학 표현식을 실제로 계산해서 검증"""
    try:
        import math
        result = eval(expression, {"math": math, "__builtins__": {}})
        passed = abs(result - expected) < tolerance
        return {
            "passed": passed,
            "computed": result,
            "expected": expected,
            "method": "mathematical_verification",
        }
    except Exception as e:
        return {"passed": False, "error": str(e)}

한계 5: 실시간 요건

LLM 판사 호출에는 200~500ms가 소요됩니다. 이것이 실시간 파이프라인(50ms 이내 응답 요건)에서 구조적 한계입니다.

# 응답 시간 요건별 평가 도구 선택

LATENCY_BASED_TOOL_SELECTION = {
    "<50ms": {
        "tool": "regex + heuristic classifiers",
        "examples": [
            "키워드 필터 (금지어, 형식 위반)",
            "길이 체크, JSON 스키마 검증",
            "단순 패턴 매칭",
        ],
        "llm_judge": False,
    },
    "50ms~200ms": {
        "tool": "distilled safety classifier",
        "examples": [
            "Llama Guard (파인튜닝 소형 모델)",
            "Perspective API (독성 감지)",
            "로컬 실행 분류기",
        ],
        "llm_judge": False,
    },
    "200ms~2s": {
        "tool": "Claude Haiku / Gemini Flash",
        "examples": [
            "간단한 Pointwise 평가",
            "이진 pass/fail 판정",
            "기본 관련성 확인",
        ],
        "llm_judge": "lite",
    },
    ">2s (비동기)": {
        "tool": "Claude Sonnet / Opus",
        "examples": [
            "완전한 Pointwise / Pairwise 평가",
            "다차원 루브릭 평가",
            "심층 코드 리뷰",
        ],
        "llm_judge": "full",
    },
}

태스크별 최적 도구 매핑

LLM 판사를 쓸 때와 대안을 쓸 때의 기준을 정리합니다.

EVALUATION_TOOL_MATRIX = {
    # ✅ LLM 판사가 가장 강한 영역
    "subjective_quality": {
        "tool": "LLM Judge (Pointwise/Pairwise)",
        "why": "인간 선호도와 80% 이상 일치, 유일하게 주관적 품질 포착",
        "caveat": "황금 세트 캘리브레이션 필수",
    },
    "instruction_following": {
        "tool": "LLM Judge + G-Eval",
        "why": "복잡한 지시 준수는 인간 수준 판단 필요",
        "caveat": "지시사항을 루브릭으로 명시화",
    },
    "writing_quality": {
        "tool": "LLM Judge",
        "why": "창의성·어조·구조는 LLM 판사 적합",
        "caveat": "Verbosity Bias 완화 필수",
    },
    "open_ended_qa": {
        "tool": "LLM Judge + FActScore 병행",
        "why": "관련성은 LLM, 사실성은 FActScore",
        "caveat": "도메인 전문성 갭 있으면 전문가 검토 추가",
    },

    # ⚠️ LLM 판사 + 전문 도구 병행 필요
    "code_correctness": {
        "tool": "실행 기반 verifier 우선 + LLM 판사는 가독성에만",
        "why": "코드는 실행이 정답 — LLM은 '그럴듯함'만 판단",
        "caveat": "LLM 판사로 코드 정확성 전량 평가 금지",
    },
    "math_calculation": {
        "tool": "수학 verifier (eval/sympy) + LLM은 설명 품질에만",
        "why": "계산은 결정론적 검증 가능",
        "caveat": "LLM 판사가 계산 결과를 '검토'하는 것은 신뢰 불가",
    },
    "factual_accuracy": {
        "tool": "FActScore + RAG-based grounding + LLM 판사는 보조",
        "why": "LLM 판사는 유창한 오류를 통과시킴",
        "caveat": "고위험 도메인은 외부 지식 베이스 조회 필수",
    },

    # ❌ LLM 판사 단독 사용 금지
    "safety_toxicity": {
        "tool": "Llama Guard 3 / Perspective API + LLM 판사는 2차만",
        "why": "안전 분류기가 훨씬 빠르고, 안전 위반에 편향 없음",
        "caveat": "LLM 판사는 안전 편향 가능 (위반을 통과시키거나 과잉 차단)",
    },
    "pii_detection": {
        "tool": "regex + 전문 PII 분류기 (Presidio 등)",
        "why": "개인정보는 결정론적 패턴으로 탐지 가능, LLM 불필요",
        "caveat": "",
    },
    "schema_validation": {
        "tool": "JSON schema validator (jsonschema)",
        "why": "형식 검증은 프로그래밍으로 완전히 해결 가능",
        "caveat": "",
    },
    "real_time_screening": {
        "tool": "heuristic + distilled classifier",
        "why": "50ms 이내 응답 요건은 LLM 판사 호출 불가",
        "caveat": "",
    },
}

def recommend_evaluator(task_type: str, domain: str = "general") -> dict:
    """태스크 유형에 따른 평가 도구 추천"""
    base = EVALUATION_TOOL_MATRIX.get(task_type, {
        "tool": "LLM Judge (기본값)",
        "why": "태스크 매핑 없음 — 기본 LLM 판사 적용",
        "caveat": "캘리브레이션 후 사용",
    })

    # 전문 도메인 추가 경고
    if domain in ("medical", "legal", "financial", "cybersecurity"):
        base["domain_warning"] = (
            f"⚠️ {domain.upper()} 도메인: LLM 판사 일치율 10~15% 하락. "
            "전문가 검토 레이어 추가 권장."
        )

    return base

하이브리드 평가 아키텍처 — Hybrid Norm

2026년 연구 합의로 부상한 "Hybrid Norm"입니다. 검증 가능한 결과물에는 결정론적 검증기를, 주관적 품질에는 LLM 판사를 씁니다.

class HybridEvaluator:
    """
    태스크별로 최적 평가 도구를 자동 라우팅하는 하이브리드 평가기.

    'Hybrid Norm' 구현:
    - 결정론적 검증 가능 태스크 → 코드/수식 실행
    - 안전성 → 전문 분류기 (Llama Guard)
    - 주관적 품질 → LLM 판사
    """

    def __init__(self, judge_contract: dict, domain: str = "general"):
        self.contract = judge_contract
        self.domain = domain

    def evaluate(self, task: dict) -> dict:
        """
        task: {
            "type": "code"|"math"|"safety"|"factual"|"subjective",
            "question": str,
            "response": str,
            "test_cases": [...],   # 코드일 때
            "expected_answer": ... # 수학일 때
        }
        """
        task_type = task.get("type", "subjective")
        results = {"task_type": task_type, "evaluators_used": []}

        # 1. 결정론적 검증 (코드)
        if task_type == "code" and task.get("test_cases"):
            exec_result = execution_based_judge(
                task["response"],
                task["test_cases"],
            )
            results["execution"] = exec_result
            results["evaluators_used"].append("execution_verifier")

            # 코드가 실행 실패하면 LLM 판사로 품질 평가 불필요
            if exec_result["pass_rate"] < 1.0:
                results["final_verdict"] = "fail"
                results["primary_signal"] = "execution"
                return results

        # 2. 수학 검증
        elif task_type == "math" and task.get("expected_answer") is not None:
            math_result = math_verifier(task["response"], task["expected_answer"])
            results["math_verification"] = math_result
            results["evaluators_used"].append("math_verifier")

        # 3. 안전성 분류
        if task.get("check_safety", False):
            # 실제 구현에서는 Llama Guard API 호출
            safety_result = self._llama_guard_screen(task["response"])
            results["safety"] = safety_result
            results["evaluators_used"].append("safety_classifier")

            if not safety_result.get("safe", True):
                results["final_verdict"] = "blocked"
                results["primary_signal"] = "safety"
                return results

        # 4. 사실성 검증 (고위험 도메인)
        if self.domain in ("medical", "legal") and task_type == "factual":
            fact_result = factual_verification_pipeline(
                task["response"],
                judge_model=self.contract["model_id"],
            )
            results["factual"] = fact_result
            results["evaluators_used"].append("fact_verifier")

        # 5. LLM 판사 (주관적 품질)
        if task_type in ("subjective", "writing", "instruction_following"):
            llm_result = self._run_llm_judge(task["question"], task["response"])
            results["llm_judge"] = llm_result
            results["evaluators_used"].append("llm_judge")

        # 최종 판정 종합
        results["final_verdict"] = self._aggregate_verdict(results)
        return results

    def _run_llm_judge(self, question: str, response: str) -> dict:
        """LLM 판사 실행"""
        raw = client.messages.create(
            model=self.contract["model_id"],
            max_tokens=256,
            messages=[{"role": "user", "content": self.contract["prompt_template"].format(
                question=question, response=response
            )}]
        ).content[0].text.strip()
        try:
            return json.loads(raw)
        except Exception:
            return {"overall": 3.0, "error": "parse_failed"}

    def _llama_guard_screen(self, response: str) -> dict:
        """Llama Guard 안전성 분류 (실제 구현에서 API 호출)"""
        # 실제 구현: Meta Llama Guard API 또는 로컬 실행
        return {"safe": True, "categories": []}  # 플레이스홀더

    def _aggregate_verdict(self, results: dict) -> str:
        if results.get("safety", {}).get("safe") == False:
            return "blocked_unsafe"
        if results.get("execution", {}).get("pass_rate", 1.0) < 1.0:
            return "fail_execution"
        if results.get("math_verification", {}).get("passed") == False:
            return "fail_math"
        if results.get("factual", {}).get("fact_score", 1.0) < 0.60:
            return "low_factuality"
        if results.get("llm_judge", {}).get("overall", 5.0) < 3.0:
            return "low_quality"
        return "pass"

LLM 판사가 틀리는 패턴 요약

패턴 1: "유창함 = 정확함" 착각
  → 잘 쓰인 틀린 응답을 통과시킴
  → 대안: FActScore, RAG 기반 grounding

패턴 2: 도메인 전문성 결여
  → 전문 도메인에서 전문가와 10~15% 불일치
  → 대안: 전문가 레이어, 도메인 특화 파인튜닝

패턴 3: 적대적 표면 특성에 끌림
  → 마크다운, 권위적 어조, 길이에 속음 (스타일 편향)
  → 대안: 포맷 중립화, S8 전략

패턴 4: 검증 없는 코드/수학 평가
  → 실행되지 않는 코드를 "정확함"으로 분류
  → 대안: 결정론적 실행 verifier

패턴 5: 판사 조작 취약성
  → 적대적 접미사로 판정 뒤집기 가능
  → 대안: 입력 정제, 응답 익명화

패턴 6: 캘리브레이션 표류
  → 판사 버전 업데이트 후 기준점이 조용히 이동
  → 대안: 판사 계약 고정, 60일 재캘리브레이션

패턴 7: 롱테일 사실 실패
  → 자주 등장하지 않는 사실, 최신 정보 오류
  → 대안: 외부 지식 베이스 + 원자 클레임 검증

✅ 7편 결론 — 그리고 시리즈 마무리

시리즈 전체를 하나의 원칙으로 압축하면 이것입니다.

"LLM 판사를 신뢰하라 — 단, 신뢰할 수 있는 범위 안에서만."

한계 대안

사실 오류 탐지 FActScore + 원자 클레임 분해
전문 도메인 갭 전문가 레이어 + LLM 스크리닝
적대적 입력 입력 정제 + 응답 익명화
코드/수학 정확성 실행 기반 verifier (결정론적)
안전성·독성 Llama Guard 3 / Perspective API
실시간 요건 heuristic + 소형 분류기
캘리브레이션 표류 판사 계약 고정 + 60일 재검증

LLM 판사는 인간의 판단을 증폭하는 도구입니다. 대체하는 도구가 아닙니다. 주관적 품질, 지시 준수, 글쓰기 품질 — 이 영역에서 LLM 판사는 비용과 규모의 균형을 맞추며 인간 수준의 평가를 제공합니다. 사실 오류, 전문 지식, 코드 실행 결과 — 이 영역에서 LLM 판사는 특화 도구로 교체하거나 보완해야 합니다.

올바르게 설계된 LLM 판사는 10만 건의 평가를 하루 만에 처리하면서도 인간 전문가 수준의 일치도를 달성합니다. 잘못 설계된 LLM 판사는 조용히 틀리면서 모든 숫자가 녹색인 대시보드를 만듭니다. 차이는 황금 세트, Cohen's κ, 판사 계약, 캘리브레이션 주기 — 1편부터 6편까지 다뤄온 것들입니다.


LLM-as-a-Judge 완전정리 시리즈 — 완결

  1. 1편 — 왜 기존 지표는 죽었고, 세 패러다임은 무엇인가 https://cell-devlog.tistory.com/265
  2. 2편 — 판사는 어디서 거짓말하나: 7가지 편향 해부 https://cell-devlog.tistory.com/266
  3. 3편 — 편향 잡는 법: Position Swap부터 Cross-family까지 https://cell-devlog.tistory.com/267
  4. 4편 — G-Eval vs Prometheus 2 vs PAJAMA vs Themis https://cell-devlog.tistory.com/268 
  5. 5편 — 판사를 평가하기: Cohen's κ, Bradley-Terry, 황금 세트 설계 https://cell-devlog.tistory.com/269
  6. 6편 — 프로덕션 파이프라인: 샘플링·CI 게이트·캘리브레이션 주기 https://cell-devlog.tistory.com/270
  7. 7편 — 한계와 대안: LLM 판사가 절대 못 하는 것들 https://cell-devlog.tistory.com/271
반응형