다중 LLM 전환 전략

단일 LLM에 의존하는 것은 비용, 성능, 안정성 측면에서 비효율적입니다. 작업 복잡도에 따라 적절한 LLM을 선택하고, 실패 시 자동으로 대체하며, 부하를 분산하는 전략을 통해 견고하고 경제적인 AI 시스템을 구축하는 방법을 다룹니다.

업데이트 안내: 모델/요금/버전/정책 등 시점에 민감한 정보는 변동될 수 있습니다. 최신 내용은 공식 문서를 확인하세요.
핵심 포인트
  • 작업 복잡도에 따른 LLM 라우팅으로 비용 절감 (최대 70%)
  • 폴백 체인으로 99.9% 가용성 확보
  • 로드 밸런싱으로 처리량 증대 및 Rate Limit 회피
  • LiteLLM, Portkey 등 통합 프레임워크 활용
  • Codex 같은 에이전트 도구는 별도 운영 정책 필요 (계정/승인/권한, Codex 가이드)
  • 실시간 모니터링으로 최적 모델 자동 선택

다중 LLM의 필요성

비용 최적화

비용
실제 사용 사례별 비용 비교:

// 시나리오: 고객 지원 챗봇 (10만 요청/월)
┌──────────────────────┬──────────────┬────────────┬──────────────┐
│ 전략                 │ 평균 비용    │ 월 총비용  │ 절감율       │
├──────────────────────┼──────────────┼────────────┼──────────────┤
│ Claude Opus만        │ 변동/req   │ 변동,500     │ 기준         │
│ GPT-4o만             │ 변동/req   │ 변동,800     │ 60%          │
│ 복잡도 기반 라우팅   │ 변동/req   │ 변동,200     │ 73%          │
│ 로컬 LLM 우선        │ 변동/req   │ 변동       │ 93%          │
└──────────────────────┴──────────────┴────────────┴──────────────┘

// 복잡도 기반 라우팅 분포
60% 단순 질문 → Claude Haiku ($0.25/1M 입력)
30% 중간 복잡도 → GPT-4o Mini ($0.15/1M)
10% 복잡한 추론 → Claude Opus ($15/1M)

// 실제 절감 계산
기존: 100,000 × $0.045 = $4,500
최적화:
  60,000 × $0.003 = $180  (Haiku)
  30,000 × $0.008 = $240  (GPT-4o Mini)
  10,000 × $0.045 = $450  (Opus)
  총: $870 (81% 절감)

안정성 향상

안정성
// 단일 LLM vs 다중 LLM 가용성
단일 LLM:
  가용성: 99.5% (OpenAI SLA)
  월 다운타임: ~3.6시간
  연간 손실: 중단 시간 × 시간당 매출

폴백 체인 (3개 LLM):
  주 LLM: OpenAI (99.5%)
  폴백 1: Anthropic (99.5%)
  폴백 2: 로컬 LLM (100%)

  복합 가용성: 1 - (0.005 × 0.005 × 0) ≈ 99.9975%
  월 다운타임: ~1.3// 실제 장애 사례
2024-11-15: OpenAI API 다운 (2시간)
  • 폴백 없음: 전체 서비스 중단
  • 폴백 있음: 자동 전환, 사용자 영향 최소

// Rate Limit 회피
단일 계정:
  • GPT-4: 10,000 RPM (요청/분)
  • 트래픽 급증 시 거부

다중 계정/제공자:
  • OpenAI: 10,000 RPM
  • Anthropic: 5,000 RPM
  • Google: 1,500 RPM
  • 총: 16,500 RPM

성능 최적화

