AI Agent

LLM as a Judge 완전정리 3편 — 편향 잡는 법: 전략별 코드와 효과 비교

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

2편에서 7가지 편향을 측정했습니다. 3편은 그것들을 고치는 법입니다. 중요한 전제: "더 좋은 프롬프트"는 해결책이 아닙니다. 2024년 arXiv:2604.23178은 9가지 완화 전략을 5개 판사, 3개 벤치마크, 225개 통제 케이스에서 체계적으로 비교했습니다. 결론은 명확합니다. 편향 완화는 프롬프트가 아니라 기계적 절차로 해결합니다. 각 편향마다 다른 처방이 필요하고, 조합할 때 비용 대비 효율이 달라집니다. 편향별 완화 전략을 코드와 함께, 그리고 실제로 얼마나 효과가 있는지 수치와 함께 정리합니다.


3편이 다루는 것 → arXiv:2604.23178의 S1~S8 전략 체계 — 비용·효과 매핑 → 편향 7종 각각에 대한 완화 전략 코드 → Length-Controlled Win Rate (Dubois 2024) — GLM 기반 사후 보정 → Cross-family 판사 선택 — Self-preference 완화의 가장 실용적 방법 → CyclicJudge — 라운드로빈 순환으로 분산 최소화 → PREPAIR — Pointwise 분석 + Pairwise 판정 결합 → 조합 전략의 비용·효과 트레이드오프 — 언제 무엇을 쓸지


완화 전략 프레임워크 — S1~S8

"Judging the Judges"(arXiv:2604.23178, 2026년 4월)는 편향 완화 전략을 체계적으로 코드화했습니다. 이 분류를 기준으로 편향별 전략을 정리합니다.

전략 설명 API 호출 비용 주요 효과

S1 Position Swap 위치 편향 제거
S2 Same-family 앙상블 (temp 변화) 분산 감소
S3 Cross-family 앙상블 Self-preference 제거
S4 구조화 루브릭 (5차원, 각 1~10) 스타일 편향 부분 완화
S5 CoT (단계적 추론 의무화) 피상적 판단 방지
S6 Reference-guided (참조 답안) 사실 정확성 편향 완화
S7 S3 + S4 + S5 + S6 결합 최대 효과
S8 S1 + S4 + S5 결합 (Budget) 실용적 최선

핵심 발견: S8(Position Swap + CoT + 구조화 루브릭, 비용 2×)이 가장 실용적인 고효과 조합입니다. Claude를 판사로 쓸 때 S8은 스타일 편향까지 거의 제거했습니다. S7은 최대 효과지만 비용이 3×이므로 고위험 평가에 한정합니다.


1. Position Bias 완화 — S1: Position Swap

위치 편향은 현세대 프론티어 모델에서 ≤0.04로 사실상 소멸했지만, 코드 평가 같은 특정 도메인에서는 여전히 10% 이상 정확도가 이동합니다. 구현 비용이 낮으므로 일단 기계적으로 적용합니다.

from anthropic import Anthropic
import json

client = Anthropic()

JUDGE_SYSTEM = "당신은 두 AI 응답의 품질을 공정하게 비교하는 전문 평가자입니다."

PAIRWISE_TEMPLATE = """[질문]
{question}

[응답 {la}]
{ra}

[응답 {lb}]
{rb}

{cot_instruction}

JSON으로만 출력:
{{"winner": "{la}"|"{lb}"|"tie", "confidence": "high"|"medium"|"low",
  "reasoning": "2-3문장 근거"}}"""

COT_INSTRUCTION = """평가 전 다음 단계를 따르세요:
1단계: 응답 {la}의 강점과 약점을 분석합니다.
2단계: 응답 {lb}의 강점과 약점을 분석합니다.
3단계: 어떤 기준으로 더 나은지 결정합니다.
4단계: 최종 판정을 내립니다."""

