반응형
에이전트 만들었는데 데모에선 완벽하고 프로덕션에선 이상하게 돌아가는 경험, 다들 있으시죠?
핵심 요약
→ LangChain 2026: 프로덕션 에이전트 팀의 32%가 품질을 최대 장벽으로 꼽음
→ 63%의 에이전트가 복잡한 다단계 태스크에서 실패 — 데모는 통과해도
→ 에이전트 평가 ≠ 일반 LLM 평가 — 비결정성·도구 체인·궤적이 핵심
→ 3계층 평가: L1 결과(맞냐?) → L2 도구 호출(올바른 툴?) → L3 궤적(효율적?)
→ LLM-as-Judge: 인간 평가자와 85% 일치 — 인간끼리 일치율보다 높음
→ CLEAR 프레임워크: Cost·Latency·Effectiveness·Accuracy·Reliability 5축 평가
→ 황금 케이스 50~100개 수작업 → 프로덕션 트레이스 500개+ 확보 필수
→ CI/CD에 eval 통합 → 배포 전 품질 게이트 자동화
실전 1 — 왜 에이전트 품질 평가가 일반 LLM과 다른가
# ❌ 일반 LLM 평가 방식 — 에이전트엔 안 통함
def evaluate_llm(prompt: str, response: str) -> float:
# 입력 → 출력 1:1 비교
# 정답지와 비교하면 끝
return similarity_score(response, expected_answer)
# 에이전트는 왜 이게 안 되나?
# 문제 1: 비결정성 체인
# 10~20개 LLM 호출을 연속으로 실행
# 3번째 호출의 미세한 차이 → 7번째 호출에서 완전히 다른 결과
# 같은 입력으로 100번 돌리면 100가지 다른 경로
# 문제 2: 과정 없이 결과만 봐선 안 됨
# 최종 답이 맞아도 → 중간에 잘못된 도구 5번 호출했을 수 있음
# 프로덕션에서는 결국 터짐
# 문제 3: 도구 호출 정확도 별도 평가 필요
# "검색 도구를 써야 할 때 계산 도구를 씀" → 결과는 우연히 맞음
# 이런 에이전트는 조금만 입력이 바뀌면 완전히 실패
개념 정리
→ L1 평가 (결과): 최종 답이 맞냐 — 가장 기본, 하지만 불충분
→ L2 평가 (도구): 올바른 도구를 올바른 순서로 호출했나
→ L3 평가 (궤적): 최적 경로로 태스크를 완료했나 (불필요한 단계 없이)
→ 2026 실무 기준: 세 계층 모두 평가해야 '프로덕션 안전' 에이전트
실전 2 — 3계층 평가 프레임워크 구현
from anthropic import Anthropic
from dataclasses import dataclass
from typing import Any
client = Anthropic()
@dataclass
class AgentTrace:
"""에이전트 실행 기록 — 평가의 기본 단위"""
task_id: str
user_input: str
tool_calls: list[dict] # 도구 호출 순서 및 파라미터
final_output: str
total_tokens: int
latency_seconds: float
step_count: int
@dataclass
class EvalResult:
l1_outcome_score: float # 0~1: 최종 결과 품질
l2_tool_score: float # 0~1: 도구 호출 정확도
l3_trajectory_score: float # 0~1: 경로 효율성
passed: bool
failure_reason: str | None
# ─────────────────────────────────────────
# L1: 결과 평가 — 최종 답이 맞냐
# ─────────────────────────────────────────
def evaluate_outcome(trace: AgentTrace, expected: str) -> float:
"""LLM-as-Judge로 최종 결과 품질 평가"""
judge_prompt = f"""
태스크: {trace.user_input}
에이전트 출력: {trace.final_output}
기대 결과: {expected}
평가 기준:
1. 태스크를 완료했는가? (0~40점)
2. 출력이 정확한가? (0~40점)
3. 불필요한 내용 없이 간결한가? (0~20점)
점수를 0~100 사이 숫자 하나만 출력하세요. 설명 없이 숫자만.
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
messages=[{"role": "user", "content": judge_prompt}]
)
try:
score = float(response.content[0].text.strip()) / 100
return min(max(score, 0.0), 1.0)
except ValueError:
return 0.0
# ─────────────────────────────────────────
# L2: 도구 호출 평가 — 올바른 툴을 썼냐
# ─────────────────────────────────────────
def evaluate_tool_calls(
trace: AgentTrace,
expected_tools: list[str] # 이 태스크에서 써야 하는 도구 목록
) -> float:
actual_tools = [call["name"] for call in trace.tool_calls]
# 필수 도구 누락 체크
missing = set(expected_tools) - set(actual_tools)
# 불필요한 도구 호출 체크
unexpected = set(actual_tools) - set(expected_tools)
# 도구 호출 순서 체크 (순서가 중요한 경우)
order_correct = actual_tools[:len(expected_tools)] == expected_tools
# 점수 계산
base_score = 1.0
base_score -= len(missing) * 0.3 # 누락 도구당 -0.3
base_score -= len(unexpected) * 0.1 # 불필요 도구당 -0.1
if not order_correct:
base_score -= 0.2 # 순서 틀리면 -0.2
return max(base_score, 0.0)
# ─────────────────────────────────────────
# L3: 궤적 평가 — 효율적으로 달성했냐
# ─────────────────────────────────────────
def evaluate_trajectory(
trace: AgentTrace,
optimal_steps: int # 이 태스크의 최적 단계 수
) -> float:
# 효율성: 실제 단계 수 vs 최적 단계 수
efficiency = optimal_steps / max(trace.step_count, 1)
efficiency_score = min(efficiency, 1.0) # 최적보다 적게 쓰면 1.0
# 토큰 효율성 (단계당 평균 토큰)
tokens_per_step = trace.total_tokens / max(trace.step_count, 1)
token_score = 1.0 if tokens_per_step < 2000 else max(0, 1 - (tokens_per_step - 2000) / 10000)
# 응답 속도
latency_score = 1.0 if trace.latency_seconds < 10 else max(0, 1 - (trace.latency_seconds - 10) / 50)
return (efficiency_score * 0.5 + token_score * 0.3 + latency_score * 0.2)
# ─────────────────────────────────────────
# 통합 평가 실행
# ─────────────────────────────────────────
def run_full_eval(
trace: AgentTrace,
expected_output: str,
expected_tools: list[str],
optimal_steps: int
) -> EvalResult:
l1 = evaluate_outcome(trace, expected_output)
l2 = evaluate_tool_calls(trace, expected_tools)
l3 = evaluate_trajectory(trace, optimal_steps)
# 가중 합산 — L1 가장 중요
weighted_score = l1 * 0.5 + l2 * 0.3 + l3 * 0.2
passed = weighted_score >= 0.7 # 70% 이상이면 통과
failure_reason = None
if not passed:
if l1 < 0.5:
failure_reason = f"결과 품질 미달 (L1: {l1:.2f})"
elif l2 < 0.5:
failure_reason = f"잘못된 도구 호출 (L2: {l2:.2f})"
else:
failure_reason = f"비효율적 궤적 (L3: {l3:.2f})"
return EvalResult(l1, l2, l3, passed, failure_reason)
개념 정리
→ T-Eval: 각 단계에서 다음 도구 호출이 기대값과 일치하는지 채점
→ Progress Rate: 실제 궤적 vs 기대 궤적 비교
→ "올바른 결과를 잘못된 방법으로 낸 에이전트" = 프로덕션 시한폭탄
→ 도구 호출 정확도는 결과 정확도와 별도로 반드시 측정
실전 3 — CLEAR 프레임워크: 5축 품질 대시보드
단일 점수로 에이전트 품질을 요약하면 놓치는 게 너무 많음. CLEAR 5축으로 입체 평가.
from dataclasses import dataclass
import statistics
@dataclass
class CLEARMetrics:
"""CLEAR: Cost / Latency / Effectiveness / Accuracy / Reliability"""
# C — Cost: 쿼리당 비용
cost_per_query: float # $
cost_vs_baseline: float # baseline 대비 비율 (1.0 = 동일)
# L — Latency: 응답 속도
p50_latency: float # 중앙값 응답시간 (초)
p99_latency: float # 99퍼센타일 (느린 케이스)
# E — Effectiveness: 태스크 완료율
task_completion_rate: float # 0~1
user_satisfaction_score: float # 0~1 (human eval 또는 thumbs up/down)
# A — Accuracy: 정확도
l1_outcome_accuracy: float # 결과 정확도
l2_tool_accuracy: float # 도구 정확도
# R — Reliability: 안정성
error_rate: float # 실패율
consistency_score: float # 같은 입력 10회 실행 시 일관성
def compute_clear(traces: list[AgentTrace], eval_results: list[EvalResult]) -> CLEARMetrics:
costs = [t.total_tokens * 0.000003 for t in traces] # sonnet 기준
latencies = [t.latency_seconds for t in traces]
return CLEARMetrics(
# Cost
cost_per_query=statistics.mean(costs),
cost_vs_baseline=statistics.mean(costs) / 0.01, # 기준 비용 $0.01
# Latency
p50_latency=statistics.median(latencies),
p99_latency=sorted(latencies)[int(len(latencies) * 0.99)],
# Effectiveness
task_completion_rate=sum(1 for r in eval_results if r.passed) / len(eval_results),
user_satisfaction_score=statistics.mean([r.l1_outcome_score for r in eval_results]),
# Accuracy
l1_outcome_accuracy=statistics.mean([r.l1_outcome_score for r in eval_results]),
l2_tool_accuracy=statistics.mean([r.l2_tool_score for r in eval_results]),
# Reliability
error_rate=sum(1 for r in eval_results if not r.passed) / len(eval_results),
consistency_score=1 - statistics.stdev([r.l1_outcome_score for r in eval_results])
)
def print_clear_report(metrics: CLEARMetrics):
print("=" * 50)
print("CLEAR 품질 대시보드")
print("=" * 50)
print(f"💰 Cost: ${metrics.cost_per_query:.4f}/쿼리 (기준 대비 {metrics.cost_vs_baseline:.1f}x)")
print(f"⚡ Latency: P50={metrics.p50_latency:.1f}s P99={metrics.p99_latency:.1f}s")
print(f"✅ Effectiveness: 완료율 {metrics.task_completion_rate:.1%} 만족도 {metrics.user_satisfaction_score:.1%}")
print(f"🎯 Accuracy: 결과 {metrics.l1_outcome_accuracy:.1%} 도구 {metrics.l2_tool_accuracy:.1%}")
print(f"🛡 Reliability: 오류율 {metrics.error_rate:.1%} 일관성 {metrics.consistency_score:.1%}")
# 자동 경보
if metrics.task_completion_rate < 0.8:
print(f"\n⚠️ 경보: 완료율 {metrics.task_completion_rate:.1%} — 목표 80% 미달")
if metrics.p99_latency > 30:
print(f"\n⚠️ 경보: P99 레이턴시 {metrics.p99_latency:.1f}s — 30초 초과")
if metrics.error_rate > 0.05:
print(f"\n⚠️ 경보: 오류율 {metrics.error_rate:.1%} — 5% 초과")
개념 정리
→ 95% 정확도지만 비용이 10배 → 프로덕션 불가
→ CLEAR는 품질과 효율을 동시에 측정하는 균형 프레임워크
→ P99 레이턴시: 평균이 아닌 느린 케이스를 봐야 실제 사용자 경험
→ 일관성 점수: 표준편차 기반 — 낮을수록 결과가 예측 가능
실전 4 — CI/CD에 Eval 통합: 배포 전 품질 게이트
import subprocess
import json
import sys
# eval 데이터셋 — 황금 케이스 50~100개 수작업 + 프로덕션 트레이스 500개+
GOLDEN_DATASET = [
{
"task_id": "search_and_summarize_001",
"user_input": "최신 Python 릴리즈 노트 요약해줘",
"expected_tools": ["web_search", "text_summarizer"],
"expected_output_keywords": ["Python", "버전", "새 기능"],
"optimal_steps": 3
},
{
"task_id": "code_review_001",
"user_input": "이 Python 함수의 버그 찾아줘",
"expected_tools": ["code_analyzer", "linter"],
"expected_output_keywords": ["버그", "수정", "라인"],
"optimal_steps": 2
},
# ... 나머지 케이스
]
def run_eval_suite(agent_version: str) -> dict:
"""CI/CD에서 호출하는 eval 스위트"""
results = []
for case in GOLDEN_DATASET:
# 에이전트 실행 (실제 구현에서 에이전트 호출)
trace = run_agent(case["user_input"])
# 3계층 평가
eval_result = run_full_eval(
trace=trace,
expected_output=" ".join(case["expected_output_keywords"]),
expected_tools=case["expected_tools"],
optimal_steps=case["optimal_steps"]
)
results.append(eval_result)
# 집계
pass_rate = sum(1 for r in results if r.passed) / len(results)
avg_l1 = sum(r.l1_outcome_score for r in results) / len(results)
avg_l2 = sum(r.l2_tool_score for r in results) / len(results)
return {
"version": agent_version,
"pass_rate": pass_rate,
"avg_outcome_score": avg_l1,
"avg_tool_score": avg_l2,
"failures": [r.failure_reason for r in results if not r.passed]
}
def quality_gate(report: dict) -> bool:
"""배포 차단 조건 — 이 기준 미달 시 PR 머지 불가"""
# ❌ 차단 조건
if report["pass_rate"] < 0.80:
print(f"❌ 배포 차단: 통과율 {report['pass_rate']:.1%} < 80%")
return False
if report["avg_outcome_score"] < 0.75:
print(f"❌ 배포 차단: 결과 품질 {report['avg_outcome_score']:.1%} < 75%")
return False
if report["avg_tool_score"] < 0.70:
print(f"❌ 배포 차단: 도구 정확도 {report['avg_tool_score']:.1%} < 70%")
return False
# ✅ 통과
print(f"✅ 품질 게이트 통과: {report['pass_rate']:.1%}")
return True
# GitHub Actions에서 실행
if __name__ == "__main__":
agent_version = sys.argv[1] # 버전 태그 받음
print(f"🔍 Eval 시작: {agent_version}")
report = run_eval_suite(agent_version)
# 결과 저장
with open("eval_report.json", "w") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
# 품질 게이트 — 실패 시 exit(1) → CI 파이프라인 중단
if not quality_gate(report):
sys.exit(1)
print("🚀 배포 진행")
# .github/workflows/agent-eval.yml
name: Agent Quality Gate
on:
pull_request:
branches: [main]
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run eval suite
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
pip install anthropic
python eval/run_eval.py ${{ github.sha }}
- name: Upload eval report
uses: actions/upload-artifact@v4
with:
name: eval-report
path: eval_report.json
if: always() # 실패해도 리포트는 업로드
개념 정리
→ 황금 케이스 50~100개: 수작업, 가장 중요한 시나리오 커버
→ 프로덕션 트레이스 500개+: 실제 실패 케이스 기반 → 생태학적 타당성 높음
→ 500개 확보 전까지는 집계 지표를 신뢰하지 말 것 (2026 업계 권고)
→ eval 비용: 판사 모델(sonnet) 10회/트레이스 × 1000트레이스 = 월 $30 수준
→ 비용이 LLM 청구서 10% 초과 시 → 소형 증류 판사 모델(Galileo Luna)로 교체
마무리
✅ 품질 관리 시스템 구축 후
→ 데모 통과 → 프로덕션 실패 패턴 사전 차단
→ L1·L2·L3 3계층으로 "맞는 답을 잘못된 방법으로 낸" 에이전트 탐지
→ CI/CD 품질 게이트 → 품질 회귀 자동 차단
→ CLEAR 대시보드 → 품질·비용·속도 동시 모니터링
❌ 품질 관리 없이 배포하면
→ 63%의 에이전트가 복잡한 태스크에서 실패
→ 프로덕션에서 터질 때까지 문제를 모름
→ 도구 호출 오류가 쌓여 어느 날 갑자기 대규모 장애
→ 32%가 품질 문제로 에이전트 프로덕션 포기
관련글
- AI 에이전트 Capacity Engineering: https://cell-devlog.tistory.com/236
- 에이전트 옵저버빌리티 실전: https://cell-devlog.tistory.com/90
- LLM-as-Judge 평가 방법론: https://cell-devlog.tistory.com/155
- 에이전트 롤백 전략: https://cell-devlog.tistory.com/152
반응형
'AI Agent' 카테고리의 다른 글
| AI 에이전트 Durable Execution 실전 1편 — 에이전트가 죽어도 이어지는 워크플로우 설계 (0) | 2026.05.21 |
|---|---|
| OpenTelemetry로 LLM 모니터링 — 블랙박스 에이전트를 투명하게 만드는 법 (0) | 2026.05.21 |
| LLM 에이전트 Capacity Engineering — 프로덕션 오류의 1/3이 rate limit인 이유 (0) | 2026.05.21 |
| Eval-Driven Development 완전 가이드 — AI 에이전트를 TDD처럼 개발하는 법 (0) | 2026.05.21 |
| AI 에이전트 메모리 관리 실전 — 세션 간 상태 유지, 컨텍스트 압축, 레포 재탐색 방지 (0) | 2026.05.21 |