성능
// 작업별 최적 모델 선택
┌──────────────────────┬──────────────────┬──────────────────┐
│ 작업                 │ 최적 모델        │ 이유             │
├──────────────────────┼──────────────────┼──────────────────┤
│ 코드 생성            │ Claude Sonnet    │ 높은 정확도      │
│ 번역                 │ GPT-4o           │ 다국어 지원      │
│ 요약                 │ Claude Haiku     │ 빠름+저렴        │
│ 수학                 │ o1 (OpenAI)      │ 추론 특화        │
│ 긴 컨텍스트 분석     │ Gemini 1.5 Pro   │ 2M 토큰          │
│ 실시간 대화          │ Haiku/Mini       │ 낮은 지연시간    │
│ JSON 추출            │ GPT-4o Mini      │ 구조화 출력 우수 │
└──────────────────────┴──────────────────┴──────────────────┘

// 응답 시간 비교
Claude Haiku: ~1초 (TTFT: 0.3초)
GPT-4o Mini: ~1.5초 (TTFT: 0.5초)
Claude Opus: ~3초 (TTFT: 1초)
로컬 Llama 8B: ~5초 (TTFT: 2초, GPU)

TTFT = Time To First Token (스트리밍 시작)

라우팅 전략

복잡도 기반 라우팅

python
import re
from typing import Literal

class ComplexityRouter:
    """사용자 쿼리 복잡도 분석하여 적절한 모델 선택"""

    def __init__(self):
        self.simple_patterns = [
            r'^(안녕|hi|hello|ㅎㅇ)',
            r'^(뭐야|what|who)',
            r'시간|날씨|weather',
        ]
        self.complex_keywords = [
            '분석', '비교', '설명', '추론', '왜',
            'analyze', 'compare', 'explain', 'why'
        ]

    def classify(self, query: str) -> Literal['simple', 'medium', 'complex']:
        query_lower = query.lower()

        # 단순 패턴 매칭
        for pattern in self.simple_patterns:
            if re.search(pattern, query_lower):
                return 'simple'

        # 길이 기반
        words = query.split()
        if len(words) < 5:
            return 'simple'

        # 복잡한 키워드
        for keyword in self.complex_keywords:
            if keyword in query_lower:
                return 'complex'

        # 코드 블록 포함
        if '```' in query or 'def ' in query:
            return 'complex'

        # 긴 쿼리
        if len(words) > 50:
            return 'complex'

        return 'medium'

    def select_model(self, query: str) -> str:
        complexity = self.classify(query)

        models = {
            'simple': 'claude-',      # 빠르고 저렴
            'medium': 'gpt-4o-mini',          # 균형잡힘
            'complex': 'claude-',       # 최고 품질
        }

        return models[complexity]


# 사용 예제
router = ComplexityRouter()

queries = [
    "안녕",  # simple
    "Python에서 리스트와 튜플의 차이는?",  # medium
    "다음 코드를 분석하고 시간 복잡도를 개선하는 방법을 제안해줘...",  # complex
]

for q in queries:
    model = router.select_model(q)
    print(f"Query: {q[:30]}... → Model: {model}")

ML 기반 라우팅

python
# 임베딩 기반 분류 (더 정확)

from openai import OpenAI
from sklearn.ensemble import RandomForestClassifier
import numpy as np
import pickle

class MLRouter:
    """과거 데이터 학습하여 최적 모델 예측"""

    def __init__(self):
        self.client = OpenAI()
        self.classifier = None
        self.model_mapping = {
            0: 'claude-haiku',
            1: 'gpt-4o-mini',
            2: 'claude-opus',
        }

    def get_embedding(self, text: str) -> np.ndarray:
        """텍스트를 벡터로 변환"""
        response = self.client.embeddings.create(
            model="text-embedding-3-small",
            input=text
        )
        return np.array(response.data[0].embedding)

    def train(self, training_data: list):
        """
        training_data: [(query, best_model_idx), ...]
        best_model_idx: 0=haiku, 1=mini, 2=opus
        """
        X = []
        y = []

        for query, model_idx in training_data:
            embedding = self.get_embedding(query)
            X.append(embedding)
            y.append(model_idx)

        self.classifier = RandomForestClassifier(n_estimators=100)
        self.classifier.fit(X, y)

        # 모델 저장
        with open('router_model.pkl', 'wb') as f:
            pickle.dump(self.classifier, f)

    def load(self):
        """저장된 모델 로드"""
        with open('router_model.pkl', 'rb') as f:
            self.classifier = pickle.load(f)

    def route(self, query: str) -> str:
        """쿼리에 최적 모델 반환"""
        embedding = self.get_embedding(query)
        prediction = self.classifier.predict([embedding])[0]
        return self.model_mapping[prediction]


