Community코딩 & 개발github.com

LLM 평가 및 테스트

프롬프트 테스트, 환각 탐지, 벤치마크 생성 및 품질 지표를 포함한 LLM 평가 및 테스트 패턴입니다. LLM 애플리케이션 테스트, 프롬프트 품질 검증, 체계적인 평가 구현 또는 LLM 성능 측정 시 사용하세요.

LLM 평가 및 테스트란 무엇인가요?

이 스킬은 LLM 애플리케이션을 평가하고 테스트하기 위한 포괄적인 가이드를 제공합니다. 전통적인 지표(BLEU, ROUGE, BERTScore, Perplexity)부터 LLM-as-Judge 평가, 환각 탐지, RAG(검색 증강 생성) 특화 평가까지 다양한 방법을 다룹니다. 주요 기능으로는 루브릭 기반 점수 매기기, 이진 합격/불합격 평가, 쌍별 비교(A/B 테스트)가 있으며, grounding 확인, 사실성 검증, 자기 일관성 확인을 통한 환각 탐지 방법도 포함됩니다. 또한 지속적 평가(CI/CD)를 위한 체계적인 접근 방식을 제공하며, 평가 데이터셋 구조 및 고급 기술(메타 평가, 비용-품질 트레이드오프, 다중 모델 평가, 적응형 평가)에 대한 템플릿도 포함되어 있습니다. Python, OpenAI API, LangChain, 기타 LLM 프레임워크와 함께 사용하도록 설계되어 있습니다.

지원 대상~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/applied-artificial-intelligence/claude-code-toolkit/tree/main/skills/llm-evaluation

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

LLM 평가 및 테스트은(는) 무엇을 하나요?

프롬프트 테스트, 출력 검증, 환각 탐지, 벤치마크 생성, A/B 테스트 및 품질 지표를 포함한 LLM 애플리케이션 평가 및 테스트에 대한 종합 가이드입니다.


빠른 참조

이 스킬을 사용해야 하는 경우:

  • LLM 애플리케이션 출력 테스트
  • 프롬프트 품질 및 일관성 검증
  • 환각 및 사실 오류 탐지
  • 평가 벤치마크 생성
  • 프롬프트 또는 모델 A/B 테스트
  • 지속적 평가(CI/CD) 구현
  • 검색 품질 측정(RAG용)
  • 예상치 못한 LLM 동작 디버깅

다루는 지표:

  • 전통적: BLEU, ROUGE, BERTScore, Perplexity
  • LLM-as-Judge: GPT-4 평가, 루브릭 기반 점수 매기기
  • 작업별: 정확 일치, F1, 정확도, 재현율
  • 품질: 유해성, 편향, 일관성, 관련성

1부: 평가 기본

LLM 평가의 과제

LLM 평가가 어려운 이유:

  1. 주관적 품질 - '좋은' 출력이 사용 사례에 따라 다름
  2. 단일 정답 없음 - 여러 유효한 답변 존재
  3. 맥락 의존적 - 동일 출력이 다른 시나리오에서 좋음/나쁨
  4. 레이블링 비용 높음 - 인간 평가는 확장성 부족
  5. 적대적 취약성 - 작은 프롬프트 변경 = 큰 출력 변경

해결책: 다중 계층 평가

Layer 1: 자동화된 지표 (빠름, 확장 가능)
  ↓
Layer 2: LLM-as-Judge (유연함, 미묘함)
  ↓
Layer 3: 인간 검토 (최고 표준, 비용 높음)

평가 데이터셋 구조

from dataclasses import dataclass
from typing import List, Optional

@dataclass
class EvalExample:
    """단일 평가 예시."""
    input: str  # 사용자 입력 / 프롬프트
    expected_output: Optional[str]  # 정답(있는 경우)
    context: Optional[str]  # 추가 맥락(RAG용)
    metadata: dict  # 카테고리, 난이도 등

