모델 운영

MLOPS / 중급

LLMOps 관측성: 프로덕션 LLM 시스템 들여다보기

Langfuse와 분산 트레이싱으로 LLM 애플리케이션의 내부를 투명하게 관측하는 방법을 다룹니다. 트레이스 설계, 핵심 메트릭 수집, 대시보드 구성, 이상 알림까지 실전 코드와 함께 정리합니다.

PALDYN Team6 MIN READ

지난 글에서 평가 파이프라인으로 LLM 출력 품질을 자동으로 측정하는 방법을 살펴봤다. 평가가 "배포 전 검증"이라면, 관측성(Observability) 은 "배포 후 감시"다. 프로덕션에서 LLM이 어떻게 동작하는지 실시간으로 파악하는 능력이다.

블랙박스 LLM 시스템은 문제가 생겨도 원인을 찾을 수 없다. 응답이 느려졌을 때 "LLM API 레이턴시가 높아진 건지, 검색 단계가 느려진 건지, 컨텍스트가 너무 길어진 건지" 알 수 없다면 개선이 불가능하다. 관측성은 이 블랙박스를 투명하게 만든다.

관측성 스택 구조

LLM 관측성 스택

LLM 관측성 스택은 세 레이어를 쌓는다.

트레이스(Trace): 단일 요청의 전체 생애주기를 기록한다. 입력 프롬프트, 검색된 컨텍스트, LLM 호출 결과, 후처리 단계가 하나의 트레이스로 연결된다.

메트릭(Metrics): 레이턴시·에러율·토큰 사용량 같은 수치 지표를 시계열로 수집한다. 대시보드와 알림의 기반이 된다.

비용(Cost): 토큰 사용량에 단가를 곱해 요청별·기능별·팀별 비용을 추적한다.

분산 트레이스 설계

분산 트레이스: RAG 파이프라인 예시

트레이스는 루트 스팬 → 자식 스팬 계층으로 설계한다. RAG 파이프라인이라면 user_request 루트 아래 vector_retrieve, rerank, llm_generate 자식 스팬을 둔다.

from langfuse import Langfuse
from langfuse.decorators import observe, langfuse_context
import anthropic

lf = Langfuse()
claude = anthropic.Anthropic()

@observe()  # 자동으로 스팬 생성
def retrieve_chunks(query: str, top_k: int = 5) -> list[str]:
    langfuse_context.update_current_observation(
        metadata={"top_k": top_k, "backend": "pgvector"},
    )
    return vector_db.search(query, k=top_k)

@observe()
def rerank(query: str, chunks: list[str]) -> list[str]:
    return cohere_reranker.rerank(query, chunks)

@observe()
def generate_answer(question: str, context: str) -> str:
    response = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"컨텍스트: {context}\n\n질문: {question}"
        }],
    )
    langfuse_context.update_current_observation(
        usage={
            "input": response.usage.input_tokens,
            "output": response.usage.output_tokens,
        },
    )
    return response.content[0].text

@observe(name="rag_pipeline")  # 루트 트레이스
def answer_question(question: str) -> str:
    langfuse_context.update_current_trace(
        tags=["rag", "v2"],
        metadata={"user_id": get_current_user()},
    )
    chunks = retrieve_chunks(question)
    ranked = rerank(question, chunks)
    context = "\n\n".join(ranked[:3])
    return generate_answer(question, context)

핵심 메트릭 수집

from prometheus_client import Counter, Histogram, Gauge
import time

# 메트릭 정의
llm_requests_total = Counter(
    "llm_requests_total",
    "Total LLM API calls",
    ["model", "status", "feature"],
)
llm_latency_seconds = Histogram(
    "llm_latency_seconds",
    "LLM response time",
    ["model", "feature"],
    buckets=[0.5, 1, 2, 5, 10, 30],
)
llm_tokens_total = Counter(
    "llm_tokens_total",
    "Total tokens used",
    ["model", "type"],  # type: input/output
)
llm_cost_usd = Counter(
    "llm_cost_usd_total",
    "Total LLM cost in USD",
    ["model", "feature"],
)

PRICES_PER_M = {
    "claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
    "claude-haiku-4-5": {"input": 0.8, "output": 4.0},
}

def tracked_llm_call(model: str, feature: str, messages: list) -> str:
    start = time.time()
    try:
        response = claude.messages.create(
            model=model, max_tokens=1024, messages=messages
        )
        latency = time.time() - start
        
        in_tok = response.usage.input_tokens
        out_tok = response.usage.output_tokens
        price = PRICES_PER_M.get(model, {"input": 3.0, "output": 15.0})
        cost = (in_tok * price["input"] + out_tok * price["output"]) / 1_000_000

        llm_requests_total.labels(model=model, status="success", feature=feature).inc()
        llm_latency_seconds.labels(model=model, feature=feature).observe(latency)
        llm_tokens_total.labels(model=model, type="input").inc(in_tok)
        llm_tokens_total.labels(model=model, type="output").inc(out_tok)
        llm_cost_usd.labels(model=model, feature=feature).inc(cost)

        return response.content[0].text
    except Exception as e:
        llm_requests_total.labels(model=model, status="error", feature=feature).inc()
        raise

