멀티 프로바이더 LLM 라우팅 설계: Claude, GPT, Gemini를 상황에 따라 자동 선택하는 AI 인프라

AI

LLM 라우팅멀티 프로바이더AI 아키텍처ClaudeGPT

이 글은 누구를 위한 것인가

  • AI 기능을 프로덕션에서 운영 중이며 비용과 안정성을 동시에 잡아야 하는 팀
  • 특정 프로바이더 장애 시 서비스가 중단되는 문제를 해결하고 싶은 개발자
  • 작업별로 최적의 모델을 자동으로 선택하는 라우팅 시스템을 구축하려는 분

들어가며

"어제 OpenAI API가 30분 동안 다운됐는데, 우리 서비스도 같이 멈췄어요."

AI 기능을 프로덕션에 올린 팀이라면 한 번쯤 겪는 상황이다. 단일 프로바이더에 의존하면 그 프로바이더의 장애, 요금 인상, 응답 지연이 고스란히 서비스에 전달된다.

2026년 현재 LLM 시장은 춘추전국시대다. Anthropic Claude, OpenAI GPT, Google Gemini, Meta Llama, Mistral, Cohere — 각자 특화된 강점이 있다. 코드 생성은 Claude가 강하고, 빠른 응답은 GPT-4o mini가 유리하고, 대용량 문서 처리는 Gemini 1.5의 긴 컨텍스트가 빛난다.

단일 모델로 모든 작업을 처리하는 것은 망치로 나사를 조이는 것과 같다. 멀티 프로바이더 라우팅은 작업의 성격에 맞는 모델을 자동으로 선택하고, 장애 시 자동으로 대안으로 전환하는 인프라다.


1. 왜 멀티 프로바이더인가

프로바이더별 강점과 약점

모델강점약점비용
Claude Opus 4.6복잡한 추론, 코드 생성, 긴 문서 분석속도, 비용높음
Claude Haiku 4.5빠른 응답, 간단한 작업복잡한 추론낮음
GPT-4o범용, 빠른 응답최신 정보 제한중간
GPT-4o mini매우 저렴, 빠름복잡한 추론매우 낮음
Gemini 1.5 Pro초장문 컨텍스트(1M 토큰)일관성중간
Gemini Flash빠른 응답, 멀티모달추론 깊이낮음
Llama 3.3 (로컬)데이터 프라이버시, 무제한 사용설치/운영 부담고정 비용

멀티 프로바이더의 세 가지 이점

1. 비용 최적화: 단순한 분류 작업에 Claude Opus를 쓸 필요가 없다. GPT-4o mini는 Claude Opus보다 50~100배 저렴하다. 작업 복잡도에 맞는 모델을 쓰면 비용을 80% 이상 줄일 수 있다.

2. 고가용성: 하나의 프로바이더가 다운돼도 다른 프로바이더로 자동 전환된다. 99.9% 이상의 AI 서비스 가용성을 달성할 수 있다.

3. 성능 최적화: 각 작업에 가장 적합한 모델을 사용하면 품질과 속도 모두 향상된다.


2. 라우팅 아키텍처 설계

기본 구조

사용자 요청
    ↓
[라우터 레이어]
  - 작업 분류
  - 모델 선택
  - 폴백 체인 설정
    ↓
[모델 실행 레이어]
  - 선택된 모델에 요청
  - 에러 발생 시 폴백
    ↓
[결과 반환]
  - 응답 정규화
  - 메타데이터 기록

라우팅 전략 3가지

전략 1: 규칙 기반 라우팅 (간단, 예측 가능)

작업 유형에 따라 모델을 고정 매핑한다.

ROUTING_RULES = {
    "code_generation": "claude-opus-4-6",
    "code_review": "claude-sonnet-4-6",
    "simple_qa": "gpt-4o-mini",
    "translation": "gemini-flash",
    "document_summary": "gemini-1.5-pro",  # 장문 처리에 최적
    "classification": "gpt-4o-mini",
    "creative_writing": "claude-sonnet-4-6",
}

def route_by_task(task_type: str) -> str:
    return ROUTING_RULES.get(task_type, "claude-haiku-4-5")

전략 2: 비용/지연 시간 기반 동적 라우팅

입력 토큰 수와 필요한 응답 속도에 따라 모델을 선택한다.

def route_by_cost_latency(
    input_tokens: int,
    max_latency_ms: int,
    quality_requirement: str  # "high" | "medium" | "low"
) -> str:
    # 긴 컨텍스트는 Gemini
    if input_tokens > 100_000:
        return "gemini-1.5-pro"

    # 빠른 응답이 필요하고 품질이 중간 이하면 미니 모델
    if max_latency_ms < 500 and quality_requirement != "high":
        return "gpt-4o-mini"

    # 고품질 요구
    if quality_requirement == "high":
        return "claude-opus-4-6"

    return "claude-haiku-4-5"

