Skip to content
Published on

LLMOps 플랫폼 구축 가이드: 모델 배포, 모니터링, A/B 테스트 실전 아키텍처

Authors
  • Name
    Twitter
LLMOps Platform

들어가며

LLM(Large Language Model)이 프로덕션 환경에 빠르게 확산되면서, 전통적인 MLOps만으로는 해결할 수 없는 새로운 운영 과제들이 등장했다. 모델 학습보다 프롬프트 엔지니어링이 핵심이 되고, 정량적 메트릭보다 생성 품질 평가가 중요해졌으며, 토큰 단위의 비용 관리와 할루시네이션 방지를 위한 가드레일이 필수가 되었다.

Gartner에 따르면, 2026년까지 기업의 생성형 AI 배포 중 50% 이상이 운영 미성숙으로 실패할 것으로 전망된다. 이는 LLM이라는 기술 자체의 문제가 아니라, LLM을 안정적으로 운영하기 위한 플랫폼 아키텍처의 부재에서 비롯된다.

이 글에서는 LLMOps 플랫폼의 전체 아키텍처를 다룬다. vLLM/TGI 기반 모델 서빙부터 토큰 사용량 모니터링, 프롬프트 버전 관리, A/B 테스트 프레임워크, NeMo Guardrails 통합, 비용 최적화까지 프로덕션 LLM 운영에 필요한 모든 것을 코드와 함께 구축한다.

LLMOps vs MLOps: 무엇이 다른가

전통적인 MLOps가 "학습-배포-모니터링" 파이프라인을 자동화하는 데 집중했다면, LLMOps는 근본적으로 다른 패러다임에서 출발한다. MLOps는 반복 가능한 예측을 위한 시스템이고, LLMOps는 확률적 생성을 위한 시스템이다.

구분MLOpsLLMOps
핵심 활동모델 학습/재학습프롬프트 엔지니어링/파인튜닝
비용 구조학습 비용 중심추론 비용 중심 (토큰당 과금)
평가 방식Accuracy, F1, RMSEBLEU, ROUGE, LLM-as-Judge
데이터 파이프라인피처 스토어, ETLRAG, 벡터 DB, 청킹 파이프라인
버전 관리모델 아티팩트프롬프트 템플릿 + 모델 + 파라미터
모니터링데이터 드리프트, 성능 지표토큰 사용량, 레이턴시, 품질, 할루시네이션
배포 주기주/월 단위 재학습프롬프트 변경은 분 단위 가능
안전장치입력 검증가드레일, 콘텐츠 필터링, PII 탐지

LLMOps의 아키텍처는 전통 MLOps 대비 더 많은 컴포넌트를 요구한다. 모델 서버 앞에 애플리케이션 게이트웨이가 위치하여 프롬프트 라우팅, 벡터 DB 검색, 도구 호출, 캐싱 레이어를 오케스트레이션한다.

사용자 요청
┌─────────────────────────────────────────┐
Application Gateway│  ┌──────────┐ ┌────────┐ ┌───────────┐ │
│  │ 프롬프트  │ │ 벡터DB │ │ 가드레일  │ │
│  │ 라우터   │  (RAG)  │ │ 엔진     │ │
│  └──────────┘ └────────┘ └───────────┘ │
│  ┌──────────┐ ┌────────┐ ┌───────────┐ │
│  │ 캐시     │ │ A/B    │ │ 토큰     │ │
│  │ 레이어   │ │ 라우터 │ │ 미터링   │ │
│  └──────────┘ └────────┘ └───────────┘ │
└─────────────────┬───────────────────────┘
    ┌─────────────┼─────────────┐
    ▼             ▼             ▼
┌────────┐  ┌────────┐  ┌────────────┐
│ vLLM   │  │  TGI   │  │ TensorRT-Server │  │ Server │  │ LLM Server└────────┘  └────────┘  └────────────┘

모델 서빙 아키텍처

LLM 서빙 프레임워크 비교

LLM 서빙에 특화된 주요 프레임워크를 비교하면 다음과 같다.

프레임워크핵심 기술GPU 요구사항처리량레이턴시모델 호환성운영 난이도
vLLMPagedAttentionCUDA GPU높음중간HuggingFace 전체낮음
TGIFlash AttentionCUDA GPU높음낮음 (v3)HuggingFace 전체낮음
TensorRT-LLMCUDA 그래프 최적화NVIDIA 전용최고최저변환 필요높음
Triton + vLLM앙상블 파이프라인CUDA GPU높음중간멀티 모델중간