def s8_judge(
    question: str,
    response_a: str,
    response_b: str,
    judge_model: str = "claude-opus-4-7",
    use_cot: bool = True,
) -> dict:
    """
    S8 전략: Position Swap + CoT + 구조화 판단
    비용 2× API 호출
    """

    def _eval(la, ra, lb, rb):
        cot = COT_INSTRUCTION.format(la=la, lb=lb) if use_cot else ""
        prompt = PAIRWISE_TEMPLATE.format(
            question=question,
            la=la, ra=ra, lb=lb, rb=rb,
            cot_instruction=cot,
        )
        raw = client.messages.create(
            model=judge_model, max_tokens=512,
            system=JUDGE_SYSTEM,
            messages=[{"role": "user", "content": prompt}]
        ).content[0].text.strip()
        return json.loads(raw)

    # AB 순서
    result_ab = _eval("A", response_a, "B", response_b)
    # BA 순서 (레이블 그대로, 응답만 교체)
    result_ba = _eval("A", response_b, "B", response_a)

    # BA 결과를 원래 기준으로 역변환
    ba_winner_original = {
        "A": "B",   # BA에서 A 이김 = 원래 B(response_b)가 이긴 것
        "B": "A",   # BA에서 B 이김 = 원래 A(response_a)가 이긴 것
        "tie": "tie",
    }.get(result_ba["winner"], "tie")

    # 양쪽 일치 여부 확인
    ab_winner = result_ab["winner"]
    consistent = ab_winner == ba_winner_original

    return {
        "winner": ab_winner if consistent else "tie",
        "consistent": consistent,
        "ab_result": result_ab,
        "ba_result": result_ba,
        "strategy": "S8",
        "api_calls": 2,
        "note": "불일치 → 자동 Tie 처리" if not consistent else None,
    }

효과 수치: arXiv:2604.23178에서 Claude + S8 조합은 스타일 편향을 포함한 전반적 편향을 거의 제거했습니다. 위치 편향 0.04 → 사실상 0.


2. Verbosity Bias 완화 — 3가지 접근

Verbosity Bias 완화에는 세 레벨이 있습니다. 프롬프트 수준 → 사후 통계 보정 → 평가 구조 변경. 효과는 뒤로 갈수록 강하고 복잡도도 높아집니다.

레벨 1: 루브릭 지시 (가장 간단)

VERBOSITY_AWARE_RUBRIC = """
응답을 평가할 때 다음 원칙을 반드시 따르세요:

**길이에 대한 지침:**
- 응답의 길이 자체는 품질 지표가 아닙니다
- 동일한 정보를 더 짧게 전달하는 응답이 동등하거나 더 나을 수 있습니다
- 불필요한 반복, 자명한 설명, 내용 없는 머리말은 감점 요인입니다
- "정보 밀도"(토큰당 유용한 정보량)를 기준으로 평가하세요

**평가 기준:**
1. 정확성 (1-5): 사실 오류 없음
2. 관련성 (1-5): 질문 핵심 충족
3. 완전성 (1-5): 중요 내용 누락 없음
4. 간결성 (1-5): 불필요한 내용 없이 명확히 전달
5. 유용성 (1-5): 실제 도움이 되는 내용

주의: 간결성과 완전성은 서로 다른 차원입니다.
짧아도 완전할 수 있고, 길어도 불완전할 수 있습니다.
"""

레벨 2: 정보 밀도 점수화

def information_density_score(
    response: str,
    base_score: float,
    length_penalty_threshold: int = 500,  # 단어 수
    penalty_rate: float = 0.02,           # 초과 100단어당 -0.02점
) -> float:
    """
    긴 응답에 길이 페널티 적용.
    단, 짧은 응답에 보너스를 주면 안 됨 — Verbosity만 패널티.
    """
    word_count = len(response.split())
    if word_count > length_penalty_threshold:
        excess_words = word_count - length_penalty_threshold
        penalty = (excess_words / 100) * penalty_rate
        return max(1.0, base_score - penalty)
    return base_score

