콘텐츠로 이동
Data Prep
상세

LLM Observability 아키텍처

LLM 프로덕션 시스템의 관측 가능성(Observability) 설계 가이드. 트레이싱, 메트릭, 로깅, 평가를 통합하여 LLM 애플리케이션의 품질과 비용을 체계적으로 관리한다.


개요

전통적인 소프트웨어와 달리 LLM 시스템은 비결정적(non-deterministic) 출력을 생성한다. 동일한 입력에도 다른 응답이 나올 수 있고, 모델 업데이트나 프롬프트 변경이 예측 불가능한 품질 변화를 일으킨다. 이러한 특성 때문에 LLM 시스템에서는 기존 APM을 넘어선 전용 Observability 체계가 필수적이다.

LLM Observability가 필요한 이유

문제 설명 Observability 해결 방식
Hallucination 사실과 다른 내용 생성 Faithfulness 메트릭 추적
품질 저하 모델/프롬프트 변경 후 성능 하락 A/B 비교, regression 탐지
비용 폭증 불필요한 토큰 사용, 비효율적 호출 토큰/비용 실시간 모니터링
느린 응답 TTFT, TPS 저하 Latency 분포 추적
RAG 실패 검색 품질 저하, 부적절한 컨텍스트 Retrieval quality 측정
디버깅 어려움 복잡한 체인/에이전트 실패 원인 파악 구간별 트레이싱

핵심 구성요소

LLM Observability는 4개의 축(Pillar)으로 구성된다.

┌─────────────────────────────────────────────────────────────┐
│                LLM Observability Pillars                     │
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Tracing    │  │   Metrics    │  │   Logging    │      │
│  │              │  │              │  │              │      │
│  │ - Span 기반  │  │ - Latency    │  │ - Input/     │      │
│  │ - 호출 체인  │  │ - Token      │  │   Output     │      │
│  │ - 구간 분석  │  │ - Cost       │  │ - Error      │      │
│  │ - 의존성 맵  │  │ - Quality    │  │ - Metadata   │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│         │                 │                 │               │
│         └────────────┬────┴────────────┬────┘               │
│                      ▼                 ▼                    │
│              ┌──────────────┐  ┌──────────────┐            │
│              │  Evaluation  │  │  Alerting    │            │
│              │              │  │              │            │
│              │ - Online     │  │ - Drift      │            │
│              │ - Offline    │  │ - Threshold  │            │
│              │ - Human      │  │ - Anomaly    │            │
│              └──────────────┘  └──────────────┘            │
└─────────────────────────────────────────────────────────────┘

트레이싱 아키텍처

OpenTelemetry 기반 LLM Trace 설계

LLM 호출을 OpenTelemetry의 Trace/Span 개념으로 구조화한다. 하나의 사용자 요청이 여러 LLM 호출, 검색, 후처리를 거치는 과정을 계층적으로 추적한다.

Trace: user_query_12345
├── Span: intent_classification (23ms)
│   ├── attr: model=gpt-4o-mini
│   ├── attr: input_tokens=150
│   └── attr: output_tokens=5
├── Span: rag_retrieval (180ms)
│   ├── Span: embedding (45ms)
│   │   └── attr: model=text-embedding-3-small
│   ├── Span: vector_search (120ms)
│   │   └── attr: top_k=20, results=20
│   └── Span: reranking (15ms)
│       └── attr: model=cohere-rerank-v3, top_n=5
├── Span: llm_generation (1200ms)
│   ├── attr: model=gpt-4o
│   ├── attr: input_tokens=2500
│   ├── attr: output_tokens=350
│   ├── attr: ttft=280ms
│   └── attr: temperature=0.1
└── Span: post_processing (15ms)
    └── attr: format=markdown

Span 속성 설계

속성 타입 설명
llm.model string 사용 모델명
llm.provider string OpenAI, Anthropic 등
llm.input_tokens int 입력 토큰 수
llm.output_tokens int 출력 토큰 수
llm.total_tokens int 총 토큰 수
llm.cost_usd float 호출 비용 (USD)
llm.ttft_ms float Time to First Token
llm.tps float Tokens per Second
llm.temperature float 샘플링 온도
llm.prompt_template string 사용된 프롬프트 템플릿 ID
rag.query string 검색 쿼리
rag.num_results int 검색 결과 수
rag.relevance_score float 평균 관련성 점수