전략 3: LLM 라우터 (메타 라우팅)

라우팅 결정 자체를 작은 LLM에게 맡기는 방식이다.

async def llm_based_routing(user_request: str) -> str:
    routing_prompt = f"""
다음 사용자 요청을 분석하고 가장 적합한 모델을 선택하세요.

사용자 요청: {user_request}

선택 가능한 모델:
- claude-opus-4-6: 복잡한 추론, 코드 생성, 상세한 분석
- gpt-4o-mini: 빠른 응답, 간단한 질답, 분류
- gemini-1.5-pro: 매우 긴 문서 처리
- claude-haiku-4-5: 일반적인 중간 복잡도 작업

JSON 형식으로 응답: {{"model": "선택한 모델", "reason": "이유"}}
"""
    # 라우팅 결정에는 저렴한 모델 사용
    response = await call_llm("gpt-4o-mini", routing_prompt)
    return json.loads(response)["model"]

3. 폴백 체인 구현

핵심은 한 프로바이더가 실패했을 때 자동으로 다음 옵션으로 넘어가는 것이다.

import asyncio
from typing import Optional
from dataclasses import dataclass

@dataclass
class ModelConfig:
    model_id: str
    provider: str
    max_retries: int = 2
    timeout_seconds: int = 30

class LLMRouter:
    def __init__(self):
        self.fallback_chains = {
            "high_quality": [
                ModelConfig("claude-opus-4-6", "anthropic"),
                ModelConfig("gpt-4o", "openai"),
                ModelConfig("gemini-1.5-pro", "google"),
            ],
            "fast": [
                ModelConfig("gpt-4o-mini", "openai"),
                ModelConfig("claude-haiku-4-5", "anthropic"),
                ModelConfig("gemini-flash", "google"),
            ],
            "long_context": [
                ModelConfig("gemini-1.5-pro", "google"),
                ModelConfig("claude-opus-4-6", "anthropic"),
            ],
        }

    async def complete(
        self,
        prompt: str,
        chain_name: str = "high_quality",
        **kwargs
    ) -> dict:
        chain = self.fallback_chains[chain_name]
        last_error = None

        for model_config in chain:
            try:
                result = await self._call_with_timeout(
                    model_config, prompt, **kwargs
                )
                # 성공 시 사용된 모델 메타데이터 포함
                return {
                    "content": result,
                    "model_used": model_config.model_id,
                    "provider": model_config.provider,
                    "fallback_used": model_config != chain[0],
                }
            except Exception as e:
                last_error = e
                print(f"[Router] {model_config.model_id} 실패: {e}, 다음 모델로 전환")
                continue

        raise Exception(f"모든 모델 실패. 마지막 에러: {last_error}")

    async def _call_with_timeout(
        self,
        config: ModelConfig,
        prompt: str,
        **kwargs
    ):
        try:
            return await asyncio.wait_for(
                self._call_provider(config, prompt, **kwargs),
                timeout=config.timeout_seconds
            )
        except asyncio.TimeoutError:
            raise Exception(f"{config.model_id} 타임아웃 ({config.timeout_seconds}초)")

    async def _call_provider(self, config: ModelConfig, prompt: str, **kwargs):
        if config.provider == "anthropic":
            return await self._call_anthropic(config.model_id, prompt, **kwargs)
        elif config.provider == "openai":
            return await self._call_openai(config.model_id, prompt, **kwargs)
        elif config.provider == "google":
            return await self._call_gemini(config.model_id, prompt, **kwargs)

4. 응답 정규화: 프로바이더 차이를 숨기는 인터페이스

프로바이더마다 응답 형식이 다르다. 상위 레이어에서 이 차이를 알 필요가 없도록 정규화 레이어를 만든다.

@dataclass
class NormalizedResponse:
    content: str
    model_used: str
    provider: str
    input_tokens: int
    output_tokens: int
    cost_usd: float
    latency_ms: float

class ResponseNormalizer:
    # 1M 토큰당 가격 (2026년 4월 기준)
    PRICING = {
        "claude-opus-4-6": {"input": 15.0, "output": 75.0},
        "claude-haiku-4-5": {"input": 0.8, "output": 4.0},
        "gpt-4o": {"input": 5.0, "output": 15.0},
        "gpt-4o-mini": {"input": 0.15, "output": 0.6},
        "gemini-1.5-pro": {"input": 3.5, "output": 10.5},
        "gemini-flash": {"input": 0.075, "output": 0.3},
    }

    def normalize_anthropic(self, raw_response, model_id: str, latency_ms: float) -> NormalizedResponse:
        pricing = self.PRICING.get(model_id, {"input": 0, "output": 0})
        cost = (
            raw_response.usage.input_tokens * pricing["input"] +
            raw_response.usage.output_tokens * pricing["output"]
        ) / 1_000_000

        return NormalizedResponse(
            content=raw_response.content[0].text,
            model_used=model_id,
            provider="anthropic",
            input_tokens=raw_response.usage.input_tokens,
            output_tokens=raw_response.usage.output_tokens,
            cost_usd=cost,
            latency_ms=latency_ms,
        )

