본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 7장: 에이전트 안정성 메트릭
2026년 3월 12일·AI / ML·

7장: 에이전트 안정성 메트릭

태스크 성공률 추이, 행동 드리프트 감지, 응답 길이 변동, 지연시간 안정성, 비용 변동성 등 에이전트의 장기적 안정성을 추적하는 메트릭과 대시보드 설계를 다룹니다.

17분1,152자10개 섹션
testingaievaluationquality-assurance
공유
agent-testing7 / 10
12345678910
이전6장: 회귀 테스트 자동화다음8장: 에이전트 시뮬레이션과 레드티밍

학습 목표

  • 태스크 성공률의 시계열 추이를 분석하는 방법을 이해합니다.
  • 행동 드리프트(Behavioral Drift)의 세 가지 신호를 감지합니다.
  • 지연시간과 비용의 안정성을 추적하는 메트릭을 설계합니다.
  • 모든 메트릭을 통합한 모니터링 대시보드를 구축합니다.

안정성은 정확성과 다릅니다

에이전트가 "올바른 답변"을 한다고 해서 "안정적"인 것은 아닙니다. 오늘 90%의 성공률을 보이는 에이전트가 내일은 70%로 떨어질 수 있습니다. 모델 업데이트, 프롬프트 변경, 외부 API의 변화 등 다양한 요인이 에이전트의 행동을 변화시킵니다.

**안정성(Stability)**은 시간에 걸쳐 일관된 품질을 유지하는 능력입니다. 이를 측정하려면 단일 시점의 스냅샷이 아닌, 시계열 데이터를 수집하고 분석해야 합니다.

태스크 성공률 추이

가장 기본적인 안정성 메트릭은 **태스크 성공률(Task Success Rate)**의 시계열 추이입니다.

일별/주별 성공률 계산

success_rate_tracker.py
python
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)**는 에이전트의 행동 패턴이 시간이 지남에 따라 미묘하게 변화하는 현상입니다. 성공률은 유지되지만, 응답의 스타일이나 도구 사용 패턴이 달라질 수 있습니다. 드리프트는 세 가지 신호로 감지합니다.

신호 1: 응답 길이 변동

에이전트의 응답 길이가 갑자기 길어지거나 짧아지는 것은 드리프트의 대표적 신호입니다.

response_length_drift.py
python
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"
            ),
        }

신호 2: 신뢰도 점수 변화

에이전트가 제공하는 신뢰도(Confidence) 점수나 불확실성 표현의 빈도가 변하는 것도 드리프트 신호입니다.

confidence_drift.py
python
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"
            ),
        }

신호 3: 추론 패턴 차이

에이전트가 도구를 선택하는 패턴, 사용하는 도구의 종류, 호출 순서 등이 변하는 것도 감지해야 합니다.

reasoning_drift.py
python
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,
        }
Info

행동 드리프트는 반드시 "나쁜" 것이 아닙니다. 모델 업데이트로 인해 에이전트가 더 나은 도구 선택 패턴을 학습했을 수도 있습니다. 드리프트를 감지한 후에는 성공률과 품질 점수를 함께 확인하여, 개선인지 저하인지 판단해야 합니다.

지연시간 안정성

에이전트의 응답 시간이 불안정하면 사용자 경험이 저하됩니다. 특히 멀티스텝 에이전트는 여러 도구 호출을 순차적으로 수행하므로, 총 지연시간이 크게 변동할 수 있습니다.

latency_metrics.py
python
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 호출 비용은 토큰 사용량에 비례합니다. 에이전트의 행동이 변하면 비용도 변동합니다.

cost_metrics.py
python
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,
        }
Warning

비용 추적은 단순히 절약을 위한 것이 아닙니다. 갑작스러운 비용 증가는 에이전트가 불필요한 도구 호출을 반복하거나, 루프에 빠졌다는 신호일 수 있습니다. 비용 이상은 항상 행동 분석과 연결하여 확인해야 합니다.

통합 대시보드 설계

모든 안정성 메트릭을 하나의 대시보드에 통합하면, 에이전트의 전체 건강 상태를 한눈에 파악할 수 있습니다.

대시보드 데이터 모델

dashboard.py
python
@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)

대시보드 출력 예시

dashboard_output.txt
text
================================================================
에이전트 안정성 대시보드 — 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.py
python
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개 이상의 드리프트 신호가 동시에 감지되었습니다",
    },
]

정리

이번 장에서는 에이전트의 장기적 안정성을 추적하는 메트릭 체계를 설계했습니다.

  • 태스크 성공률 추이는 일별/주별로 추적하며, 선형 회귀로 추세를 분석합니다.
  • 행동 드리프트는 응답 길이, 신뢰도, 추론 패턴의 세 가지 신호로 감지합니다.
  • 지연시간은 P95 백분위수를 기준으로, 비용은 태스크당 평균을 기준으로 이상치를 감지합니다.
  • 통합 대시보드는 모든 메트릭을 건강 점수 하나로 요약하면서도, 상세 내역을 제공합니다.
  • 알림 규칙은 임계값 기반으로 자동 발생하며, 심각도별로 분류됩니다.

다음 장 미리보기

8장에서는 에이전트의 **견고성(Robustness)**을 검증하는 시뮬레이션과 레드티밍을 다룹니다. 적대적 사용자 시뮬레이션, 엣지 케이스 자동 생성, 스트레스 테스트, 안전성 가드레일 검증 등 에이전트의 한계를 의도적으로 시험하는 방법을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#testing#ai#evaluation#quality-assurance

관련 글

AI / ML

8장: 에이전트 시뮬레이션과 레드티밍

시뮬레이션 사용자 기반 적대적 테스트, 엣지 케이스 자동 생성, 스트레스 테스트, 안전성 가드레일 검증, 자동 레드티밍 기법을 다룹니다.

2026년 3월 14일·19분
AI / ML

6장: 회귀 테스트 자동화

평가를 회귀 테스트로 졸업시키는 패턴, Golden Dataset 관리, 롤링 성공률 모니터링, 베이스라인 관리와 변경 영향 분석을 다룹니다.

2026년 3월 10일·16분
AI / ML

9장: CI/CD 통합과 품질 게이트

GitHub Actions에서 에이전트 테스트를 실행하고, 품질 게이트 임계값을 설계하며, PR별 평가와 온라인 평가를 연결하는 자동화 전략을 다룹니다.

2026년 3월 16일·18분
이전 글6장: 회귀 테스트 자동화
다음 글8장: 에이전트 시뮬레이션과 레드티밍

댓글

목차

약 17분 남음
  • 학습 목표
  • 안정성은 정확성과 다릅니다
  • 태스크 성공률 추이
    • 일별/주별 성공률 계산
  • 행동 드리프트 감지
    • 신호 1: 응답 길이 변동
    • 신호 2: 신뢰도 점수 변화
    • 신호 3: 추론 패턴 차이
  • 지연시간 안정성
  • 비용 변동성
  • 통합 대시보드 설계
    • 대시보드 데이터 모델
    • 대시보드 출력 예시
  • 알림 규칙 설계
  • 정리
  • 다음 장 미리보기