Skip to content
isdnetworks
Go back

LiteLLM Proxy 폴백 구조를 4단에서 2단으로 줄인 이유

AI 서비스의 LLM 호출을 중앙화하기 위해 LiteLLM Proxy를 운영했다. 초기에는 모델 4개를 캐스케이드로 연결했는데, 운영하면서 구조를 2단으로 단순화했다.

Table of contents

Open Table of contents

초기 구조: 4단 캐스케이드

요청 → gemini-3-flash-preview
  ↓ (실패 시)
  → gemini-2.5-flash
    ↓ (실패 시)
    → gemini-2.5-flash-lite
      ↓ (실패 시)
      → gpt-4.1-nano (OpenAI)

각 단계마다 모델별 API 키 풀이 있고, 하나가 rate limit에 걸리면 다음 단계로 넘어가는 구조였다. 논리적으로는 깔끔해 보이지만 운영에서 문제가 생겼다.

문제점

  1. 디버깅 어려움: 요청이 어떤 모델에서 처리되었는지 추적하기 어려움
  2. 품질 불일치: 같은 서비스에서 flash-preview와 flash-lite의 응답 품질 차이가 큼
  3. rate limit 관리 복잡: 4개 모델 × N개 키의 RPM/RPD를 각각 관리해야 함
  4. 불필요한 중간 단계: flash와 flash-lite의 실질적 차이가 크지 않음

현재 구조: 2단 폴백

요청 → my-llm (Gemini N개 키, 로드밸런싱)
  ↓ (전체 실패 시)
  → my-llm-openai (gpt-4.1-nano)

단일 진입점

모든 요청이 my-llm이라는 하나의 모델 별칭으로 들어온다. 내부적으로 N개 Gemini API 키가 simple-shuffle 전략으로 분배된다.

model_list:
  - model_name: my-llm
    litellm_params:
      model: gemini/gemini-2.5-flash-lite
      api_key: os.environ/GEMINI_API_KEY_01
      rpm: 9
      rpd: 19
      tpm: 250000
  # ... N개 키 반복

  - model_name: my-llm-openai
    litellm_params:
      model: openai/gpt-4.1-nano
      api_key: os.environ/OPENAI_API_KEY

router_settings:
  routing_strategy: simple-shuffle
  enable_pre_call_checks: true
  enforce_model_rate_limits: true

litellm_settings:
  fallbacks:
    - my-llm: ["my-llm-openai"]

RPM/RPD 균형 분배

N개 키에 rate limit을 균등 분배한다.

설정총량 (N키)
RPM/키{rpm}N × {rpm} RPM
RPD/키{rpd}N × {rpd} RPD
TPM/키250,000N × 250K TPM

Redis에 RPM/RPD 카운터를 저장하여 프록시 재시작 시에도 rate limit 상태가 유지된다.

동적 모델 로테이션

하루 RPD가 부족할 수 있으므로, 3개 Gemini 모델을 자동 로테이션한다.

flash-lite (RPD 상한) → flash (RPD 상한) → 3-flash-preview (RPD 상한) → gpt-4.1-nano

총 Gemini RPD: 3모델 × RPD 상한/일. 이후 OpenAI로 자동 전환.

모니터링 스크립트 (monitor-rpd.sh):

# 로테이션 상태 확인
cat ~/.rotation-state
# current_model=gemini-2.5-flash-lite
# rotation_count=0
# last_rotation=2026-03-20T10:30:00+09:00

일일 리셋: 17:00 KST에 Gemini RPD가 리셋되면 flash-lite로 강제 복귀.

config 자동 생성

N개 키를 수동으로 YAML에 입력하는 것은 실수의 원인이다. Python 스크립트로 자동 생성한다.

# _gen_config.py (간략화)
for model in ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3-flash-preview"]:
    entries = []
    for i in range(1, N + 1):
        entries.append({
            "model_name": "my-llm",
            "litellm_params": {
                "model": f"gemini/{model}",
                "api_key": f"os.environ/GEMINI_API_KEY_{i:02d}",
                "rpm": 9, "rpd": 19, "tpm": 250000
            }
        })
    # YAML 파일 출력

4개 config 파일이 생성되고, rotate.sh가 활성 config를 config.yaml로 복사한다.

변경 전후 비교

항목4단 캐스케이드2단 폴백
진입점모델별 분리단일 (my-llm)
폴백 단계4단2단
일일 RPDN × {rpd} (단일 모델)3 × N × {rpd} (3모델 로테이션)
디버깅어떤 모델인지 추적 어려움현재 모델 명확
품질 일관성불일치동일 모델 내 일관성 유지

핵심 정리

LLM 프록시에서 폴백 단계를 늘리면 가용성은 올라가지만 복잡성도 함께 올라간다. 실제 운영에서 중요한 것은 “절대 실패하지 않는 것”이 아니라 “실패 시 빠르게 대응할 수 있는 것”이다. 단순한 2단 구조 + 자동 로테이션이 복잡한 4단 캐스케이드보다 운영하기 쉽고 디버깅도 빠르다.

참고 자료


Share this post on:

Previous Post
Android SMS 릴레이 앱에서 MMS를 SMS처럼 PDU 파싱하면 생기는 문제
Next Post
NHN KCP 본인인증을 Laravel에 통합한 구조