@dataclass
class EvalResult:
    """하나의 예시에 대한 평가 결과."""
    example_id: str
    actual_output: str
    scores: dict  # {'metric_name': score}
    passed: bool
    failure_reason: Optional[str]

# 예시 데이터셋
eval_dataset = [
    EvalExample(
        input="프랑스의 수도는 무엇인가요?",
        expected_output="파리",
        context=None,
        metadata={'category': 'factual', 'difficulty': 'easy'}
    ),
    EvalExample(
        input="양자 얽힘을 설명해주세요",
        expected_output=None,  # 단일 답변 없음
        context=None,
        metadata={'category': 'explanation', 'difficulty': 'hard'}
    )
]

2부: 전통적 지표

지표 1: 정확 일치 (가장 간단)

def exact_match(predicted: str, expected: str, case_sensitive: bool = False) -> float:
    """
    이진 지표: 일치하면 1.0, 그렇지 않으면 0.0.

    사용처: 분류, 짧은 답변, 구조화된 출력
    제한 사항: 생성 작업에는 너무 엄격함
    """
    if not case_sensitive:
        predicted = predicted.lower().strip()
        expected = expected.lower().strip()

    return 1.0 if predicted == expected else 0.0

# 예시
score = exact_match("파리", "파리")  # 1.0
score = exact_match("수도는 파리입니다", "파리")  # 0.0

지표 2: ROUGE (재현율 중심)

from rouge_score import rouge_scorer

def compute_rouge(predicted: str, expected: str) -> dict:
    """
    텍스트 중복에 대한 ROUGE 지표.

    ROUGE-1: 유니그램 중복
    ROUGE-2: 바이그램 중복
    ROUGE-L: 최장 공통 부분 수열

    사용처: 요약, 번역
    제한 사항: 의미를 포착하지 못함
    """
    scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
    scores = scorer.score(expected, predicted)

    return {
        'rouge1': scores['rouge1'].fmeasure,
        'rouge2': scores['rouge2'].fmeasure,
        'rougeL': scores['rougeL'].fmeasure
    }

# 예시
scores = compute_rouge(
    predicted="파리는 프랑스의 수도입니다",
    expected="프랑스의 수도는 파리입니다"
)
# {'rouge1': 0.82, 'rouge2': 0.67, 'rougeL': 0.82}

지표 3: BERTScore (의미적 유사성)

from bert_score import score as bert_score

def compute_bertscore(predicted: List[str], expected: List[str]) -> dict:
    """
    BERT 임베딩을 사용한 의미적 유사성.

    ROUGE보다 나은 점:
    - 의역
    - 의미적 동등성
    - 생성 품질

    반환: 정밀도, 재현율, F1
    """
    P, R, F1 = bert_score(predicted, expected, lang="en", verbose=False)

    return {
        'precision': P.mean().item(),
        'recall': R.mean().item(),
        'f1': F1.mean().item()
    }

# 예시
scores = compute_bertscore(
    predicted=["프랑스의 수도는 파리입니다"],
    expected=["파리는 프랑스의 수도입니다"]
)
# {'precision': 0.94, 'recall': 0.91, 'f1': 0.92}

지표 4: Perplexity (모델 신뢰도)

import torch
from transformers import GPT2LMHeadModel, GPT2Tokenizer

def compute_perplexity(text: str, model_name: str = "gpt2") -> float:
    """
    Perplexity: 모델이 이 텍스트에 대해 얼마나 '놀랐는가'?

    낮을수록 = 더 자연스럽고 유창함
    사용처: 유창성, 자연스러움
    제한 사항: 정확성을 측정하지 않음
    """
    model = GPT2LMHeadModel.from_pretrained(model_name)
    tokenizer = GPT2Tokenizer.from_pretrained(model_name)

    inputs = tokenizer(text, return_tensors="pt")

    with torch.no_grad():
        outputs = model(**inputs, labels=inputs["input_ids"])
        loss = outputs.loss

    perplexity = torch.exp(loss).item()
    return perplexity

