평가를 회귀 테스트로 졸업시키는 패턴, Golden Dataset 관리, 롤링 성공률 모니터링, 베이스라인 관리와 변경 영향 분석을 다룹니다.
5장에서 다룬 **평가(Evaluation)**는 에이전트의 현재 품질을 측정하는 도구입니다. 반면 **회귀 테스트(Regression Test)**는 에이전트의 품질이 이전보다 나빠지지 않았는지 확인하는 방어 장치입니다.
| 구분 | 평가 (Eval) | 회귀 테스트 (Regression) |
|---|---|---|
| 목적 | 현재 품질 측정 | 품질 저하 방지 |
| 실행 시점 | 개발 중, 탐색적 | 매 변경(PR, 배포)마다 |
| 기준 | 절대 임계값 | 이전 베이스라인 대비 |
| 실패 의미 | "개선 필요" | "이 변경이 품질을 떨어뜨림" |
| 엄격도 | 유연 | 엄격 |
모든 평가가 회귀 테스트가 될 필요는 없습니다. 졸업 기준을 정의하고, 그 기준을 충족하는 평가만 회귀 테스트 스위트에 포함시킵니다.
from dataclasses import dataclass
@dataclass
class GraduationCriteria:
"""평가를 회귀 테스트로 졸업시키기 위한 기준"""
# 최소 연속 통과 횟수
min_consecutive_passes: int = 5
# 최소 평균 점수
min_average_score: float = 0.85
# 최대 허용 표준편차 (안정성)
max_score_stdev: float = 0.10
# 최소 실행 기간 (일)
min_observation_days: int = 7
# 최소 총 실행 횟수
min_total_runs: int = 10
def check_graduation(
eval_history: list[dict],
criteria: GraduationCriteria,
) -> dict:
"""졸업 기준 충족 여부를 확인"""
import statistics
from datetime import datetime, timedelta
scores = [h["score"] for h in eval_history]
dates = [h["date"] for h in eval_history]
passed = [h["passed"] for h in eval_history]
# 기준 1: 최소 실행 횟수
enough_runs = len(eval_history) >= criteria.min_total_runs
# 기준 2: 최소 관찰 기간
if dates:
observation_days = (max(dates) - min(dates)).days
enough_days = observation_days >= criteria.min_observation_days
else:
enough_days = False
# 기준 3: 연속 통과
consecutive = 0
max_consecutive = 0
for p in passed:
if p:
consecutive += 1
max_consecutive = max(max_consecutive, consecutive)
else:
consecutive = 0
enough_consecutive = max_consecutive >= criteria.min_consecutive_passes
# 기준 4: 평균 점수
avg_score = statistics.mean(scores) if scores else 0
high_enough = avg_score >= criteria.min_average_score
# 기준 5: 안정성
stdev = statistics.stdev(scores) if len(scores) > 1 else 0
stable_enough = stdev <= criteria.max_score_stdev
ready = all([enough_runs, enough_days, enough_consecutive,
high_enough, stable_enough])
return {
"ready_to_graduate": ready,
"criteria_met": {
"min_total_runs": enough_runs,
"min_observation_days": enough_days,
"min_consecutive_passes": enough_consecutive,
"min_average_score": high_enough,
"max_score_stdev": stable_enough,
},
"stats": {
"total_runs": len(eval_history),
"average_score": avg_score,
"stdev": stdev,
"max_consecutive_passes": max_consecutive,
},
}졸업 기준은 프로젝트 초기에는 느슨하게 설정하고, 시스템이 성숙해짐에 따라 강화합니다. 초기에 지나치게 엄격한 기준을 설정하면 회귀 테스트 스위트가 비어 있게 되어, 변경의 영향을 전혀 감지할 수 없습니다.
Golden Dataset은 회귀 테스트의 기반이 되는 검증된 입력-기대출력 쌍의 집합입니다. 에이전트 테스트에서 Golden Dataset은 전통적인 소프트웨어의 "테스트 픽스처"에 해당합니다.
from dataclasses import dataclass, field
@dataclass
class GoldenTestCase:
"""Golden Dataset의 개별 테스트 케이스"""
id: str
category: str
input_message: str
expected_tools: list[str] # 호출해야 할 도구 목록
expected_tool_params: dict | None # 핵심 파라미터
quality_criteria: list[str] # LLM-as-Judge 평가 기준
min_score: float # 최소 통과 점수
reference_response: str | None # 참조 답변 (선택)
tags: list[str] = field(default_factory=list)
created_at: str = ""
last_passed: str = ""
pass_rate: float = 0.0
@dataclass
class GoldenDataset:
"""회귀 테스트용 Golden Dataset"""
name: str
version: str
test_cases: list[GoldenTestCase]
metadata: dict = field(default_factory=dict)
def filter_by_category(self, category: str) -> list[GoldenTestCase]:
return [tc for tc in self.test_cases if tc.category == category]
def filter_by_tags(self, tags: list[str]) -> list[GoldenTestCase]:
return [
tc for tc in self.test_cases
if any(tag in tc.tags for tag in tags)
]name: customer-support-agent-golden
version: "1.3.0"
metadata:
agent: customer-support-v2
created: "2026-03-01"
last_updated: "2026-03-28"
test_cases:
- id: "CS-001"
category: "order_inquiry"
input_message: "주문번호 ORD-12345의 배송 상태를 확인해 주세요"
expected_tools:
- "lookup_order"
- "get_shipping_status"
expected_tool_params:
lookup_order:
order_id: "ORD-12345"
quality_criteria:
- "주문번호를 정확히 조회했는가"
- "배송 상태 정보가 포함되어 있는가"
- "예상 배송일이 안내되었는가"
min_score: 0.85
tags: ["order", "shipping", "core"]
- id: "CS-002"
category: "refund"
input_message: "지난주 구매한 상품을 환불하고 싶습니다"
expected_tools:
- "get_recent_orders"
- "check_refund_eligibility"
quality_criteria:
- "최근 주문 목록을 조회했는가"
- "환불 정책을 안내했는가"
- "환불 절차를 단계별로 설명했는가"
min_score: 0.80
tags: ["refund", "core"]Golden Dataset은 코드와 마찬가지로 버전 관리가 필요합니다.
import hashlib
import json
def compute_dataset_hash(dataset: GoldenDataset) -> str:
"""데이터셋의 해시를 계산하여 변경을 추적"""
content = json.dumps(
[vars(tc) for tc in dataset.test_cases],
sort_keys=True,
ensure_ascii=False,
)
return hashlib.sha256(content.encode()).hexdigest()[:12]
def track_dataset_change(old_dataset, new_dataset) -> dict:
"""두 데이터셋 버전 간의 차이를 분석"""
old_ids = {tc.id for tc in old_dataset.test_cases}
new_ids = {tc.id for tc in new_dataset.test_cases}
return {
"added": new_ids - old_ids,
"removed": old_ids - new_ids,
"modified": {
tc.id for tc in new_dataset.test_cases
if tc.id in old_ids and tc != get_by_id(old_dataset, tc.id)
},
"unchanged": old_ids & new_ids,
}회귀 테스트의 핵심은 시간에 따른 성공률 추이를 모니터링하는 것입니다. 단일 실행의 성패보다 **추세(Trend)**가 더 중요합니다.
from datetime import datetime, timedelta
from collections import defaultdict
class RollingMetrics:
"""롤링 윈도우 기반 성공률 모니터링"""
def __init__(self, results: list[dict]):
self.results = sorted(results, key=lambda r: r["date"])
def success_rate(self, window_days: int) -> float:
"""지정된 기간의 성공률 계산"""
cutoff = datetime.now() - timedelta(days=window_days)
recent = [r for r in self.results if r["date"] >= cutoff]
if not recent:
return 0.0
passed = sum(1 for r in recent if r["passed"])
return passed / len(recent)
def by_category(self, window_days: int) -> dict:
"""카테고리별 성공률"""
cutoff = datetime.now() - timedelta(days=window_days)
recent = [r for r in self.results if r["date"] >= cutoff]
categories = defaultdict(lambda: {"passed": 0, "total": 0})
for r in recent:
cat = r["category"]
categories[cat]["total"] += 1
if r["passed"]:
categories[cat]["passed"] += 1
return {
cat: data["passed"] / data["total"]
for cat, data in categories.items()
}
def detect_regression(
self,
baseline_window: int = 30,
current_window: int = 7,
threshold_drop: float = 0.10,
) -> dict:
"""현재 성공률이 베이스라인 대비 하락했는지 감지"""
baseline_rate = self.success_rate(baseline_window)
current_rate = self.success_rate(current_window)
drop = baseline_rate - current_rate
is_regression = drop >= threshold_drop
return {
"baseline_rate": baseline_rate,
"current_rate": current_rate,
"drop": drop,
"is_regression": is_regression,
"message": (
f"성공률이 {drop:.1%} 하락했습니다 "
f"({baseline_rate:.1%} -> {current_rate:.1%})"
if is_regression
else "정상 범위입니다"
),
}10% 하락 플래그는 절대적 규칙이 아니라 시작점입니다. 테스트 케이스 수가 적은 초기에는 단일 실패가 큰 비율 변동을 일으킬 수 있으므로, 최소 샘플 수 조건도 함께 적용해야 합니다.
**베이스라인(Baseline)**은 현재 에이전트의 "정상 성능"을 나타내는 기준값입니다. 모든 회귀 판단은 이 베이스라인과의 비교로 이루어집니다.
@dataclass
class Baseline:
"""에이전트 성능 베이스라인"""
agent_version: str
created_at: str
overall_success_rate: float
category_rates: dict
average_scores: dict
sample_size: int
@classmethod
def from_results(cls, agent_version: str, results: list[dict]):
"""실행 결과로부터 베이스라인 생성"""
import statistics
passed = sum(1 for r in results if r["passed"])
scores = [r["score"] for r in results]
# 카테고리별 성공률
categories = defaultdict(list)
for r in results:
categories[r["category"]].append(r["passed"])
return cls(
agent_version=agent_version,
created_at=datetime.now().isoformat(),
overall_success_rate=passed / len(results),
category_rates={
cat: sum(vals) / len(vals)
for cat, vals in categories.items()
},
average_scores={
"mean": statistics.mean(scores),
"stdev": statistics.stdev(scores),
"p50": sorted(scores)[len(scores) // 2],
},
sample_size=len(results),
)
def should_update_baseline(
current_baseline: Baseline,
new_results: list[dict],
improvement_threshold: float = 0.05,
) -> bool:
"""베이스라인을 갱신해야 하는지 판단"""
new_rate = sum(1 for r in new_results if r["passed"]) / len(new_results)
improvement = new_rate - current_baseline.overall_success_rate
# 유의미한 개선이 있을 때만 갱신
return improvement >= improvement_threshold프롬프트 변경, 도구 추가, 모델 교체 등의 변경이 에이전트 품질에 미치는 영향을 분석합니다.
async def analyze_change_impact(
agent_before,
agent_after,
golden_dataset: GoldenDataset,
num_runs: int = 3,
) -> dict:
"""변경 전후의 에이전트 성능을 비교 분석"""
before_results = []
after_results = []
for test_case in golden_dataset.test_cases:
# 변경 전 에이전트 실행
for _ in range(num_runs):
result = await run_test(agent_before, test_case)
before_results.append({**result, "test_id": test_case.id})
# 변경 후 에이전트 실행
for _ in range(num_runs):
result = await run_test(agent_after, test_case)
after_results.append({**result, "test_id": test_case.id})
# 테스트 케이스별 비교
comparison = {}
for tc in golden_dataset.test_cases:
before = [r for r in before_results if r["test_id"] == tc.id]
after = [r for r in after_results if r["test_id"] == tc.id]
before_rate = sum(r["passed"] for r in before) / len(before)
after_rate = sum(r["passed"] for r in after) / len(after)
comparison[tc.id] = {
"before": before_rate,
"after": after_rate,
"delta": after_rate - before_rate,
"status": (
"improved" if after_rate > before_rate
else "degraded" if after_rate < before_rate
else "unchanged"
),
}
# 전체 요약
improved = sum(1 for c in comparison.values() if c["status"] == "improved")
degraded = sum(1 for c in comparison.values() if c["status"] == "degraded")
unchanged = sum(1 for c in comparison.values() if c["status"] == "unchanged")
return {
"summary": {
"improved": improved,
"degraded": degraded,
"unchanged": unchanged,
"total": len(comparison),
},
"details": comparison,
"recommendation": (
"APPROVE" if degraded == 0
else "REVIEW" if degraded <= 2
else "BLOCK"
),
}졸업, 실행, 모니터링, 알림을 하나의 자동화 파이프라인으로 통합합니다.
async def regression_pipeline(
agent,
golden_dataset: GoldenDataset,
baseline: Baseline,
) -> dict:
"""회귀 테스트 자동화 파이프라인"""
# 1단계: 회귀 테스트 실행
results = []
for tc in golden_dataset.test_cases:
result = await run_regression_test(agent, tc)
results.append(result)
# 2단계: 롤링 메트릭 계산
metrics = RollingMetrics(results)
regression = metrics.detect_regression()
# 3단계: 베이스라인 비교
current_rate = sum(r["passed"] for r in results) / len(results)
baseline_delta = current_rate - baseline.overall_success_rate
# 4단계: 판정
verdict = "PASS"
if regression["is_regression"]:
verdict = "FAIL"
elif baseline_delta < -0.05:
verdict = "WARNING"
# 5단계: 리포트 생성
report = {
"verdict": verdict,
"current_success_rate": current_rate,
"baseline_success_rate": baseline.overall_success_rate,
"delta": baseline_delta,
"regression_detected": regression["is_regression"],
"failed_tests": [r for r in results if not r["passed"]],
"total_tests": len(results),
}
return report이번 장에서는 평가를 회귀 테스트로 전환하는 체계적인 방법론을 다루었습니다.
7장에서는 회귀 테스트를 넘어 에이전트의 장기적 안정성을 추적하는 메트릭을 다룹니다. 태스크 성공률 추이, 행동 드리프트 감지, 응답 길이 변동, 신뢰도 변화, 지연시간 안정성 등을 측정하고 대시보드로 시각화하는 방법을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
태스크 성공률 추이, 행동 드리프트 감지, 응답 길이 변동, 지연시간 안정성, 비용 변동성 등 에이전트의 장기적 안정성을 추적하는 메트릭과 대시보드 설계를 다룹니다.
LLM-as-Judge 패턴으로 에이전트의 비결정적 출력을 평가하는 방법, 품질 차원별 점수 산출, 임계값 설정, pass@k 전략을 상세히 다룹니다.
시뮬레이션 사용자 기반 적대적 테스트, 엣지 케이스 자동 생성, 스트레스 테스트, 안전성 가드레일 검증, 자동 레드티밍 기법을 다룹니다.