# 학습 데이터 생성 (실제로는 사용자 피드백 수집)
training_data = [
    ("안녕", 0),
    ("파이썬 리스트 설명해줘", 1),
    ("복잡한 분산 시스템 아키텍처 설계...", 2),
    # ... 수백~수천 개
]

router = MLRouter()
router.train(training_data)

# 실시간 라우팅
model = router.route("Kubernetes 클러스터 최적화 방법은?")
print(f"Selected model: {model}")

시맨틱 라우팅

python
# 작업 유형별 전문 모델 라우팅

from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder

# 경로 정의
routes = [
    Route(
        name="code",
        utterances=[
            "Python 코드 작성해줘",
            "이 함수 디버깅해줘",
            "알고리즘 구현",
            "코드 리뷰",
        ],
        model="claude-"  # 코딩 특화
    ),
    Route(
        name="translate",
        utterances=[
            "한국어로 번역",
            "영어로 바꿔줘",
            "일본어 번역",
        ],
        model="gpt-4o"  # 번역 우수
    ),
    Route(
        name="summarize",
        utterances=[
            "요약해줘",
            "핵심만 알려줘",
            "간단히 설명",
        ],
        model="claude-haiku"  # 빠르고 저렴
    ),
    Route(
        name="reasoning",
        utterances=[
            "수학 문제 풀이",
            "논리적 추론",
            "복잡한 분석",
        ],
        model="o1"  # 추론 특화
    ),
]

# 라우터 초기화
encoder = OpenAIEncoder()
router_layer = RouteLayer(encoder=encoder, routes=routes)

# 라우팅
def smart_route(query: str) -> str:
    route = router_layer(query)

    if route.name:
        return route.model
    else:
        # 매칭 실패 시 기본 모델
        return "gpt-4o-mini"


# 사용
queries = [
    "FastAPI로 REST API 만들어줘",
    "이 영문 계약서 한국어로 번역",
    "5페이지 논문 핵심 3줄 요약",
]

for q in queries:
    model = smart_route(q)
    print(f"{q[:30]}... → {model}")

폴백 구현

기본 폴백 체인

python
from openai import OpenAI, APIError, RateLimitError
from anthropic import Anthropic, APIConnectionError
import logging

logger = logging.getLogger(__name__)

class FallbackLLM:
    """주 LLM 실패 시 자동으로 대체 LLM 사용"""

    def __init__(self):
        self.openai = OpenAI()
        self.anthropic = Anthropic()

        # 우선순위: OpenAI → Anthropic → 로컬
        self.providers = [
            ('openai', self._openai_call),
            ('anthropic', self._anthropic_call),
            ('local', self._local_call),
        ]

    def _openai_call(self, messages: list) -> str:
        response = self.openai.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            timeout=30
        )
        return response.choices[0].message.content

    def _anthropic_call(self, messages: list) -> str:
        # OpenAI 형식을 Anthropic 형식으로 변환
        system = ""
        user_messages = []

        for msg in messages:
            if msg['role'] == 'system':
                system = msg['content']
            else:
                user_messages.append(msg)

        response = self.anthropic.messages.create(
            model="claude-",
            system=system,
            messages=user_messages,
            max_tokens=2000,
            timeout=30
        )
        return response.content[0].text

    def _local_call(self, messages: list) -> str:
        # 로컬 Ollama 사용
        import ollama

        response = ollama.chat(
            model='llama3.1:8b',
            messages=messages
        )
        return response['message']['content']

    def chat(self, messages: list) -> str:
        """폴백 체인으로 응답 생성"""
        last_error = None

        for provider_name, provider_func in self.providers:
            try:
                logger.info(f"Trying {provider_name}...")
                result = provider_func(messages)
                logger.info(f"Success with {provider_name}")
                return result

            except RateLimitError as e:
                logger.warning(f"{provider_name} rate limited: {e}")
                last_error = e
                continue

            except APIConnectionError as e:
                logger.warning(f"{provider_name} connection error: {e}")
                last_error = e
                continue

            except Exception as e:
                logger.error(f"{provider_name} failed: {e}")
                last_error = e
                continue

        # 모든 제공자 실패
        raise Exception(f"All providers failed. Last error: {last_error}")