vLLM은 PagedAttention을 통해 KV 캐시를 가상 메모리처럼 페이지 단위로 관리하여 GPU 메모리 단편화를 최소화한다. 동일 VRAM에서 더 많은 동시 요청을 처리할 수 있어 혼합 길이 워크로드에서 TGI 대비 10~30% 높은 처리량을 보인다.

TGI v3는 장문 프롬프트(200K+ 토큰) 처리에서 vLLM 대비 최대 13배 빠른 성능을 보여주며, Hugging Face 생태계와의 긴밀한 통합이 강점이다.

TensorRT-LLM은 H100 하드웨어에서 CUDA 그래프 최적화와 퓨즈드 커널을 통해 vLLM/TGI 대비 20~40% 높은 원시 처리량을 달성하지만, 모델 변환 과정과 NVIDIA 하드웨어 종속이 필요하다.

vLLM 배포 설정

Kubernetes 환경에서 vLLM을 배포하는 실전 설정이다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama3-70b
  namespace: llm-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: vllm-llama3-70b
  template:
    metadata:
      labels:
        app: vllm-llama3-70b
    spec:
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.7.3
          args:
            - '--model'
            - 'meta-llama/Llama-3.3-70B-Instruct'
            - '--tensor-parallel-size'
            - '4'
            - '--max-model-len'
            - '8192'
            - '--gpu-memory-utilization'
            - '0.90'
            - '--enable-chunked-prefill'
            - '--max-num-batched-tokens'
            - '32768'
            - '--port'
            - '8000'
          ports:
            - containerPort: 8000
          resources:
            limits:
              nvidia.com/gpu: 4
            requests:
              nvidia.com/gpu: 4
              memory: '64Gi'
              cpu: '16'
          env:
            - name: HUGGING_FACE_HUB_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: token
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 180
            periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-llama3-70b-svc
  namespace: llm-serving
spec:
  selector:
    app: vllm-llama3-70b
  ports:
    - port: 8000
      targetPort: 8000
  type: ClusterIP

핵심 파라미터 설명:

  • tensor-parallel-size: 4: 70B 모델을 4개 GPU에 분산하여 추론한다.
  • gpu-memory-utilization: 0.90: GPU 메모리의 90%를 KV 캐시에 할당하여 동시 요청 수를 극대화한다.
  • enable-chunked-prefill: 프리필과 디코딩을 인터리빙하여 TTFT(Time To First Token)를 줄인다.
  • max-num-batched-tokens: 32768: 배치당 최대 토큰 수로 처리량과 레이턴시의 균형을 잡는다.

모니터링 전략

LLM 모니터링은 전통 ML 모니터링과 달리 세 가지 차원을 동시에 추적해야 한다: 성능(Performance), 비용(Cost), 품질(Quality).

핵심 메트릭 체계

성능 메트릭                  비용 메트릭              품질 메트릭
├── TTFT (Time To           ├── 입력 토큰 수         ├── 응답 관련성 점수
First Token)            ├── 출력 토큰 수         ├── 할루시네이션 비율
├── TPOT (Time Per          ├── 요청당 비용          ├── 가드레일 위반률
Output Token)           ├── 모델별 비용 비교     ├── 사용자 피드백
├── 총 생성 시간            ├── 캐시 히트율             (thumbs up/down)
├── 요청 처리량 (RPS)       └── 일/월별 비용 추세    └── LLM-as-Judge 점수
├── GPU 사용률
└── 큐 대기 시간

Prometheus 메트릭 수집 구현

vLLM이 노출하는 메트릭을 Prometheus로 수집하고, 커스텀 비즈니스 메트릭을 추가하는 Python 미들웨어 예시다.

import time
import tiktoken
from prometheus_client import (
    Counter, Histogram, Gauge, start_http_server
)
from functools import wraps

# 성능 메트릭
REQUEST_LATENCY = Histogram(
    "llm_request_latency_seconds",
    "LLM 요청 레이턴시",
    ["model", "endpoint"],
    buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0],
)
TTFT_LATENCY = Histogram(
    "llm_ttft_seconds",
    "Time To First Token",
    ["model"],
    buckets=[0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0],
)