레벨 3: Length-Controlled Win Rate (Dubois 2024)

AlpacaEval 2.0에서 도입된 방법입니다. 길이 차이를 공변량으로 포함한 GLM을 적합해, "같은 길이였다면 어느 쪽이 이겼을까"를 추정합니다. Chatbot Arena와의 Spearman 상관이 0.94 → 0.98로 향상됐습니다.

import numpy as np
from sklearn.linear_model import LogisticRegression

def compute_length_controlled_winrate(
    comparisons: list[dict],
    # comparisons: [{"winner": "A"|"B"|"tie",
    #                "len_a": int, "len_b": int}, ...]
) -> dict:
    """
    Dubois et al. 2024 방법 간략 구현.
    
    GLM으로 길이 차이를 통제한 실제 품질 기반 승률 추정.
    """

    # 타이 제외
    valid = [c for c in comparisons if c["winner"] != "tie"]
    if len(valid) < 10:
        return {"error": "충분한 비교 데이터 없음 (최소 10개 필요)"}

    # 피처: 길이 차이 (A 길이 - B 길이)
    X = np.array([[c["len_a"] - c["len_b"]] for c in valid])
    # 레이블: A 승리 = 1, B 승리 = 0
    y = np.array([1 if c["winner"] == "A" else 0 for c in valid])

    # GLM (로지스틱 회귀)
    model = LogisticRegression()
    model.fit(X, y)

    # Length-Controlled Win Rate: 길이 차이 = 0일 때의 승률
    # (같은 길이라고 가정했을 때 A가 이길 확률)
    lc_winrate = model.predict_proba([[0]])[0][1]

    # 원래 win rate (보정 전)
    raw_winrate = y.mean()

    # 길이 편향 크기 추정
    # 길이가 100단어 증가할 때 승률 변화
    delta_100 = (
        model.predict_proba([[100]])[0][1]
        - model.predict_proba([[0]])[0][1]
    )

    return {
        "raw_winrate": round(raw_winrate, 4),
        "length_controlled_winrate": round(lc_winrate, 4),
        "winrate_correction": round(lc_winrate - raw_winrate, 4),
        "length_bias_per_100_words": round(delta_100, 4),
        "n_comparisons": len(valid),
        "interpretation": (
            "길이 편향 강함 (>0.05 보정)" if abs(lc_winrate - raw_winrate) > 0.05
            else "길이 편향 약함"
        ),
    }

주의: Length-Controlled Win Rate는 pairwise 비교가 충분히 쌓인 뒤 사후 보정에 쓰는 방법입니다. 실시간 단일 평가에는 적용할 수 없습니다.


3. Self-preference Bias 완화 — Cross-family 판사 선택

가장 직접적이고 효과적인 방법은 생성 모델과 다른 패밀리의 모델을 판사로 쓰는 것입니다.

# 패밀리 정의
MODEL_FAMILIES = {
    "anthropic": ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"],
    "openai":    ["gpt-5.5", "gpt-5-mini"],
    "google":    ["gemini-3.5-flash", "gemini-3.1-pro"],
    "meta":      ["llama-3.3-70b", "llama-3.1-8b"],
    "xai":       ["grok-4.3"],
}

def _get_family(model_id: str) -> str:
    mid = model_id.lower()
    for family, models in MODEL_FAMILIES.items():
        if any(m.split("-")[0] in mid for m in models):
            return family
        if family in mid:
            return family
    return "unknown"