# 사용
llm = FallbackLLM()

try:
    response = llm.chat([
        {"role": "user", "content": "Python의 GIL이란?"}
    ])
    print(response)
except Exception as e:
    print(f"Failed: {e}")

재시도 로직

python
import time
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type
)

class ResilientLLM:
    """재시도 + 폴백 조합"""

    @retry(
        stop=stop_after_attempt(3),  # 최대 3회
        wait=wait_exponential(multiplier=1, min=2, max=10),  # 2초, 4초, 8초
        retry=retry_if_exception_type(RateLimitError)  # Rate Limit만 재시도
    )
    def _call_with_retry(self, func, *args, **kwargs):
        return func(*args, **kwargs)

    def chat(self, messages: list) -> str:
        # 1단계: OpenAI (재시도)
        try:
            return self._call_with_retry(
                openai_client.chat.completions.create,
                model="gpt-4o",
                messages=messages
            ).choices[0].message.content
        except Exception as e:
            logger.warning(f"OpenAI failed after retries: {e}")

        # 2단계: Anthropic (재시도)
        try:
            return self._call_with_retry(
                anthropic_client.messages.create,
                model="claude-",
                messages=messages,
                max_tokens=2000
            ).content[0].text
        except Exception as e:
            logger.warning(f"Anthropic failed after retries: {e}")

        # 3단계: 로컬 (항상 성공)
        return local_llm_call(messages)

Circuit Breaker 패턴

python
# 연속 실패 시 일시적으로 제공자 차단

from datetime import datetime, timedelta

class CircuitBreaker:
    """제공자별 장애 감지 및 자동 차단"""

    def __init__(self, failure_threshold=5, timeout=60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout  # 차단 시간 (초)
        self.failures = {}  # {provider: count}
        self.last_failure_time = {}
        self.state = {}  # {provider: 'closed'|'open'|'half-open'}

    def is_available(self, provider: str) -> bool:
        """제공자 사용 가능 여부"""
        state = self.state.get(provider, 'closed')

        if state == 'closed':
            return True

        if state == 'open':
            # 타임아웃 후 half-open 전환
            last_failure = self.last_failure_time.get(provider)
            if datetime.now() - last_failure > timedelta(seconds=self.timeout):
                self.state[provider] = 'half-open'
                return True
            return False

        # half-open: 시험적으로 허용
        return True

    def record_success(self, provider: str):
        """성공 기록 → closed 상태로"""
        self.failures[provider] = 0
        self.state[provider] = 'closed'

    def record_failure(self, provider: str):
        """실패 기록 → 임계값 초과 시 open"""
        self.failures[provider] = self.failures.get(provider, 0) + 1
        self.last_failure_time[provider] = datetime.now()

        if self.failures[provider] >= self.failure_threshold:
            self.state[provider] = 'open'
            logger.error(f"Circuit breaker OPEN for {provider}")


# 통합 예제
class SmartLLM:
    def __init__(self):
        self.circuit_breaker = CircuitBreaker()
        self.providers = ['openai', 'anthropic', 'local']

    def chat(self, messages: list) -> str:
        for provider in self.providers:
            if not self.circuit_breaker.is_available(provider):
                logger.info(f"Skipping {provider} (circuit open)")
                continue

            try:
                result = self._call_provider(provider, messages)
                self.circuit_breaker.record_success(provider)
                return result
            except Exception as e:
                self.circuit_breaker.record_failure(provider)
                logger.warning(f"{provider} failed: {e}")

        raise Exception("All providers unavailable")

로드 밸런싱

라운드 로빈

python
import itertools
from threading import Lock

class LoadBalancedLLM:
    """여러 계정/엔드포인트 간 요청 분산"""

    def __init__(self, api_keys: list):
        self.clients = [OpenAI(api_key=key) for key in api_keys]
        self.iterator = itertools.cycle(range(len(self.clients)))
        self.lock = Lock()

    def _get_next_client(self) -> OpenAI:
        with self.lock:
            idx = next(self.iterator)
            return self.clients[idx]

    def chat(self, messages: list) -> str:
        client = self._get_next_client()
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        )
        return response.choices[0].message.content