# 비용 메트릭
TOKEN_COUNTER = Counter(
    "llm_tokens_total",
    "총 토큰 사용량",
    ["model", "direction"],  # direction: input/output
)
REQUEST_COST = Counter(
    "llm_request_cost_dollars",
    "요청별 비용 (USD)",
    ["model"],
)

# 품질 메트릭
QUALITY_SCORE = Histogram(
    "llm_quality_score",
    "LLM 응답 품질 점수",
    ["model", "evaluator"],
    buckets=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
)
GUARDRAIL_VIOLATIONS = Counter(
    "llm_guardrail_violations_total",
    "가드레일 위반 횟수",
    ["model", "violation_type"],
)

# 모델별 토큰 단가 (USD per 1K tokens)
PRICING = {
    "llama-3.3-70b": {"input": 0.00059, "output": 0.00079},
    "gpt-4o": {"input": 0.0025, "output": 0.01},
    "claude-sonnet": {"input": 0.003, "output": 0.015},
}


class LLMMetricsCollector:
    def __init__(self, model_name: str):
        self.model_name = model_name
        self.encoder = tiktoken.get_encoding("cl100k_base")

    def record_request(self, prompt: str, response: str,
                       latency: float, ttft: float):
        input_tokens = len(self.encoder.encode(prompt))
        output_tokens = len(self.encoder.encode(response))

        # 성능 기록
        REQUEST_LATENCY.labels(
            model=self.model_name, endpoint="/v1/chat/completions"
        ).observe(latency)
        TTFT_LATENCY.labels(model=self.model_name).observe(ttft)

        # 토큰 사용량 기록
        TOKEN_COUNTER.labels(
            model=self.model_name, direction="input"
        ).inc(input_tokens)
        TOKEN_COUNTER.labels(
            model=self.model_name, direction="output"
        ).inc(output_tokens)

        # 비용 계산 및 기록
        pricing = PRICING.get(self.model_name, PRICING["llama-3.3-70b"])
        cost = (
            input_tokens / 1000 * pricing["input"]
            + output_tokens / 1000 * pricing["output"]
        )
        REQUEST_COST.labels(model=self.model_name).inc(cost)

    def record_quality(self, score: float, evaluator: str = "auto"):
        QUALITY_SCORE.labels(
            model=self.model_name, evaluator=evaluator
        ).observe(score)

    def record_guardrail_violation(self, violation_type: str):
        GUARDRAIL_VIOLATIONS.labels(
            model=self.model_name, violation_type=violation_type
        ).inc()


if __name__ == "__main__":
    start_http_server(9090)
    collector = LLMMetricsCollector("llama-3.3-70b")

Grafana 대시보드 주요 패널

Prometheus 메트릭을 기반으로 구성하는 Grafana 대시보드의 핵심 PromQL 쿼리다.

# P99 레이턴시 (5분 윈도우)
histogram_quantile(0.99, rate(llm_request_latency_seconds_bucket[5m]))

# 분당 토큰 소비량
rate(llm_tokens_total[1m])

# 시간당 비용 추세
rate(llm_request_cost_dollars[1h]) * 3600

# 가드레일 위반률
rate(llm_guardrail_violations_total[5m]) / rate(llm_request_latency_seconds_count[5m])

# 평균 품질 점수
histogram_quantile(0.5, rate(llm_quality_score_bucket[1h]))

프롬프트 버전 관리

LLMOps에서 프롬프트는 전통 MLOps의 모델 아티팩트에 해당하는 핵심 자산이다. 프롬프트 템플릿, 모델 버전, 생성 파라미터(temperature, top_p 등)를 함께 버전 관리해야 재현 가능한 배포와 세밀한 롤백이 가능하다.

import json
import hashlib
from datetime import datetime
from dataclasses import dataclass, field, asdict
from typing import Optional


@dataclass
class PromptVersion:
    name: str
    template: str
    model: str
    temperature: float = 0.7
    top_p: float = 0.9
    max_tokens: int = 2048
    system_prompt: str = ""
    version: str = ""
    created_at: str = field(
        default_factory=lambda: datetime.utcnow().isoformat()
    )

    def __post_init__(self):
        if not self.version:
            content = f"{self.template}{self.model}{self.temperature}"
            self.version = hashlib.sha256(
                content.encode()
            ).hexdigest()[:8]

    def to_dict(self) -> dict:
        return asdict(self)


