반응형
LLM을 프로덕션에 올리면 첫 달 청구서가 이렇게 나와요.
예상: $300/월
실제: $2,400/월
원인 분석해보면 이래요.
고객: "배송 얼마나 걸려요?"
→ Claude Opus 4.6 응답 ($0.015/1K토큰)
고객: "안녕하세요"
→ Claude Opus 4.6 응답 ($0.015/1K토큰)
고객: "취소 어떻게 해요?"
→ Claude Opus 4.6 응답 ($0.015/1K토큰)
모든 요청에 제일 비싼 모델을 쓰고 있어요.
모델별 비용 현실
2026년 4월 기준 (Anthropic):
Claude Haiku 4.5:
입력 $1/M토큰 | 출력 $5/M토큰
→ 빠름, 저렴, 단순 작업에 충분
Claude Sonnet 4.6:
입력 $3/M토큰 | 출력 $15/M토큰
→ 중간, 대부분 작업에 적합
Claude Opus 4.6:
입력 $5/M토큰 | 출력 $25/M토큰
→ 고성능, 복잡한 추론에 필요
같은 요청 10만건/월 처리 시:
전부 Opus: $5,000+
라우팅 적용: $800~1,500
→ 60~84% 절감
LLM 라우터의 구조
요청 → [라우터] → 모델 선택 → LLM API 호출
라우터가 판단하는 것:
1. 이 요청이 얼마나 복잡한가?
2. 어떤 모델이 품질을 만족하면서 가장 저렴한가?
3. 폴백 전략은?
방법 1 — 규칙 기반 라우터 (제일 간단)
def rule_based_router(prompt: str) -> str:
prompt_lower = prompt.lower()
token_count = len(prompt.split())
# 단순 패턴 → Haiku
simple_patterns = [
"안녕", "hello", "감사", "취소", "배송",
"얼마", "언제", "어디"
]
if any(p in prompt_lower for p in simple_patterns):
return "claude-haiku-4-5"
# 짧은 요청 → Haiku
if token_count < 20:
return "claude-haiku-4-5"
# 중간 길이 → Sonnet
if token_count < 200:
return "claude-sonnet-4-6"
# 긴 요청, 코드, 분석 → Opus
complex_keywords = ["분석", "설계", "아키텍처", "최적화", "코드 리뷰"]
if any(k in prompt_lower for k in complex_keywords):
return "claude-opus-4-6"
return "claude-sonnet-4-6" # 기본값
빠르고 예측 가능해요. 단순한 패턴에서 잘 작동해요. 근데 미처 못 잡는 케이스가 생겨요.
방법 2 — LLM 분류기 라우터 (권장)
작은 모델로 복잡도를 먼저 판단한 다음 적절한 모델로 보내요.
import anthropic
client = anthropic.Anthropic()
CLASSIFIER_PROMPT = """다음 요청의 복잡도를 분류해.
요청: {prompt}
응답 형식 (JSON만):
{{"complexity": "simple"|"medium"|"complex", "reason": "이유"}}
기준:
- simple: 단순 질문, 짧은 답변, FAQ
- medium: 일반 코드 생성, 중간 길이 분석
- complex: 시스템 설계, 복잡한 추론, 멀티스텝 코딩"""
def classify_complexity(prompt: str) -> str:
"""Haiku로 복잡도 분류 → 분류 비용 극소화"""
response = client.messages.create(
model="claude-haiku-4-5", # 분류는 Haiku로
max_tokens=100,
messages=[{
"role": "user",
"content": CLASSIFIER_PROMPT.format(prompt=prompt)
}]
)
import json
result = json.loads(response.content[0].text)
return result["complexity"]
def llm_classifier_router(prompt: str) -> str:
complexity = classify_complexity(prompt)
routing_map = {
"simple": "claude-haiku-4-5",
"medium": "claude-sonnet-4-6",
"complex": "claude-opus-4-6"
}
return routing_map[complexity]
분류 비용 자체가 Haiku라 거의 $0에 가까워요.
방법 3 — 캐스케이딩 라우터
작은 모델부터 시도하고, 신뢰도 낮으면 큰 모델로 에스컬레이션해요.
async def cascading_router(prompt: str, min_confidence: float = 0.8):
# 1단계: Haiku로 시도
haiku_response = await call_with_confidence(
model="claude-haiku-4-5",
prompt=prompt
)
if haiku_response.confidence >= min_confidence:
return haiku_response.content # 여기서 끝
# 2단계: Sonnet으로 에스컬레이션
sonnet_response = await call_with_confidence(
model="claude-sonnet-4-6",
prompt=prompt
)
if sonnet_response.confidence >= min_confidence:
return sonnet_response.content
# 3단계: Opus로 최종 에스컬레이션
return await call_model("claude-opus-4-6", prompt)
실제로 80~90% 요청이 Haiku에서 끝나요.
전체 라우터 구현 — 폴백 포함
import anthropic
from enum import Enum
from dataclasses import dataclass
class ModelTier(Enum):
FAST = "claude-haiku-4-5"
SMART = "claude-sonnet-4-6"
POWER = "claude-opus-4-6"
@dataclass
class RouterConfig:
default_tier: ModelTier = ModelTier.SMART
enable_classification: bool = True
fallback_on_error: bool = True
max_retries: int = 2
class LLMRouter:
def __init__(self, config: RouterConfig = RouterConfig()):
self.client = anthropic.Anthropic()
self.config = config
def route(self, prompt: str) -> str:
"""복잡도 기반 모델 선택"""
if not self.config.enable_classification:
return self.config.default_tier.value
try:
complexity = self._classify(prompt)
return self._select_model(complexity)
except Exception:
return self.config.default_tier.value # 분류 실패 시 기본값
def call(self, prompt: str, **kwargs) -> str:
"""라우팅 + 호출 + 폴백"""
model = self.route(prompt)
tiers = [model] + self._get_fallbacks(model)
for attempt, current_model in enumerate(tiers):
try:
response = self.client.messages.create(
model=current_model,
max_tokens=kwargs.get("max_tokens", 1024),
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
except anthropic.RateLimitError:
if attempt < len(tiers) - 1:
continue # 다음 모델로 폴백
raise
def _classify(self, prompt: str) -> str:
response = self.client.messages.create(
model=ModelTier.FAST.value,
max_tokens=50,
messages=[{
"role": "user",
"content": f"복잡도 분류 (simple/medium/complex만 응답): {prompt[:200]}"
}]
)
result = response.content[0].text.strip().lower()
return result if result in ["simple", "medium", "complex"] else "medium"
def _select_model(self, complexity: str) -> str:
return {
"simple": ModelTier.FAST.value,
"medium": ModelTier.SMART.value,
"complex": ModelTier.POWER.value
}.get(complexity, self.config.default_tier.value)
def _get_fallbacks(self, model: str) -> list:
fallback_chain = {
ModelTier.FAST.value: [ModelTier.SMART.value, ModelTier.POWER.value],
ModelTier.SMART.value: [ModelTier.POWER.value],
ModelTier.POWER.value: []
}
return fallback_chain.get(model, [])
# 사용
router = LLMRouter()
response = router.call("배송 얼마나 걸려요?")
# → Haiku로 자동 라우팅
시맨틱 캐시 — 같은 질문 두 번 돈 내지 않기
라우팅과 같이 쓰면 비용을 추가로 줄여요.
from sentence_transformers import SentenceTransformer
import numpy as np
import redis
class SemanticCache:
def __init__(self, similarity_threshold: float = 0.92):
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
self.redis = redis.Redis()
self.threshold = similarity_threshold
def get(self, prompt: str) -> str | None:
embedding = self.encoder.encode(prompt)
cached_keys = self.redis.keys("cache:*")
for key in cached_keys:
cached_data = self.redis.get(key)
cached_embedding = np.frombuffer(cached_data["embedding"])
similarity = np.dot(embedding, cached_embedding)
if similarity >= self.threshold:
return cached_data["response"] # 캐시 히트!
return None # 캐시 미스
def set(self, prompt: str, response: str):
embedding = self.encoder.encode(prompt)
self.redis.set(f"cache:{hash(prompt)}", {
"embedding": embedding.tobytes(),
"response": response
})
# 라우터 + 캐시 통합
class OptimizedLLM:
def __init__(self):
self.router = LLMRouter()
self.cache = SemanticCache()
def call(self, prompt: str) -> str:
# 캐시 먼저 확인
cached = self.cache.get(prompt)
if cached:
return cached # LLM 호출 없음 = $0
# 라우팅해서 호출
response = self.router.call(prompt)
# 캐시에 저장
self.cache.set(prompt, response)
return response
반복되는 FAQ 트래픽에서 캐시 히트율 30~40%는 쉽게 나와요. 캐시 히트면 LLM 호출 자체가 없어요.
비용 절감 시뮬레이션
월 100만 건 처리 가정
평균 요청: 입력 500토큰 + 출력 200토큰
전부 Opus 4.6:
입력: 500M × $5/M = $2,500
출력: 200M × $25/M = $5,000
월 총비용: $7,500
라우팅 적용 (simple 60% / medium 30% / complex 10%):
Haiku 60만건: $300 + $600 = $900
Sonnet 30만건: $450 + $900 = $1,350
Opus 10만건: $250 + $500 = $750
월 총비용: $3,000
절감: 60%
시맨틱 캐시 추가 (히트율 35%):
LLM 호출 35% 감소
월 총비용: ~$1,950
최종 절감: 74%
어떤 작업에 어떤 모델
Haiku (Claude Haiku 4.5):
✅ FAQ 답변
✅ 감정 분류
✅ 단순 요약
✅ 데이터 추출 (정형)
✅ 번역 (단순)
✅ 키워드 추출
Sonnet (Claude Sonnet 4.6):
✅ 일반 코드 생성
✅ 중간 길이 문서 작성
✅ RAG 답변 생성
✅ 고객 지원 응답
✅ 데이터 분석 (중간)
Opus (Claude Opus 4.6):
✅ 시스템 아키텍처 설계
✅ 복잡한 코드 리뷰
✅ 법률/의료 문서 분석
✅ 멀티스텝 추론
✅ 복잡한 디버깅
반응형
'AI Agent' 카테고리의 다른 글
| Google ADK 실전 가이드 — 에이전트를 백엔드 시스템처럼 만드는 법 (0) | 2026.04.17 |
|---|---|
| AI 에이전트 프로덕션 실패 7가지 패턴 (0) | 2026.04.17 |
| AI 에이전트 옵저버빌리티 완전 가이드 — 에이전트가 뭘 하는지 추적하는 법 (0) | 2026.04.15 |
| n8n으로 AI 워크플로우 자동화 — 코드 없이 Claude 에이전트 파이프라인 만들기 (0) | 2026.04.14 |
| AI 에이전트 모니터링 완전 가이드 — LangSmith vs Langfuse 실전 비교 (0) | 2026.04.09 |