# 사용: 3개 계정 순환
llm = LoadBalancedLLM([
    "sk-proj-key1...",
    "sk-proj-key2...",
    "sk-proj-key3...",
])

# 각 요청이 다른 계정 사용 → Rate Limit 3배
for i in range(100):
    response = llm.chat([{"role": "user", "content": f"질문 {i}"}])

가중 로드 밸런싱

python
import random

class WeightedLLM:
    """성능/비용에 따라 가중치 부여"""

    def __init__(self):
        self.providers = [
            {'name': 'openai', 'weight': 50, 'func': self._openai},
            {'name': 'anthropic', 'weight': 30, 'func': self._anthropic},
            {'name': 'local', 'weight': 20, 'func': self._local},
        ]
        self.total_weight = sum(p['weight'] for p in self.providers)

    def _select_provider(self):
        rand = random.uniform(0, self.total_weight)
        cumulative = 0

        for provider in self.providers:
            cumulative += provider['weight']
            if rand <= cumulative:
                return provider

        return self.providers[-1]

    def chat(self, messages: list) -> str:
        provider = self._select_provider()
        return provider['func'](messages)


# 통계: 100회 요청
# OpenAI: ~50회, Anthropic: ~30회, Local: ~20회

최소 지연시간 선택

python
import asyncio
from collections import deque

class AdaptiveLLM:
    """최근 응답 시간 기반 동적 선택"""

    def __init__(self, window_size=10):
        self.latencies = {
            'openai': deque(maxlen=window_size),
            'anthropic': deque(maxlen=window_size),
        }

    def _avg_latency(self, provider: str) -> float:
        if not self.latencies[provider]:
            return 0.0  # 미지의 제공자 우선
        return sum(self.latencies[provider]) / len(self.latencies[provider])

    def chat(self, messages: list) -> str:
        # 평균 지연시간이 낮은 제공자 선택
        provider = min(
            self.latencies.keys(),
            key=lambda p: self._avg_latency(p)
        )

        start = time.time()
        try:
            result = self._call_provider(provider, messages)
            latency = time.time() - start
            self.latencies[provider].append(latency)
            return result
        except Exception:
            # 실패 시 높은 지연시간 기록 (페널티)
            self.latencies[provider].append(999.0)
            raise

LiteLLM 활용

LiteLLM은 100+ LLM 제공자를 단일 인터페이스로 통합하는 오픈소스 프레임워크입니다.

설치 및 기본 사용

bash
pip install litellm
python
from litellm import completion
import os

# API 키 설정
os.environ["OPENAI_API_KEY"] = "..."
os.environ["ANTHROPIC_API_KEY"] = "..."
os.environ["COHERE_API_KEY"] = "..."

# 통일된 인터페이스
messages = [{"role": "user", "content": "Hello!"}]

# OpenAI
response = completion(model="gpt-4o", messages=messages)

# Anthropic
response = completion(model="claude-", messages=messages)

# Google
response = completion(model="gemini/gemini-1.5-pro", messages=messages)

# Cohere
response = completion(model="command-r-plus", messages=messages)

# 로컬 (Ollama)
response = completion(model="ollama/llama3.1:8b", messages=messages)

print(response.choices[0].message.content)

LiteLLM 라우팅

python
from litellm import Router