class PromptRegistry:
    """프롬프트 버전 관리 레지스트리"""

    def __init__(self, storage_backend="redis"):
        self.storage_backend = storage_backend
        self.prompts: dict[str, list[PromptVersion]] = {}

    def register(self, prompt: PromptVersion) -> str:
        if prompt.name not in self.prompts:
            self.prompts[prompt.name] = []
        self.prompts[prompt.name].append(prompt)
        return prompt.version

    def get_latest(self, name: str) -> Optional[PromptVersion]:
        versions = self.prompts.get(name, [])
        return versions[-1] if versions else None

    def get_version(self, name: str, version: str
                    ) -> Optional[PromptVersion]:
        versions = self.prompts.get(name, [])
        for v in versions:
            if v.version == version:
                return v
        return None

    def rollback(self, name: str, version: str) -> bool:
        target = self.get_version(name, version)
        if target:
            self.prompts[name].append(
                PromptVersion(
                    name=target.name,
                    template=target.template,
                    model=target.model,
                    temperature=target.temperature,
                    top_p=target.top_p,
                    max_tokens=target.max_tokens,
                    system_prompt=target.system_prompt,
                )
            )
            return True
        return False


# 사용 예시
registry = PromptRegistry()
v1 = PromptVersion(
    name="customer-support",
    template="고객 문의에 대해 친절하게 답변해주세요.\n\n문의: {query}",
    model="llama-3.3-70b",
    temperature=0.3,
    system_prompt="당신은 전문 고객 상담원입니다.",
)
registry.register(v1)

A/B 테스트 프레임워크

LLM의 A/B 테스트는 전통적인 웹 A/B 테스트와 근본적으로 다르다. 클릭률 같은 단순 메트릭이 아니라 생성 품질이라는 다차원 평가가 필요하며, 확률적 출력 특성상 동일 입력에 다른 응답이 나올 수 있어 더 많은 샘플이 필요하다.

A/B 테스트 라우터 구현

import random
import hashlib
from dataclasses import dataclass
from typing import Any


@dataclass
class ABVariant:
    name: str
    prompt_version: str
    model: str
    weight: float  # 트래픽 비율 (0.0 ~ 1.0)
    parameters: dict = None


class LLMABRouter:
    """LLM A/B 테스트 트래픽 라우터"""

    def __init__(self, experiment_name: str):
        self.experiment_name = experiment_name
        self.variants: list[ABVariant] = []

    def add_variant(self, variant: ABVariant):
        self.variants.append(variant)

    def route(self, user_id: str) -> ABVariant:
        """사용자 ID 기반 결정적 라우팅 (동일 사용자는 동일 variant)"""
        hash_input = f"{self.experiment_name}:{user_id}"
        hash_value = int(
            hashlib.md5(hash_input.encode()).hexdigest(), 16
        )
        normalized = (hash_value % 10000) / 10000.0

        cumulative = 0.0
        for variant in self.variants:
            cumulative += variant.weight
            if normalized < cumulative:
                return variant
        return self.variants[-1]

    def validate_weights(self) -> bool:
        total = sum(v.weight for v in self.variants)
        return abs(total - 1.0) < 0.001


# 실험 설정 예시
experiment = LLMABRouter("customer-support-v2-test")
experiment.add_variant(ABVariant(
    name="control",
    prompt_version="v1-abc123",
    model="llama-3.3-70b",
    weight=0.7,
    parameters={"temperature": 0.3},
))
experiment.add_variant(ABVariant(
    name="treatment",
    prompt_version="v2-def456",
    model="llama-3.3-70b",
    weight=0.3,
    parameters={"temperature": 0.5},
))

# 사용자별 라우팅
variant = experiment.route(user_id="user-12345")
print(f"Assigned variant: {variant.name}")

통계적 유의성 판정

LLM A/B 테스트에서 통계적 유의성을 판정하는 핵심 로직이다.

import numpy as np
from scipy import stats