# 예시
ppl = compute_perplexity("파리는 프랑스의 수도입니다")  # 낮음 (자연스러움)
ppl2 = compute_perplexity("수도 프랑스 파리 의 는 입니다")  # 높음 (어색함)

3부: LLM-as-Judge 평가

패턴 1: 루브릭 기반 점수 매기기

from openai import OpenAI

client = OpenAI()

EVALUATION_PROMPT = """
당신은 전문 평가자입니다. 각 기준에 대해 1-5점 척도로 어시스턴트의 응답을 평가하세요:

**기준:**
1. **정확성**: 정보가 사실적으로 정확한가요?
2. **완전성**: 질문에 완전히 답변했나요?
3. **명확성**: 이해하기 쉬운가요?
4. **간결성**: 적절히 간결한가요?

**평가할 응답:**
{response}

**예상 답변 (참조):**
{expected}

JSON 형식으로 점수를 제공하세요:
{{
  "accuracy": <1-5>,
  "completeness": <1-5>,
  "clarity": <1-5>,
  "conciseness": <1-5>,
  "reasoning": "간단한 설명"
}}
"""

def llm_judge_score(response: str, expected: str) -> dict:
    """
    GPT-4를 루브릭 점수 매기기용 판사로 사용.

    장점: 유연함, 미묘함, 확장성 좋음
    단점: 비용 발생, 잠재적 편향, 느림
    """
    prompt = EVALUATION_PROMPT.format(response=response, expected=expected)

    completion = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"}
    )

    import json
    scores = json.loads(completion.choices[0].message.content)
    return scores

# 예시
scores = llm_judge_score(
    response="파리는 프랑스의 수도이며, 국가의 북중부에 위치합니다.",
    expected="파리"
)
# {'accuracy': 5, 'completeness': 5, 'clarity': 5, 'conciseness': 3, 'reasoning': '...'}

패턴 2: 이진 합격/불합격 평가

PASS_FAIL_PROMPT = """
어시스턴트의 응답이 수용 가능한지 평가하세요.

**질문:** {question}
**응답:** {response}
**기준:** {criteria}

"PASS" 또는 "FAIL"과 함께 한 문장 이유만 반환하세요.
"""

def binary_eval(question: str, response: str, criteria: str) -> tuple[bool, str]:
    """
    간단한 합격/불합격 평가.

    사용처: 단위 테스트, 회귀 테스트, CI/CD
    """
    prompt = PASS_FAIL_PROMPT.format(
        question=question,
        response=response,
        criteria=criteria
    )

    completion = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0  # 결정적
    )

    result = completion.choices[0].message.content
    passed = result.startswith("PASS")
    reason = result.split(":", 1)[1].strip() if ":" in result else result

    return passed, reason

# 예시
passed, reason = binary_eval(
    question="프랑스의 수도는 무엇인가요?",
    response="수도는 파리입니다",
    criteria="응답이 파리를 언급해야 함"
)
# (True, "응답이 파리를 수도로 올바르게 식별")

패턴 3: 쌍별 비교 (A/B 테스트)

PAIRWISE_PROMPT = """
동일한 질문에 대한 두 응답을 비교하세요. 어느 것이 더 나은가요?

**질문:** {question}

**응답 A:**
{response_a}

**응답 B:**
{response_b}

**기준:** {criteria}

"A", "B" 또는 "TIE"와 함께 한 문장 설명만 반환하세요.
"""

def pairwise_comparison(
    question: str,
    response_a: str,
    response_b: str,
    criteria: str = "전반적 품질, 정확성 및 유용성"
) -> tuple[str, str]:
    """
    두 응답을 A/B 테스트.

    사용처: 프롬프트 엔지니어링, 모델 비교
    """
    prompt = PAIRWISE_PROMPT.format(
        question=question,
        response_a=response_a,
        response_b=response_b,
        criteria=criteria
    )

    completion = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.0
    )

    result = completion.choices[0].message.content
    winner = result.split()[0]  # "A", "B" 또는 "TIE"
    reason = result.split(":", 1)[1].strip() if ":" in result else result

    return winner, reason