이상 감지와 알림

# 알림 규칙 예시 (Prometheus AlertManager)
# rules/llm_alerts.yml
ALERT_RULES = """
groups:
  - name: llm_alerts
    rules:
      - alert: HighLLMLatency
        expr: histogram_quantile(0.99, llm_latency_seconds) > 10
        for: 5m
        annotations:
          summary: "LLM P99 레이턴시 10초 초과"

      - alert: HighErrorRate  
        expr: rate(llm_requests_total{status="error"}[5m]) / rate(llm_requests_total[5m]) > 0.05
        for: 2m
        annotations:
          summary: "LLM 에러율 5% 초과"

      - alert: CostSpike
        expr: increase(llm_cost_usd_total[1h]) > 50
        annotations:
          summary: "시간당 LLM 비용 $50 초과"
"""

품질 점수 실시간 추적

# 프로덕션 요청의 일부를 샘플링해 품질 평가
import random

def sample_and_evaluate(question: str, answer: str, sample_rate: float = 0.05):
    if random.random() > sample_rate:
        return  # 5%만 평가 (비용 절감)
    
    score = llm_judge(question=question, answer=answer)
    
    # Langfuse에 품질 점수 기록
    lf.score(
        trace_id=get_current_trace_id(),
        name="quality",
        value=score["score"] / 5.0,  # 0~1 정규화
        comment=score["reason"],
    )
    
    # 임계값 이하면 Slack 알림
    if score["score"] < 2:
        send_slack_alert(
            channel="#llm-quality",
            message=f"⚠️ 낮은 품질 응답 감지\n질문: {question[:100]}\n점수: {score['score']}/5",
        )

Langfuse 대시보드 활용

Langfuse 대시보드에서 확인해야 할 핵심 지표들이다.

지표 설명 경고 기준
P50/P99 레이턴시 응답 시간 분포 P99 > 5초
에러율 API 호출 실패 비율 > 1%
토큰/요청 평균 프롬프트 길이 추세 갑작스런 증가
비용/시간 시간별 누적 비용 예산 대비 비율
품질 점수 LLM-as-Judge 평균 < 3.5/5
캐시 히트율 시맨틱 캐시 효율 < 20% (개선 여지)

로그 구조화

비정형 로그는 분석이 어렵다. LLM 관련 이벤트는 구조화된 JSON으로 기록한다.

import structlog

log = structlog.get_logger()

def log_llm_event(
    event_type: str,
    model: str,
    input_tokens: int,
    output_tokens: int,
    latency_ms: float,
    **kwargs,
):
    log.info(
        event_type,
        model=model,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        latency_ms=round(latency_ms, 2),
        cost_usd=round((input_tokens * 3 + output_tokens * 15) / 1_000_000, 6),
        **kwargs,
    )

# 출력 예:
# {"event": "llm_call", "model": "claude-sonnet-4-6",
#  "input_tokens": 342, "output_tokens": 189,
#  "latency_ms": 1240.5, "cost_usd": 0.004}

지난 글: LLM 평가 파이프라인: 자동화된 품질 보장

다음 글: LLM 비용 추적: 토큰 낭비 없이 운영하기


읽어주셔서 감사합니다. 😊

KEEP READING

이어 읽기

모델 운영2026.05.29

파인튜닝 파이프라인 구축: 데이터부터 배포까지

QLoRA 기반 파인튜닝 파이프라인을 처음부터 구축한다. 데이터 준비, 학습 루프, 체크포인트, 평가, GGUF 변환, Ollama 배포까지 엔드-투-엔드 실전 가이드.

중급14 MIN
모델 운영2026.05.29

평가 하네스 구축: LLM 성능을 체계적으로 측정하라

직접 만드는 LLM 평가 시스템 — 테스트셋 설계, 자동 채점(LLM-as-Judge), 사람 평가 워크플로우, 지표 집계, 회귀 감지까지 실전 평가 하네스 구축 가이드.

중급17 MIN
모델 운영2026.05.29

GPU 메모리 최적화: OOM 없이 더 크게 훈련하는 법

GPU OOM 에러 원인 분석부터 gradient checkpointing, mixed precision, gradient accumulation, 메모리 프로파일링까지 — 제한된 VRAM으로 더 큰 모델을 훈련하는 실전 기법 완전 정복.

중급10 MIN