def calculate_ab_significance(
    control_scores: list[float],
    treatment_scores: list[float],
    alpha: float = 0.05,
    min_samples: int = 100,
) -> dict:
    """A/B 테스트 결과의 통계적 유의성 판정"""

    if (len(control_scores) < min_samples
            or len(treatment_scores) < min_samples):
        return {
            "status": "insufficient_samples",
            "control_n": len(control_scores),
            "treatment_n": len(treatment_scores),
            "min_required": min_samples,
        }

    control_mean = np.mean(control_scores)
    treatment_mean = np.mean(treatment_scores)
    lift = (treatment_mean - control_mean) / control_mean

    # Welch's t-test (등분산 가정 불필요)
    t_stat, p_value = stats.ttest_ind(
        control_scores, treatment_scores, equal_var=False
    )

    # 효과 크기 (Cohen's d)
    pooled_std = np.sqrt(
        (np.std(control_scores) ** 2 + np.std(treatment_scores) ** 2)
        / 2
    )
    cohens_d = (
        (treatment_mean - control_mean) / pooled_std
        if pooled_std > 0 else 0
    )

    return {
        "status": "significant" if p_value < alpha else "not_significant",
        "control_mean": round(control_mean, 4),
        "treatment_mean": round(treatment_mean, 4),
        "lift": round(lift * 100, 2),
        "p_value": round(p_value, 6),
        "cohens_d": round(cohens_d, 4),
        "recommendation": (
            "DEPLOY treatment"
            if p_value < alpha and lift > 0
            else "KEEP control"
        ),
    }

가드레일 통합

프로덕션 LLM에서 가드레일은 선택이 아니라 필수다. NVIDIA NeMo Guardrails를 활용하면 입출력 필터링, 주제 이탈 방지, PII 탐지, 할루시네이션 체크를 선언적으로 구성할 수 있다.

NeMo Guardrails 설정

# config.yml - NeMo Guardrails 설정
models:
  - type: main
    engine: vllm
    parameters:
      base_url: 'http://vllm-llama3-70b-svc:8000/v1'
      model_name: 'meta-llama/Llama-3.3-70B-Instruct'

rails:
  input:
    flows:
      - self check input # 입력 유해성 체크
      - check jailbreak # 탈옥 시도 탐지
      - mask pii # PII 마스킹

  output:
    flows:
      - self check output # 출력 유해성 체크
      - check hallucination # 할루시네이션 탐지
      - check topic relevance # 주제 관련성 확인

  config:
    enable_multi_step_generation: true
    lowest_temperature: 0.1
    enable_rails_exceptions: true

instructions:
  - type: general
    content: |
      아래 가이드라인을 반드시 준수하세요:
      1. 확인되지 않은 사실은 추측이라고 명시
      2. 개인정보가 포함된 응답 금지
      3. 의료/법률/금융 조언은 전문가 상담 권고
      4. 폭력적이거나 유해한 콘텐츠 생성 금지

sample_conversation: |
  user "안녕하세요, 도움이 필요합니다."
    express greeting
  bot express greeting and offer help
    "안녕하세요! 무엇을 도와드릴까요?"

가드레일 미들웨어 통합

from nemoguardrails import RailsConfig, LLMRails


class GuardrailMiddleware:
    """LLM 가드레일 미들웨어"""

    def __init__(self, config_path: str):
        config = RailsConfig.from_path(config_path)
        self.rails = LLMRails(config)

    async def process(self, user_message: str,
                      context: dict = None) -> dict:
        try:
            response = await self.rails.generate_async(
                messages=[{"role": "user", "content": user_message}]
            )
            return {
                "status": "success",
                "response": response["content"],
                "guardrail_actions": response.get(
                    "log", {}
                ).get("activated_rails", []),
            }
        except Exception as e:
            return {
                "status": "blocked",
                "reason": str(e),
                "response": "요청을 처리할 수 없습니다. "
                            "다른 질문을 해주세요.",
            }

비용 최적화

LLM 운영에서 비용은 토큰 사용량에 비례하므로, 체계적인 비용 최적화 전략이 필요하다.

비용 절감 전략

  1. 시맨틱 캐싱: 유사한 질문에 대한 응답을 벡터 유사도 기반으로 캐싱하여 반복 추론을 방지한다. 일반적으로 20~40%의 비용 절감 효과가 있다.

  2. 프롬프트 압축: 불필요한 토큰을 제거하고 핵심 정보만 전달하여 입력 토큰을 줄인다. LLMLingua 같은 도구를 활용하면 프롬프트를 50% 이상 압축할 수 있다.

  3. 모델 라우팅: 쿼리 난이도에 따라 경량 모델(7B)과 대형 모델(70B)을 자동 라우팅한다. 간단한 질문은 경량 모델로 처리하면 비용을 80% 이상 절감할 수 있다.

  4. KV 캐시 최적화: vLLM의 prefix caching 기능을 활용하여 시스템 프롬프트나 공통 컨텍스트의 KV 캐시를 재사용한다.