def select_cross_family_judge(
    generator_model: str,
    available_judges: list[str],
    preferred_capability: str = "high",   # "high" | "medium"
) -> str:
    """
    생성 모델과 다른 패밀리의 판사 중 가장 적합한 것 선택.
    """
    gen_family = _get_family(generator_model)

    # 같은 패밀리 제외
    eligible = [
        j for j in available_judges
        if _get_family(j) != gen_family and _get_family(j) != "unknown"
    ]

    if not eligible:
        raise ValueError(
            f"가용 판사({available_judges})가 모두 생성 모델({generator_model})과 "
            f"같은 패밀리({gen_family})입니다. "
            f"다른 패밀리의 판사를 추가하세요."
        )

    # 능력 기준 정렬 (고성능 모델 우선)
    HIGH_CAPABILITY = ["claude-opus", "gpt-5.5", "gemini-3.5-flash"]
    if preferred_capability == "high":
        for cap_model in HIGH_CAPABILITY:
            for judge in eligible:
                if cap_model in judge.lower():
                    return judge

    return eligible[0]


# S3: Cross-family 앙상블
def s3_cross_family_ensemble(
    question: str,
    response: str,
    generator_model: str,
    n_judges: int = 3,
) -> dict:
    """
    생성 모델과 다른 패밀리 3개 모델로 앙상블 평가.
    """
    gen_family = _get_family(generator_model)

    # 다른 패밀리에서 판사 선발
    judge_pool = [
        m for family, models in MODEL_FAMILIES.items()
        for m in models
        if family != gen_family
    ]
    judges = judge_pool[:n_judges]

    scores = {}
    for judge in judges:
        score = pointwise_judge_single(question, response, judge)
        scores[judge] = score

    values = list(scores.values())
    return {
        "ensemble_mean": round(sum(values) / len(values), 2),
        "ensemble_std": round(float(np.std(values)), 2),
        "individual_scores": scores,
        "judges_used": judges,
        "generator_family_excluded": gen_family,
        "api_calls": n_judges,
    }

효과: Self-preference가 10~25% 점수 인플레이션을 유발하는 반면, Cross-family 판사는 이 편향을 구조적으로 제거합니다. 앙상블(S3)은 비용 3×이지만 분산도 함께 줄어 고위험 평가에 적합합니다.


4. Style/Format Bias 완화 — 내용 격리 + 포맷 지시 제거

스타일 편향(0.76~0.92)이 가장 강력한 편향입니다. 두 가지 방향으로 완화합니다.

방향 1: 응답 포맷 표준화 (평가 전 처리)

import re

def strip_formatting(text: str) -> str:
    """
    평가 전 마크다운 포맷 제거.
    내용은 보존하되 시각적 구조는 제거.
    
    주의: 이 방법은 코드 블록이 포함된 응답에는 부적합.
    """
    # 마크다운 헤더 제거 (## Title → Title)
    text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
    # 볼드/이탤릭 제거 (**bold** → bold)
    text = re.sub(r'\*{1,3}([^*]+)\*{1,3}', r'\1', text)
    # 불릿 포인트를 일반 문장으로 (- item → item)
    text = re.sub(r'^\s*[-*+]\s+', '', text, flags=re.MULTILINE)
    # 번호 목록 제거 (1. item → item)
    text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE)
    # 연속된 빈 줄 정리
    text = re.sub(r'\n{3,}', '\n\n', text)
    return text.strip()


def format_neutralized_judge(
    question: str,
    response_a: str,
    response_b: str,
    judge_model: str = "claude-opus-4-7",
    strip_format: bool = True,
) -> dict:
    """
    포맷 중립화 후 평가.
    코드가 없는 응답에 사용.
    """
    ra = strip_formatting(response_a) if strip_format else response_a
    rb = strip_formatting(response_b) if strip_format else response_b
    return s8_judge(question, ra, rb, judge_model)

방향 2: 루브릭에서 포맷을 명시적으로 제외

