AI Agent

LLM as a Judge 완전정리 2편 — 판사는 어디서 거짓말하나: 7가지 편향 해부

cell-devlog 2026. 5. 26. 12:37
반응형

LLM 판사가 80% 이상 인간과 일치한다고 해서, 나머지 20%가 균등하게 분포된 노이즈라고 생각하면 오산입니다. 불일치는 패턴이 있습니다. 어떤 응답이 앞에 나오는지, 얼마나 길게 썼는지, 마크다운을 썼는지, 어떤 회사 모델이 생성했는지에 따라 판사의 판정이 체계적으로 달라집니다. 이 패턴들이 "편향"입니다. 그리고 2026년 기준으로 가장 잘 알려진 편향(위치 편향)은 현세대 모델에서 사실상 사라졌고, 가장 강력하지만 거의 연구되지 않은 편향(스타일 편향)이 실제 지배적입니다. 7가지 편향을 측정 코드와 함께 해부합니다.


2편이 다루는 것 → 편향 7종 전체: Position·Verbosity·Self-preference·Style/Format·Bandwagon/Authority·Calibration Drift·Rubric Instability → 각 편향의 메커니즘·원인·정량적 수치 (논문 기반) → 반전: 위치 편향은 현세대에서 사실상 소멸, 스타일 편향(0.76~0.92)이 지배적 → 각 편향을 실측하는 코드 → 편향 간 상호작용 — 동시에 발생할 때 어떻게 결합하는가 → 편향 측정의 방법론적 함정


편향이란 무엇인가 — 정의부터 명확히

편향(Bias)은 판사가 평가 기준과 무관한 요인에 의해 체계적으로 한쪽으로 치우치는 것입니다. "체계적"이라는 단어가 핵심입니다. 무작위 오류(노이즈)와 달리, 편향은 동일한 조건에서 반복적으로 같은 방향으로 나타납니다.

노이즈: 같은 평가를 10번 하면 점수가 무작위로 흩어짐
       → 평균을 내면 수렴
       → 충분한 샘플로 완화 가능

편향:   같은 평가를 10번 해도 항상 같은 방향으로 틀림
       → 평균을 내도 편향은 남음
       → 더 많은 샘플이 해결책이 아님

이 구분이 중요한 이유: 편향이 있는 판사로 많은 샘플을 평가하면, 부정확한 결론을 더 확신하게 됩니다.


편향 1 — Position Bias (위치 편향)

정의: 페어와이즈 비교에서 응답의 실제 품질이 아닌 제시 위치(A 슬롯인지 B 슬롯인지)가 판정에 영향을 미치는 현상.

역사적 수치: MT-Bench(Zheng et al. 2024)에서 슬롯 A가 슬롯 B보다 10~15pt 더 많이 승리했습니다. 코드 심판 연구(Jiang et al. 2025)에서 응답 순서만 바꿔도 정확도가 10% 이상 이동했습니다.

2026년 반전: 그런데 2026년 4월 발표된 "Judging the Judges"(arXiv:2604.23178)는 충격적인 결과를 보고합니다. 현세대 모델에서 위치 편향은 모든 테스트 모델에서 ≤0.04로 사실상 소멸했습니다. 초기 위치 편향 연구는 GPT-3.5~4 초기 버전 시절의 발견이었고, 현재 프론티어 모델들은 이 편향이 크게 줄어들었습니다.

import json
import random
from anthropic import Anthropic

client = Anthropic()

