본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 5장: 비결정적 출력 평가
2026년 3월 8일·AI / ML·

5장: 비결정적 출력 평가

LLM-as-Judge 패턴으로 에이전트의 비결정적 출력을 평가하는 방법, 품질 차원별 점수 산출, 임계값 설정, pass@k 전략을 상세히 다룹니다.

17분676자10개 섹션
testingaievaluationquality-assurance
공유
agent-testing5 / 10
12345678910
이전4장: 엔드투엔드 시나리오 테스트다음6장: 회귀 테스트 자동화

학습 목표

  • 결정론적 검증과 점수 기반 평가의 차이점과 적용 영역을 구분합니다.
  • LLM-as-Judge 패턴의 구현과 신뢰성 확보 방법을 학습합니다.
  • 충실성, 관련성, 일관성, 환각의 4가지 품질 차원을 이해합니다.
  • pass@k와 pass-all@k 전략의 차이를 파악하고 적절히 선택합니다.

결정론적 검증의 한계

3장과 4장에서 다룬 도구 호출 검증과 워크플로우 완료 검증은 **결정론적(Deterministic)**입니다. "이 도구가 호출되었는가", "이 파라미터가 전달되었는가"는 참/거짓으로 명확히 판단할 수 있습니다.

그러나 에이전트의 자연어 응답 품질은 결정론적으로 판단할 수 없습니다. "좋은 답변"과 "나쁜 답변"의 경계는 명확하지 않으며, 같은 의미를 가진 다양한 표현이 존재합니다.

deterministic_vs_scoring.py
python
# 결정론적 검증 — 가능
assert state.has_tool_call("search_flights")  # True or False
 
# 점수 기반 평가 — 필요
# "에이전트의 응답이 사용자의 질문에 충분히 답변했는가?"
# -> 0.0 ~ 1.0 사이의 점수로 평가

이 장에서는 비결정적 출력을 **점수(Score)**로 평가하는 방법론을 다룹니다.

LLM-as-Judge 패턴

LLM-as-Judge는 다른 LLM이 평가자 역할을 수행하는 패턴입니다. 에이전트의 입력, 출력, 컨텍스트를 평가 LLM에 전달하고, 사전에 정의된 기준에 따라 점수를 매기도록 합니다.

기본 구현

llm_judge.py
python
from anthropic import Anthropic
 
client = Anthropic()
 
def llm_judge(
    question: str,
    agent_response: str,
    criteria: list[str],
    reference: str | None = None,
) -> dict:
    """LLM-as-Judge 기본 구현"""
 
    rubric = "\n".join(f"- {c}" for c in criteria)
    reference_section = ""
    if reference:
        reference_section = f"\n\n## 참조 답변\n{reference}"
 
    prompt = f"""당신은 AI 에이전트 응답의 품질을 평가하는 전문 평가자입니다.
 
## 사용자 질문
{question}
 
## 에이전트 응답
{agent_response}
{reference_section}
 
## 평가 기준
{rubric}
 
## 지시사항
각 평가 기준에 대해 0.0(매우 나쁨)에서 1.0(매우 좋음) 사이의 점수를 매기세요.
반드시 JSON 형식으로 응답하세요.
 
응답 형식:
{{
  "scores": {{
    "criterion_1": 0.85,
    "criterion_2": 0.72
  }},
  "overall": 0.78,
  "reasoning": "평가 근거 설명"
}}"""
 
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
 
    import json
    return json.loads(response.content[0].text)
Warning

LLM-as-Judge의 평가자 모델은 평가 대상 에이전트와 동등하거나 더 강력한 모델을 사용해야 합니다. 약한 모델이 강한 모델을 평가하면 신뢰도가 떨어집니다. 일반적으로 평가에는 가장 강력한 모델(예: Claude Opus)을 사용하는 것을 권장합니다.

Judge의 신뢰성 확보

LLM-as-Judge 자체도 비결정적이므로, 평가 결과의 신뢰성을 높이는 전략이 필요합니다.

reliable_judge.py
python
import statistics
 