STYLE_NEUTRAL_SYSTEM = """당신은 AI 응답의 내용 품질만을 평가하는 전문가입니다.

**절대 평가하지 말 것:**
- 마크다운 포맷 (헤더, 불릿, 볼드)
- 응답의 시각적 구조
- 응답의 길이
- 글쓰기 스타일이나 어조

**오직 평가할 것:**
- 사실적 정확성
- 질문에 대한 실질적 답변 여부
- 정보의 유용성과 완전성
- 논리적 타당성

포맷이 화려해도 내용이 없으면 낮은 점수를 주세요.
포맷이 없어도 내용이 좋으면 높은 점수를 주세요."""

5. Bandwagon/Authority Bias 완화 — 익명화 + 명시적 차단

import re

def anonymize_response(response: str) -> str:
    """
    응답에서 출처·권위 신호 제거.
    """
    patterns = [
        # 모델 이름 언급 제거
        r'\b(GPT|Claude|Gemini|Llama|Grok|ChatGPT|Copilot)\b',
        # 인용 형식 (저자 et al., 2024) → [인용됨]
        r'\([A-Z][a-z]+ et al\.,?\s*\d{4}\)',
        # URL 제거
        r'https?://\S+',
        # "연구에 따르면", "전문가들은" 같은 권위 신호
        r'(연구에 따르면|전문가들은|학계에서는|[0-9]+%의 사람들이)',
    ]

    result = response
    for pattern in patterns:
        result = re.sub(pattern, '[익명화됨]', result, flags=re.IGNORECASE)
    return result


ANTI_BANDWAGON_SYSTEM = """응답을 평가할 때 다음 규칙을 반드시 따르세요:

1. 인용이나 참고문헌의 존재 여부는 무시하세요
2. "많은 사람들이 동의한다"는 식의 주장은 근거로 인정하지 마세요
3. 권위 있는 출처 언급은 내용이 아닙니다 — 실질적 주장을 평가하세요
4. 확신에 찬 어조가 정확성을 의미하지 않습니다
5. 사실 확인이 불가능한 주장은 불확실한 것으로 간주하세요

응답의 내용 자체만을 근거로 판단하세요."""

6. Calibration Drift 완화 — 버전 고정 + 월별 재검증

import hashlib
import json
from datetime import datetime, timedelta

class JudgeContract:
    """
    판사 설정을 버전 관리하는 불변 계약.
    (judge_model_id, rubric_version, prompt_hash) 트리플렛이 고정돼야
    점수의 시계열 비교가 유의미해집니다.
    """

    def __init__(
        self,
        model_id: str,
        rubric: str,
        system_prompt: str,
        rubric_version: str = "v1.0",
    ):
        self.model_id = model_id
        self.rubric = rubric
        self.system_prompt = system_prompt
        self.rubric_version = rubric_version
        self.prompt_hash = self._hash_prompt()
        self.created_at = datetime.utcnow().isoformat()

    def _hash_prompt(self) -> str:
        content = self.system_prompt + self.rubric
        return hashlib.sha256(content.encode()).hexdigest()[:12]

    def fingerprint(self) -> str:
        """이 판사 설정의 고유 식별자"""
        return f"{self.model_id}:{self.rubric_version}:{self.prompt_hash}"

    def to_metadata(self) -> dict:
        return {
            "judge_model": self.model_id,
            "rubric_version": self.rubric_version,
            "prompt_hash": self.prompt_hash,
            "fingerprint": self.fingerprint(),
            "created_at": self.created_at,
        }