주요 도구 비교

Observability 플랫폼 비교표

도구 유형 강점 약점 가격 모델 적합한 팀
LangSmith SaaS LangChain 네이티브, 평가 통합 LangChain 종속성 Free tier + 유료 LangChain 사용 팀
Langfuse OSS/SaaS 오픈소스, 셀프호스팅 가능 생태계 작음 무료(셀프) / 유료(클라우드) 커스텀 필요 팀
Phoenix (Arize) OSS 노트북 통합, 로컬 분석 프로덕션 스케일 제한 무료 실험/연구 팀
OpenLLMetry OSS OpenTelemetry 네이티브 UI 없음 (백엔드 연동) 무료 OTel 인프라 보유 팀
Helicone SaaS 프록시 방식, 코드 변경 최소 프록시 레이턴시 Free tier + 유료 빠른 도입 필요 팀
W&B Weave SaaS 실험 추적 통합, ML 워크플로 LLM 전용 기능 제한적 Free tier + 유료 ML 팀 (W&B 기존 사용)

도구 선택 의사결정 흐름

셀프호스팅 필수?
├── Yes → Langfuse (Docker/K8s)
└── No
    ├── LangChain 사용? → LangSmith
    ├── 기존 OTel 인프라? → OpenLLMetry + Grafana
    ├── 코드 변경 최소화? → Helicone (프록시)
    └── 실험/연구 목적? → Phoenix (Arize)

메트릭 설계

Latency 메트릭

메트릭 정의 측정 방법 목표
TTFT (Time to First Token) 요청 후 첫 토큰까지 시간 스트리밍 첫 chunk 타임스탬프 < 500ms
TPS (Tokens per Second) 초당 생성 토큰 수 output_tokens / generation_time > 30 TPS
E2E Latency 전체 요청-응답 시간 요청 시작 ~ 응답 완료 < 3s (RAG 포함)
Retrieval Latency 검색 소요 시간 embedding + search + rerank < 500ms

Token 및 비용 메트릭

메트릭 단위 집계 용도
input_tokens count 모델별, 기능별, 시간별 프롬프트 최적화
output_tokens count 모델별, 기능별, 시간별 출력 제한 조정
cost_usd USD 모델별, 팀별, 기능별 예산 관리
cache_hit_rate ratio 시간별 캐시 효율
error_rate ratio 모델별, 에러타입별 안정성 모니터링

품질 메트릭

메트릭 범위 측정 방법 설명
Faithfulness 0-1 NLI 기반 자동 평가 컨텍스트 대비 사실 정확도
Relevance 0-1 LLM-as-Judge 질문 대비 답변 관련성
Completeness 0-1 LLM-as-Judge 답변의 완전성
Toxicity 0-1 분류 모델 유해 콘텐츠 여부
User Satisfaction 1-5 사용자 피드백 (thumbs up/down) 실제 만족도

평가 파이프라인

3단계 평가 체계

┌─────────────────────────────────────────────────────────┐
│                 Evaluation Pipeline                      │
│                                                         │
│  ┌──────────────┐                                       │
│  │ Online Eval  │  실시간, 모든 요청                    │
│  │              │  - Latency 체크                        │
│  │              │  - Token count                         │
│  │              │  - 기본 품질 체크 (길이, 형식)         │
│  │              │  - 유해성 필터                          │
│  └──────┬───────┘                                       │
│         ▼                                               │
│  ┌──────────────┐                                       │
│  │ Offline Eval │  배치, 샘플링 (일 1회)                │
│  │              │  - LLM-as-Judge                        │
│  │              │  - RAG 품질 (RAGAS)                    │
│  │              │  - Golden dataset 비교                 │
│  │              │  - Regression 테스트                   │
│  └──────┬───────┘                                       │
│         ▼                                               │
│  ┌──────────────┐                                       │
│  │  Human Eval  │  주기적, 핵심 케이스 (주 1회)         │
│  │              │  - 전문가 리뷰                         │
│  │              │  - Edge case 검증                      │
│  │              │  - 신규 기능 QA                        │
│  └──────────────┘                                       │
└─────────────────────────────────────────────────────────┘

