태스크 성공률 추이, 행동 드리프트 감지, 응답 길이 변동, 지연시간 안정성, 비용 변동성 등 에이전트의 장기적 안정성을 추적하는 메트릭과 대시보드 설계를 다룹니다.
에이전트가 "올바른 답변"을 한다고 해서 "안정적"인 것은 아닙니다. 오늘 90%의 성공률을 보이는 에이전트가 내일은 70%로 떨어질 수 있습니다. 모델 업데이트, 프롬프트 변경, 외부 API의 변화 등 다양한 요인이 에이전트의 행동을 변화시킵니다.
**안정성(Stability)**은 시간에 걸쳐 일관된 품질을 유지하는 능력입니다. 이를 측정하려면 단일 시점의 스냅샷이 아닌, 시계열 데이터를 수집하고 분석해야 합니다.
가장 기본적인 안정성 메트릭은 **태스크 성공률(Task Success Rate)**의 시계열 추이입니다.
from datetime import datetime, timedelta
from collections import defaultdict
import statistics
class SuccessRateTracker:
"""태스크 성공률의 시계열 추적"""
def __init__(self, results: list[dict]):
self.results = results
def daily_rates(self, days: int = 30) -> list[dict]:
"""일별 성공률 계산"""
cutoff = datetime.now() - timedelta(days=days)
recent = [r for r in self.results if r["timestamp"] >= cutoff]
by_day = defaultdict(list)
for r in recent:
day_key = r["timestamp"].strftime("%Y-%m-%d")
by_day[day_key].append(r["passed"])
return [
{
"date": day,
"success_rate": sum(results) / len(results),
"total": len(results),
"passed": sum(results),
}
for day, results in sorted(by_day.items())
]
def weekly_rates(self, weeks: int = 12) -> list[dict]:
"""주별 성공률 계산"""
cutoff = datetime.now() - timedelta(weeks=weeks)
recent = [r for r in self.results if r["timestamp"] >= cutoff]
by_week = defaultdict(list)
for r in recent:
week_key = r["timestamp"].strftime("%Y-W%W")
by_week[week_key].append(r["passed"])
return [
{
"week": week,
"success_rate": sum(results) / len(results),
"total": len(results),
}
for week, results in sorted(by_week.items())
]
def trend_analysis(self, window_days: int = 14) -> dict:
"""성공률 추세 분석 (상승/하락/안정)"""
daily = self.daily_rates(window_days)
if len(daily) < 3:
return {"trend": "insufficient_data"}
rates = [d["success_rate"] for d in daily]
# 단순 선형 회귀
n = len(rates)
x_mean = (n - 1) / 2
y_mean = statistics.mean(rates)
numerator = sum((i - x_mean) * (r - y_mean) for i, r in enumerate(rates))
denominator = sum((i - x_mean) ** 2 for i in range(n))
slope = numerator / denominator if denominator != 0 else 0
if slope > 0.01:
trend = "improving"
elif slope < -0.01:
trend = "declining"
else:
trend = "stable"
return {
"trend": trend,
"slope": slope,
"current_rate": rates[-1],
"period_average": y_mean,
}**행동 드리프트(Behavioral Drift)**는 에이전트의 행동 패턴이 시간이 지남에 따라 미묘하게 변화하는 현상입니다. 성공률은 유지되지만, 응답의 스타일이나 도구 사용 패턴이 달라질 수 있습니다. 드리프트는 세 가지 신호로 감지합니다.
에이전트의 응답 길이가 갑자기 길어지거나 짧아지는 것은 드리프트의 대표적 신호입니다.
class ResponseLengthMonitor:
"""응답 길이 변동 모니터링"""
def __init__(self, baseline_stats: dict):
self.baseline_mean = baseline_stats["mean"]
self.baseline_stdev = baseline_stats["stdev"]
def check(self, recent_lengths: list[int]) -> dict:
"""최근 응답 길이가 베이스라인에서 벗어났는지 확인"""
current_mean = statistics.mean(recent_lengths)
current_stdev = statistics.stdev(recent_lengths) if len(recent_lengths) > 1 else 0
# Z-score 기반 이상 감지
z_score = (
(current_mean - self.baseline_mean) / self.baseline_stdev
if self.baseline_stdev > 0 else 0
)
is_drifting = abs(z_score) > 2.0 # 2 표준편차 이상 벗어남
return {
"baseline_mean": self.baseline_mean,
"current_mean": current_mean,
"z_score": z_score,
"is_drifting": is_drifting,
"direction": (
"longer" if z_score > 0
else "shorter" if z_score < 0
else "stable"
),
}에이전트가 제공하는 신뢰도(Confidence) 점수나 불확실성 표현의 빈도가 변하는 것도 드리프트 신호입니다.
class ConfidenceDriftDetector:
"""에이전트 신뢰도 변화 감지"""
def __init__(self, baseline_confidence: float, tolerance: float = 0.15):
self.baseline = baseline_confidence
self.tolerance = tolerance
def analyze(self, recent_responses: list[str]) -> dict:
"""응답에서 불확실성 표현의 빈도를 분석"""
uncertainty_markers = [
"아마", "추정", "정확하지 않", "확실하지 않",
"가능성이", "것 같습니다", "것으로 보입니다",
"수도 있", "한 것으로 판단",
]
total = len(recent_responses)
uncertain_count = 0
for response in recent_responses:
if any(marker in response for marker in uncertainty_markers):
uncertain_count += 1
uncertainty_rate = uncertain_count / total if total > 0 else 0
delta = uncertainty_rate - self.baseline
return {
"baseline_uncertainty": self.baseline,
"current_uncertainty": uncertainty_rate,
"delta": delta,
"is_drifting": abs(delta) > self.tolerance,
"direction": (
"more_uncertain" if delta > 0
else "more_confident" if delta < 0
else "stable"
),
}에이전트가 도구를 선택하는 패턴, 사용하는 도구의 종류, 호출 순서 등이 변하는 것도 감지해야 합니다.
from collections import Counter
class ReasoningPatternMonitor:
"""추론 패턴(도구 사용 패턴) 변화 감지"""
def __init__(self, baseline_distribution: dict):
self.baseline = baseline_distribution
def check(self, recent_tool_calls: list[list[str]]) -> dict:
"""최근 도구 사용 분포를 베이스라인과 비교"""
# 현재 도구 사용 분포 계산
all_tools = []
for call_sequence in recent_tool_calls:
all_tools.extend(call_sequence)
current_counter = Counter(all_tools)
total = sum(current_counter.values())
current_distribution = {
tool: count / total
for tool, count in current_counter.items()
}
# 분포 차이 계산 (Jensen-Shannon Divergence 간소화 버전)
all_keys = set(self.baseline.keys()) | set(current_distribution.keys())
divergence = sum(
abs(
self.baseline.get(k, 0) - current_distribution.get(k, 0)
)
for k in all_keys
) / 2 # 정규화
# 새로 등장하거나 사라진 도구 감지
new_tools = set(current_distribution.keys()) - set(self.baseline.keys())
missing_tools = set(self.baseline.keys()) - set(current_distribution.keys())
return {
"divergence": divergence,
"is_drifting": divergence > 0.20, # 20% 이상 차이
"new_tools": list(new_tools),
"missing_tools": list(missing_tools),
"baseline_distribution": self.baseline,
"current_distribution": current_distribution,
}행동 드리프트는 반드시 "나쁜" 것이 아닙니다. 모델 업데이트로 인해 에이전트가 더 나은 도구 선택 패턴을 학습했을 수도 있습니다. 드리프트를 감지한 후에는 성공률과 품질 점수를 함께 확인하여, 개선인지 저하인지 판단해야 합니다.
에이전트의 응답 시간이 불안정하면 사용자 경험이 저하됩니다. 특히 멀티스텝 에이전트는 여러 도구 호출을 순차적으로 수행하므로, 총 지연시간이 크게 변동할 수 있습니다.
class LatencyTracker:
"""에이전트 응답 지연시간 추적"""
def __init__(self, results: list[dict]):
self.latencies = [r["duration_ms"] for r in results]
def percentiles(self) -> dict:
"""주요 백분위수 계산"""
sorted_lat = sorted(self.latencies)
n = len(sorted_lat)
return {
"p50": sorted_lat[int(n * 0.50)],
"p90": sorted_lat[int(n * 0.90)],
"p95": sorted_lat[int(n * 0.95)],
"p99": sorted_lat[int(n * 0.99)] if n >= 100 else None,
"mean": statistics.mean(self.latencies),
"stdev": statistics.stdev(self.latencies),
}
def detect_slowdown(
self,
baseline_p95: float,
threshold_factor: float = 1.5,
) -> dict:
"""P95 지연시간이 베이스라인 대비 증가했는지 감지"""
current_p95 = sorted(self.latencies)[int(len(self.latencies) * 0.95)]
is_slow = current_p95 > baseline_p95 * threshold_factor
return {
"baseline_p95_ms": baseline_p95,
"current_p95_ms": current_p95,
"ratio": current_p95 / baseline_p95 if baseline_p95 > 0 else 0,
"is_slowdown": is_slow,
}LLM API 호출 비용은 토큰 사용량에 비례합니다. 에이전트의 행동이 변하면 비용도 변동합니다.
class CostTracker:
"""에이전트 실행 비용 추적"""
def __init__(self, results: list[dict]):
self.results = results
def per_task_cost(self) -> dict:
"""태스크당 비용 통계"""
costs = [r["total_cost_usd"] for r in self.results]
return {
"mean": statistics.mean(costs),
"median": statistics.median(costs),
"stdev": statistics.stdev(costs) if len(costs) > 1 else 0,
"min": min(costs),
"max": max(costs),
"total": sum(costs),
}
def token_usage(self) -> dict:
"""토큰 사용량 통계"""
input_tokens = [r["input_tokens"] for r in self.results]
output_tokens = [r["output_tokens"] for r in self.results]
return {
"input": {
"mean": statistics.mean(input_tokens),
"total": sum(input_tokens),
},
"output": {
"mean": statistics.mean(output_tokens),
"total": sum(output_tokens),
},
}
def detect_cost_anomaly(
self,
baseline_mean_cost: float,
threshold_factor: float = 2.0,
) -> dict:
"""비용 이상치 감지"""
current_mean = statistics.mean(
[r["total_cost_usd"] for r in self.results]
)
is_anomaly = current_mean > baseline_mean_cost * threshold_factor
return {
"baseline_mean_usd": baseline_mean_cost,
"current_mean_usd": current_mean,
"ratio": current_mean / baseline_mean_cost if baseline_mean_cost > 0 else 0,
"is_anomaly": is_anomaly,
}비용 추적은 단순히 절약을 위한 것이 아닙니다. 갑작스러운 비용 증가는 에이전트가 불필요한 도구 호출을 반복하거나, 루프에 빠졌다는 신호일 수 있습니다. 비용 이상은 항상 행동 분석과 연결하여 확인해야 합니다.
모든 안정성 메트릭을 하나의 대시보드에 통합하면, 에이전트의 전체 건강 상태를 한눈에 파악할 수 있습니다.
@dataclass
class StabilityDashboard:
"""에이전트 안정성 대시보드"""
agent_name: str
report_period: str
generated_at: str
# 핵심 메트릭
success_rate: dict # 일별/주별 성공률 추이
trend: dict # 상승/하락/안정 추세
# 드리프트 신호
response_length: dict # 응답 길이 변동
confidence: dict # 신뢰도 변화
reasoning_pattern: dict # 추론 패턴 차이
# 운영 메트릭
latency: dict # 지연시간 백분위수
cost: dict # 비용 통계
# 종합 건강 점수
health_score: float # 0.0 ~ 1.0
@property
def status(self) -> str:
if self.health_score >= 0.9:
return "HEALTHY"
elif self.health_score >= 0.7:
return "WARNING"
else:
return "CRITICAL"
def calculate_health_score(dashboard_data: dict) -> float:
"""각 메트릭을 종합하여 건강 점수 계산"""
scores = []
# 성공률 점수 (가중치 40%)
success_score = dashboard_data["success_rate"]["current"]
scores.append(("success_rate", success_score, 0.40))
# 안정성 점수 (가중치 25%)
stability_score = 1.0 - dashboard_data["trend"].get("volatility", 0)
scores.append(("stability", max(0, stability_score), 0.25))
# 드리프트 점수 (가중치 20%)
drift_count = sum(1 for signal in ["response_length", "confidence", "reasoning"]
if dashboard_data.get(signal, {}).get("is_drifting", False))
drift_score = 1.0 - (drift_count / 3)
scores.append(("drift", drift_score, 0.20))
# 운영 점수 (가중치 15%)
latency_ok = not dashboard_data.get("latency", {}).get("is_slowdown", False)
cost_ok = not dashboard_data.get("cost", {}).get("is_anomaly", False)
ops_score = (int(latency_ok) + int(cost_ok)) / 2
scores.append(("operations", ops_score, 0.15))
return sum(score * weight for _, score, weight in scores)================================================================
에이전트 안정성 대시보드 — customer-support-v2
기간: 2026-03-25 ~ 2026-03-31
================================================================
상태: WARNING (건강 점수: 0.76)
--- 성공률 ---
7일 평균: 87.3% (베이스라인: 91.2%, -3.9%)
30일 평균: 90.1%
추세: declining (기울기: -0.008)
--- 드리프트 신호 ---
응답 길이: 정상 (z-score: 0.8)
신뢰도: 경고 (불확실성 +12%)
추론 패턴: 정상 (분포 차이: 8%)
--- 운영 메트릭 ---
P95 지연시간: 3,240ms (베이스라인: 2,800ms, +15.7%)
평균 비용: $0.042/task (베이스라인: $0.038, +10.5%)
================================================================대시보드의 메트릭에 기반하여 자동 알림을 발생시킵니다.
ALERT_RULES = [
{
"name": "success_rate_drop",
"condition": lambda d: d["success_rate"]["current"] < d["success_rate"]["baseline"] - 0.10,
"severity": "critical",
"message": "성공률이 베이스라인 대비 10% 이상 하락했습니다",
},
{
"name": "latency_spike",
"condition": lambda d: d["latency"]["current_p95"] > d["latency"]["baseline_p95"] * 2,
"severity": "warning",
"message": "P95 지연시간이 베이스라인의 2배를 초과했습니다",
},
{
"name": "cost_anomaly",
"condition": lambda d: d["cost"]["current_mean"] > d["cost"]["baseline_mean"] * 2,
"severity": "warning",
"message": "태스크당 평균 비용이 베이스라인의 2배를 초과했습니다",
},
{
"name": "multi_drift",
"condition": lambda d: sum(
1 for s in ["response_length", "confidence", "reasoning"]
if d.get(s, {}).get("is_drifting", False)
) >= 2,
"severity": "warning",
"message": "2개 이상의 드리프트 신호가 동시에 감지되었습니다",
},
]이번 장에서는 에이전트의 장기적 안정성을 추적하는 메트릭 체계를 설계했습니다.
8장에서는 에이전트의 **견고성(Robustness)**을 검증하는 시뮬레이션과 레드티밍을 다룹니다. 적대적 사용자 시뮬레이션, 엣지 케이스 자동 생성, 스트레스 테스트, 안전성 가드레일 검증 등 에이전트의 한계를 의도적으로 시험하는 방법을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
시뮬레이션 사용자 기반 적대적 테스트, 엣지 케이스 자동 생성, 스트레스 테스트, 안전성 가드레일 검증, 자동 레드티밍 기법을 다룹니다.
평가를 회귀 테스트로 졸업시키는 패턴, Golden Dataset 관리, 롤링 성공률 모니터링, 베이스라인 관리와 변경 영향 분석을 다룹니다.
GitHub Actions에서 에이전트 테스트를 실행하고, 품질 게이트 임계값을 설계하며, PR별 평가와 온라인 평가를 연결하는 자동화 전략을 다룹니다.