def recalibration_check(
    contract: JudgeContract,
    golden_set: list[dict],
    judge_fn,
    baseline_kappa: float,
    last_calibrated: datetime,
    kappa_drop_threshold: float = 0.1,
    days_threshold: int = 60,
) -> dict:
    """
    재캘리브레이션 필요 여부 판단.
    golden_set: [{"question": str, "response": str, "human_score": int}, ...]
    """
    from sklearn.metrics import cohen_kappa_score

    days_elapsed = (datetime.utcnow() - last_calibrated).days
    current_scores = [judge_fn(s["question"], s["response"]) for s in golden_set]
    human_scores = [s["human_score"] for s in golden_set]

    current_kappa = cohen_kappa_score(
        [round(s) for s in human_scores],
        [round(s) for s in current_scores],
    )
    kappa_drop = baseline_kappa - current_kappa
    mean_shift = abs(
        sum(current_scores) / len(current_scores)
        - sum(human_scores) / len(human_scores)
    )

    triggers = []
    if days_elapsed >= days_threshold:
        triggers.append(f"주기 도달 ({days_elapsed}일 경과)")
    if kappa_drop > kappa_drop_threshold:
        triggers.append(f"κ 하락 ({kappa_drop:.3f})")
    if mean_shift > 2.0:
        triggers.append(f"평균 이동 ({mean_shift:.1f}점)")
    if current_kappa < 0.5:
        triggers.append(f"κ 임계치 미달 ({current_kappa:.3f})")

    return {
        "contract_fingerprint": contract.fingerprint(),
        "current_kappa": round(current_kappa, 3),
        "baseline_kappa": round(baseline_kappa, 3),
        "kappa_drop": round(kappa_drop, 3),
        "mean_shift": round(mean_shift, 2),
        "days_since_calibration": days_elapsed,
        "recalibration_needed": len(triggers) > 0,
        "triggers": triggers,
        "alert_level": (
            "critical" if current_kappa < 0.4 or mean_shift > 5
            else "warning" if triggers
            else "ok"
        ),
    }

7. Rubric Instability 완화 — 균형 순열

루브릭 항목 순서 편향을 제거하는 방법입니다. 아이템이 N개일 때 모든 순열을 평균내거나, 대표 순열들을 순환합니다.

from itertools import permutations
import random

def balanced_rubric_evaluation(
    question: str,
    response: str,
    rubric_items: list[str],
    judge_fn_with_rubric,     # (q, r, rubric_order) → score
    n_permutations: int = 4,  # 전체 순열이 너무 많으면 샘플링
) -> dict:
    """
    rubric 순서를 무작위로 섞어 복수 평가 후 평균.
    arXiv:2602.02219의 Balanced Permutation 방법.
    """
    all_perms = list(permutations(rubric_items))

    # 항목이 많으면 랜덤 샘플링
    if len(all_perms) > n_permutations:
        sampled_perms = [rubric_items] + random.sample(all_perms, n_permutations - 1)
    else:
        sampled_perms = all_perms

    results = []
    for perm in sampled_perms:
        score = judge_fn_with_rubric(question, response, list(perm))
        results.append({"rubric_order": list(perm), "score": score})

    scores = [r["score"] for r in results]
    return {
        "balanced_score": round(sum(scores) / len(scores), 2),
        "score_variance": round(float(np.var(scores)), 3),
        "rubric_instability": round(max(scores) - min(scores), 2),
        "permutation_results": results,
        "n_permutations_tested": len(sampled_perms),
    }

8. CyclicJudge — 라운드로빈 순환 앙상블

단일 판사의 분산을 줄이는 가장 효율적인 방법으로 arXiv:2603.01865에서 제안됐습니다. 여러 판사를 라운드로빈으로 순환시키면, 단순 다수결 앙상블보다 낮은 분산을 동일 예산에서 달성합니다.

from itertools import cycle