Offline 평가 구현

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)

def run_offline_eval(dataset: list[dict]) -> dict:
    """일일 오프라인 평가 실행

    Args:
        dataset: [{"question": ..., "answer": ..., 
                   "contexts": [...], "ground_truth": ...}]
    """
    results = evaluate(
        dataset=dataset,
        metrics=[
            faithfulness,
            answer_relevancy,
            context_precision,
            context_recall,
        ],
    )

    return {
        "faithfulness": results["faithfulness"],
        "relevancy": results["answer_relevancy"],
        "context_precision": results["context_precision"],
        "context_recall": results["context_recall"],
        "timestamp": datetime.now().isoformat(),
    }

RAG Observability

RAG 파이프라인의 각 단계별 품질을 측정한다.

RAG 메트릭 체계

Query → [Retrieval] → [Context] → [Generation] → Answer
         │               │              │
         ▼               ▼              ▼
    - Recall@k      - Relevance    - Faithfulness
    - Precision@k   - Coverage     - Completeness
    - MRR           - Noise ratio  - Hallucination rate
    - nDCG          - Redundancy   - Citation accuracy

단계별 측정 코드

class RAGObserver:
    """RAG 파이프라인 관측"""

    def __init__(self, metrics_client):
        self.metrics = metrics_client

    def observe_retrieval(self, query: str, results: list, 
                          ground_truth: list = None):
        """검색 단계 관측"""
        self.metrics.record({
            "rag.retrieval.num_results": len(results),
            "rag.retrieval.avg_score": np.mean([r.score for r in results]),
            "rag.retrieval.max_score": max(r.score for r in results),
            "rag.retrieval.min_score": min(r.score for r in results),
        })

        if ground_truth:
            retrieved_ids = {r.doc_id for r in results}
            relevant_ids = set(ground_truth)
            recall = len(retrieved_ids & relevant_ids) / len(relevant_ids)
            precision = len(retrieved_ids & relevant_ids) / len(retrieved_ids)
            self.metrics.record({
                "rag.retrieval.recall": recall,
                "rag.retrieval.precision": precision,
            })

    def observe_context(self, query: str, contexts: list[str]):
        """컨텍스트 구성 단계 관측"""
        total_tokens = sum(count_tokens(c) for c in contexts)
        self.metrics.record({
            "rag.context.num_chunks": len(contexts),
            "rag.context.total_tokens": total_tokens,
            "rag.context.avg_chunk_tokens": total_tokens / len(contexts),
        })

    def observe_generation(self, query: str, answer: str, 
                           contexts: list[str]):
        """생성 단계 관측"""
        # Faithfulness: 답변이 컨텍스트에 근거하는지
        faith_score = self._check_faithfulness(answer, contexts)
        self.metrics.record({
            "rag.generation.faithfulness": faith_score,
            "rag.generation.answer_length": len(answer),
        })

    def _check_faithfulness(self, answer: str, contexts: list[str]) -> float:
        """NLI 기반 faithfulness 체크"""
        claims = self._extract_claims(answer)
        supported = 0
        for claim in claims:
            context_text = " ".join(contexts)
            if self._nli_check(claim, context_text):
                supported += 1
        return supported / len(claims) if claims else 1.0

실시간 대시보드 설계

Grafana + Prometheus 구성

# prometheus.yml
scrape_configs:
  - job_name: 'llm-app'
    scrape_interval: 15s
    static_configs:
      - targets: ['llm-app:8000']

# 커스텀 메트릭 정의
# metrics.py
from prometheus_client import Counter, Histogram, Gauge

llm_request_total = Counter(
    'llm_request_total',
    'Total LLM requests',
    ['model', 'endpoint', 'status']
)

llm_latency = Histogram(
    'llm_latency_seconds',
    'LLM request latency',
    ['model', 'endpoint'],
    buckets=[0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]
)

llm_tokens = Counter(
    'llm_tokens_total',
    'Total tokens used',
    ['model', 'direction']  # direction: input/output
)

llm_cost = Counter(
    'llm_cost_usd_total',
    'Total cost in USD',
    ['model', 'team', 'feature']
)