# 라우터 설정
router = Router(
    model_list=[
        {
            "model_name": "gpt-4",
            "litellm_params": {
                "model": "gpt-4o",
                "api_key": os.getenv("OPENAI_API_KEY")
            }
        },
        {
            "model_name": "gpt-4",  # 동일 이름 (로드 밸런싱)
            "litellm_params": {
                "model": "azure/gpt-4o",
                "api_key": os.getenv("AZURE_API_KEY"),
                "api_base": os.getenv("AZURE_API_BASE")
            }
        },
        {
            "model_name": "claude",
            "litellm_params": {
                "model": "claude-",
                "api_key": os.getenv("ANTHROPIC_API_KEY")
            }
        }
    ],
    fallbacks=[
        {"gpt-4": ["claude"]},  # GPT-4 실패 시 Claude
    ],
    context_window_fallbacks=[
        {"gpt-4": ["claude"]}  # 컨텍스트 초과 시
    ],
    routing_strategy="least-latency",  # 또는 "simple-shuffle", "usage-based"
)

# 사용 (자동 로드 밸런싱 + 폴백)
response = router.completion(
    model="gpt-4",
    messages=[{"role": "user", "content": "Explain quantum entanglement"}]
)

설정 파일 기반

yaml
# config.yaml
model_list:
  - model_name: gpt-4
    litellm_params:
      model: gpt-4o
      api_key: os.environ/OPENAI_API_KEY
      rpm: 10000  # requests per minute

  - model_name: gpt-4
    litellm_params:
      model: azure/gpt-4o
      api_key: os.environ/AZURE_API_KEY
      api_base: https://my-azure.openai.azure.com
      rpm: 5000

  - model_name: claude
    litellm_params:
      model: claude-
      api_key: os.environ/ANTHROPIC_API_KEY

router_settings:
  routing_strategy: usage-based-routing  # RPM 한도 고려
  num_retries: 3
  timeout: 30
  fallbacks:
    - gpt-4: [claude]
  allowed_fails: 3  # Circuit breaker
python
from litellm import Router

# YAML 설정 로드
router = Router(config_file="config.yaml")

# 자동 로드 밸런싱, Rate Limit 관리, 폴백
response = router.completion(
    model="gpt-4",
    messages=messages
)

LiteLLM 프록시 서버

bash
# 프록시 서버 시작 (OpenAI 호환 엔드포인트)
litellm --config config.yaml --port 8000

# 또는 Docker
docker run -p 8000:8000 \
  -v $(pwd)/config.yaml:/app/config.yaml \
  ghcr.io/berriai/litellm:main \
  --config /app/config.yaml
python
# 클라이언트: OpenAI SDK 그대로 사용
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000",
    api_key="anything"  # 프록시가 실제 키 관리
)

# 프록시가 자동으로 최적 모델 선택 + 폴백
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello"}]
)

Portkey 활용

Portkey는 엔터프라이즈급 LLM 게이트웨이로, 라우팅, 캐싱, 모니터링을 통합 제공합니다.

설정

bash
pip install portkey-ai
python
from portkey_ai import Portkey

# Portkey 클라이언트 (가상 키 사용)
portkey = Portkey(
    api_key="YOUR_PORTKEY_API_KEY",  # Portkey 대시보드에서 발급
    virtual_key="openai-virtual-key"  # Portkey에 등록한 가상 키
)

# OpenAI 형식으로 호출
response = portkey.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello Portkey!"}]
)

print(response.choices[0].message.content)

Portkey Config (라우팅 + 폴백)

python
from portkey_ai import Portkey, PortkeyConfig

# JSON 설정
config = PortkeyConfig(
    strategy={
        "mode": "fallback",  # 또는 "loadbalance"
    },
    targets=[
        {
            "virtual_key": "openai-key",
            "override_params": {"model": "gpt-4o"}
        },
        {
            "virtual_key": "anthropic-key",
            "override_params": {"model": "claude-"}
        },
        {
            "virtual_key": "google-key",
            "override_params": {"model": "gemini-1.5-pro"}
        }
    ]
)