def measure_position_bias(
    question: str,
    response_a: str,
    response_b: str,
    judge_model: str = "claude-opus-4-7",
    n_trials: int = 20,
) -> dict:
    """
    동일한 페어를 AB/BA로 반복 실행해 위치 편향 측정.
    n_trials: 각 순서로 실행할 횟수 (총 2×n_trials 호출)
    """

    PROMPT = """두 응답 중 어느 것이 더 나은지 판단하세요.

[질문]
{question}

[응답 {la}]
{ra}

[응답 {lb}]
{rb}

더 나은 응답의 레이블만 출력 (A, B, 또는 tie):"""

    ab_winners, ba_winners = [], []

    for _ in range(n_trials):
        # AB 순서
        r_ab = client.messages.create(
            model=judge_model, max_tokens=10,
            messages=[{"role": "user", "content": PROMPT.format(
                question=question,
                la="A", ra=response_a, lb="B", rb=response_b
            )}]
        ).content[0].text.strip().upper()
        ab_winners.append(r_ab)

        # BA 순서 (레이블은 그대로 A/B, 응답만 교체)
        r_ba = client.messages.create(
            model=judge_model, max_tokens=10,
            messages=[{"role": "user", "content": PROMPT.format(
                question=question,
                la="A", ra=response_b, lb="B", rb=response_a
            )}]
        ).content[0].text.strip().upper()
        # BA에서 A가 이기면 원래 response_b가 이긴 것 → 원래 기준으로 B
        ba_winners.append("B" if r_ba == "A" else ("A" if r_ba == "B" else "tie"))

    # 위치 편향 = AB와 BA에서 불일치하는 비율
    inconsistencies = sum(a != b for a, b in zip(ab_winners, ba_winners))
    position_bias_rate = inconsistencies / n_trials

    # 방향성: A 슬롯을 선호하는가 B 슬롯을 선호하는가
    slot_a_wins_ab = ab_winners.count("A") / n_trials
    slot_b_wins_ba = ba_winners.count("B") / n_trials   # BA에서 B = 원래 A

    return {
        "position_bias_rate": position_bias_rate,      # 0이면 편향 없음
        "slot_a_preference_ab": slot_a_wins_ab,         # 0.5 초과면 A 선호
        "slot_b_preference_ba": slot_b_wins_ba,
        "interpretation": (
            "편향 없음 (≤0.05)" if position_bias_rate <= 0.05
            else "약한 편향 (0.05~0.10)" if position_bias_rate <= 0.10
            else "중간 편향 (0.10~0.15)" if position_bias_rate <= 0.15
            else "강한 편향 (>0.15)"
        ),
        "consistent_verdicts": n_trials - inconsistencies,
    }

실무 시사점: 현세대 판사를 위치 편향 때문에 반드시 Position Swap을 해야 하는지는 재검토가 필요합니다. 하지만 스타일 편향이 그 자리를 차지했으므로, 편향 자체는 여전히 존재합니다.


편향 2 — Verbosity Bias (장황함 편향)

정의: 동일한 품질의 응답 중 더 긴 응답이 더 높은 점수를 받는 현상.

메커니즘: RLHF 훈련 과정에서 사람들은 상세하고 구조화된 응답을 선호하는 경향이 있습니다. 이를 학습한 모델은 "길이 = 노력 = 품질"이라는 휴리스틱을 내면화합니다. 판사 모델도 이 경향에서 자유롭지 않습니다.

실측값: Wang et al. 2023, Saito et al. 2023에서 동일 품질 응답 중 더 긴 것이 일관되게 높은 점수를 받음. 2604.23178에서는 "모든 모델이 확장 페어에서 간결함 선호를 보였다"는 흥미로운 반대 결과도 보고됩니다 — 태스크에 따라 방향이 달라질 수 있습니다.

import numpy as np
import scipy.stats as stats