# 예시
winner, reason = pairwise_comparison(
    question="양자 컴퓨팅을 설명해주세요",
    response_a="양자 컴퓨터는 비트 대신 큐비트를 사용합니다...",
    response_b="양자 컴퓨팅은 복잡합니다. 양자 역학을 사용합니다."
)
# ("A", "응답 A가 더 자세한 설명과 정보를 제공")

4부: 환각 탐지

방법 1: 근거 확인

def check_grounding(response: str, context: str) -> dict:
    """
    응답이 제공된 맥락에 근거하는지 확인.

    RAG 시스템에 중요.
    """
    GROUNDING_PROMPT = """
    맥락: {context}

    응답: {response}

    응답이 맥락에 의해 완전히 뒷받침됩니까? 다음 중 하나로 답하세요:
    - "GROUNDED": 모든 주장이 뒷받침됨
    - "PARTIALLY_GROUNDED": 일부 주장이 뒷받침되지 않음
    - "NOT_GROUNDED": 뒷받침되지 않는 주장 포함

    뒷받침되지 않는 주장을 나열하세요.
    """

    prompt = GROUNDING_PROMPT.format(context=context, response=response)

    completion = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    result = completion.choices[0].message.content
    status = result.split("\n")[0]
    unsupported = [line for line in result.split("\n")[1:] if line.strip()]

    return {
        'grounding_status': status,
        'unsupported_claims': unsupported,
        'is_hallucination': status != "GROUNDED"
    }

방법 2: 사실성 확인 (외부 검증)

def check_factuality(claim: str, use_search: bool = True) -> dict:
    """
    외부 소스를 사용하여 사실적 주장 검증.

    옵션:
    1. 웹 검색 + 검증
    2. 지식 베이스 조회
    3. 신뢰할 수 있는 소스와 교차 참조
    """
    if use_search:
        # 웹 검색을 사용하여 검증
        from tavily import TavilyClient
        tavily = TavilyClient(api_key="your-key")

        # 증거 검색
        results = tavily.search(claim, max_results=3)

        # 검색 결과를 기반으로 LLM에 검증 요청
        VERIFY_PROMPT = """
        주장: {claim}

        검색 결과:
        {results}

        이 주장이 이러한 출처에 의해 뒷받침됩니까? TRUE, FALSE 또는 UNCERTAIN으로 답하세요.
        설명:
        """

        prompt = VERIFY_PROMPT.format(
            claim=claim,
            results="\n\n".join([r['content'] for r in results])
        )

        completion = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )

        result = completion.choices[0].message.content
        is_factual = result.startswith("TRUE")

        return {
            'claim': claim,
            'factual': is_factual,
            'evidence': results,
            'explanation': result
        }

방법 3: 자기 일관성 확인

def self_consistency_check(question: str, num_samples: int = 5) -> dict:
    """
    여러 응답을 생성하여 일관성 확인.

    모델이 확신한다면 응답이 일관되어야 함.
    불일치는 환각 위험을 시사.
    """
    responses = []

    for _ in range(num_samples):
        completion = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": question}],
            temperature=0.7  # 약간의 무작위성
        )
        responses.append(completion.choices[0].message.content)

    # 쌍별 유사도 계산
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.metrics.pairwise import cosine_similarity

    vectorizer = TfidfVectorizer()
    vectors = vectorizer.fit_transform(responses)
    similarities = cosine_similarity(vectors)

    # 평균 쌍별 유사도
    avg_similarity = similarities.sum() / (len(responses) * (len(responses) - 1))

    return {
        'responses': responses,
        'avg_similarity': avg_similarity,
        'is_consistent': avg_similarity > 0.7,  # 임계값
        'confidence': 'high' if avg_similarity > 0.85 else 'medium' if avg_similarity > 0.7 else 'low'
    }

5부: RAG 특화 평가

검색 품질 지표