async def reliable_judge(
    question: str,
    agent_response: str,
    criteria: list[str],
    num_judges: int = 3,
) -> dict:
    """다수 Judge의 평균으로 신뢰성을 높인 평가"""
 
    results = []
    for _ in range(num_judges):
        result = llm_judge(question, agent_response, criteria)
        results.append(result)
 
    # 각 기준별 평균 점수 계산
    all_criteria = results[0]["scores"].keys()
    averaged_scores = {}
 
    for criterion in all_criteria:
        scores = [r["scores"][criterion] for r in results]
        averaged_scores[criterion] = {
            "mean": statistics.mean(scores),
            "stdev": statistics.stdev(scores) if len(scores) > 1 else 0,
            "min": min(scores),
            "max": max(scores),
        }
 
    overall_scores = [r["overall"] for r in results]
 
    return {
        "scores": averaged_scores,
        "overall": {
            "mean": statistics.mean(overall_scores),
            "stdev": statistics.stdev(overall_scores),
        },
        "judge_agreement": 1.0 - statistics.stdev(overall_scores),
        "raw_results": results,
    }

4가지 품질 차원

에이전트 응답의 품질은 단일 점수가 아닌, 여러 차원으로 분리하여 평가해야 합니다. 각 차원은 독립적인 문제를 포착합니다.

충실성 (Faithfulness)

에이전트의 응답이 제공된 정보에 충실한지 평가합니다. 도구가 반환한 데이터를 왜곡하거나 과장하지 않았는지 확인합니다.

eval_faithfulness.py
python
FAITHFULNESS_CRITERIA = [
    "에이전트의 응답이 도구 반환 데이터와 일치하는가",
    "도구 결과에 없는 정보를 추가하지 않았는가",
    "숫자나 날짜 등 정량적 정보가 정확한가",
    "조건부 정보를 단정적으로 표현하지 않았는가",
]

관련성 (Relevance)

에이전트의 응답이 사용자의 질문에 직접적으로 관련되는지 평가합니다.

eval_relevance.py
python
RELEVANCE_CRITERIA = [
    "응답이 사용자의 질문에 직접 답변하는가",
    "불필요한 정보가 포함되지 않았는가",
    "핵심 정보가 누락되지 않았는가",
    "사용자가 요청하지 않은 추가 작업을 수행하지 않았는가",
]

일관성 (Coherence)

멀티턴 대화에서 에이전트가 이전 발화와 모순되지 않는지 평가합니다.

eval_coherence.py
python
COHERENCE_CRITERIA = [
    "이전 턴에서 제공한 정보와 현재 응답이 일치하는가",
    "에이전트가 스스로 세운 계획을 따르고 있는가",
    "동일한 질문에 일관된 답변을 제공하는가",
    "맥락 전환 시 자연스럽게 연결되는가",
]

환각 (Hallucination)

에이전트가 사실이 아닌 정보를 생성하지 않았는지 평가합니다. 에이전트 테스트에서 가장 중요한 차원 중 하나입니다.

eval_hallucination.py
python
HALLUCINATION_CRITERIA = [
    "존재하지 않는 기능이나 서비스를 언급하지 않았는가",
    "확인되지 않은 사실을 단정적으로 진술하지 않았는가",
    "도구 호출 없이 실시간 데이터를 제공하지 않았는가",
    "가격, 일정 등 구체적 수치가 도구 결과에 기반하는가",
]

통합 평가 함수

comprehensive_eval.py
python
async def comprehensive_evaluation(
    conversation: list[dict],
    tool_results: list[dict],
) -> dict:
    """4가지 차원의 통합 품질 평가"""
 
    dimensions = {
        "faithfulness": FAITHFULNESS_CRITERIA,
        "relevance": RELEVANCE_CRITERIA,
        "coherence": COHERENCE_CRITERIA,
        "hallucination": HALLUCINATION_CRITERIA,
    }
 
    results = {}
    for dimension, criteria in dimensions.items():
        result = await reliable_judge(
            question=format_conversation(conversation),
            agent_response=conversation[-1]["content"],
            criteria=criteria,
            num_judges=3,
        )
        results[dimension] = result
 
    # 차원별 가중 평균
    weights = {
        "faithfulness": 0.30,
        "relevance": 0.25,
        "coherence": 0.20,
        "hallucination": 0.25,
    }
 
    weighted_score = sum(
        results[dim]["overall"]["mean"] * weight
        for dim, weight in weights.items()
    )
 
    return {
        "dimensions": results,
        "weighted_score": weighted_score,
        "passed": weighted_score >= 0.75,
    }

임계값 설정

