본문 바로가기

AI Agent

LLM 모델 라우팅 완전 가이드 — 분류기, 캐스케이딩, 시맨틱 캐시 실전

반응형

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):
✅ 시스템 아키텍처 설계
✅ 복잡한 코드 리뷰
✅ 법률/의료 문서 분석
✅ 멀티스텝 추론
✅ 복잡한 디버깅

 

반응형