5. 모니터링: 라우팅 결정을 추적하라

멀티 프로바이더 시스템은 어떤 모델이 얼마나 호출됐고, 어디서 폴백이 발생했는지 추적해야 한다.

import time
from functools import wraps

class RouterMetrics:
    def __init__(self):
        self.calls = []  # 프로덕션에서는 Prometheus/Datadog 사용

    def record(self, model: str, provider: str, latency_ms: float,
               cost_usd: float, success: bool, fallback_used: bool):
        self.calls.append({
            "timestamp": time.time(),
            "model": model,
            "provider": provider,
            "latency_ms": latency_ms,
            "cost_usd": cost_usd,
            "success": success,
            "fallback_used": fallback_used,
        })

    def summary(self) -> dict:
        if not self.calls:
            return {}

        total_cost = sum(c["cost_usd"] for c in self.calls)
        avg_latency = sum(c["latency_ms"] for c in self.calls) / len(self.calls)
        fallback_rate = sum(1 for c in self.calls if c["fallback_used"]) / len(self.calls)
        model_distribution = {}
        for call in self.calls:
            model_distribution[call["model"]] = model_distribution.get(call["model"], 0) + 1

        return {
            "total_calls": len(self.calls),
            "total_cost_usd": round(total_cost, 4),
            "avg_latency_ms": round(avg_latency, 1),
            "fallback_rate": round(fallback_rate, 3),
            "model_distribution": model_distribution,
        }

대시보드에서 확인해야 할 핵심 지표:

[모니터링 핵심 지표]
- 모델별 호출 비율: 비용 최적화가 제대로 되는지 확인
- 폴백 발생률: 높으면 특정 프로바이더 문제 신호
- 평균 지연 시간: 모델별 비교
- 일별 비용: 비용 급증 감지
- 에러율: 프로바이더별 안정성 추적

6. 실전 적용: 고객 지원 AI 사례

실제 고객 지원 AI 시스템에서의 라우팅 전략 예시다.

class CustomerSupportRouter:
    def __init__(self):
        self.router = LLMRouter()

    async def handle_request(self, ticket: dict) -> NormalizedResponse:
        # 단순 FAQ 분류: 저렴하고 빠른 모델
        category = await self.classify_ticket(ticket["content"])

        if category == "simple_faq":
            # 미리 정해진 답변 반환 (LLM 불필요)
            return self.get_faq_answer(ticket["content"])

        elif category == "billing_dispute":
            # 민감한 금융 문제: 품질 최우선
            return await self.router.complete(
                prompt=self.build_billing_prompt(ticket),
                chain_name="high_quality"
            )

        elif category == "technical_issue":
            # 기술 문제: 코드 이해 능력 중요
            return await self.router.complete(
                prompt=self.build_technical_prompt(ticket),
                chain_name="high_quality"
            )

        else:
            # 일반 문의: 빠른 응답 우선
            return await self.router.complete(
                prompt=self.build_general_prompt(ticket),
                chain_name="fast"
            )

    async def classify_ticket(self, content: str) -> str:
        # 분류는 가장 저렴한 모델로
        response = await self.router.complete(
            prompt=f"다음 고객 문의를 분류하세요...\n{content}",
            chain_name="fast"
        )
        return response["content"].strip()

이 구조로 단순 문의에는 GPT-4o mini($0.15/1M), 복잡한 분쟁에는 Claude Opus($15/1M)를 써서 품질을 유지하면서 비용을 70% 이상 절감할 수 있다.


맺으며

단일 LLM 프로바이더 의존은 단일 장애점(Single Point of Failure)이다. 2026년처럼 프로바이더 간 경쟁이 치열하고, 모델 특성이 다양해진 시대에는 멀티 프로바이더 라우팅이 기본 인프라가 돼야 한다.

처음부터 완벽한 라우팅 시스템을 만들 필요는 없다. 현재 가장 많이 호출되는 작업 유형 두세 가지를 골라, 규칙 기반 라우팅과 간단한 폴백 체인부터 시작하자. 어떤 모델이 어떤 작업에 얼마나 쓰이는지 데이터가 쌓이면, 그 데이터를 바탕으로 라우팅 전략을 정교하게 만들 수 있다.

중요한 것은 모든 모델을 동일하게 대우하지 않는 것이다. 최적의 도구를 최적의 작업에 사용하는 것이 비용, 성능, 안정성 세 가지를 동시에 잡는 길이다.