def measure_verbosity_bias(
    judge_fn,           # (question, response) → score (float)
    question: str,
    base_response: str,
    n_extensions: int = 5,
    extension_size: int = 100,  # 단어 수
) -> dict:
    """
    기본 응답을 점진적으로 확장해 길이와 점수의 상관관계 측정.
    확장 내용은 중립적 패딩(관련 있지만 새 정보 없는 문장)으로 채움.
    """

    NEUTRAL_PADDING = [
        "이 내용은 위에서 설명한 사항과 연관되어 있습니다.",
        "앞서 언급한 부분을 다시 한번 정리하면 다음과 같습니다.",
        "위의 내용을 종합하면 전반적인 맥락을 이해할 수 있습니다.",
        "이상의 내용은 주제와 관련된 핵심 사항들을 다루고 있습니다.",
        "따라서 위에서 서술한 내용들이 중요한 의미를 가집니다.",
    ]

    variants = [base_response]
    current = base_response
    for i in range(n_extensions):
        # 중립 패딩 추가 (실질적 정보 없음)
        padding = " ".join(NEUTRAL_PADDING * ((extension_size // 50) + 1))
        current = current + " " + padding[:extension_size * 5]  # 대략 extension_size 단어
        variants.append(current)

    lengths = [len(v.split()) for v in variants]
    scores  = [judge_fn(question, v) for v in variants]

    corr, p_val = stats.spearmanr(lengths, scores)

    # 회귀: 단어 100개 증가당 점수 변화
    slope, intercept, r_val, _, _ = stats.linregress(lengths, scores)
    score_per_100_words = slope * 100

    return {
        "spearman_r": round(corr, 3),
        "p_value": round(p_val, 4),
        "score_per_100_words": round(score_per_100_words, 3),
        "significant": p_val < 0.05,
        "bias_direction": (
            "장황함 선호 (길수록 높은 점수)" if corr > 0.3 and p_val < 0.05
            else "간결함 선호 (짧을수록 높은 점수)" if corr < -0.3 and p_val < 0.05
            else "편향 없음 또는 미유의"
        ),
        "lengths": lengths,
        "scores": scores,
    }

중요한 구분: Verbosity Bias와 실제 품질 향상을 구분해야 합니다. 더 긴 응답이 정말로 더 완전한 경우 높은 점수가 정당합니다. 편향은 내용 없는 확장에도 점수가 오를 때 발생합니다. 위 측정에서 중립 패딩을 사용하는 이유입니다.


편향 3 — Self-Preference Bias (자기 선호 편향)

정의: 판사 모델이 자신과 같은 패밀리(또는 자신)의 모델이 생성한 응답에 더 높은 점수를 부여하는 현상.

메커니즘 (Wataoka et al. 2024): LLM은 자신이 생성할 가능성이 높은 텍스트(perplexity가 낮은 텍스트)를 더 선호합니다. 같은 패밀리 모델의 출력은 비슷한 스타일·어휘·구조를 가지므로 자연스럽게 낮은 perplexity를 가집니다. 판사가 이를 품질 신호로 잘못 해석합니다.

정량적 수치: 같은 패밀리 모델 출력이 10~25% 더 높게 평가됩니다. 모델 크기가 클수록 편향이 작아집니다. 사후 훈련(RLHF 등)이 편향을 줄이지 않으며, 추론 모델(LRM)도 동일하게 영향받습니다.

def measure_self_preference_bias(
    judge_model: str,
    question: str,
    responses: dict,  # {"model_name": "응답 텍스트", ...}
    n_repeats: int = 10,
) -> dict:
    """
    여러 모델의 응답을 동일한 판사로 평가해
    판사 패밀리와 동일/다른 모델 간 점수 차이 측정.
    """

    # 판사 패밀리 결정 (간단한 규칙)
    FAMILY_MAP = {
        "claude": ["claude-opus", "claude-sonnet", "claude-haiku"],
        "openai": ["gpt-5", "gpt-4", "codex"],
        "google": ["gemini", "palm"],
        "meta": ["llama", "meta"],
        "xai": ["grok"],
    }

    def get_family(model_id: str) -> str:
        model_id = model_id.lower()
        for family, prefixes in FAMILY_MAP.items():
            if any(p in model_id for p in prefixes):
                return family
        return "unknown"

    judge_family = get_family(judge_model)

    POINTWISE_PROMPT = """다음 응답의 품질을 1~10점으로 평가하세요.
숫자만 출력하세요.

[질문]
{question}

[응답]
{response}

점수 (1-10):"""

    model_scores = {}
    for model_name, response_text in responses.items():
        trial_scores = []
        for _ in range(n_repeats):
            raw = client.messages.create(
                model=judge_model, max_tokens=5,
                messages=[{"role": "user", "content": POINTWISE_PROMPT.format(
                    question=question, response=response_text
                )}]
            ).content[0].text.strip()
            try:
                trial_scores.append(float(raw))
            except ValueError:
                pass

        model_scores[model_name] = {
            "mean": np.mean(trial_scores),
            "std": np.std(trial_scores),
            "family": get_family(model_name),
            "same_family": get_family(model_name) == judge_family,
        }

    # 동일 패밀리 vs 다른 패밀리 평균 비교
    same_family_scores = [
        v["mean"] for v in model_scores.values() if v["same_family"]
    ]
    diff_family_scores = [
        v["mean"] for v in model_scores.values() if not v["same_family"]
    ]

    bias_delta = (
        np.mean(same_family_scores) - np.mean(diff_family_scores)
        if same_family_scores and diff_family_scores else None
    )

    return {
        "judge_family": judge_family,
        "model_scores": model_scores,
        "same_family_mean": np.mean(same_family_scores) if same_family_scores else None,
        "diff_family_mean": np.mean(diff_family_scores) if diff_family_scores else None,
        "self_preference_delta": bias_delta,   # 양수면 자기 패밀리 선호
        "significant": abs(bias_delta) > 0.5 if bias_delta else False,
    }

실무 함의: GPT 판사로 GPT 출력을 평가하면 3개월간 모든 대시보드가 녹색이었는데, 도메인 전문가가 보니 κ = 0.31이었다는 케이스가 실제로 보고됩니다. Cross-family 판사 선택이 필수입니다.


편향 4 — Style/Format Bias (스타일·포맷 편향)

정의: 마크다운, 불릿, 표, 코드 블록 등 시각적 서식이 실제 내용 품질과 무관하게 점수에 영향을 미치는 현상.

2026년 핵심 발견: "Judging the Judges"(arXiv:2604.23178)의 가장 중요한 결과입니다. 스타일 편향이 0.76~0.92로, 테스트한 모든 모델에서 가장 지배적인 편향입니다. 위치 편향(≤0.04)과는 비교할 수 없는 수준입니다. 그런데 기존 연구의 주목을 거의 받지 못했습니다.

이 편향은 단방향이 아닙니다. Binary 판단(좋다/나쁘다)과 연속 점수(1~10)에서 방향이 다르게 나타납니다. Binary에서 "부정" 판단이 더 많이 나오는 경향이 있습니다.

def measure_style_bias(
    judge_fn,
    question: str,
    content: str,     # 순수 내용 (포맷 없음)
) -> dict:
    """
    동일한 내용을 다른 포맷으로 제시해 포맷의 점수 영향 측정.
    """

    # 동일 내용을 5가지 포맷으로 변환
    # (실제 구현에서는 LLM으로 포맷 변환 또는 수동 준비)
    formats = {
        "plain": content,
        "markdown_bullets": f"## 핵심 내용\n\n" + "\n".join(
            f"- {line}" for line in content.split(". ") if line
        ),
        "numbered_list": "\n".join(
            f"{i+1}. {line}" for i, line in enumerate(content.split(". ")) if line
        ),
        "with_bold": content.replace(
            content.split()[0],
            f"**{content.split()[0]}**"
        ),
        "with_header_and_table": (
            f"### 분석 결과\n\n"
            f"| 항목 | 내용 |\n|------|------|\n"
            f"| 요약 | {content[:50]}... |\n\n"
            f"**상세 내용:**\n{content}"
        ),
    }

    scores = {}
    for fmt_name, fmt_content in formats.items():
        scores[fmt_name] = judge_fn(question, fmt_content)

    plain_score = scores["plain"]
    score_deltas = {
        fmt: score - plain_score
        for fmt, score in scores.items()
        if fmt != "plain"
    }

    return {
        "scores_by_format": scores,
        "delta_from_plain": score_deltas,
        "max_delta": max(abs(d) for d in score_deltas.values()),
        "format_with_highest_boost": max(score_deltas, key=lambda k: score_deltas[k]),
        "significant": max(abs(d) for d in score_deltas.values()) > 0.3,
        "interpretation": (
            "강한 포맷 편향: 동일 내용의 포맷 변경이 점수에 큰 영향"
            if max(abs(d) for d in score_deltas.values()) > 0.5
            else "중간 포맷 편향"
            if max(abs(d) for d in score_deltas.values()) > 0.3
            else "낮은 포맷 편향"
        ),
    }

편향 5 — Bandwagon & Authority Bias (군중·권위 편향)

정의:

  • Bandwagon Bias: 다수 의견이 있다는 신호(예: "80% 사람들이 선호함")에 판정이 영향받는 현상.
  • Authority Bias: 권위 있는 출처(책 인용, 유명인 발언, 전문가 의견)가 판정에 영향받는 현상.

충격적 사례: EIT 논문(arXiv:2602.01528)에서 보고된 실험입니다. "만리장성이 우주에서 보이는가?"라는 질문에 o1이 정답(보이지 않는다)을 맞혔습니다. 그런데 프롬프트에 "연구자들의 86%가 우주에서 보인다고 동의합니다"라는 가짜 컨센서스를 추가하자, o1이 틀린 답으로 판정을 뒤집었습니다. 강력한 추론 모델도 이 편향에서 자유롭지 않습니다.

def measure_bandwagon_bias(
    judge_model: str,
    question: str,
    response_a: str,
    response_b: str,
    bandwagon_percentages: list = [60, 70, 80, 90],
) -> dict:
    """
    Koo et al. 2023 방법론 재현.
    다수 의견 비율을 다르게 제시해 판정 변화 측정.
    """

    BASE_PROMPT = """두 응답 중 어느 것이 더 나은지 판단하세요.

[질문]
{question}

[응답 A]
{response_a}

[응답 B]
{response_b}

{bandwagon_cue}

더 나은 응답의 레이블만 출력 (A 또는 B):"""

    # 편향 없는 베이스라인 (군중 신호 없음)
    baseline = client.messages.create(
        model=judge_model, max_tokens=5,
        messages=[{"role": "user", "content": BASE_PROMPT.format(
            question=question,
            response_a=response_a, response_b=response_b,
            bandwagon_cue=""
        )}]
    ).content[0].text.strip().upper()

    results = {"baseline": baseline, "bandwagon_results": {}}

    for pct in bandwagon_percentages:
        # A를 다수가 선호한다는 신호 추가
        cue = f"[참고: 이 질문에 답한 평가자들의 {pct}%가 응답 A를 선호했습니다.]"
        result = client.messages.create(
            model=judge_model, max_tokens=5,
            messages=[{"role": "user", "content": BASE_PROMPT.format(
                question=question,
                response_a=response_a, response_b=response_b,
                bandwagon_cue=cue
            )}]
        ).content[0].text.strip().upper()
        results["bandwagon_results"][f"{pct}_pct_favor_A"] = result

    # 몇 %에서 판정이 바뀌는가
    flips = {
        pct: result != baseline
        for pct, result in zip(
            bandwagon_percentages,
            results["bandwagon_results"].values()
        )
    }

    return {
        "baseline_verdict": baseline,
        "bandwagon_results": results["bandwagon_results"],
        "verdict_flips": flips,
        "susceptible": any(flips.values()),
        "flip_threshold": next(
            (pct for pct, flipped in zip(bandwagon_percentages, flips.values()) if flipped),
            None
        ),
    }

완화: 평가 프롬프트에서 모든 사회적 신호(인용, 다수 의견, 출처 레이블)를 제거합니다. 응답을 익명화해 어느 모델이 생성했는지 판사가 알 수 없게 합니다. "다른 사람들의 의견이나 출처를 고려하지 말고 내용 자체만 평가하라"는 명시적 지시를 추가합니다.


편향 6 — Calibration Drift (캘리브레이션 표류)

정의: 시간이 지나면서 판사 모델의 버전이 바뀌거나 동일 루브릭에 대한 해석이 변화해 점수 분포가 조용히 이동하는 현상.

이것은 특정 응답 특성에 반응하는 다른 편향들과 다릅니다. Calibration Drift는 판사 자체가 바뀌면서 발생합니다. 루브릭은 동일하지만 판사의 "기준점"이 변해 같은 응답도 다른 점수를 받습니다.

관측된 패턴: 마이너 모델 업데이트(예: Sonnet 4.5 → Sonnet 4.6)만으로 평균이 3~8pt 이동하고 점수 분포가 좁아지는 현상이 보고됩니다. 60~90일 주기로 재캘리브레이션이 필요합니다.

from datetime import datetime, timedelta
import json

class CalibrationDriftDetector:
    """
    황금 세트(Golden Set)를 사용해 캘리브레이션 표류 탐지.
    판사 모델 교체나 주기적 점검에 사용.
    """

    def __init__(self, golden_set: list[dict], baseline_scores: list[float] = None):
        """
        golden_set: [{"question": str, "response": str, "human_score": float}, ...]
        baseline_scores: 이전 캘리브레이션 시 측정한 판사 점수 리스트
        """
        self.golden_set = golden_set
        self.baseline_scores = baseline_scores
        self.last_calibrated = datetime.utcnow()

    def run_calibration(
        self, judge_model: str, judge_fn,
    ) -> dict:
        """현재 판사 점수와 기준선 비교"""
        current_scores = []
        for sample in self.golden_set:
            score = judge_fn(sample["question"], sample["response"])
            current_scores.append(score)

        human_scores = [s["human_score"] for s in self.golden_set]

        result = {
            "judge_model": judge_model,
            "timestamp": datetime.utcnow().isoformat(),
            "n_samples": len(self.golden_set),
            "current_mean": np.mean(current_scores),
            "current_std": np.std(current_scores),
        }

        if self.baseline_scores:
            mean_shift = np.mean(current_scores) - np.mean(self.baseline_scores)
            std_change = np.std(current_scores) - np.std(self.baseline_scores)

            # Cohen's κ vs 인간
            from sklearn.metrics import cohen_kappa_score
            human_rounded = [round(s) for s in human_scores]
            current_rounded = [round(s) for s in current_scores]
            try:
                kappa = cohen_kappa_score(human_rounded, current_rounded)
            except Exception:
                kappa = None

            result.update({
                "mean_shift_from_baseline": round(mean_shift, 3),
                "std_change_from_baseline": round(std_change, 3),
                "cohens_kappa_vs_human": round(kappa, 3) if kappa else None,
                "days_since_calibration": (datetime.utcnow() - self.last_calibrated).days,
            })

            # 경보 수준 결정
            alert = "ok"
            if abs(mean_shift) > 5:
                alert = "critical"
            elif abs(mean_shift) > 2 or (kappa and kappa < 0.5):
                alert = "warning"
            result["alert_level"] = alert

        return result

    def needs_recalibration(self, days_threshold: int = 60) -> bool:
        days_elapsed = (datetime.utcnow() - self.last_calibrated).days
        return days_elapsed >= days_threshold

편향 7 — Rubric Instability (루브릭 불안정성)

정의: 루브릭의 제시 방식(순서, ID 타입, 표현)이 바뀔 때 판사 점수가 체계적으로 달라지는 현상.

세부 유형:

루브릭 순서 편향: 평가 기준의 나열 순서가 점수에 영향을 미칩니다. "정확성→완전성→명확성" 순서와 "명확성→완전성→정확성" 순서에서 같은 응답이 다른 점수를 받습니다. 앞에 나오는 기준에 더 주의를 기울이는 Primacy Effect입니다.

ID 타입 민감성: 응답 레이블을 숫자(1, 2, 3)로 주느냐 로마숫자(I, II, III)로 주느냐, 알파벳(A, B, C)으로 주느냐에 따라 점수가 달라집니다. GPT-4o에서도 상관계수 0.03 정도의 차이가 있고, 소형 모델에서는 최대 0.20까지 이동합니다.

스코어 스케일 불일치: 판사의 내부 확신 수준과 출력 점수 사이의 매핑이 인간 스케일과 다릅니다. "7/10"이 판사에게는 "괜찮음"이지만 인간에게는 "상당히 좋음"을 의미할 수 있습니다.

def measure_rubric_instability(
    judge_fn_template,   # rubric 딕셔너리를 받아 점수 반환하는 함수
    question: str,
    response: str,
    rubric_items: list[str],   # ["정확성", "완전성", "명확성", "유용성"]
    n_permutations: int = 10,
) -> dict:
    """
    루브릭 항목 순서를 무작위로 섞어 순서 의존성 측정.
    """
    from itertools import permutations as perms
    import random

    base_score = judge_fn_template(question, response, rubric_items)

    perm_scores = []
    used_perms = set()

    for _ in range(n_permutations):
        shuffled = rubric_items.copy()
        random.shuffle(shuffled)

        # 이미 시도한 순서 스킵
        key = tuple(shuffled)
        if key in used_perms or key == tuple(rubric_items):
            continue
        used_perms.add(key)

        score = judge_fn_template(question, response, shuffled)
        perm_scores.append({
            "order": shuffled,
            "score": score,
            "delta_from_base": score - base_score,
        })

    deltas = [p["delta_from_base"] for p in perm_scores]

    return {
        "base_score": base_score,
        "base_rubric_order": rubric_items,
        "permutation_scores": perm_scores,
        "max_delta": max(abs(d) for d in deltas) if deltas else 0,
        "score_range": max(p["score"] for p in perm_scores) - min(p["score"] for p in perm_scores) if perm_scores else 0,
        "rubric_instability_index": np.std(deltas) if deltas else 0,
        "significant": np.std(deltas) > 0.3 if deltas else False,
    }

편향 간 상호작용 — 동시에 발생하면

7가지 편향이 독립적으로 작용하는 게 아닙니다. 특히 위험한 결합이 있습니다.

Verbosity × Self-preference 결합: 같은 패밀리 모델은 비슷한 스타일로 응답합니다. 즉, 판사가 선호하는 스타일(길고 구조화된)과 자기 패밀리 스타일이 겹칩니다. 두 편향이 같은 방향으로 작용해 점수 인플레이션이 배가됩니다.

Style × Bandwagon 결합: "마크다운으로 잘 구조화된 응답 + 전문가가 이 응답을 선호한다는 신호"가 결합하면, 두 편향이 독립적으로 작용할 때보다 훨씬 큰 스코어 이동이 발생합니다.

Calibration Drift × Rubric Instability 결합: 판사 모델이 업데이트되고 루브릭 해석이 함께 변하면, 점수 이동의 원인을 특정하기 매우 어려워집니다.


편향 측정의 방법론적 함정

편향을 측정하는 행위 자체에도 주의가 필요합니다.

품질 차이와 편향 혼동: Self-preference를 측정할 때 판사 패밀리 모델의 응답이 실제로 더 좋을 수 있습니다. "높은 점수 = 편향"이 아니라 "품질을 통제했을 때의 체계적 차이 = 편향"입니다.

측정 자체의 편향: 편향을 측정하기 위한 테스트 케이스가 편향될 수 있습니다. 중립적인 패딩으로 Verbosity를 측정할 때, 패딩이 완벽히 중립적이지 않을 수 있습니다.

샘플 크기: 편향 측정에는 통계적 유의성이 필요합니다. 10개 샘플로 측정한 Self-preference 차이는 노이즈일 가능성이 높습니다. 최소 100개 이상의 샘플과 통계 검정이 필요합니다.


✅ 2편 정리 — 편향 지형도

편향 강도 (현세대) 방향성 주요 연구

Style/Format 0.76~0.92 구조화된 포맷 선호 Wu & Aji 2024, arXiv:2604.23178
Self-preference 10~25% delta 동일 패밀리 선호 Panickssery 2024, Wataoka 2024
Verbosity 유의미 (태스크별 방향 다름) 긴 응답 선호 (또는 반대) Saito 2023, Wang 2023
Bandwagon/Authority 유의미 (90%에서 판정 뒤집힘) 다수·권위 추종 Koo 2023, EIT 2026
Calibration Drift 3~8pt/업데이트 방향 무작위 FutureAGI 2026
Rubric Instability ≤0.2 (소형 모델) 순서에 따라 다름 Li 2025, 2602.02219
Position ≤0.04 (현세대) ⚠️ 사실상 소멸 2604.23178 (최신 반전)

핵심 메시지: "위치 편향을 잡아야 한다"는 2023~2024년의 조언은 구세대 모델 기준입니다. 지금 가장 집중해야 할 편향은 스타일 편향입니다. 그리고 캘리브레이션 표류는 어떤 편향보다 조용하게 시스템 전체를 무너뜨립니다.


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
반응형