LLM 평가 및 테스트은(는) 무엇을 하나요?
프롬프트 테스트, 출력 검증, 환각 탐지, 벤치마크 생성, A/B 테스트 및 품질 지표를 포함한 LLM 애플리케이션 평가 및 테스트에 대한 종합 가이드입니다.
빠른 참조
이 스킬을 사용해야 하는 경우:
- LLM 애플리케이션 출력 테스트
- 프롬프트 품질 및 일관성 검증
- 환각 및 사실 오류 탐지
- 평가 벤치마크 생성
- 프롬프트 또는 모델 A/B 테스트
- 지속적 평가(CI/CD) 구현
- 검색 품질 측정(RAG용)
- 예상치 못한 LLM 동작 디버깅
다루는 지표:
- 전통적: BLEU, ROUGE, BERTScore, Perplexity
- LLM-as-Judge: GPT-4 평가, 루브릭 기반 점수 매기기
- 작업별: 정확 일치, F1, 정확도, 재현율
- 품질: 유해성, 편향, 일관성, 관련성
1부: 평가 기본
LLM 평가의 과제
LLM 평가가 어려운 이유:
- 주관적 품질 - '좋은' 출력이 사용 사례에 따라 다름
- 단일 정답 없음 - 여러 유효한 답변 존재
- 맥락 의존적 - 동일 출력이 다른 시나리오에서 좋음/나쁨
- 레이블링 비용 높음 - 인간 평가는 확장성 부족
- 적대적 취약성 - 작은 프롬프트 변경 = 큰 출력 변경
해결책: 다중 계층 평가
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 평가 | 검색 지표 + 근거 확인 | 중간 | 사실적 정확성 |
| 프로덕션 모니터링 | 다중 계층 접근 방식 | 높음 | 신뢰성 |
| 모델 선택 | 포괄적 벤치마크 | 높음 | 정확성 |
| 환각 탐지 | 교차 검증 + 외부 사실 확인 | 낮음 | 안전 |
일반적인 함정
- 단일 지표에 과적합 - 항상 다양한 접근 방식 혼합 사용
- LLM 판사 편향 무시 - GPT-4가 더 길거나 특정 형식의 답변을 선호
- 테스트 데이터 누수 - 프로덕션 데이터와 평가 데이터 분리 유지
- 비용 무시 - 상세한 LLM-as-Judge 평가는 빠르게 비용 증가
- 테스트 데이터 부족 - 통계적으로 유의미한 결과를 위해 최소 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 평가를 위한 종합 참고 자료를 제공합니다. 특정 요구 사항(예: 다국어, 특정 도메인)에 맞게 패턴과 코드를 조정하세요.