점수 기반 평가에서 **임계값(Threshold)**은 "통과"와 "실패"를 가르는 기준선입니다. 임계값 설정은 과학보다 예술에 가깝지만, 몇 가지 원칙이 있습니다.

초기 임계값 설정

threshold_config.py
python
INITIAL_THRESHOLDS = {
    "faithfulness": 0.85,    # 높게 — 사실 왜곡은 허용 불가
    "relevance": 0.75,       # 중간 — 약간의 추가 정보는 허용
    "coherence": 0.70,       # 중간 — 멀티턴에서 약간의 반복 허용
    "hallucination": 0.90,   # 매우 높게 — 환각은 최소화
    "overall": 0.75,         # 종합 임계값
}
Tip

임계값은 고정이 아닌 점진적 조정이 필요합니다. 초기에는 낮게 설정하여 기준선(Baseline)을 확보하고, 에이전트가 개선됨에 따라 점차 높여 나갑니다. 너무 높은 초기 임계값은 대부분의 테스트가 실패하게 만들어, 신호 대비 잡음 비율을 악화시킵니다.

동적 임계값

과거 실행 데이터를 기반으로 임계값을 동적으로 조정하는 전략도 있습니다.

dynamic_threshold.py
python
def calculate_dynamic_threshold(
    historical_scores: list[float],
    percentile: float = 0.1,  # 하위 10%를 실패로 간주
) -> float:
    """과거 점수의 분포를 기반으로 동적 임계값 계산"""
    import numpy as np
 
    scores = np.array(historical_scores)
    threshold = np.percentile(scores, percentile * 100)
 
    return float(threshold)

pass@k vs pass-all@k

비결정적 시스템의 평가에서는 단일 실행의 결과로 판단하면 안 됩니다. 동일한 테스트를 여러 번 실행하고, 그 결과를 종합하는 전략이 필요합니다.

pass@k

k번 실행 중 최소 1번 성공하면 통과로 간주합니다. 에이전트가 "할 수 있는 능력"을 측정합니다.

pass_at_k.py
python
async def pass_at_k(
    test_fn,
    k: int = 5,
) -> dict:
    """k번 중 1번이라도 성공하면 통과"""
    results = []
 
    for i in range(k):
        result = await test_fn()
        results.append(result.success)
 
    return {
        "passed": any(results),
        "successes": sum(results),
        "total": k,
        "success_rate": sum(results) / k,
    }

pass-all@k

k번 실행 중 모두 성공해야 통과로 간주합니다. 에이전트의 "안정성"을 측정합니다. 훨씬 엄격한 기준입니다.

pass_all_at_k.py
python
async def pass_all_at_k(
    test_fn,
    k: int = 3,
) -> dict:
    """k번 모두 성공해야 통과"""
    results = []
 
    for i in range(k):
        result = await test_fn()
        results.append(result.success)
 
        # 하나라도 실패하면 조기 종료 (비용 절감)
        if not result.success:
            break
 
    return {
        "passed": all(results),
        "successes": sum(results),
        "total": len(results),
        "success_rate": sum(results) / len(results),
    }

선택 기준

전략적합한 상황엄격도
pass@k능력 검증, 탐색적 평가낮음
pass-all@k회귀 테스트, 프로덕션 게이트높음
평균 점수 >= 임계값점진적 품질 추적중간
Info

실무에서는 평가 단계에서 pass@k, 회귀 테스트에서 pass-all@k를 사용하는 조합이 일반적입니다. 신규 기능은 pass@k로 능력을 확인하고, 안정화되면 pass-all@k로 기준을 높입니다. 이 "졸업" 패턴은 6장에서 자세히 다룹니다.

3회 이상 평균 실행

비용과 정확도의 균형을 위해, 최소 3회 실행의 평균을 사용하는 것이 권장됩니다.

averaged_evaluation.py
python
async def averaged_evaluation(
    agent,
    scenario_config: dict,
    num_runs: int = 3,
    threshold: float = 0.75,
) -> dict:
    """다회 실행 평균 기반 평가"""
 
    all_scores = []
    all_results = []
 
    for run_idx in range(num_runs):
        result = await Scenario(**scenario_config, agent=agent).run()
        eval_result = await comprehensive_evaluation(
            result.conversation,
            result.state.get_all_tool_calls(),
        )
        all_scores.append(eval_result["weighted_score"])
        all_results.append(eval_result)
 
    avg_score = statistics.mean(all_scores)
    score_stdev = statistics.stdev(all_scores) if len(all_scores) > 1 else 0
 
    return {
        "average_score": avg_score,
        "stdev": score_stdev,
        "min_score": min(all_scores),
        "max_score": max(all_scores),
        "passed": avg_score >= threshold,
        "runs": all_results,
        "stability": 1.0 - score_stdev,  # 표준편차가 낮을수록 안정
    }