class CyclicJudge:
    """
    여러 판사를 라운드로빈으로 순환해 평가 분산 최소화.
    arXiv:2603.01865 (CyclicJudge) 구현.
    """

    def __init__(self, judges: list[str]):
        """
        judges: 순환할 판사 모델 리스트
        가능하면 다른 패밀리에서 선택 (Cross-family 효과 결합)
        """
        self.judges = judges
        self._cycle = cycle(judges)
        self._call_count = {j: 0 for j in judges}

    def next_judge(self) -> str:
        """다음 순서 판사 반환"""
        judge = next(self._cycle)
        self._call_count[judge] += 1
        return judge

    def evaluate_batch(
        self,
        evaluations: list[dict],   # [{"question": str, "response": str}, ...]
        judge_fn,                   # (question, response, judge_model) → score
    ) -> list[dict]:
        """
        배치 평가: 각 샘플에 순환 판사 할당.
        총 판사 호출 = len(evaluations) × 1 (단일 판사보다 저렴)
        단, 판사 다양성으로 분산은 앙상블과 유사하게 감소.
        """
        results = []
        for eval_item in evaluations:
            judge = self.next_judge()
            score = judge_fn(
                eval_item["question"],
                eval_item["response"],
                judge,
            )
            results.append({
                **eval_item,
                "score": score,
                "judge_model": judge,
            })
        return results

    def usage_stats(self) -> dict:
        return {
            "total_calls": sum(self._call_count.values()),
            "calls_per_judge": self._call_count,
            "balance_ratio": min(self._call_count.values()) / max(self._call_count.values())
            if max(self._call_count.values()) > 0 else 1.0,
        }


# 사용 예시
cyclic = CyclicJudge(judges=[
    "claude-opus-4-7",    # Anthropic
    "gpt-5.5",            # OpenAI
    "gemini-3.5-flash",   # Google
])

batch_results = cyclic.evaluate_batch(
    evaluations=[
        {"question": "...", "response": "..."},
        {"question": "...", "response": "..."},
        # ...
    ],
    judge_fn=lambda q, r, m: pointwise_judge_single(q, r, m),
)

CyclicJudge의 장점: 배치 평가에서 단일 판사와 동일한 비용(1×)으로 앙상블에 준하는 분산 감소를 달성합니다. 모든 평가에 같은 판사를 쓸 때 발생하는 자기 패밀리 편향이 순환으로 분산됩니다.


전략 선택 가이드 — 비용 vs 효과

평가 유형별 권장 전략:

[일상적 품질 모니터링 - 비용 최소화]
→ S4 (구조화 루브릭) + Cross-family 판사 선택
→ CyclicJudge로 배치 처리
→ 비용: 1×, 효과: 중간

[A/B 테스트 / 모델 교체 결정 - 균형]
→ S8 (Position Swap + CoT + 루브릭)
→ Cross-family 판사
→ 비용: 2×, 효과: 높음
→ arXiv:2604.23178에서 권장하는 실용적 최선

[RLHF 선호 데이터 수집 - 높은 품질 요구]
→ S7 (S3 + S4 + S5 + S6)
→ Length-Controlled Win Rate 사후 보정
→ 비용: 3×, 효과: 최대

[긴급 인시던트 조사 - 단발성 고신뢰]
→ S3 Cross-family 앙상블 3개 판사
→ 황금 세트 즉시 재캘리브레이션
→ 비용: 3×, 신뢰도: 최대

[대규모 배치 스크리닝 - 처리량 우선]
→ Distilled Judge (4편에서 상세 다룸)
→ 비용: ~1/50×, 효과: 보통

✅ 3편 정리 — 완화 전략 매핑

편향 1순위 완화 2순위 완화 비용

Position S1 Position Swap S8 결합
Verbosity 루브릭 지시 + 간결성 차원 LC Win Rate 1× / 사후
Self-preference Cross-family 판사 S3 앙상블 1× / 3×
Style/Format 포맷 중립화 + 명시적 지시 S4 구조화 루브릭
Bandwagon/Authority 익명화 + Anti-bandwagon 지시 S6 Reference
Calibration Drift JudgeContract 버전 고정 60일 재캘리브레이션
Rubric Instability 균형 순열 평균 루브릭 순서 고정

황금 룰: 편향 완화에 "만능 해결책"은 없습니다. 편향마다 다른 처방이 필요하고, 가장 실용적인 기본값은 S8 + Cross-family 판사입니다. 이 두 가지만 적용해도 현재 알려진 주요 편향의 대부분을 의미 있게 줄일 수 있습니다.


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

 

 

반응형