반응형
기본 개념은 알겠는데, 실제로 쓰면 점수가 이상합니다. 짧은 답변이 낮게 나오고, 순서만 바꿔도 결과가 뒤집힙니다. 이게 왜 일어나는지, 어떻게 막는지 파고들어 봅니다.
📌 핵심 요약
→ LLM Judge의 인간 동의율 85% — 하지만 편향이 있으면 그 85%가 틀린 방향으로 수렴
→ 주요 편향 4가지: Position / Verbosity / Self-preference / Preference Leakage
→ G-Eval: 루브릭 기반 채점, CoT로 신뢰도 10~15% 향상
→ DAG 방식: 비결정적 G-Eval의 한계를 구조화된 평가 그래프로 보완
→ LLM Jury: 여러 모델을 배심원단처럼 운용, 단일 Judge 편향 희석
→ Preference Leakage: Judge와 피평가 모델이 같은 계열이면 채점이 오염됨
→ 프로덕션 황금 공식: LLM Judge(속도) + 인간 리뷰(엣지케이스) 혼합
→ 실전 코드: G-Eval, Jury 패턴, 위치 편향 방어까지 전부 다룸
실전1 — 편향 4종 해부: 왜 점수가 틀리는가
LLM Judge가 낮은 점수를 주는데 실제로는 좋은 답변인 경우, 대부분 아래 4가지 편향 중 하나입니다.
# 편향 실험: 순서만 바꿔도 결과가 달라지는지 확인
import anthropic
client = anthropic.Anthropic()
JUDGE_PROMPT = """
다음 두 답변 중 더 나은 것을 고르세요.
질문: {question}
답변 A: {answer_a}
답변 B: {answer_b}
더 나은 답변은 A입니까 B입니까? 이유를 포함해 답하세요.
"""
question = "Python에서 리스트와 튜플의 차이는?"
good_answer = "리스트는 mutable(변경 가능), 튜플은 immutable(변경 불가)입니다. 리스트는 [], 튜플은 ()로 선언합니다."
bad_answer = "둘 다 여러 값을 담는 자료구조인데 조금 다릅니다."
# 순서 1: good이 A
r1 = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": JUDGE_PROMPT.format(
question=question, answer_a=good_answer, answer_b=bad_answer
)}]
)
# 순서 2: good이 B (순서만 뒤집음)
r2 = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": JUDGE_PROMPT.format(
question=question, answer_a=bad_answer, answer_b=good_answer
)}]
)
print("순서1 결과:", r1.content[0].text[:100])
print("순서2 결과:", r2.content[0].text[:100])
# → 실제로 돌려보면 같은 모델이 다른 답을 고르는 경우가 10~15% 발생
→ [Position Bias] 첫 번째 위치의 답변을 더 선호하는 경향 — 순서만 바꿔도 판정 뒤집힘
→ [Verbosity Bias] 긴 답변을 무조건 더 좋다고 평가 — RLHF 학습 과정의 부산물
→ [Self-preference Bias] Claude가 Judge면 Claude스러운 답변에 높은 점수
∟ 자신의 출력 패턴과 낮은 perplexity = 더 친숙 = 더 높은 점수
→ [Preference Leakage] Judge와 피평가 모델이 같은 계열(같은 베이스모델, 파인튜닝 관계)이면
∟ 학습 데이터가 겹쳐 "아는 맛" 효과 발생 — ICLR 2026 채택 논문에서 공식 확인
실전2 — G-Eval: 루브릭 기반 채점으로 신뢰도 올리기
단순히 "어느 게 낫냐"고 묻는 대신, 평가 기준을 명시적으로 정의하고 CoT로 채점하는 방식입니다.
from anthropic import Anthropic
client = Anthropic()
G_EVAL_PROMPT = """당신은 AI 답변 품질 평가 전문가입니다.
[평가 기준]
1. 정확성 (Accuracy): 사실적으로 올바른가? (0~5)
2. 관련성 (Relevance): 질문에 직접 답하는가? (0~5)
3. 완결성 (Completeness): 중요한 내용이 빠지지 않았는가? (0~5)
4. 간결성 (Conciseness): 불필요한 내용이 없는가? (0~5)
[평가 방법]
각 기준마다 아래 순서로 평가하세요:
1) 해당 기준에서 좋은 점 나열
2) 부족한 점 나열
3) 점수 결정 및 이유 한 줄 요약
[입력]
질문: {question}
답변: {answer}
위 기준에 따라 단계적으로 평가하고, 마지막에 JSON으로 결과를 출력하세요:
{{"accuracy": N, "relevance": N, "completeness": N, "conciseness": N, "total": N, "reason": "..."}}"""
def g_eval(question: str, answer: str) -> dict:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
messages=[{
"role": "user",
"content": G_EVAL_PROMPT.format(question=question, answer=answer)
}]
)
text = response.content[0].text
# JSON 파싱
import json, re
match = re.search(r'\{.*\}', text, re.DOTALL)
if match:
return json.loads(match.group())
return {}
result = g_eval(
question="Python GIL이 무엇인가요?",
answer="GIL(Global Interpreter Lock)은 CPython에서 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 하는 뮤텍스입니다. CPU-bound 작업에서 멀티스레딩 성능을 제한하지만, I/O-bound 작업에서는 큰 영향이 없습니다."
)
print(result)
# → {"accuracy": 5, "relevance": 5, "completeness": 4, "conciseness": 5, "total": 19, "reason": "..."}
→ CoT(단계적 사고) 강제로 신뢰도 10~15% 향상 — "왜"를 먼저 쓰게 하면 결론이 달라짐
→ 루브릭을 구체적으로 쓸수록 편향 감소 — "좋다" 대신 "정확한 기술 용어 사용 여부"
→ G-Eval의 한계: 비결정적 — 같은 입력에 다른 점수가 나올 수 있음
→ 해결: temperature=0 고정, 동일 입력 3회 실행 후 평균 사용 권장
실전3 — DAG 방식: 결정론적 평가가 필요할 때
G-Eval이 주관적 평가에 강하다면, DAG(Directed Acyclic Graph)는 명확한 기준이 있는 평가에 적합합니다.
# DAG 평가: 조건을 트리 구조로 순차 검증
from anthropic import Anthropic
import json
client = Anthropic()
def check_single_criterion(answer: str, criterion: str, description: str) -> dict:
"""단일 기준 하나만 체크 — 결정론적"""
prompt = f"""다음 답변이 아래 기준을 충족하는지 판단하세요.
기준: {criterion}
설명: {description}
답변:
{answer}
반드시 JSON으로만 응답하세요:
{{"pass": true/false, "reason": "한 줄 이유"}}"""
r = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=200,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(r.content[0].text)
def dag_evaluate(answer: str) -> dict:
"""
DAG 평가 흐름:
[사실 확인] → pass → [관련성 확인] → pass → [안전성 확인] → 최종 점수
→ fail → 즉시 0점 반환
"""
results = {}
# 노드 1: 사실 확인 (가장 중요, 실패시 즉시 종료)
r1 = check_single_criterion(
answer,
"사실 정확성",
"답변에 명백히 틀린 기술 정보가 없어야 함"
)
results["accuracy"] = r1
if not r1["pass"]:
return {"score": 0, "reason": "사실 오류 감지", "details": results}
# 노드 2: 관련성 확인
r2 = check_single_criterion(
answer,
"질문 관련성",
"질문에서 벗어난 내용이 없어야 함"
)
results["relevance"] = r2
if not r2["pass"]:
return {"score": 3, "reason": "관련성 부족", "details": results}
# 노드 3: 안전성 확인
r3 = check_single_criterion(
answer,
"안전성",
"유해하거나 부적절한 내용이 없어야 함"
)
results["safety"] = r3
if not r3["pass"]:
return {"score": 0, "reason": "안전성 위반", "details": results}
return {"score": 10, "reason": "모든 기준 통과", "details": results}
result = dag_evaluate("Python의 GIL은 멀티스레딩을 완전히 불가능하게 만듭니다.")
print(json.dumps(result, ensure_ascii=False, indent=2))
# → score: 0 (사실 오류 — I/O 스레딩은 가능하므로)
→ DAG의 핵심: 한 번에 하나의 기준만 판단 → 결정론적, 디버깅 가능
→ 중요한 기준을 앞에 배치해 실패시 early exit → API 비용 절감
→ G-Eval vs DAG 선택 기준:
∙ 주관적 품질(창의성, 일관성) → G-Eval
∙ 명확한 패스/실패 기준(사실 오류, 포맷 검사) → DAG
실전4 — LLM Jury: 배심원단 패턴으로 편향 희석
단일 Judge의 편향을 줄이는 가장 강력한 방법은 여러 모델을 배심원단처럼 운용하는 것입니다.
import anthropic
from statistics import mean, stdev
# 여러 모델을 배심원단으로 구성
# (실제 프로덕션에서는 OpenAI, Gemini API도 함께 사용 권장)
JUDGES = [
{"model": "claude-sonnet-4-20250514", "name": "Claude Sonnet"},
# {"model": "gpt-4o", "name": "GPT-4o"}, # openai 클라이언트 필요
# {"model": "gemini-2.0-flash", "name": "Gemini"}, # google 클라이언트 필요
]
SCORE_PROMPT = """다음 답변의 전반적인 품질을 0~10 사이 정수로만 답하세요.
평가 기준: 정확성, 명확성, 완결성
질문: {question}
답변: {answer}
점수(숫자만):"""
def jury_evaluate(question: str, answer: str, n_runs: int = 3) -> dict:
"""
같은 모델을 여러 번 실행 + 위치 편향 방어(순서 교란) 포함
실제로는 다른 모델들을 섞어 쓰는 것이 더 효과적
"""
client = anthropic.Anthropic()
all_scores = []
for judge in JUDGES:
scores_for_judge = []
for _ in range(n_runs):
try:
r = client.messages.create(
model=judge["model"],
max_tokens=10,
messages=[{
"role": "user",
"content": SCORE_PROMPT.format(
question=question,
answer=answer
)
}]
)
score = int(r.content[0].text.strip())
scores_for_judge.append(score)
except:
continue
if scores_for_judge:
all_scores.extend(scores_for_judge)
print(f"[{judge['name']}] 점수들: {scores_for_judge} → 평균: {mean(scores_for_judge):.1f}")
if len(all_scores) < 2:
return {"final_score": all_scores[0] if all_scores else None}
final = mean(all_scores)
variance = stdev(all_scores)
return {
"final_score": round(final, 2),
"variance": round(variance, 2),
"all_scores": all_scores,
# variance가 2 이상이면 판정이 불안정 → 인간 검토 플래그
"needs_human_review": variance >= 2.0
}
result = jury_evaluate(
question="비동기 프로그래밍이란?",
answer="비동기 프로그래밍은 작업 완료를 기다리지 않고 다음 작업을 실행하는 방식입니다. I/O 바운드 작업에서 성능을 크게 향상시킵니다."
)
print(result)
→ Jury 패턴의 핵심 원리: 다른 계열 모델이 같은 방향으로 편향될 확률은 낮음
→ variance(분산)가 높으면 판정 불안정 신호 → 인간 검토 트리거로 활용
→ 비용: 단일 Judge 대비 3~5배 → 중요한 평가에만 선택적 적용
→ Jury-on-Demand: 신뢰도 예측기로 "이 샘플은 Jury 필요" 판단 (2025년 연구)
실전5 — Preference Leakage 방어: 같은 계열 모델 금지
2026년 ICLR에서 공식 확인된 새로운 편향입니다. Judge와 피평가 모델이 같은 계열이면 채점이 오염됩니다.
# Preference Leakage 방어 체크리스트 구현
LEAKAGE_RISK_MAP = {
# (평가 모델 계열, 피평가 모델 계열): 위험도
("claude", "claude"): "HIGH", # 같은 모델 → 최고 위험
("gpt", "gpt"): "HIGH",
("claude", "gpt"): "LOW", # 다른 계열 → 안전
("gpt", "claude"): "LOW",
("claude", "gemini"): "LOW",
("gemini", "claude"): "LOW",
}
def check_leakage_risk(judge_model: str, target_model: str) -> str:
"""
사용 예:
judge_model = "claude-sonnet-4-20250514"
target_model = "claude-haiku-3-5" → HIGH RISK
"""
def get_family(model_name: str) -> str:
model_lower = model_name.lower()
if "claude" in model_lower: return "claude"
if "gpt" in model_lower: return "gpt"
if "gemini" in model_lower: return "gemini"
if "llama" in model_lower: return "llama"
if "qwen" in model_lower: return "qwen"
return "unknown"
judge_family = get_family(judge_model)
target_family = get_family(target_model)
risk = LEAKAGE_RISK_MAP.get((judge_family, target_family), "MEDIUM")
print(f"Judge: {judge_family} | Target: {target_family} | Risk: {risk}")
if risk == "HIGH":
print("⚠️ 같은 계열 모델 — Preference Leakage 위험. 다른 계열 Judge 사용 권장")
return risk
# 실전 가이드라인
check_leakage_risk("claude-sonnet-4-20250514", "claude-haiku-4-5")
# → HIGH RISK
check_leakage_risk("claude-sonnet-4-20250514", "gpt-4o")
# → LOW RISK
→ Preference Leakage 3가지 유형:
1) 동일 모델 (claude judge → claude output): 최고 위험
2) 상속 관계 (파인튜닝 베이스 동일): 중간 위험
3) 같은 패밀리 (claude sonnet → claude haiku): 중간 위험
→ 방어법: Judge는 반드시 다른 계열 모델 사용
→ 예산 없으면: 최소한 같은 모델의 다른 버전이라도 사용
실전6 — 프로덕션 Judge 파이프라인 전체 구조
import anthropic
import json
from enum import Enum
client = anthropic.Anthropic()
class ReviewLevel(Enum):
AUTO_PASS = "auto_pass" # 자동 통과
AUTO_FAIL = "auto_fail" # 자동 실패
HUMAN_REVIEW = "human_review" # 인간 검토 필요
def production_judge_pipeline(question: str, answer: str) -> dict:
"""
프로덕션 Judge 파이프라인
1) 빠른 DAG 검사 (치명적 오류 필터)
2) G-Eval 점수 (주관적 품질)
3) 점수 기반 라우팅 (자동 처리 vs 인간 리뷰)
"""
# Step 1: 치명적 오류 먼저 걸러내기 (DAG)
safety_check = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=100,
messages=[{"role": "user", "content": f"""
답변에 아래 중 하나라도 해당하면 "FAIL", 아니면 "PASS"만 출력하세요.
- 명백한 사실 오류
- 유해하거나 위험한 내용
- 질문과 전혀 무관한 내용
질문: {question}
답변: {answer}
"""}]
)
if "FAIL" in safety_check.content[0].text:
return {
"level": ReviewLevel.AUTO_FAIL.value,
"score": 0,
"reason": "안전성/사실성 검사 실패"
}
# Step 2: G-Eval 점수
score_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=300,
messages=[{"role": "user", "content": f"""
답변 품질을 평가하세요. 반드시 JSON만 출력:
질문: {question}
답변: {answer}
평가 (각 항목 0~10):
{{"clarity": N, "completeness": N, "accuracy": N}}"""}]
)
try:
scores = json.loads(score_response.content[0].text)
total = sum(scores.values()) / len(scores)
except:
total = 5.0 # 파싱 실패시 중간값
# Step 3: 점수 기반 라우팅
if total >= 8.0:
level = ReviewLevel.AUTO_PASS.value
elif total <= 3.0:
level = ReviewLevel.AUTO_FAIL.value
else:
level = ReviewLevel.HUMAN_REVIEW.value # 중간 구간 → 인간 검토
return {
"level": level,
"score": round(total, 2),
"details": scores if 'scores' in dir() else {}
}
result = production_judge_pipeline(
"파이썬 async/await 사용법은?",
"async def 함수에 await를 붙여 비동기 작업을 실행합니다. asyncio.run()으로 이벤트 루프를 시작합니다."
)
print(result)
→ 핵심 설계 원칙: LLM Judge는 속도, 인간은 정확도 — 역할 분리
→ 중간 구간(3~8점)만 인간 리뷰 → 비용 최소화하면서 품질 유지
→ IBM 연구자 말: "Judge로 판단력을 향상시켜라, 대체하지 말고"
→ RAND 연구(2026.03): 포맷 변경, 패러프레이징만으로도 Judge 일관성 붕괴 확인
∟ → 프로덕션에선 입력 정규화(공백, 마크다운 제거) 필수
✅ 이 글 핵심 정리
✅ 편향 4종 파악: Position / Verbosity / Self-preference / Preference Leakage
✅ G-Eval로 주관적 품질 채점, DAG로 결정론적 기준 검사
✅ Jury 패턴으로 단일 Judge 편향 희석 — variance로 불안정 감지
✅ Preference Leakage 방어: Judge ≠ 피평가 모델 계열
✅ 프로덕션 공식: DAG 필터 → G-Eval 점수 → 인간 리뷰 라우팅
❌ G-Eval 단독으론 결정론적 평가 불가 — DAG 병행 필수
❌ 같은 계열 모델로 Judge 쓰면 점수 오염
❌ Judge 완전 자동화는 현시점 무리 — 엣지케이스 인간 검토 필수
❌ temperature 미고정시 같은 입력에서 다른 점수 → 신뢰도 훼손
관련글
- LLM as a Judge 기초편 — 개념부터 첫 구현까지
- AI 에이전트 품질관리 전략 — Judge를 에이전트 파이프라인에 통합하는 법
반응형
'AI Agent' 카테고리의 다른 글
| LLM as a Judge 완전정리 1편 — 왜 기존 평가 지표는 죽었고, 무엇이 그 자리를 차지했나 (0) | 2026.05.26 |
|---|---|
| LLM-as-Judge 완전 가이드 3편—에이전트 자동 평가 루프와 CI/CD 통합, Judge를 진짜 파이프라인에 박아라 (0) | 2026.05.22 |
| AI 에이전트 Durable Execution 실전 2편 — Human-in-the-Loop·멀티에이전트·Serverless (0) | 2026.05.21 |
| AI 에이전트 Durable Execution 실전 1편 — 에이전트가 죽어도 이어지는 워크플로우 설계 (0) | 2026.05.21 |
| OpenTelemetry로 LLM 모니터링 — 블랙박스 에이전트를 투명하게 만드는 법 (0) | 2026.05.21 |