def evaluate_retrieval(query: str, retrieved_docs: List[dict], relevant_doc_ids: List[str]) -> dict:
    """
    IR 지표를 사용한 검색 품질 평가.

    Precision: 검색된 문서 중 관련 문서의 비율은?
    Recall: 관련 문서 중 검색된 비율은?
    MRR: 평균 역순위
    NDCG: 정규화된 할인 누적 이득
    """
    retrieved_ids = [doc['id'] for doc in retrieved_docs]

    # Precision
    relevant_retrieved = set(retrieved_ids) & set(relevant_doc_ids)
    precision = len(relevant_retrieved) / len(retrieved_ids) if retrieved_ids else 0.0

    # Recall
    recall = len(relevant_retrieved) / len(relevant_doc_ids) if relevant_doc_ids else 0.0

    # MRR
    mrr = 0.0
    for rank, doc_id in enumerate(retrieved_ids, 1):
        if doc_id in relevant_doc_ids:
            mrr = 1.0 / rank
            break

    # F1
    f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0

    return {
        'precision': precision,
        'recall': recall,
        'mrr': mrr,
        'f1': f1
    }

# 예시
eval_results = evaluate_retrieval(
    query="프랑스의 수도는?",
    retrieved_docs=[
        {'id': 'doc1', 'content': '파리는 프랑스의 수도입니다.'},
        {'id': 'doc2', 'content': '프랑스는 유럽에 위치해 있습니다.'},
        {'id': 'doc3', 'content': '루브르 박물관은 파리에 있습니다.'}
    ],
    relevant_doc_ids=['doc1', 'doc4']
)
# {'precision': 0.33, 'recall': 0.5, 'mrr': 1.0, 'f1': 0.4}

생성-검색 상호 평가

def evaluate_rag_pipeline(query: str, retrieved_docs: List[str], generated_answer: str) -> dict:
    """
    RAG 파이프라인의 검색 및 생성 구성 요소 모두를 평가합니다.

    1. 검색 관련성
    2. 생성 답변의 근거
    3. 전반적 출력 품질
    """
    # 검색 평가
    retrieval_eval = {
        'docs_retrieved': len(retrieved_docs),
        'avg_doc_length': sum(len(d) for d in retrieved_docs) / len(retrieved_docs) if retrieved_docs else 0
    }

    # 답변 근거 평가
    GROUNDING_PROMPT = """
    답변: {answer}

    검색된 문서:
    {docs}

    답변이 검색된 문서에 의해 완전히 뒷받침됩니까? 점수(1-5)와 설명을 제공하세요.
    """

    prompt = GROUNDING_PROMPT.format(
        answer=generated_answer,
        docs="\n\n".join(retrieved_docs)
    )

    completion = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    return {
        'retrieval_quality': retrieval_eval,
        'grounding_assessment': completion.choices[0].message.content,
        'pipeline_status': 'PASS' if len(retrieved_docs) > 0 else 'FAIL'
    }

6부: 지속적 평가 (CI/CD)

자동화된 평가 파이프라인

from typing import List, Callable
from datetime import datetime
import json

