LLM Judge 심화
LLM Judge 심화 (LLM Judge Deep Dive)
LLMJudge 평가자는 루브릭을 기준으로 출력의 주관적 특성을 평가하기 위해 LLM을 사용해요.
출처: 문서
본문
LLM-as-a-Judge는 언제 쓸까요?
LLM judge는 이해와 판단이 필요한 특성을 평가하는 데 이상적이에요:
좋은 사용 사례:
- 사실 정확성
- 도움됨과 관련성
- 톤과 스타일 준수
- 응답의 완성도
- 복잡한 지시사항 따르기
- RAG 근거 강도 (답변이 제공된 컨텍스트를 사용하나요?)
- 인용 정확성
나쁜 사용 사례:
- 포맷 검증 (대신
IsInstance사용) - 정확한 일치 (대신
EqualsExpected) - 성능 검사 (대신
MaxDuration) - 결정적 로직 (커스텀 평가자 작성)
기본 사용법
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
dataset = Dataset(
name='factual_accuracy',
cases=[Case(inputs='test')],
evaluators=[
LLMJudge(rubric='Response is factually accurate'),
],
)
설정 옵션
루브릭
rubric은 평가 기준을 정의해요. 구체적이고 명확하게 작성해요:
나쁜 루브릭 (모호함):
from pydantic_evals.evaluators import LLMJudge
LLMJudge(rubric='Good response') # 너무 모호함
LLMJudge(rubric='Check quality') # 품질의 어떤 측면?
좋은 루브릭 (구체적):
from pydantic_evals.evaluators import LLMJudge
LLMJudge(rubric='Response directly answers the user question without hallucination')
LLMJudge(rubric='Response uses formal, professional language appropriate for business communication')
LLMJudge(rubric='All factual claims in the response are supported by the provided context')
컨텍스트 포함
판단자가 보는 정보를 제어해요:
from pydantic_evals.evaluators import LLMJudge
# 출력만 (기본)
LLMJudge(rubric='Response is polite')
# 출력 + 입력
LLMJudge(
rubric='Response accurately answers the input question',
include_input=True,
)
# 출력 + 입력 + 기대 출력
LLMJudge(
rubric='Response is semantically equivalent to the expected output',
include_input=True,
include_expected_output=True,
)
예시:
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
dataset = Dataset(
name='math_check',
cases=[
Case(
inputs='What is 2+2?',
expected_output='4',
),
],
evaluators=[
# 이 판단자는 다음을 봐요: 출력 + 입력 + 기대 출력
LLMJudge(
rubric='Response provides the same answer as expected, possibly with explanation',
include_input=True,
include_expected_output=True,
),
],
)
모델 선택
비용/품질 트레이드오프에 따라 judge 모델을 선택해요:
from pydantic_evals.evaluators import LLMJudge
# 기본: openai:gpt-5.2
LLMJudge(rubric='...')
# Anthropic Claude (대안 기본값)
LLMJudge(
rubric='...',
model='anthropic:claude-sonnet-4-6',
)
# 간단한 검사를 위한 경량 옵션
LLMJudge(
rubric='Response contains profanity',
model='openai:gpt-5.6-luna',
)
# 세밀한 평가를 위한 가장 강력한 옵션
LLMJudge(
rubric='Response demonstrates deep understanding of quantum mechanics',
model='openai:gpt-5.6-sol',
)
판단자 모델이 텍스트를 생성할 수 없을 때(예: TypeSafe의 Jev), LLMJudge는 합격/불합격 판정만 요청해요. 결과는 조작된 이유 대신 이유가 없으며, 점수는 합격이면 1.0, 불합격이면 0.0이에요. 모델은 프로필의 supports_text_output=False로 이를 알려줘요.
판정은 프롬프트가 담고 있는 섹션만 지목하는 하나의 예/아니오 질문이에요. 그래서 기대 출력 없이 판단된 루브릭은 기대 출력을 평가하도록 요청받지 않아요.
모델 설정
모델 동작을 커스터마이즈해요:
from pydantic_ai import ModelSettings
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='...',
model_settings=ModelSettings(
temperature=0.0, # 결정적 평가
max_tokens=100, # 더 짧은 응답
),
)
출력 모드
판정만 (기본)
이유와 함께 합격/불합격을 반환해요:
from pydantic_evals.evaluators import LLMJudge
LLMJudge(rubric='Response is accurate')
# 반환: {'LLMJudge_pass': EvaluationReason(value=True, reason='...')}
보고서에서:
┃ Assertions ┃
┃ ✔ ┃
점수만
숫자 점수(0.0 ~ 1.0)를 반환해요:
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='Response quality',
score={'include_reason': True},
assertion=False,
)
# 반환: {'LLMJudge_score': EvaluationReason(value=0.85, reason='...')}
보고서에서:
┃ Scores ┃
┃ LLMJudge_score: 0.85 ┃
점수와 판정 둘 다
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='Response quality',
score={'include_reason': True},
assertion={'include_reason': True},
)
# 반환: {
# 'LLMJudge_score': EvaluationReason(value=0.85, reason='...'),
# 'LLMJudge_pass': EvaluationReason(value=True, reason='...'),
# }
커스텀 이름
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='Response is factually accurate',
assertion={
'evaluation_name': 'accuracy',
'include_reason': True,
},
)
# 반환: {'accuracy': EvaluationReason(value=True, reason='...')}
보고서에서:
┃ Assertions ┃
┃ accuracy: ✔ ┃
실용 예시
RAG 평가
RAG 시스템이 제공된 컨텍스트를 사용하는지 평가해요:
from dataclasses import dataclass
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
@dataclass
class RAGInput:
question: str
context: str
dataset = Dataset(
name='rag_evaluation',
cases=[
Case(
inputs=RAGInput(
question='What is the capital of France?',
context='France is a country in Europe. Its capital is Paris.',
),
),
],
evaluators=[
LLMJudge(
rubric='Response answers the question using only information from the provided context',
include_input=True,
assertion={'evaluation_name': 'grounded', 'include_reason': True},
),
LLMJudge(
rubric='Response cites specific quotes or facts from the context',
include_input=True,
assertion={'evaluation_name': 'uses_citations', 'include_reason': True},
),
],
)
케이스별 루브릭으로 레시피 생성
이 예시는 데이터셋 수준과 케이스별 평가자를 모두 사용하는 방법을 보여줘요:
recipe_evaluation.py
from __future__ import annotations
from typing import Any
from pydantic import BaseModel
from pydantic_ai import Agent, format_as_xml
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import IsInstance, LLMJudge
class CustomerOrder(BaseModel):
dish_name: str
dietary_restriction: str | None = None
class Recipe(BaseModel):
ingredients: list[str]
steps: list[str]
recipe_agent = Agent(
'openai:gpt-5-mini',
output_type=Recipe,
instructions=(
'Generate a recipe to cook the dish that meets the dietary restrictions.'
),
)
async def transform_recipe(customer_order: CustomerOrder) -> Recipe:
r = await recipe_agent.run(format_as_xml(customer_order))
return r.output
recipe_dataset = Dataset[CustomerOrder, Recipe, Any](
name='recipe_evaluation',
cases=[
Case(
name='vegetarian_recipe',
inputs=CustomerOrder(
dish_name='Spaghetti Bolognese', dietary_restriction='vegetarian'
),
expected_output=None,
metadata={'focus': 'vegetarian'},
evaluators=( # (1)
LLMJudge(
rubric='Recipe should not contain meat or animal products',
),
),
),
Case(
name='gluten_free_recipe',
inputs=CustomerOrder(
dish_name='Chocolate Cake', dietary_restriction='gluten-free'
),
expected_output=None,
metadata={'focus': 'gluten-free'},
evaluators=( # (2)
LLMJudge(
rubric='Recipe should not contain gluten or wheat products',
),
),
),
],
evaluators=[ # (3)
IsInstance(type_name='Recipe'),
LLMJudge(
rubric='Recipe should have clear steps and relevant ingredients',
include_input=True,
model='anthropic:claude-sonnet-4-6',
),
],
)
report = recipe_dataset.evaluate_sync(transform_recipe)
print(report)
"""
Evaluation Summary: transform_recipe
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Case ID ┃ Assertions ┃ Duration ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━┩
│ vegetarian_recipe │ ✔✔✔ │ 38.1s │
├────────────────────┼────────────┼──────────┤
│ gluten_free_recipe │ ✔✔✔ │ 22.4s │
├────────────────────┼────────────┼──────────┤
│ Averages │ 100.0% ✔ │ 30.3s │
└────────────────────┴────────────┴──────────┘
"""
케이스별 평가자 - 채식 레시피 케이스에서만 실행됨
케이스별 평가자 - 글루텐 프리 레시피 케이스에서만 실행됨
데이터셋 수준 평가자 - 모든 케이스에서 실행됨
다중 측면 평가
다양한 품질 차원에 여러 judge를 사용해요:
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
dataset = Dataset(
name='multi_aspect',
cases=[Case(inputs='test')],
evaluators=[
# 정확성
LLMJudge(
rubric='Response is factually accurate',
include_input=True,
assertion={'evaluation_name': 'accurate'},
),
# 도움됨
LLMJudge(
rubric='Response is helpful and actionable',
include_input=True,
score={'evaluation_name': 'helpfulness'},
assertion=False,
),
# 톤
LLMJudge(
rubric='Response uses professional, respectful language',
assertion={'evaluation_name': 'professional_tone'},
),
# 안전성
LLMJudge(
rubric='Response contains no harmful, biased, or inappropriate content',
assertion={'evaluation_name': 'safe'},
),
],
)
비교 평가
출력을 기대 출력과 비교해요:
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
dataset = Dataset(
name='comparative_eval',
cases=[
Case(
name='translation',
inputs='Hello world',
expected_output='Bonjour le monde',
),
],
evaluators=[
LLMJudge(
rubric='Response is semantically equivalent to the expected output',
include_input=True,
include_expected_output=True,
score={'evaluation_name': 'semantic_similarity'},
assertion={'evaluation_name': 'correct_meaning'},
),
],
)
모범 사례
1. 루브릭을 구체적으로 작성하기
나쁨:
from pydantic_evals.evaluators import LLMJudge
LLMJudge(rubric='Good answer')
더 좋음:
from pydantic_evals.evaluators import LLMJudge
LLMJudge(rubric='Response accurately answers the question without hallucinating facts')
최고:
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='''
Response must:
1. Directly answer the question asked
2. Use only information from the provided context
3. Cite specific passages from the context
4. Acknowledge if information is insufficient
''',
include_input=True,
)
2. 여러 judge 사용하기
항상 한 루브릭으로 모든 것을 평가하려 하지 마세요:
from pydantic_evals.evaluators import LLMJudge
# 이렇게 하지 말고:
LLMJudge(rubric='Response is good, accurate, helpful, and safe')
# 이렇게 하세요:
evaluators = [
LLMJudge(rubric='Response is factually accurate'),
LLMJudge(rubric='Response is helpful and actionable'),
LLMJudge(rubric='Response is safe and appropriate'),
]
3. 결정적 검사와 결합하기
결정적으로 할 수 있는 검사에는 LLM 평가를 쓰지 마세요:
from pydantic_evals.evaluators import Contains, IsInstance, LLMJudge
evaluators = [
IsInstance(type_name='str'),
Contains(value='required_section'),
LLMJudge(rubric='Response quality is high'),
]
4. 일관성을 위해 온도 0 사용하기
from pydantic_ai import ModelSettings
from pydantic_evals.evaluators import LLMJudge
LLMJudge(
rubric='...',
model_settings=ModelSettings(temperature=0.0),
)
한계
비결정성
LLM judge는 결정적이지 않아요. 같은 출력이 실행마다 다른 점수를 받을 수 있어요.
완화 방법:
- 일관성을 위해
temperature=0.0사용 - 여러 번 평가하고 평균내기
- 불안정한 평가에 재시도 전략 사용
비용
LLM judge는 API 호출을 하므로 비용과 시간이 들어요.
완화 방법:
- 간단한 검사에는 경량 모델 사용 (
gpt-5.6-luna) - 먼저 결정적 검사를 실행해 빠르게 실패시키기
- 가능하면 결과 캐시
- 변경된 케이스만 평가 제한
모델 편향
LLM judge는 훈련 데이터에서 편향을 물려받아요.
완화 방법:
- 여러 judge 모델을 사용해 비교
- 점수뿐 아니라 평가 이유도 검토
- 사람이 라벨링한 테스트 세트로 judge 검증
- 알려진 편향(길이 편향, 스타일 선호)을 인지
컨텍스트 한계
judge는 입력에 대한 토큰 한계가 있어요.
완화 방법:
- 긴 입력/출력을 지능적으로 잘라내기
- 전체 컨텍스트가 필요 없는 집중된 루브릭 사용
- 매우 긴 콘텐츠는 청크 평가 고려
LLM Judge 디버깅
이유 보기
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
def my_task(inputs: str) -> str:
return f'Result: {inputs}'
dataset = Dataset(
name='debug_reasons',
cases=[Case(inputs='test')],
evaluators=[LLMJudge(rubric='Response is clear')],
)
report = dataset.evaluate_sync(my_task)
report.print(include_reasons=True)
"""
Evaluation Summary: my_task
┏━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Case ID ┃ Assertions ┃ Duration ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ Case 1 │ LLMJudge: ✔ │ 10ms │
│ │ Reason: - │ │
│ │ │ │
│ │ │ │
├──────────┼─────────────┼──────────┤
│ Averages │ 100.0% ✔ │ 10ms │
└──────────┴─────────────┴──────────┘
"""
출력:
┃ Assertions ┃
┃ accuracy: ✔ ┃
┃ Reason: The response │
┃ correctly states... │
프로그래밍 방식으로 접근
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
def my_task(inputs: str) -> str:
return f'Result: {inputs}'
dataset = Dataset(
name='programmatic_access',
cases=[Case(inputs='test')],
evaluators=[LLMJudge(rubric='Response is clear')],
)
report = dataset.evaluate_sync(my_task)
for case in report.cases:
for name, result in case.assertions.items():
print(f'{name}: {result.value}')
#> LLMJudge: True
if result.reason:
print(f' Reason: {result.reason}')
#> Reason: -
judge 비교
같은 케이스를 다른 judge 모델로 테스트해요:
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge
def my_task(inputs: str) -> str:
return f'Result: {inputs}'
judges = [
LLMJudge(rubric='Response is clear', model='openai:gpt-5.2'),
LLMJudge(rubric='Response is clear', model='anthropic:claude-sonnet-4-6'),
LLMJudge(rubric='Response is clear', model='openai:gpt-5-mini'),
]
for judge in judges:
dataset = Dataset(name='judge_comparison', cases=[Case(inputs='test')], evaluators=[judge])
report = dataset.evaluate_sync(my_task)
# 결과 비교
고급: 커스텀 judge 모델
모든 LLMJudge 평가자의 기본 judge 모델을 설정해요:
from pydantic_evals.evaluators import LLMJudge
from pydantic_evals.evaluators.llm_as_a_judge import set_default_judge_model
# 기본값을 Claude로 설정
set_default_judge_model('anthropic:claude-sonnet-4-6')
# 이제 모든 LLMJudge 인스턴스가 기본으로 Claude를 사용해요
LLMJudge(rubric='...') # Claude 사용
다음 단계
더 알아보기 (Learn more)
- Pydantic Evals 문서: LLM Judge 심화