Skip to main content

1. 왜 버전 관리와 모니터링이 중요한가

AI 에이전트의 비결정성 (Non-determinism)

전통적인 소프트웨어와 달리 AI 에이전트는 동일한 입력에도 다른 출력을 생성할 수 있습니다. 이 특성은 다음과 같은 운영 리스크를 만들어냅니다.
  • LLM 드리프트 (LLM Drift): 파운데이션 모델 제공자(예: OpenAI, Anthropic)가 내부적으로 모델을 업데이트하면 동일한 API 호출도 다른 결과를 낳습니다.
  • 데이터 드리프트 (Data Drift): RAG 파이프라인의 지식 베이스(Knowledge Base)가 변경되면 검색 결과가 달라지고, 에이전트의 응답 품질이 변합니다.
  • 프롬프트 민감성 (Prompt Sensitivity): 프롬프트 템플릿의 사소한 변경이 응답 스타일이나 정확도에 큰 영향을 줄 수 있습니다.

프로덕션 장애의 대표 패턴

장애 유형원인영향
할루시네이션 급증프롬프트 변경 또는 모델 업그레이드잘못된 정보 제공, 신뢰도 손상
응답 지연 급증도구 호출 루프, 컨텍스트 길이 초과타임아웃, 사용자 이탈
오류율 증가외부 API 장애, 스키마 변경서비스 중단
비용 폭증토큰 소비 이상, 무한 루프예산 초과
이러한 리스크를 통제하려면 모든 변경을 버전으로 추적하고, 프로덕션 동작을 지속적으로 관찰해야 합니다.

2. 모델 버전 관리 — Unity Catalog Model Registry

Unity Catalog에서의 버전 등록

Databricks Unity Catalog (UC)는 모델을 3단계 네임스페이스 (catalog.schema.model_name)로 관리합니다. 모델을 등록하면 버전 번호가 자동으로 부여됩니다.
import mlflow

mlflow.set_registry_uri("databricks-uc")

with mlflow.start_run(run_name="customer-support-agent-v4"):
    model_info = mlflow.langchain.log_model(
        lc_model=agent_chain,
        artifact_path="agent",
        registered_model_name="catalog.schema.customer_support_agent",
        input_example={"messages": [{"role": "user", "content": "환불 방법을 알려주세요."}]},
    )
    print(f"등록된 버전: {model_info.registered_model_version}")

Champion/Challenger 패턴

앨리어스 (Alias) 를 활용하면 버전 번호 대신 역할(Role) 기반으로 모델을 참조할 수 있습니다.
from databricks.sdk import WorkspaceClient

w = WorkspaceClient()

# champion: 현재 프로덕션 버전
# challenger: 검증 중인 신규 버전
w.model_registry.set_registered_model_alias(
    full_name="catalog.schema.customer_support_agent",
    alias="champion",
    version_num=3
)
w.model_registry.set_registered_model_alias(
    full_name="catalog.schema.customer_support_agent",
    alias="challenger",
    version_num=4
)

자동 승격 기준 (Promotion Criteria)

Challenger 버전이 아래 조건을 모두 만족하면 Champion으로 자동 승격합니다.
메트릭최소 기준측정 기간
정확도 (Accuracy)Champion 대비 ≥ 95%24시간
p95 Latency≤ 3,000ms24시간
Error Rate≤ 1%24시간
사용자 긍정 피드백≥ 70%24시간
def auto_promote_if_ready(challenger_metrics: dict, champion_metrics: dict) -> bool:
    """Challenger가 Champion 기준을 초과하면 자동 승격"""
    conditions = [
        challenger_metrics["accuracy"] >= champion_metrics["accuracy"] * 0.95,
        challenger_metrics["p95_latency_ms"] <= 3000,
        challenger_metrics["error_rate"] <= 0.01,
        challenger_metrics["positive_feedback_rate"] >= 0.70,
    ]
    return all(conditions)

3. 배포 전략 (Deployment Strategies)

3-1. Canary 배포

신규 버전에 소량의 트래픽(예: 5–10%)만 먼저 흘려 리스크를 제한합니다.
from databricks.sdk.service.serving import (
    ServedEntityInput, TrafficConfig, Route, EndpointCoreConfigInput
)

# Canary: v4에 10% 트래픽 할당
w.serving_endpoints.update_config(
    name="customer-support-agent",
    served_entities=[
        ServedEntityInput(
            entity_name="catalog.schema.customer_support_agent",
            entity_version="3",
            name="v3-stable",
            workload_size="Small",
            scale_to_zero_enabled=False
        ),
        ServedEntityInput(
            entity_name="catalog.schema.customer_support_agent",
            entity_version="4",
            name="v4-canary",
            workload_size="Small",
            scale_to_zero_enabled=False
        ),
    ],
    traffic_config=TrafficConfig(
        routes=[
            Route(served_model_name="v3-stable", traffic_percentage=90),
            Route(served_model_name="v4-canary",  traffic_percentage=10),
        ]
    )
)

