LLM Judge 심화

LLM Judge 심화 (LLM Judge Deep Dive)

LLMJudge 평가자는 루브릭을 기준으로 출력의 주관적 특성을 평가하기 위해 LLM을 사용해요.

출처: 문서

본문

LLM-as-a-Judge는 언제 쓸까요?

LLM judge는 이해와 판단이 필요한 특성을 평가하는 데 이상적이에요:

좋은 사용 사례:

  • 사실 정확성
  • 도움됨과 관련성
  • 톤과 스타일 준수
  • 응답의 완성도
  • 복잡한 지시사항 따르기
  • RAG 근거 강도 (답변이 제공된 컨텍스트를 사용하나요?)
  • 인용 정확성

나쁜 사용 사례:

기본 사용법

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)