평가 결과 리포트

평가 결과를 구조화된 리포트로 출력하면 문제를 빠르게 파악할 수 있습니다.

eval_report.py
python
def generate_eval_report(results: dict) -> str:
    """평가 결과를 가독성 높은 리포트로 변환"""
    lines = []
    lines.append("=" * 60)
    lines.append("에이전트 품질 평가 리포트")
    lines.append("=" * 60)
 
    lines.append(f"\n종합 점수: {results['average_score']:.3f}")
    lines.append(f"표준편차: {results['stdev']:.3f}")
    lines.append(f"판정: {'PASS' if results['passed'] else 'FAIL'}")
 
    lines.append("\n--- 차원별 점수 ---")
    for dim, data in results["runs"][0]["dimensions"].items():
        score = data["overall"]["mean"]
        status = "OK" if score >= INITIAL_THRESHOLDS.get(dim, 0.75) else "LOW"
        lines.append(f"  {dim:20s}: {score:.3f} [{status}]")
 
    return "\n".join(lines)

정리

이번 장에서는 비결정적 출력 평가의 전체 방법론을 다루었습니다.

  • LLM-as-Judge는 자연어 품질 평가의 핵심 패턴이며, 다수 Judge의 평균으로 신뢰성을 높입니다.
  • 품질은 충실성, 관련성, 일관성, 환각의 4가지 독립 차원으로 분리하여 평가합니다.
  • 임계값은 초기에 보수적으로 시작하고, 데이터가 쌓임에 따라 점진적으로 조정합니다.
  • pass@k는 능력 검증에, pass-all@k는 안정성 검증에 적합합니다.
  • 최소 3회 이상의 평균 실행으로 비결정적 변동을 흡수합니다.

다음 장 미리보기

6장에서는 평가를 회귀 테스트로 전환하는 "졸업" 패턴을 다룹니다. 안정적으로 통과하는 평가를 회귀 테스트 스위트에 포함시키고, Golden Dataset을 관리하며, 롤링 성공률을 모니터링하여 품질 저하를 자동으로 감지하는 파이프라인을 구축합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#testing#ai#evaluation#quality-assurance

관련 글

AI / ML

6장: 회귀 테스트 자동화

평가를 회귀 테스트로 졸업시키는 패턴, Golden Dataset 관리, 롤링 성공률 모니터링, 베이스라인 관리와 변경 영향 분석을 다룹니다.

2026년 3월 10일·16분
AI / ML

4장: 엔드투엔드 시나리오 테스트

사용자 시뮬레이션 기반 멀티턴 대화 테스트, 워크플로우 완료 검증, 반복 호출 및 모순적 계획 감지 등 E2E 시나리오 테스트의 전체 방법론을 다룹니다.

2026년 3월 6일·18분
AI / ML

7장: 에이전트 안정성 메트릭

태스크 성공률 추이, 행동 드리프트 감지, 응답 길이 변동, 지연시간 안정성, 비용 변동성 등 에이전트의 장기적 안정성을 추적하는 메트릭과 대시보드 설계를 다룹니다.

2026년 3월 12일·17분
이전 글4장: 엔드투엔드 시나리오 테스트
다음 글6장: 회귀 테스트 자동화

댓글

목차

약 17분 남음
  • 학습 목표
  • 결정론적 검증의 한계
  • LLM-as-Judge 패턴
    • 기본 구현
    • Judge의 신뢰성 확보
  • 4가지 품질 차원
    • 충실성 (Faithfulness)
    • 관련성 (Relevance)
    • 일관성 (Coherence)
    • 환각 (Hallucination)
    • 통합 평가 함수
  • 임계값 설정
    • 초기 임계값 설정
    • 동적 임계값
  • pass@k vs pass-all@k
    • pass@k
    • pass-all@k
    • 선택 기준
  • 3회 이상 평균 실행
  • 평가 결과 리포트
  • 정리
  • 다음 장 미리보기