다중 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의 필요성 조건을 문서화해 대응 시간을 줄이세요.