rag_faithfulness = Gauge(
    'rag_faithfulness_score',
    'RAG faithfulness score (rolling avg)',
    ['pipeline']
)

대시보드 패널 구성

패널 메트릭 시각화 알림 조건
요청량 llm_request_total 시계열 그래프 급증/급감
응답 시간 llm_latency_seconds 히스토그램 P95 > 5s
토큰 사용량 llm_tokens_total 스택형 바 차트 일일 한도 80%
비용 추적 llm_cost_usd_total 파이 차트 + 시계열 일일 예산 초과
에러율 llm_request_total{status=error} 비율 게이지 > 5%
RAG 품질 rag_faithfulness_score 게이지 + 트렌드 < 0.8
모델별 비교 복합 테이블 -

알림 및 이상 탐지

알림 규칙

# alerting_rules.yml
groups:
  - name: llm_alerts
    rules:
      - alert: HighLatency
        expr: histogram_quantile(0.95, llm_latency_seconds) > 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "LLM P95 latency > 5s"

      - alert: HighErrorRate
        expr: >
          rate(llm_request_total{status="error"}[5m]) 
          / rate(llm_request_total[5m]) > 0.05
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "LLM error rate > 5%"

      - alert: CostBudgetExceeded
        expr: >
          sum(increase(llm_cost_usd_total[24h])) > 100
        labels:
          severity: warning
        annotations:
          summary: "Daily LLM cost exceeded $100"

      - alert: QualityDegradation
        expr: rag_faithfulness_score < 0.8
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "RAG faithfulness dropped below 0.8"

Drift Detection

품질 드리프트를 탐지하기 위한 통계적 접근:

import numpy as np
from scipy import stats

class DriftDetector:
    """품질 메트릭 드리프트 탐지"""

    def __init__(self, window_size: int = 100, threshold: float = 0.05):
        self.window_size = window_size
        self.threshold = threshold
        self.baseline: list[float] = []
        self.current: list[float] = []

    def set_baseline(self, scores: list[float]):
        """기준 분포 설정 (배포/업데이트 직후)"""
        self.baseline = scores

    def add_score(self, score: float) -> dict | None:
        """새로운 점수 추가 및 드리프트 체크"""
        self.current.append(score)

        if len(self.current) < self.window_size:
            return None

        window = self.current[-self.window_size:]
        stat, p_value = stats.ks_2samp(self.baseline, window)

        if p_value < self.threshold:
            return {
                "drift_detected": True,
                "p_value": p_value,
                "baseline_mean": np.mean(self.baseline),
                "current_mean": np.mean(window),
                "delta": np.mean(window) - np.mean(self.baseline),
            }
        return None

구현: Python Decorator 패턴

LLM 호출 트레이싱 데코레이터

import time
import functools
import uuid
from opentelemetry import trace
from opentelemetry.trace import StatusCode

tracer = trace.get_tracer("llm-app")

# 모델별 가격 (USD per 1K tokens)
PRICING = {
    "gpt-4o": {"input": 0.0025, "output": 0.01},
    "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
    "claude-3.5-sonnet": {"input": 0.003, "output": 0.015},
}

def trace_llm_call(model: str = None, feature: str = "default"):
    """LLM 호출 트레이싱 데코레이터

    사용법:
        @trace_llm_call(model="gpt-4o", feature="rag")
        async def generate_answer(query, context):
            ...
    """
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            span_name = f"llm.{func.__name__}"

            with tracer.start_as_current_span(span_name) as span:
                span.set_attribute("llm.model", model or "unknown")
                span.set_attribute("llm.feature", feature)
                span.set_attribute("llm.call_id", str(uuid.uuid4()))

                start_time = time.perf_counter()

                try:
                    result = await func(*args, **kwargs)
                    elapsed = time.perf_counter() - start_time

                    # 응답에서 메트릭 추출
                    if hasattr(result, 'usage'):
                        input_tokens = result.usage.prompt_tokens
                        output_tokens = result.usage.completion_tokens

                        span.set_attribute("llm.input_tokens", input_tokens)
                        span.set_attribute("llm.output_tokens", output_tokens)
                        span.set_attribute("llm.total_tokens", 
                                         input_tokens + output_tokens)

                        # 비용 계산
                        if model in PRICING:
                            cost = (
                                input_tokens / 1000 * PRICING[model]["input"]
                                + output_tokens / 1000 * PRICING[model]["output"]
                            )
                            span.set_attribute("llm.cost_usd", cost)

                            # Prometheus 메트릭
                            llm_cost.labels(
                                model=model, team="default", feature=feature
                            ).inc(cost)

                    span.set_attribute("llm.latency_ms", elapsed * 1000)
                    llm_latency.labels(
                        model=model, endpoint=func.__name__
                    ).observe(elapsed)

                    llm_request_total.labels(
                        model=model, endpoint=func.__name__, status="success"
                    ).inc()

                    span.set_status(StatusCode.OK)
                    return result

                except Exception as e:
                    span.set_status(StatusCode.ERROR, str(e))
                    span.record_exception(e)
                    llm_request_total.labels(
                        model=model, endpoint=func.__name__, status="error"
                    ).inc()
                    raise

        return wrapper
    return decorator