class EvalPipeline:
    """
    CI/CD 통합을 위한 자동화된 평가 파이프라인.

    GitHub Actions, GitLab CI, Jenkins 등과 함께 사용.
    """
    def __init__(self, model: str = "gpt-4", test_data: List[EvalExample] = None):
        self.model = model
        self.test_data = test_data or []
        self.results = []

    def add_test(self, example: EvalExample):
        """평가 데이터셋에 테스트 케이스 추가."""
        self.test_data.append(example)

    def run(self, llm_function: Callable) -> dict:
        """
        모든 테스트 케이스 실행.

        Args:
            llm_function: 입력을 받아 출력을 반환하는 함수
        """
        for i, example in enumerate(self.test_data):
            try:
                actual = llm_function(example.input)

                # 전통적 지표 계산
                from rouge_score import rouge_scorer
                scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)

                if example.expected_output:
                    rouge_scores = scorer.score(example.expected_output, actual)
                    exact = exact_match(actual, example.expected_output)
                else:
                    rouge_scores = {}
                    exact = 0.0

                result = EvalResult(
                    example_id=f"test_{i}",
                    actual_output=actual,
                    scores={
                        'exact_match': exact,
                        'rouge1': rouge_scores.get('rouge1', {}).get('fmeasure', 0),
                        'rougeL': rouge_scores.get('rougeL', {}).get('fmeasure', 0)
                    },
                    passed=(exact > 0.5 if example.expected_output else True),
                    failure_reason=None
                )

            except Exception as e:
                result = EvalResult(
                    example_id=f"test_{i}",
                    actual_output="ERROR",
                    scores={},
                    passed=False,
                    failure_reason=str(e)
                )

            self.results.append(result)

        return self.summarize()

    def summarize(self) -> dict:
        """모든 테스트 케이스에 대한 요약 통계."""
        passed = sum(1 for r in self.results if r.passed)
        total = len(self.results)

        avg_scores = {}
        for r in self.results:
            for metric, score in r.scores.items():
                if metric not in avg_scores:
                    avg_scores[metric] = []
                avg_scores[metric].append(score)

        avg_scores = {k: sum(v)/len(v) for k, v in avg_scores.items()}

        return {
            'timestamp': datetime.now().isoformat(),
            'model': self.model,
            'total_tests': total,
            'passed': passed,
            'failed': total - passed,
            'pass_rate': passed / total if total > 0 else 0,
            'average_scores': avg_scores,
            'failures': [
                {
                    'id': r.example_id,
                    'reason': r.failure_reason,
                    'actual': r.actual_output[:200]  # 로그 크기 제한
                }
                for r in self.results if not r.passed
            ],
            'all_results': self.results
        }

    def to_dict(self) -> dict:
        """JSON 직렬화를 위해 결과를 딕셔너리로 변환."""
        return self.summarize()


# CI/CD 사용 예시:
"""
# .github/workflows/eval.yml
name: LLM Evaluation
on: [push, pull_request]

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run LLM Evaluation
        run: |
          python evaluate.py  # 이 스크립트 실행
      - name: Upload Results
        uses: actions/upload-artifact@v3
        with:
          name: eval-results
          path: eval_results.json
"""

# 예시 사용법
pipeline = EvalPipeline(model="gpt-3.5-turbo")
pipeline.add_test(EvalExample(
    input="프랑스의 수도는?",
    expected_output="파리",
    metadata={'test_type': 'factual'}
))
pipeline.add_test(EvalExample(
    input="행복이란 무엇인가?",  # 정답이 없는 개방형 질문
    expected_output=None,
    metadata={'test_type': 'open_ended'}
))

results = pipeline.run(llm_function=lambda x: client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": x}]
).choices[0].message.content)

회귀 테스트 스위트

class RegressionTestSuite:
    """
    회귀를 탐지하기 위한 알려진 '좋은' 및 '나쁜' 예시 관리.
    """
    def __init__(self):
        self.examples = []  # (input, expected_behavior)

    def add_positive_example(self, input_text: str, expected_output: str):
        """알려진 좋은 동작 추가."""
        self.examples.append({
            'input': input_text,
            'type': 'positive',
            'expected': expected_output
        })

    def add_negative_example(self, input_text: str, forbidden_pattern: str):
        """알려진 나쁜 동작(피해야 할 것) 추가."""
        self.examples.append({
            'input': input_text,
            'type': 'negative',
            'forbidden': forbidden_pattern
        })

    def run_tests(self, llm_function: Callable) -> List[dict]:
        """회귀 테스트 실행."""
        results = []

        for example in self.examples:
            output = llm_function(example['input'])

            if example['type'] == 'positive':
                passed = example['expected'].lower() in output.lower()
            else:
                passed = example['forbidden'].lower() not in output.lower()

            results.append({
                'input': example['input'],
                'type': example['type'],
                'passed': passed,
                'output': output[:100]  # 로그 크기 제한
            })

        return results