portkey = Portkey(
    api_key="YOUR_PORTKEY_API_KEY",
    config=config
)

# 자동 폴백: OpenAI → Anthropic → Google
response = portkey.chat.completions.create(
    messages=[{"role": "user", "content": "Explain AI"}]
)

고급 기능

python
# 1. 가중 로드 밸런싱
config = PortkeyConfig(
    strategy={"mode": "loadbalance"},
    targets=[
        {"virtual_key": "openai-key", "weight": 0.7},  # 70%
        {"virtual_key": "anthropic-key", "weight": 0.3},  # 30%
    ]
)

# 2. 조건부 라우팅
config = PortkeyConfig(
    strategy={"mode": "conditional"},
    targets=[
        {
            "virtual_key": "claude-key",
            "condition": "request.messages[0].content.includes('코드')"
        },
        {
            "virtual_key": "gpt-key",
            "condition": "default"
        }
    ]
)

# 3. 캐싱 (동일 요청 재사용)
portkey = Portkey(
    api_key="...",
    cache="simple",  # 또는 "semantic"
    cache_ttl=3600  # 1시간
)

# 4. 재시도 설정
config = PortkeyConfig(
    retry={
        "attempts": 3,
        "on_status_codes": [429, 500, 502, 503]
    }
)

# 5. 메타데이터 (분석용)
response = portkey.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    metadata={
        "user_id": "user123",
        "environment": "production",
        "feature": "chat"
    }
)

모니터링 및 최적화

주요 메트릭

python
import time
from dataclasses import dataclass
from collections import defaultdict

@dataclass
class Metrics:
    provider: str
    latency: float
    tokens_in: int
    tokens_out: int
    cost: float
    success: bool
    error: str = ""

class MetricsCollector:
    """LLM 호출 메트릭 수집"""

    def __init__(self):
        self.metrics = []
        self.stats = defaultdict(lambda: {
            'total_calls': 0,
            'successes': 0,
            'failures': 0,
            'total_latency': 0.0,
            'total_cost': 0.0,
            'total_tokens': 0,
        })

    def record(self, metric: Metrics):
        self.metrics.append(metric)

        stats = self.stats[metric.provider]
        stats['total_calls'] += 1
        stats['total_latency'] += metric.latency
        stats['total_cost'] += metric.cost
        stats['total_tokens'] += metric.tokens_in + metric.tokens_out

        if metric.success:
            stats['successes'] += 1
        else:
            stats['failures'] += 1

    def report(self):
        print("=== LLM Usage Report ===")
        for provider, stats in self.stats.items():
            calls = stats['total_calls']
            success_rate = stats['successes'] / calls * 100
            avg_latency = stats['total_latency'] / calls

            print(f"\n{provider}:")
            print(f"  Calls: {calls}")
            print(f"  Success Rate: {success_rate:.1f}%")
            print(f"  Avg Latency: {avg_latency:.2f}s")
            print(f"  Total Cost: ${stats['total_cost']:.2f}")
            print(f"  Total Tokens: {stats['total_tokens']:,}")


# 사용
collector = MetricsCollector()

def llm_call_with_metrics(provider, func, *args, **kwargs):
    start = time.time()

    try:
        response = func(*args, **kwargs)
        latency = time.time() - start

        metric = Metrics(
            provider=provider,
            latency=latency,
            tokens_in=response.usage.prompt_tokens,
            tokens_out=response.usage.completion_tokens,
            cost=calculate_cost(provider, response.usage),
            success=True
        )
        collector.record(metric)
        return response

    except Exception as e:
        latency = time.time() - start
        metric = Metrics(
            provider=provider,
            latency=latency,
            tokens_in=0,
            tokens_out=0,
            cost=0.0,
            success=False,
            error=str(e)
        )
        collector.record(metric)
        raise

# 주기적 리포트
import atexit
atexit.register(collector.report)

비용 추적