# 사용 예시
@trace_llm_call(model="gpt-4o", feature="rag")
async def generate_rag_answer(query: str, context: str):
    response = await openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": f"Context: {context}"},
            {"role": "user", "content": query},
        ],
        temperature=0.1,
    )
    return response

비용 모니터링

비용 추적 차원

비용 집계 차원:
├── 모델별: gpt-4o, claude-3.5-sonnet, ...
├── 팀별: data-team, product-team, ...
├── 기능별: rag, sql-gen, classification, ...
├── 환경별: production, staging, development
└── 시간별: 시간, 일, 주, 월

비용 리포트 생성

class CostReporter:
    """LLM 비용 리포트 생성"""

    def __init__(self, db):
        self.db = db

    def daily_report(self, date: str) -> dict:
        """일일 비용 리포트"""
        query = """
        SELECT 
            model,
            feature,
            team,
            COUNT(*) as request_count,
            SUM(input_tokens) as total_input_tokens,
            SUM(output_tokens) as total_output_tokens,
            SUM(cost_usd) as total_cost
        FROM llm_traces
        WHERE DATE(created_at) = :date
        GROUP BY model, feature, team
        ORDER BY total_cost DESC
        """
        rows = self.db.execute(query, {"date": date})

        return {
            "date": date,
            "total_cost": sum(r.total_cost for r in rows),
            "by_model": self._group_by(rows, "model"),
            "by_feature": self._group_by(rows, "feature"),
            "by_team": self._group_by(rows, "team"),
            "top_expensive_calls": self._top_calls(date, limit=10),
        }

    def cost_forecast(self, days: int = 30) -> dict:
        """비용 예측 (최근 7일 기반)"""
        recent = self._get_recent_daily_costs(7)
        avg_daily = np.mean(recent)

        return {
            "avg_daily_cost": avg_daily,
            "projected_monthly": avg_daily * days,
            "trend": "increasing" if recent[-1] > recent[0] else "stable",
        }

비용 최적화 체크리스트

항목 방법 절감 효과
모델 다운그레이드 간단한 작업에 mini 모델 50-90%
프롬프트 압축 불필요한 지시문 제거 10-30%
캐싱 동일 쿼리 캐시 20-60%
배치 처리 Batch API 활용 50%
토큰 제한 max_tokens 설정 10-20%
라우팅 쿼리 복잡도별 모델 분기 30-50%

프로덕션 체크리스트

도입 전 확인해야 할 항목:

단계 항목 확인
기본 모든 LLM 호출에 trace 적용
기본 input/output 로깅 (PII 마스킹)
기본 latency, token, error 메트릭 수집
중급 비용 모니터링 + 예산 알림
중급 RAG 품질 메트릭 (faithfulness, relevance)
중급 대시보드 구성
고급 자동 평가 파이프라인 (일 1회)
고급 드리프트 탐지
고급 A/B 테스트 인프라

참고 자료

자료 링크
OpenTelemetry Semantic Conventions for GenAI https://opentelemetry.io/docs/specs/semconv/gen-ai/
RAGAS Documentation https://docs.ragas.io/
Langfuse Self-hosting Guide https://langfuse.com/docs/deployment/self-host
OpenLLMetry https://github.com/traceloop/openllmetry

최종 업데이트: 2026-03-25