# 모델 라우팅 예시: 쿼리 복잡도에 따른 자동 라우팅
class ModelRouter:
    def __init__(self):
        self.complexity_threshold = 0.6
        self.models = {
            "simple": {
                "name": "llama-3.2-8b",
                "endpoint": "http://vllm-8b:8000/v1",
                "cost_per_1k": 0.00010,
            },
            "complex": {
                "name": "llama-3.3-70b",
                "endpoint": "http://vllm-70b:8000/v1",
                "cost_per_1k": 0.00079,
            },
        }

    def classify_complexity(self, query: str) -> float:
        """쿼리 복잡도를 0~1 사이 점수로 평가"""
        indicators = [
            len(query) > 500,          # 긴 질문
            "비교" in query,            # 비교 분석 요구
            "분석" in query,            # 분석 요구
            "코드" in query,            # 코드 생성
            query.count("?") > 2,       # 다중 질문
        ]
        return sum(indicators) / len(indicators)

    def route(self, query: str) -> dict:
        complexity = self.classify_complexity(query)
        if complexity >= self.complexity_threshold:
            return self.models["complex"]
        return self.models["simple"]

실패 사례와 교훈

사례 1: 모델 서빙 OOM (Out of Memory)

70B 모델을 4x A100 80GB에 배포했으나, max-model-len을 32768로 설정하여 동시 요청 증가 시 OOM이 발생했다.

원인: KV 캐시가 시퀀스 길이에 비례하여 메모리를 소비한다. 32K 길이에서 동시 요청 50개가 들어오면 KV 캐시만으로 300GB+ 메모리가 필요하다.

해결: max-model-len을 8192로 줄이고, gpu-memory-utilization을 0.90으로 설정한 후, 긴 입력은 별도 인스턴스로 라우팅했다.

사례 2: 프롬프트 회귀 (Prompt Regression)

고객 상담 프롬프트를 "더 친절하게" 수정했더니, 기술 지원 정확도가 30% 하락했다.

원인: 프롬프트 변경 시 A/B 테스트 없이 전체 트래픽에 즉시 배포했다. "친절함" 강조가 정확한 기술 용어 사용을 억제하는 부작용을 초래했다.

해결: 모든 프롬프트 변경은 반드시 10% 트래픽으로 카나리 배포하고, 정확도/관련성/유용성 3가지 품질 메트릭을 모두 통과해야 전체 배포를 진행하도록 정책을 수립했다.

사례 3: A/B 테스트 통계적 오류

500건의 샘플로 A/B 테스트를 종료하고 treatment 배포를 결정했으나, 이후 성능이 control보다 하락했다.

원인: LLM의 확률적 특성으로 분산이 크며, 500건은 통계적 검정력(power)이 부족했다. 또한 주말/평일 트래픽 패턴 차이를 고려하지 않았다.

해결: 최소 샘플 크기를 1000건 이상으로 설정하고, 최소 1주일 이상 실험을 운영하여 시간대 효과를 상쇄하도록 했다. Cohen's d 기반 효과 크기도 함께 확인한다.

운영 체크리스트

배포 전

  • 모델 서빙 프레임워크의 healthcheck 엔드포인트가 정상 응답하는지 확인
  • GPU 메모리 사용률이 95%를 넘지 않는지 확인
  • 프롬프트 버전이 레지스트리에 등록되어 있는지 확인
  • 가드레일 설정이 최신 정책을 반영하는지 확인
  • 롤백 프롬프트 버전이 명확히 지정되어 있는지 확인

배포 중

  • 카나리 트래픽 비율을 10%에서 시작하여 단계적으로 증가
  • TTFT, TPOT, 총 레이턴시 3가지 성능 메트릭 실시간 모니터링
  • 가드레일 위반률이 baseline 대비 급증하지 않는지 확인
  • 토큰 사용량과 비용이 예산 범위 내인지 확인

배포 후

  • A/B 테스트 결과의 통계적 유의성을 확인한 후 전체 배포 결정
  • 품질 메트릭(관련성, 정확도, 유용성) 대시보드를 주 단위로 리뷰
  • 월별 비용 리포트를 생성하여 예산 대비 실적을 추적
  • 사용자 피드백 데이터를 수집하여 프롬프트 개선에 반영
  • 가드레일 로그를 분석하여 새로운 위험 패턴을 정책에 추가

참고자료