python
# 모델별 가격 (2026년 1월 기준)
PRICING = {
    'gpt-4o': {'input': 2.5 / 1_000_000, 'output': 10 / 1_000_000},
    'gpt-4o-mini': {'input': 0.15 / 1_000_000, 'output': 0.6 / 1_000_000},
    'claude-': {'input': 15 / 1_000_000, 'output': 75 / 1_000_000},
    'claude-': {'input': 3 / 1_000_000, 'output': 15 / 1_000_000},
    'claude-haiku': {'input': 0.25 / 1_000_000, 'output': 1.25 / 1_000_000},
}

def calculate_cost(model: str, usage) -> float:
    """토큰 사용량으로 비용 계산"""
    if model not in PRICING:
        return 0.0

    pricing = PRICING[model]
    cost = (
        usage.prompt_tokens * pricing['input'] +
        usage.completion_tokens * pricing['output']
    )
    return cost

# 예산 제한
class BudgetLimiter:
    def __init__(self, daily_budget: float):
        self.daily_budget = daily_budget
        self.spent_today = 0.0
        self.date = datetime.now().date()

    def check_budget(self, estimated_cost: float) -> bool:
        # 날짜 변경 시 리셋
        today = datetime.now().date()
        if today != self.date:
            self.spent_today = 0.0
            self.date = today

        if self.spent_today + estimated_cost > self.daily_budget:
            raise Exception(f"Daily budget ${self.daily_budget} exceeded")

        return True

    def record_spend(self, cost: float):
        self.spent_today += cost

모범 사례

모범 사례
// 1. 점진적 도입
• 단일 LLM으로 시작 → 안정화
• 비용 분석 후 복잡도 기반 라우팅 추가
• 폴백 체인 구축
• 모니터링으로 효과 검증

// 2. 실패 처리
• 재시도는 Rate Limit에만 적용
• Connection Error는 즉시 폴백
• Circuit Breaker로 장애 격리
• 최종 폴백은 로컬 LLM 또는 에러 메시지

// 3. 비용 최적화
• 단순 작업에 고가 모델 사용 금지
• 캐싱으로 중복 호출 제거
• 프롬프트 압축 (불필요한 컨텍스트 제거)
• 예산 알림 설정

// 4. 성능 최적화
• 응답 시간 중요 → Haiku, Mini 우선
• 품질 중요 → Opus, o1 우선
• 긴 컨텍스트 → Gemini, Claude
• 코드 생성 → Claude Sonnet

// 5. 모니터링
• 제공자별 성공률, 지연시간, 비용 추적
• 이상 탐지 (급격한 비용 증가, 성공률 하락)
• 사용자 피드백으로 라우팅 개선
• A/B 테스트로 최적 전략 발견

// 6. 보안
• API 키는 환경 변수 또는 비밀 관리 서비스
• 프록시 서버로 키 노출 최소화
• Rate Limiting으로 남용 방지
• 로그에 민감 정보 제외

// 7. 확장성
• 상태 비저장 설계 (수평 확장 가능)
• 메트릭은 중앙 집중식 저장 (Prometheus, CloudWatch)
• 비동기 처리로 처리량 증대
• 큐 기반 아키텍처 (Celery, SQS)
다음 단계
  • API 모범 사례: 에러 처리, Rate Limiting, 캐싱, 보안
  • 프롬프트 엔지니어링: 품질 향상으로 저렴한 모델로도 우수한 결과
  • RAG 시스템: 외부 지식 활용으로 환각 감소

핵심 정리

  • 다중 LLM 전환 전략의 핵심 개념과 흐름을 정리합니다.
  • 다중 LLM의 필요성를 단계별로 이해합니다.
  • 실전 적용 시 기준과 주의점을 확인합니다.

실무 팁

  • 입력/출력 예시를 고정해 재현성을 확보하세요.
  • 다중 LLM 전환 전략 범위를 작게 잡고 단계적으로 확장하세요.
  • 다중 LLM의 필요성 조건을 문서화해 대응 시간을 줄이세요.