LLM 판사를 만들고 나서 가장 많이 하는 실수는 "그냥 돌려본다"는 것입니다. 판사가 얼마나 신뢰할 수 있는지 측정하지 않고 사용하는 것입니다. 기존 연구들이 주로 Pearson/Spearman 상관관계에 의존했다면, 더 엄밀한 접근은 Cohen's κ로 실제 일치도를 정량화하는 것입니다. 판사가 실제로 의미 있는 평가를 하고 있는지 알려면 세 가지가 필요합니다. 인간 레이블이 달린 황금 세트, 판사와 인간 판단의 일치도를 측정하는 통계 기법, 그리고 페어와이즈 비교 결과를 절대 강도로 변환하는 Bradley-Terry 모델. 황금 세트 설계부터 통계 검정, Bradley-Terry 구현, 그리고 판사를 신뢰할 수 있는지 판단하는 실용적 기준까지 전부 다룹니다.
5편이 다루는 것 → 황금 세트(Golden Set) 설계 원칙 — 크기·구성·레이블 품질 기준 → 주석 가이드라인 설계 — 경계 케이스, 엣지 케이스 처리 → Cohen's κ 완전 해부 — 계산·해석·가중 카파·Krippendorff's α → 상관 vs 일치: 왜 Pearson r만으로 부족한가 → Bradley-Terry 모델 — 페어와이즈 비교 → 절대 강도 변환 → 판사 신뢰도 등급 결정 — 프로덕션 사용 가능 임계값 → Meta-evaluation 파이프라인 — 전체 구현
1. 왜 Meta-evaluation이 필요한가
판사를 평가하지 않으면 무슨 일이 일어나는지, 실제 사례가 있습니다.
한 팀이 3개월 동안 Claude Sonnet 판사로 모델 품질을 모니터링했습니다. 대시보드는 계속 녹색이었습니다. 그런데 분기 리뷰에서 도메인 전문가가 판사 출력을 살펴봤더니 κ = 0.31이었습니다. 판사가 사실 오류를 "정확함"으로, 안전 위반을 "적절함"으로 체계적으로 잘못 분류하고 있었습니다. 3개월치 "모니터링" 데이터가 의미 없는 숫자였습니다.
이런 사례가 LLM 판사 연구에서 중요한 전환점을 만들었습니다: 상관관계 기반 검증에서 Cohen's κ를 통한 실제 일치도 측정으로의 전환입니다.
Meta-evaluation이 답해야 하는 질문:
1. 이 판사가 인간 전문가와 얼마나 일치하는가?
2. 그 일치가 우연에 의한 것인가, 실제 능력에 의한 것인가?
3. 어떤 유형의 질문/응답에서 판사가 틀리는가?
4. 이 판사를 어떤 목적에 신뢰할 수 있는가?
2. 황금 세트 설계
황금 세트(Golden Set)는 인간 전문가가 레이블을 달아 검증된 평가 기준 데이터셋입니다. 판사를 테스트하는 기준점입니다.
2.1 크기와 구성
# 황금 세트 구성 원칙
GOLDEN_SET_SPEC = {
"min_size": 200, # 최소 200개 — 통계적 신뢰성의 하한
"recommended": 500, # 권장 500개 — 서브카테고리 분석 가능
"production": 1000, # 프로덕션 — 세밀한 캘리브레이션
"composition": {
"easy_clear": 0.25, # 명확하게 좋은/나쁜 응답
"medium_typical": 0.50, # 일반적인 프로덕션 분포
"hard_ambiguous": 0.20, # 경계 케이스 (판사가 틀리기 쉬운)
"adversarial": 0.05, # 의도적으로 판사를 속이려는 케이스
},
"quality_distribution": {
# Pointwise 1~5점 기준
"score_1": 0.10, # 명확히 나쁜 응답
"score_2": 0.15,
"score_3": 0.30, # 중간 — 가장 많아야 함
"score_4": 0.30,
"score_5": 0.15, # 명확히 좋은 응답
}
}
경계 케이스(hard_ambiguous)를 의도적으로 포함하는 이유가 있습니다. 판사가 쉬운 케이스에서 높은 정확도를 보이는 것은 의미가 없습니다. 실제 프로덕션에서 문제가 생기는 것은 항상 경계 케이스입니다.
도메인 특화 평가 세트 구축 연구에서, 수동 큐레이션과 반지도 학습 클러스터링, 계층적 샘플링을 결합해 14개 카테고리에 걸친 1,573개 샘플로 Chatbot Arena와 84% 일치도를 달성한 사례가 있습니다.
2.2 주석 가이드라인 설계
황금 세트의 품질은 주석 가이드라인의 명확성에 달려 있습니다. 애매한 지침은 주석자 간 불일치를 만들고, 불일치는 신뢰할 수 없는 황금 세트를 만듭니다.
# 잘 설계된 루브릭 예시 (코딩 어시스턴트 평가)
CODING_RUBRIC_DETAILED = {
"dimension": "코드 정확성",
"scale": "1-5",
"anchors": {
1: {
"label": "완전히 잘못됨",
"criteria": [
"코드가 실행되지 않음 (구문 오류)",
"요청된 기능을 전혀 구현하지 않음",
"핵심 로직이 명백히 틀림",
],
"example": "def sort_list(lst): return lst # 정렬 안 함",
},
2: {
"label": "심각한 오류 있음",
"criteria": [
"실행은 되지만 주요 케이스에서 잘못된 결과",
"엣지 케이스 처리 완전 누락",
"시간/공간 복잡도가 요구사항의 10배 이상",
],
"example": "버블 정렬 요청에 O(n³) 구현",
},
3: {
"label": "부분적으로 정확",
"criteria": [
"주요 기능은 동작하지만 일부 케이스에서 실패",
"엣지 케이스 일부 처리",
"요구사항의 80% 이상 구현",
],
"example": "양수만 정렬하는 코드 (음수 미처리)",
},
4: {
"label": "대부분 정확",
"criteria": [
"명시된 요구사항 모두 충족",
"일반적인 엣지 케이스 처리",
"성능 요구사항 충족",
"경미한 스타일 문제 허용",
],
"example": "기능적으로 완전하지만 변수명이 불명확",
},
5: {
"label": "완벽히 정확",
"criteria": [
"모든 요구사항 충족",
"엣지 케이스 모두 처리",
"최적 또는 근최적 알고리즘",
"에러 처리 포함",
"명확한 코드 구조",
],
"example": "요구사항 완전 충족 + 독엣 스트링 포함",
},
},
# 경계 케이스 처리 지침
"edge_case_rules": [
"기능은 맞지만 보안 취약점이 있으면 → 최대 3점",
"동작하지만 deprecated API 사용 → 4점",
"올바르지만 매우 비효율적 O(n²) 가능한 O(n) → 최대 3점",
],
# 불일치 해소 프로세스
"disagreement_resolution": "±1점 이내 불일치는 평균, 그 이상은 제3 주석자",
}
2.3 주석자 간 일치도 체크 (파일럿 라운드)
황금 세트를 전량 레이블링하기 전에, 50개 샘플로 파일럿을 돌려 주석자 간 일치도를 확인합니다.
from sklearn.metrics import cohen_kappa_score
import numpy as np
def pilot_annotation_check(
annotator_scores: dict[str, list[int]],
# {"annotator_1": [3, 4, 2, ...], "annotator_2": [3, 5, 2, ...], ...}
min_kappa: float = 0.60,
) -> dict:
"""
파일럿 라운드 주석자 간 일치도 측정.
κ < min_kappa면 가이드라인 재검토 필요.
"""
annotators = list(annotator_scores.keys())
n = len(annotators)
pairwise_kappas = {}
for i in range(n):
for j in range(i + 1, n):
a1, a2 = annotators[i], annotators[j]
kappa = cohen_kappa_score(
annotator_scores[a1],
annotator_scores[a2],
weights="quadratic", # 순서형 척도에 가중 카파
)
pairwise_kappas[f"{a1} vs {a2}"] = round(kappa, 3)
mean_kappa = np.mean(list(pairwise_kappas.values()))
# 가장 많이 불일치하는 샘플 인덱스
all_scores = list(annotator_scores.values())
score_ranges = [
max(scores[i] for scores in all_scores)
- min(scores[i] for scores in all_scores)
for i in range(len(all_scores[0]))
]
problematic_indices = [
i for i, r in enumerate(score_ranges) if r >= 2
]
return {
"pairwise_kappas": pairwise_kappas,
"mean_kappa": round(mean_kappa, 3),
"production_ready": mean_kappa >= min_kappa,
"problematic_sample_indices": problematic_indices,
"recommendation": (
"파일럿 통과 — 전량 레이블링 진행" if mean_kappa >= min_kappa
else f"가이드라인 재검토 필요 (κ={mean_kappa:.3f}, 목표 {min_kappa})"
),
}
3. Cohen's κ — 완전 해부
3.1 원리: 왜 단순 정확도가 아닌가
단순 일치율(Percent Agreement)은 우연에 의한 일치를 포함합니다. 10개 중 7개를 동의하면 70%지만, 두 주석자가 무작위로 평가해도 특정 확률로 우연히 일치합니다. Cohen's κ는 우연 일치를 제거합니다.
κ = (Po - Pe) / (1 - Pe)
Po = 관찰된 일치율 (Observed Agreement)
Pe = 우연에 의한 기대 일치율 (Expected Agreement by Chance)
from sklearn.metrics import cohen_kappa_score
import numpy as np
from scipy import stats
def comprehensive_agreement(
human_scores: list[int],
judge_scores: list[int],
scale: str = "ordinal", # "binary" | "ordinal" | "continuous"
) -> dict:
"""
판사-인간 일치도 종합 측정.
"""
h = np.array(human_scores)
j = np.array(judge_scores)
# 1. 단순 일치율 (기준선)
exact_match = float(np.mean(h == j))
off_by_one = float(np.mean(np.abs(h - j) <= 1))
# 2. Cohen's κ (일반 — 비순서형)
kappa_simple = cohen_kappa_score(h, j)
# 3. 가중 Cohen's κ (순서형 척도 권장)
# quadratic weight: 2점 차이는 1점 차이보다 4배 패널티
kappa_weighted = cohen_kappa_score(h, j, weights="quadratic")
# 4. Pearson 상관 (연속형 점수)
pearson_r, pearson_p = stats.pearsonr(h, j)
# 5. Spearman 상관 (순위 기반)
spearman_r, spearman_p = stats.spearmanr(h, j)
# 6. 평균 절대 오차
mae = float(np.mean(np.abs(h - j)))
# 7. 스코어별 오류 분포 (어떤 점수에서 틀리는가)
error_by_score = {}
for score in sorted(set(h)):
mask = h == score
if mask.sum() > 0:
score_mae = float(np.mean(np.abs(h[mask] - j[mask])))
error_by_score[int(score)] = {
"n": int(mask.sum()),
"mae": round(score_mae, 3),
"mean_judge_score": round(float(j[mask].mean()), 2),
}
return {
"n_samples": len(h),
"exact_match_rate": round(exact_match, 4),
"off_by_one_rate": round(off_by_one, 4),
"cohens_kappa_simple": round(kappa_simple, 4),
"cohens_kappa_weighted": round(kappa_weighted, 4),
"pearson_r": round(pearson_r, 4),
"spearman_r": round(spearman_r, 4),
"mean_absolute_error": round(mae, 3),
"error_by_score": error_by_score,
"production_grade": _grade_judge(kappa_weighted),
}
def _grade_judge(kappa: float) -> dict:
"""판사 신뢰도 등급 결정"""
if kappa >= 0.80:
return {"grade": "A", "label": "거의 완벽한 일치", "production": True, "note": "고위험 평가에도 사용 가능"}
elif kappa >= 0.60:
return {"grade": "B", "label": "강한 일치", "production": True, "note": "일반 모니터링에 적합"}
elif kappa >= 0.40:
return {"grade": "C", "label": "보통 일치", "production": False, "note": "S8 완화 전략 적용 후 재측정"}
elif kappa >= 0.20:
return {"grade": "D", "label": "약한 일치", "production": False, "note": "판사 교체 또는 루브릭 재설계 필요"}
else:
return {"grade": "F", "label": "우연 수준", "production": False, "note": "이 판사는 사용하면 안 됨"}
3.2 가중 κ vs 일반 κ
순서형 척도(1~5점)에서는 반드시 **가중 카파(Weighted κ)**를 써야 합니다. 3점을 4점으로 틀리는 것과 3점을 1점으로 틀리는 것은 같은 "불일치"가 아닙니다.
# 가중 방식 비교
# Linear weights: |i-j|에 비례한 패널티
kappa_linear = cohen_kappa_score(human, judge, weights="linear")
# Quadratic weights: (i-j)²에 비례 — 큰 불일치를 더 강하게 패널티
kappa_quad = cohen_kappa_score(human, judge, weights="quadratic")
# 권장 가이드라인:
# 1~5점 리커트 척도 → quadratic weights
# 이진(pass/fail) → 일반 κ
# 다범주 순서 없는 레이블 → 일반 κ
3.3 Krippendorff's α — 다중 주석자, 결측치 처리
황금 세트에 주석자가 3명 이상이거나, 일부 샘플에 레이블이 없을 때 Cohen's κ보다 Krippendorff's α가 더 적합합니다.
def krippendorff_alpha(
ratings: list[list[float | None]], # ratings[rater][item], None = 결측
level: str = "ordinal", # "nominal" | "ordinal" | "interval" | "ratio"
) -> float:
"""
Krippendorff's α 계산.
다중 주석자 + 결측치 환경에 적합.
"""
import itertools
# None 제거하고 유효한 (rater, item) 쌍만 사용
n_items = len(ratings[0])
n_raters = len(ratings)
# 차이 함수
def delta(v1, v2, level):
if level == "nominal":
return 0 if v1 == v2 else 1
elif level in ("ordinal", "interval", "ratio"):
return (v1 - v2) ** 2
# 관찰된 불일치 Do
observed_disagreement = 0.0
n_observed_pairs = 0
for item_idx in range(n_items):
item_ratings = [
r[item_idx] for r in ratings
if r[item_idx] is not None
]
if len(item_ratings) < 2:
continue
for v1, v2 in itertools.combinations(item_ratings, 2):
observed_disagreement += delta(v1, v2, level)
n_observed_pairs += 1
if n_observed_pairs == 0:
return float("nan")
Do = observed_disagreement / n_observed_pairs
# 기대 불일치 De (모든 값의 쌍)
all_valid = [
v for rater in ratings for v in rater if v is not None
]
expected_disagreement = sum(
delta(v1, v2, level)
for v1, v2 in itertools.combinations(all_valid, 2)
)
n_all_pairs = len(all_valid) * (len(all_valid) - 1) / 2
De = expected_disagreement / n_all_pairs if n_all_pairs > 0 else 0
if De == 0:
return 1.0 if Do == 0 else float("-inf")
return round(1 - Do / De, 4)
3.4 상관 vs 일치 — 왜 r만으로 부족한가
많은 팀이 Pearson r이 높으면 판사가 신뢰할 수 있다고 생각합니다. 틀렸습니다.
# 상관은 높지만 실제로 쓸모없는 판사의 예시
human = [1, 2, 3, 4, 5] # 인간 점수
judge = [2, 3, 4, 5, 6] # 판사 점수 (체계적으로 +1 편향)
from scipy.stats import pearsonr
from sklearn.metrics import cohen_kappa_score
r, _ = pearsonr(human, judge)
print(f"Pearson r: {r:.3f}") # → 1.000 (완벽한 상관!)
# 하지만 실제 판단을 5점 척도 기준으로 맞추면
human_clipped = [min(5, max(1, h)) for h in human]
judge_clipped = [min(5, max(1, j)) for j in judge]
kappa = cohen_kappa_score(human_clipped, judge_clipped, weights="quadratic")
print(f"Weighted κ: {kappa:.3f}") # → 낮음 (체계적 편향 포착)
# 핵심 차이:
# 상관: 선형 관계의 강도 (순위가 같으면 높음)
# κ: 실제 레이블이 얼마나 일치하는가 (절대값 중요)
루브릭 기반 평가를 통계적으로 분석할 때는 쌍 측정으로 처리해야 합니다. 서열 척도나 비정규 분포에서는 Wilcoxon 부호 순위 검정, 이진 pass/fail에는 McNemar 검정을 사용하는 것이 적절합니다.
4. Bradley-Terry 모델 — 페어와이즈 비교를 절대 강도로
Pointwise 평가는 절대 점수를 직접 주지만, Pairwise 비교는 상대적 우위만 알려줍니다. 여러 모델의 페어와이즈 비교 결과를 일관된 절대 강도 추정치로 변환하는 것이 Bradley-Terry 모델입니다.
LMSYS는 Chatbot Arena의 랭킹 방법을 온라인 Elo에서 Bradley-Terry 모델로 전환했고, 이 전환이 훨씬 안정적인 평점과 정밀한 신뢰 구간을 제공했습니다.
4.1 원리
Bradley-Terry 모델:
P(모델 i가 모델 j를 이김) = exp(βᵢ) / (exp(βᵢ) + exp(βⱼ))
βᵢ: 모델 i의 로그 강도 (log-strength)
강도 매개변수 β를 최대우도추정(MLE)으로 추정합니다. 비교 전체를 동시에 고려하므로, Elo처럼 최근 게임에 더 가중치를 두지 않습니다.
import numpy as np
from scipy.optimize import minimize
from scipy.stats import chi2
def bradley_terry_fit(
wins_matrix: np.ndarray,
# wins_matrix[i][j] = 모델 i가 모델 j를 이긴 횟수
model_names: list[str] | None = None,
tie_weight: float = 0.5, # 타이는 0.5승으로 처리
) -> dict:
"""
Bradley-Terry 모델 MLE 추정.
Chatbot Arena 방식.
"""
n = wins_matrix.shape[0]
if model_names is None:
model_names = [f"model_{i}" for i in range(n)]
def neg_log_likelihood(log_strengths: np.ndarray) -> float:
"""음의 로그우도 (최소화)"""
total = 0.0
for i in range(n):
for j in range(i + 1, n):
n_ij = wins_matrix[i][j] + wins_matrix[j][i] # 총 비교 수
if n_ij == 0:
continue
w_ij = wins_matrix[i][j] # i가 j를 이긴 횟수
# P(i wins) = softmax(β_i, β_j)
log_p_i = log_strengths[i] - np.logaddexp(
log_strengths[i], log_strengths[j]
)
log_p_j = log_strengths[j] - np.logaddexp(
log_strengths[i], log_strengths[j]
)
total -= w_ij * log_p_i + wins_matrix[j][i] * log_p_j
return total
# 최적화 (β₀ = 0으로 고정해 식별 가능성 확보)
result = minimize(
neg_log_likelihood,
x0=np.zeros(n),
method="L-BFGS-B",
options={"maxiter": 1000, "ftol": 1e-10},
)
log_strengths = result.x
# 0번 모델을 기준(0)으로 재정규화
log_strengths -= log_strengths[0]
# 강도를 Elo 스케일로 변환 (선택적 — 가독성)
# Elo ≈ β × (400 / ln(10))
elo_scores = log_strengths * (400 / np.log(10)) + 1000
# 모델 간 예측 승률
win_probs = {}
for i in range(n):
for j in range(i + 1, n):
p_i = np.exp(log_strengths[i]) / (
np.exp(log_strengths[i]) + np.exp(log_strengths[j])
)
win_probs[f"{model_names[i]} vs {model_names[j]}"] = {
"p_win_a": round(p_i, 4),
"p_win_b": round(1 - p_i, 4),
}
# 랭킹
ranking = sorted(
enumerate(log_strengths),
key=lambda x: x[1],
reverse=True
)
return {
"log_strengths": {model_names[i]: round(s, 4) for i, s in enumerate(log_strengths)},
"elo_scores": {model_names[i]: round(e, 1) for i, e in enumerate(elo_scores)},
"ranking": [(model_names[i], round(log_strengths[i], 4)) for i, _ in ranking],
"win_probabilities": win_probs,
"converged": result.success,
}
# 사용 예시
models = ["GPT-5.5", "Claude Opus 4.7", "Gemini 3.5 Flash", "Grok 4.3"]
# 모의 wins_matrix: wins_matrix[i][j] = i가 j를 이긴 횟수
wins = np.array([
[0, 45, 60, 70], # GPT-5.5의 승리
[55, 0, 58, 72], # Claude의 승리
[40, 42, 0, 65], # Gemini Flash의 승리
[30, 28, 35, 0], # Grok의 승리
])
result = bradley_terry_fit(wins, models)
print(result["ranking"])
# → [("Claude Opus 4.7", 0.245), ("GPT-5.5", 0.178), ...]
4.2 부트스트랩 신뢰 구간
단일 추정치만으로는 불충분합니다. 불확실성을 정량화해야 합니다.
def bt_bootstrap_ci(
wins_matrix: np.ndarray,
model_names: list[str],
n_bootstrap: int = 1000,
ci_level: float = 0.95,
) -> dict:
"""
부트스트랩으로 Bradley-Terry 추정의 신뢰 구간 계산.
"""
n = wins_matrix.shape[0]
bootstrap_strengths = {name: [] for name in model_names}
for _ in range(n_bootstrap):
# 비교 데이터를 리샘플링
resampled_wins = np.zeros_like(wins_matrix)
for i in range(n):
for j in range(i + 1, n):
total = int(wins_matrix[i][j] + wins_matrix[j][i])
if total == 0:
continue
p_i = wins_matrix[i][j] / total
# 이항 리샘플링
resampled_i = int(np.random.binomial(total, p_i))
resampled_wins[i][j] = resampled_i
resampled_wins[j][i] = total - resampled_i
try:
fit = bradley_terry_fit(resampled_wins, model_names)
for name, strength in fit["log_strengths"].items():
bootstrap_strengths[name].append(strength)
except Exception:
continue
alpha = 1 - ci_level
ci_results = {}
for name, strengths in bootstrap_strengths.items():
strengths = np.array(strengths)
ci_results[name] = {
"mean": round(float(np.mean(strengths)), 4),
"lower": round(float(np.percentile(strengths, alpha/2 * 100)), 4),
"upper": round(float(np.percentile(strengths, (1-alpha/2) * 100)), 4),
"std": round(float(np.std(strengths)), 4),
}
return ci_results
5. 실용 기준 — 언제 판사를 신뢰할 수 있나
현재 최고 수준의 LLM 판사(GPT-4 계열, DeepSeek-V2.5)도 인간 판단을 완전히 대체하지 못하며, 최고 정확도("Accboth")가 0.7 미만입니다. 이 현실적 상한선을 고려한 실용 기준입니다.
JUDGE_PRODUCTION_CRITERIA = {
"grade_A": {
"kappa_threshold": 0.80,
"use_cases": [
"RLHF 선호 데이터 수집",
"모델 릴리즈 결정",
"고위험 안전성 평가",
],
"note": "인간 전문가 수준, 추가 검증 없이 사용 가능",
},
"grade_B": {
"kappa_threshold": 0.60,
"use_cases": [
"일반 품질 모니터링",
"A/B 테스트 (낮은 위험)",
"지속적 CI 게이트",
],
"note": "프로덕션 표준. 고위험 결정에는 인간 검토 병행",
},
"grade_C": {
"kappa_threshold": 0.40,
"use_cases": [
"대량 스크리닝 (1차 필터만)",
"경향 파악 (절대값 신뢰 불가)",
],
"note": "단독 사용 금지. S8 전략 + Cross-family 판사로 재시도",
},
"grade_D_F": {
"kappa_threshold": 0.20,
"use_cases": [],
"note": "사용 금지. 판사 교체 또는 루브릭 전면 재설계",
},
}
def judge_readiness_report(
kappa: float,
domain: str,
n_samples: int,
) -> dict:
"""판사 사용 준비도 보고서 생성"""
# 도메인 전문성 갭 보정
# 전문 도메인(의료, 법률, 금융)에서는 인간-LLM 일치율이
# 10~15% 낮아지므로 임계값을 높게 설정
DOMAIN_ADJUSTMENTS = {
"general": 0.00,
"coding": 0.00,
"medical": 0.10, # 임계값 +0.10
"legal": 0.10,
"finance": 0.05,
}
domain_adj = DOMAIN_ADJUSTMENTS.get(domain, 0.05)
adjusted_kappa = kappa - domain_adj
# 샘플 크기 신뢰도
# 200개 미만이면 신뢰 구간이 넓어 κ 해석 주의
sample_confidence = (
"high" if n_samples >= 500
else "medium" if n_samples >= 200
else "low (κ 해석 주의 — 200개 미만)"
)
# 등급 결정
if adjusted_kappa >= 0.80:
grade, production = "A", True
elif adjusted_kappa >= 0.60:
grade, production = "B", True
elif adjusted_kappa >= 0.40:
grade, production = "C", False
else:
grade, production = "D/F", False
return {
"raw_kappa": kappa,
"domain": domain,
"domain_adjustment": -domain_adj,
"adjusted_kappa": round(adjusted_kappa, 3),
"grade": grade,
"production_ready": production,
"sample_size": n_samples,
"sample_confidence": sample_confidence,
"recommendation": JUDGE_PRODUCTION_CRITERIA.get(
f"grade_{grade}", {}
).get("note", ""),
}
6. 오류 분석 — 판사가 어디서 틀리는가
κ 숫자 하나보다 판사가 어떤 케이스에서 틀리는지가 더 중요한 정보입니다.
def error_analysis(
golden_set: list[dict],
# [{"question": str, "response": str,
# "human_score": int, "judge_score": int,
# "category": str, "difficulty": str}, ...]
) -> dict:
"""
판사 오류의 구조적 패턴 분석.
"""
import pandas as pd
df = pd.DataFrame(golden_set)
df["error"] = df["judge_score"] - df["human_score"]
df["abs_error"] = df["error"].abs()
# 1. 카테고리별 오류
category_errors = df.groupby("category")["abs_error"].agg(
["mean", "std", "count"]
).round(3).to_dict("index")
# 2. 난이도별 오류
difficulty_errors = df.groupby("difficulty")["abs_error"].agg(
["mean", "count"]
).round(3).to_dict("index")
# 3. 체계적 편향 방향
# 양수: 판사가 인간보다 후하게 평가 (과대평가)
# 음수: 판사가 인간보다 낮게 평가 (과소평가)
mean_bias = float(df["error"].mean())
# 4. 최악의 오류 케이스
worst_cases = (
df.nlargest(5, "abs_error")
[["question", "human_score", "judge_score", "error"]]
.to_dict("records")
)
# 5. 점수별 혼동 행렬
from sklearn.metrics import confusion_matrix
labels = sorted(df["human_score"].unique())
cm = confusion_matrix(
df["human_score"],
df["judge_score"],
labels=labels,
).tolist()
return {
"overall_mae": round(float(df["abs_error"].mean()), 3),
"systematic_bias": round(mean_bias, 3),
"bias_direction": (
"과대평가 (판사가 인간보다 후함)" if mean_bias > 0.2
else "과소평가 (판사가 인간보다 엄격)" if mean_bias < -0.2
else "중립적"
),
"category_errors": category_errors,
"difficulty_errors": difficulty_errors,
"worst_cases": worst_cases,
"confusion_matrix": {"labels": labels, "matrix": cm},
}
7. 전체 Meta-evaluation 파이프라인
class MetaEvaluationPipeline:
"""
판사 신뢰도 측정 전체 파이프라인.
황금 세트 → 측정 → 보고서 → 캘리브레이션 결정.
"""
def __init__(
self,
golden_set: list[dict],
judge_fn, # (question, response) → score
judge_config: dict, # {model_id, rubric_version, prompt_hash}
domain: str = "general",
):
self.golden_set = golden_set
self.judge_fn = judge_fn
self.judge_config = judge_config
self.domain = domain
def run(self) -> dict:
"""전체 메타평가 실행"""
# 1. 판사 점수 수집
judge_scores = []
for sample in self.golden_set:
score = self.judge_fn(sample["question"], sample["response"])
judge_scores.append(round(score))
human_scores = [s["human_score"] for s in self.golden_set]
# 2. 일치도 측정
agreement = comprehensive_agreement(human_scores, judge_scores)
# 3. 준비도 보고서
readiness = judge_readiness_report(
kappa=agreement["cohens_kappa_weighted"],
domain=self.domain,
n_samples=len(self.golden_set),
)
# 4. 오류 분석
annotated = [
{**s, "judge_score": js}
for s, js in zip(self.golden_set, judge_scores)
]
errors = error_analysis(annotated)
# 5. 캘리브레이션 결정
recal_needed = (
not readiness["production_ready"]
or errors["systematic_bias"] > 0.3
)
return {
"judge_config": self.judge_config,
"evaluation_date": __import__("datetime").datetime.utcnow().isoformat(),
"agreement_metrics": agreement,
"readiness_report": readiness,
"error_analysis": errors,
"recalibration_needed": recal_needed,
"recommended_actions": self._recommend(readiness, errors),
}
def _recommend(self, readiness: dict, errors: dict) -> list[str]:
actions = []
grade = readiness["grade"]
if grade == "A":
actions.append("✅ 프로덕션 사용 가능 — 60일 후 재캘리브레이션")
elif grade == "B":
actions.append("✅ 일반 모니터링 사용 가능")
actions.append("⚠️ 고위험 결정에는 인간 검토 병행")
elif grade == "C":
actions.append("❌ 단독 사용 금지")
actions.append("🔧 S8 전략 + Cross-family 판사 적용 후 재측정")
else:
actions.append("❌ 이 판사 사용 금지")
actions.append("🔧 루브릭 전면 재설계 또는 판사 모델 교체")
if errors["bias_direction"] == "과대평가 (판사가 인간보다 후함)":
actions.append("📊 루브릭에 엄격한 기준 예시 추가")
elif errors["bias_direction"] == "과소평가 (판사가 인간보다 엄격)":
actions.append("📊 루브릭에 긍정적 케이스 예시 추가")
# 특정 카테고리에서 큰 오류
worst_category = max(
errors["category_errors"].items(),
key=lambda x: x[1]["mean"],
default=(None, None),
)
if worst_category[0] and worst_category[1]["mean"] > 1.0:
actions.append(
f"🎯 '{worst_category[0]}' 카테고리에서 MAE {worst_category[1]['mean']:.2f} "
f"— 해당 카테고리 루브릭 별도 설계 고려"
)
return actions
✅ 5편 정리
항목 핵심 원칙
| 황금 세트 크기 | 최소 200개, 권장 500개, easy/medium/hard/adversarial 분층 |
| 주석 가이드라인 | 점수별 앵커 + 경계 케이스 규칙 + 불일치 해소 프로세스 |
| 파일럿 라운드 | 50개로 주석자 간 κ 측정, κ < 0.60이면 가이드라인 재검토 |
| Cohen's κ | 순서형 척도 → 가중 κ (quadratic weights) 필수 |
| κ 해석 | 0.80+ Grade A, 0.60+ Grade B(프로덕션 OK), 0.40 미만 사용 금지 |
| 상관 vs 일치 | Pearson r 높아도 체계적 편향 있으면 κ 낮음 — κ가 실무 기준 |
| Bradley-Terry | 페어와이즈 결과 → 절대 강도, 부트스트랩으로 CI 산출 |
| 오류 분석 | 카테고리별 MAE + 체계적 편향 방향이 κ보다 더 많은 정보 제공 |
루브릭 기반 평가에서 "원시 판사 점수는 캘리브레이션 없이 크로스 스터디나 시간 비교에 직접 사용해선 안 된다"는 것이 연구 합의입니다. Meta-evaluation은 판사를 신뢰하기 위한 투자가 아닙니다. 판사를 틀리게 신뢰하지 않기 위한 투자입니다.
LLM-as-a-Judge 완전정리 시리즈 — 완결
- ✅ 1편 — 왜 기존 지표는 죽었고, 세 패러다임은 무엇인가 https://cell-devlog.tistory.com/265
- ✅ 2편 — 판사는 어디서 거짓말하나: 7가지 편향 해부 https://cell-devlog.tistory.com/266
- ✅ 3편 — 편향 잡는 법: Position Swap부터 Cross-family까지 https://cell-devlog.tistory.com/267
- ✅ 4편 — G-Eval vs Prometheus 2 vs PAJAMA vs Themis https://cell-devlog.tistory.com/268
- ✅ 5편 — 판사를 평가하기: Cohen's κ, Bradley-Terry, 황금 세트 설계 https://cell-devlog.tistory.com/269
- ✅ 6편 — 프로덕션 파이프라인: 샘플링·CI 게이트·캘리브레이션 주기 https://cell-devlog.tistory.com/270
- ✅ 7편 — 한계와 대안: LLM 판사가 절대 못 하는 것들 https://cell-devlog.tistory.com/271
'AI Agent' 카테고리의 다른 글
| LLM as a Judge 완전정리 7편 — 판사가 절대 못 하는 것들: 한계와 대안 (0) | 2026.05.26 |
|---|---|
| LLM as a Judge 완전정리 6편 — 프로덕션 파이프라인: 샘플링·CI 게이트·캘리브레이션 주기 (0) | 2026.05.26 |
| LLM as a Judge 완전정리 4편 — G-Eval vs Prometheus 2 vs PAJAMA vs Themis: 무엇을 쓸 것인가 (0) | 2026.05.26 |
| LLM as a Judge 완전정리 3편 — 편향 잡는 법: 전략별 코드와 효과 비교 (0) | 2026.05.26 |
| LLM as a Judge 완전정리 2편 — 판사는 어디서 거짓말하나: 7가지 편향 해부 (0) | 2026.05.26 |