7부: 고급 기술

메타 평가 (평가자 평가)

def meta_evaluation(evaluations: List[dict], ground_truth: List[dict]) -> dict:
    """
    다양한 평가 방법의 정확성을 평가합니다.

    어떤 평가자가 인간 판단과 가장 잘 일치하나요?
    어떤 방법이 가장 비용 효율적인가요?
    """
    results = {}

    for eval_method in evaluations:
        correct = 0
        total = len(eval_method['results'])

        for pred, truth in zip(eval_method['results'], ground_truth):
            if pred['passed'] == truth['passed']:
                correct += 1

        accuracy = correct / total if total > 0 else 0

        results[eval_method['name']] = {
            'accuracy': accuracy,
            'cost': eval_method.get('cost', 0),
            'latency': eval_method.get('latency', 0),
            'efficiency_score': accuracy / (eval_method.get('cost', 1) * eval_method.get('latency', 1))
        }

    return results

비용-품질 트레이드오프

def cost_quality_analysis(results: dict, cost_per_eval: dict) -> dict:
    """
    다양한 평가 방법의 비용 대비 품질 분석.

    도움말: 제약 조건에 따라 올바른 평가 방법 선택.
    """
    analysis = {}

    for method, metrics in results.items():
        cost = cost_per_eval.get(method, 0)
        quality = metrics.get('f1', 0)

        analysis[method] = {
            'cost_per_eval': cost,
            'quality_score': quality,
            'value_ratio': quality / cost if cost > 0 else float('inf'),
            'recommendation': (
                '권장' if quality > 0.8 and cost < 0.05
                else '고품질 전용' if quality > 0.9
                else '저예산 전용' if cost < 0.01
                else '일반 용도'
            )
        }

    return analysis

다중 모델 평가

def evaluate_across_models(
    test_cases: List[EvalExample],
    models: List[str],
    evaluator: Callable
) -> dict:
    """
    동일한 테스트 케이스로 여러 모델 평가.

    모델 비교 및 선택에 유용.
    """
    results = {}

    for model in models:
        model_results = []

        for test in test_cases:
            response = query_model(model, test.input)
            score = evaluator(test.expected_output, response)
            model_results.append(score)

        results[model] = {
            'average_score': sum(model_results) / len(model_results),
            'all_scores': model_results,
            'std_dev': statistics.stdev(model_results)
        }

    return results

적응형 평가

def adaptive_evaluation(
    llm_function: Callable,
    initial_tests: List[EvalExample],
    budget: int = 100
) -> dict:
    """
    평가 예산을 가장 취약한 영역에 집중하는 적응형 접근 방식.

    초기 테스트로 시작, 실패 사례 식별, 유사한 엣지 케이스 생성.
    """
    import random

    evals_run = 0
    results = []
    edge_cases = []

    # 초기 테스트 실행
    for test in initial_tests:
        if evals_run >= budget:
            break

        response = llm_function(test.input)
        passed = evaluate_response(test, response)
        results.append({'input': test.input, 'passed': passed, 'evals_run': evals_run})

        if not passed:
            # 실패 사례에서 파생된 엣지 케이스 생성
            edge_cases.append(generate_variant(test.input))

        evals_run += 1

    # 남은 예산을 엣지 케이스에 집중
    remaining = budget - evals_run
    edge_results = []

    for edge_input in random.sample(edge_cases, min(len(edge_cases), remaining)):
        response = llm_function(edge_input)
        passed = evaluate_response_simple(response)
        edge_results.append({'input': edge_input, 'passed': passed})

    return {
        'initial_results': results,
        'edge_results': edge_results,
        'summary': {
            'total_evals': evals_run + len(edge_results),
            'failure_rate': calculate_failure_rate(results, edge_results)
        }
    }

8부: 모범 사례

평가 전략 가이드