3-2. Blue-Green 배포

두 개의 동일한 환경(Blue = 현재, Green = 신규)을 운영하고, 검증 후 즉시 전환합니다. 트래픽 전환이 순간적(0→100%)이므로 롤백도 빠릅니다.
# Green 환경 검증 완료 후 100% 전환
w.serving_endpoints.update_config(
    name="customer-support-agent",
    served_entities=[
        ServedEntityInput(
            entity_name="catalog.schema.customer_support_agent",
            entity_version="4",  # Green → 전체 트래픽
            workload_size="Small",
            scale_to_zero_enabled=True
        )
    ]
)

3-3. A/B 테스트 (Traffic Splitting)

두 버전을 동시에 운영하며 비즈니스 메트릭(전환율, 해결률 등)을 비교합니다.
# A/B 테스트: 50/50 분할
traffic_config = TrafficConfig(
    routes=[
        Route(served_model_name="variant-a", traffic_percentage=50),
        Route(served_model_name="variant-b", traffic_percentage=50),
    ]
)
참고: A/B 테스트는 통계적 유의성(Statistical Significance)이 확보될 때까지 유지해야 합니다. 최소 수백 건 이상의 샘플이 필요합니다.

4. 모니터링 핵심 메트릭

인프라 메트릭 (Infrastructure Metrics)

메트릭설명권장 임계값
Latency (p50/p95/p99)응답 시간 분포p95 > 5s → 알림
Throughput (RPS)초당 요청 수비정상 급증/급감 감지
Error Rate4xx/5xx 비율> 5% → 알림, > 10% → 롤백
Concurrency동시 처리 요청 수스케일 아웃 기준

LLM 특화 메트릭 (LLM-specific Metrics)

메트릭설명활용
Token 사용량입력/출력 토큰 수비용 예측 및 이상 탐지
Context Length컨텍스트 윈도우 활용률최대 한계 근접 시 최적화
Tool Call Count에이전트 루프 당 도구 호출 횟수무한 루프 탐지
Retrieval RelevanceRAG 검색 관련성 점수지식 베이스 품질 측정

품질 메트릭 (Quality Metrics)

-- LLM Judge를 활용한 응답 품질 점수 집계
SELECT
    DATE(timestamp)                                             AS date,
    AVG(quality_score)                                         AS avg_quality_score,
    AVG(relevance_score)                                       AS avg_relevance_score,
    AVG(groundedness_score)                                    AS avg_groundedness_score,
    COUNT(CASE WHEN quality_score < 3 THEN 1 END)              AS low_quality_count
FROM catalog.schema.agent_quality_assessments
GROUP BY DATE(timestamp)
ORDER BY date DESC;

5. MLflow Tracing — 추론 과정 추적

Tracing이 필요한 이유

프로덕션에서 에이전트가 잘못된 답변을 생성했을 때, 어느 단계에서 문제가 발생했는지 파악하려면 분산 추적 (Distributed Tracing) 이 필수입니다. MLflow Tracing은 LLM 호출, RAG 검색, 도구 실행 각 단계를 Span 단위로 기록합니다.

자동 추적 활성화

import mlflow

# LangChain 자동 계측
mlflow.langchain.autolog()

# 또는 LlamaIndex 자동 계측
mlflow.llama_index.autolog()

커스텀 Span 추가

import mlflow

@mlflow.trace(name="customer-support-agent", span_type="CHAIN")
def run_agent(user_query: str) -> str:

    # 검색 단계 추적
    with mlflow.start_span(name="retrieve_context", span_type="RETRIEVER") as span:
        context_docs = vector_store.similarity_search(user_query, k=5)
        span.set_attribute("num_retrieved_docs", len(context_docs))
        span.set_attribute("query", user_query)

    # LLM 호출 단계 추적
    with mlflow.start_span(name="generate_response", span_type="LLM") as span:
        response = llm.invoke(
            messages=[{"role": "user", "content": user_query}],
            context=context_docs
        )
        span.set_attribute("input_tokens",  response.usage.prompt_tokens)
        span.set_attribute("output_tokens", response.usage.completion_tokens)
        span.set_attribute("total_cost_usd", calculate_cost(response.usage))

    return response.content

프로덕션 Trace 분석

import mlflow

# 최근 실패한 Trace 조회
failed_traces = mlflow.search_traces(
    filter_string="status = 'ERROR'",
    max_results=50,
    order_by=["timestamp_ms DESC"]
)

# 특정 Request ID로 Trace 조회 (Inference Table과 연계)
trace = mlflow.get_trace(request_id="tr-abc123")
print(trace.to_json(pretty=True))