사용 사례권장 방법예산우선 순위
빠른 반복단순 지표(정확 일치, ROUGE)낮음속도
프롬프트 엔지니어링A/B 테스트 + LLM 판사중간미묘함
RAG 평가검색 지표 + 근거 확인중간사실적 정확성
프로덕션 모니터링다중 계층 접근 방식높음신뢰성
모델 선택포괄적 벤치마크높음정확성
환각 탐지교차 검증 + 외부 사실 확인낮음안전

일반적인 함정

  1. 단일 지표에 과적합 - 항상 다양한 접근 방식 혼합 사용
  2. LLM 판사 편향 무시 - GPT-4가 더 길거나 특정 형식의 답변을 선호
  3. 테스트 데이터 누수 - 프로덕션 데이터와 평가 데이터 분리 유지
  4. 비용 무시 - 상세한 LLM-as-Judge 평가는 빠르게 비용 증가
  5. 테스트 데이터 부족 - 통계적으로 유의미한 결과를 위해 최소 100개의 예시 필요

팁 요약

  • 작게 시작 - 기본 지표로 시작, 점차 복잡성 추가
  • 버전 관리 - 테스트 케이스와 결과를 Git에 커밋
  • 이상치 모니터링 - 평균 이상으로 분산이 중요한 정보 제공
  • 실패 사례 감사 - 정기적으로 실패 패턴 검토
  • 인간 루프 유지 - 완전 자동화된 평가는 드문 실패를 놓침
  • 테스트 진화 - 새로운 엣지 케이스가 발견됨에 따라 데이터셋 업데이트
  • 비용 추적 - API 비용을 주시하고 예산에 따라 방법 선택

빠른 시작 가이드

최소 설정

# 1. 필요한 라이브러리 설치
# pip install openai rouge-score bert-score scikit-learn

# 2. 간단한 평가 함수 생성
def quick_eval(predicted: str, expected: str) -> dict:
    """빠른 다중 지표 평가."""
    from rouge_score import rouge_scorer

    scorer = rouge_scorer.RougeScorer(['rouge1', 'rougeL'], use_stemmer=True)
    rouge_scores = scorer.score(expected, predicted)

    return {
        'exact_match': exact_match(predicted, expected),
        'rouge1': rouge_scores['rouge1'].fmeasure,
        'rougeL': rouge_scores['rougeL'].fmeasure,
        'bertscore': compute_bertscore([predicted], [expected])['f1']
    }

# 3. 첫 번째 평가 실행
result = quick_eval(
    predicted="파리는 프랑스의 수도입니다",
    expected="프랑스의 수도는 파리입니다"
)
print(result)

최소 유효 평가 데이터셋

MINIMAL_EVAL_SET = [
    EvalExample(input="1+1은?", expected_output="2", metadata={'type': 'math', 'difficulty': 'easy'}),
    EvalExample(input="하늘은 무슨 색인가요?", expected_output="파란색", metadata={'type': 'fact', 'difficulty': 'easy'}),
    EvalExample(input="인공지능의 미래는?", expected_output=None, metadata={'type': 'opinion', 'difficulty': 'hard'}),
    EvalExample(input="자바스크립트에서 변수를 선언하는 방법은?", expected_output="let, const, var 사용", metadata={'type': 'coding', 'difficulty': 'medium'}),
]

CI/CD 구성 예시

# .github/workflows/llm-eval.yml
name: LLM Evaluation Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 0'  # 주간 전체 회귀 테스트

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Python 설정
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: 의존성 설치
        run: |
          pip install openai rouge-score bert-score scikit-learn
      - name: 평가 실행
        run: |
          python evaluate.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      - name: 결과 업로드
        uses: actions/upload-artifact@v3
        with:
          name: evaluation-results
          path: eval_results.json

참고 자료


이 스킬은 LLM 평가를 위한 종합 참고 자료를 제공합니다. 특정 요구 사항(예: 다국어, 특정 도메인)에 맞게 패턴과 코드를 조정하세요.

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