실제 운영 환경에서 3개 제공자 LLM 폴백(Fallback) 시스템을 구축한 방법 (그리고 실제로 무엇이 고장 났는가)
요약
멀티 에이전트 LLM SaaS 운영 중 발생한 API 속도 제한(Rate Limit) 문제를 해결하기 위해 구축한 3단계 폴백(Fallback) 시스템 사례를 소개합니다. Anthropic, Google, Groq를 우선순위에 따라 체인으로 연결하여 서비스 안정성과 비용 효율성을 동시에 확보하는 방법을 다룹니다.
핵심 포인트
- 멀티 에이전트 파이프라인은 단일 API의 토큰 제한(TPM)을 쉽게 초과할 수 있음
- 서비스 안정성을 위해 모델 품질보다 비용과 속도 제한을 고려한 라우팅 설계 필요
- Anthropic → Google → Groq 순의 폴백 체인을 통해 가용성 극대화
- 무료 티어 활용 시 각 제공자의 TPM 여유 공간(Headroom)을 반드시 계산해야 함
실제 운영 환경에서 3개 제공자 LLM 폴백(Fallback) 시스템을 구축한 방법 (그리고 실제로 무엇이 고장 났는가)
저는 졸업을 앞둔 학생입니다. 저는 Socra(https://socra-production.up.railway.app/)를 구축했습니다. 이는 아키텍처 마스터플랜을 생성하기 전에 5명의 전문가 AI 페르소나(Persona)를 사용하여 사용자의 스타트업 아이디어를 심문하는 멀티 에이전트 LLM SaaS입니다. 유료 사용자도 있습니다. Railway 위에서 실행됩니다. 그리고 운영 초기 2주 동안, 실제 사용자들이 접속하기 전까지는 제가 알아채지 못할 방식으로 조용히 고장 나 있었습니다.
이것은 제가 어떻게 3개 제공자 폴백 체인(Anthropic → Google → Groq)을 구축했는지, 그 과정에서 무엇이 고장 났는지, 그리고 오늘날 실제 운영 환경에서 실행되고 있는 실제 코드에 대한 이야기입니다.
왜 폴백(Fallback) 체인이 필요한가
Socra를 처음 배포했을 때, LLM 라우팅(Routing)은 간단했습니다: 하나의 제공자, 하나의 모델, 하나의 API 키. 개발 단계에서는 잘 작동했습니다.
그러다 실제 사용자들이 사용하기 시작했습니다.
Groq의 무료 티어는 **분당 6,000 토큰(tokens per minute)**입니다. 단일 Socra 마스터플랜 파이프라인(Pipeline) — 병렬로 실행되는 5명의 전문가 에이전트, 각 에이전트당 약 1,500개의 입력 토큰 — 은 한 번의 버스트(Burst)에 대략 9,500 토큰을 소비합니다. 계산해 보면: 실제 트래픽이 발생하는 모든 세션에서 5명의 에이전트 중 3명이 Error code: 429를 반환했습니다.
앱은 사용자에게 에이전트 카드를 보여주고 있었습니다. 일부는 호박색 텍스트로 "Error"라고 표시되었습니다. 저는 이것이 레이스 컨디션(Race condition)이라고 생각했습니다. 아니었습니다. 하나의 무료 티어 API가 멀티 에이전트 파이프라인을 처리할 수 있을 것이라고 순진하게 가정했던 제 잘못이었습니다.
해결책은 최적화가 아니라 중복성(Redundancy)을 추가하는 것이었습니다.
라우팅 우선순위 체인
최종 운영 환경 라우팅 순서:
1. Anthropic Claude Haiku — ANTHROPIC_API_KEY가 설정된 경우
2. Google Gemini 2.0 Flash — GOOGLE_API_KEY가 설정된 경우 ← 운영 환경 기본값
3. Groq LLaMA 3.1 8B — GROQ_API_KEY가 설정된 경우 ← 폴백(Fallback)
...
왜 이 순서인가요? 모델의 품질이 아니라 비용과 속도 제한(Rate limits) 때문입니다:
| 제공자 (Provider) | 모델 (Model) | 입력 $/MTok | 출력 $/MTok | 무료 티어 TPM |
|---|---|---|---|---|
| Anthropic | claude-haiku-4-5 | $0.80 | $4.00 | 없음 |
| ... |
5개의 LLM 호출을 동시에 실행하는 파이프라인의 경우, Google의 무료 티어는 **Groq보다 150배 더 많은 여유 공간 (headroom)**을 제공합니다. 테스트하는 동안 LLM 비용이 거의 제로에 가까워야 하는 학생 개발 SaaS에게 이는 작은 차이가 아닙니다. 앱이 작동하느냐 작동하지 않느냐를 결정짓는 차이입니다.
구현 (The implementation)
라우팅 체크 (The routing check)
시스템의 모든 LLM 호출은 두 가지 엔트리포인트(entrypoint) 중 하나를 거칩니다: _call_llm (비스트리밍, 구조화된 JSON용) 및 _stream_llm_tokens (스트리밍, 대화 텍스트용). 두 방식 모두 동일한 라우팅 로직을 사용합니다:
# backend/llm_client.py
async def _call_llm(system: str, messages: list[dict], max_tokens: int, json_mode: bool = False) -> str:
...
매우 단순합니다. 라우팅은 단지 다음과 같습니다: 어떤 키가 설정되어 있는가? 첫 번째로 일치하는 것이 승리합니다.
OpenAI SDK를 통한 Google 사용 (우아한 해킹) (Google via the OpenAI SDK (the elegant hack))
Google AI Studio는 OpenAI와 호환되는 엔드포인트 (endpoint)를 노출합니다. 이는 Google SDK가 필요하지 않음을 의미합니다. 단지 OpenAI SDK의 베이스 URL (base URL)을 다른 곳으로 지정하기만 하면 됩니다:
async def _call_google(system: str, messages: list[dict], max_tokens: int, json_mode: bool = False) -> str:
from openai import AsyncOpenAI
client = AsyncOpenAI(
...
스트리밍에도 동일한 패턴이 작동합니다. 단지 stream=True를 사용하고 async for chunk in stream으로 반복하면 됩니다.
이는 알아둘 가치가 있는 패턴입니다: Groq, Azure OpenAI, 그리고 Google AI Studio 모두 OpenAI 호환 엔드포인트 형식을 지원합니다. 만약 설정 가능한 base_url과 api_key를 사용하여 OpenAI SDK를 기반으로 코드를 작성한다면, 추가 코드 거의 없이 멀티 제공자 (multi-provider) 지원을 얻을 수 있습니다.
구조화된 출력 문제 (The structured output problem)
여기서부터 상황이 복잡해졌습니다. 멀티 에이전트 (multi-agent) 파이프라인이 실행되어 마스터플랜을 생성한 후, Socra는 LLM으로부터 평가 점수, 가정 추적, 빠른 답변 선택과 같은 구조화된 JSON을 돌려받아야 합니다. 기존 방식은 스트림 (stream) 내에 구분자 (separator)를 사용하는 것이었습니다:
Stream: "질문은 다음과 같습니다... ###JSON###{"eval_delta": {...}, "choices": [...]}"
이 방식은 Anthropic(Claude는 포맷팅 지침을 안정적으로 따릅니다)에서는 잘 작동했습니다. 하지만 더 작은 모델들에서는 완전히 망가졌습니다.
8B Groq 모델은 때로는 구분자 (separator)를 포함하고, 때로는 포함하지 않으며, 때로는 문장 중간에 구분자를 넣기도 했습니다. 파싱 (parsing)이 소리 없이 실패했고 choices는 빈 값으로 돌아왔습니다. 결과적으로 사용자는 첫 번째 메시지 이후에 빠른 답변 옵션을 볼 수 없었습니다.
해결책: 두 번의 별도 호출.
# 호출 1: 포맷 요구 사항 없이 일반 텍스트 스트림 (stream)
async for token in _stream_llm_tokens(system, messages):
yield token
...
Anthropic 경로는 여전히 구분자를 사용합니다 (그곳에서는 신뢰할 수 있으며 API 호출을 한 번 아낄 수 있기 때문입니다). Groq와 Google 경로는 두 번의 호출을 사용합니다. 지연 시간 (latency)은 약간 늘어나지만, 파싱 실패는 제로가 됩니다.
실제 운영 환경에서 실제로 고장 난 것들
끝에 붙은 줄바꿈(newline) API 키
이 문제 때문에 45분을 허비했습니다.
Railway에 배포한 후, 모든 LLM 호출이 Illegal header value 오류와 함께 실패했습니다. API 키는 정확했습니다. Groq 콘솔에서 그대로 복사했으니까요. 하지만 사실은 아니었습니다. Railway의 Variables 탭에 붙여넣을 때 끝에 보이지 않는 \n이 포함되어 있었습니다.
해결책은 두 가지였습니다:
- 키를 수동으로 다시 입력하기 (클립보드에서 붙여넣지 말 것)
config.py에 방어적으로.strip()추가하기:
class Settings(BaseSettings):
groq_api_key: str = ""
anthropic_api_key: str = ""
...
이제 앱은 복사-붙여넣기 실수에 대해 방어적으로 작동합니다. .strip()은 비용이 들지 않으면서도 디버깅하기 정말 까다로운 종류의 오류들을 방지해 줍니다.
거짓말을 하는 스타트업 로그 (startup log)
두 번째 제공자로 Google을 추가한 후, Railway에 푸시하고 로그를 확인했습니다. 로그에는 다음과 같이 찍혀 있었습니다:
Using Groq LLaMA for LLM calls
하지만 저는 GOOGLE_API_KEY를 설정한 상태였습니다. 이틀 동안 저는 Google이 작동하지 않는 줄 알았습니다. 실제로는 작동하고 있었습니다. 스타트업 로그가 틀렸던 것입니다.
main.py의 라이프스팬 (lifespan) 체크에 버그가 있었습니다:
# 이전 — Google을 완전히 건너뜀
if settings.anthropic_api_key:
logger.info("Using Anthropic Claude")
...
_call_llm 내의 실제 라우팅 (routing)은 올바르게 작동하고 있었습니다 (Google은 두 번째로, Groq 이전에 확인되었습니다). 하지만 로그 확인 로직은 순서가 달랐습니다. 그래서 Groq도 설정되어 있었기 때문에 (실제로 설정되어 있었습니다), 모든 실제 호출이 Google로 향하고 있었음에도 불구하고 "Using Groq"라고 로그를 남겼습니다.
해결책: 시작 시점의 로그 (startup log)에 라우팅 로직을 정확히 반영하여 일치시켰습니다.
429 연쇄 오류 (429 cascade)
Groq의 6k TPM (Tokens Per Minute) 무료 티어를 대상으로 5개의 전문 에이전트 (specialist agents)를 병렬로 실행하는 것은 수학적으로 불가능한 일이었고, 저는 그것이 가능한 것처럼 가장하고 있었습니다.
각 에이전트는 약 1,500개의 입력 토큰 (input tokens)을 받고 약 400개의 출력 토큰 (output tokens)을 생성합니다 = 호출당 약 1,900 토큰. 5개의 병렬 호출 = 9,500 토큰이 동시에 실행됩니다. Groq의 속도 제한기 (rate limiter)는 동일한 1분 창(window) 내에 있는 9,500개 토큰을 모두 감지하고 초과분을 거부합니다.
제가 시도한 세 가지 접근 방식은 순서대로 다음과 같습니다:
접근 방식 1: 지수 백오프 (backoff)를 이용한 재시도. 429 오류 발생 시 4초/8초의 지수 백오프를 적용하여 3회 재시도 로직을 추가했습니다. 약간의 도움이 되었지만, 근본적인 수학적 문제는 해결되지 않았습니다.
접근 방식 2: 지연 시간을 둔 순차적 실행. asyncio.gather()에서 에이전트 간 1.5초의 간격을 두는 순차적 호출 방식으로 전환했습니다. 이를 통해 토큰 폭주 (token burst)를 여러 속도 제한 창 (rate-limit windows)에 분산시켰습니다. Groq에서는 작동했지만, 마스터플랜 파이프라인 (masterplan pipeline)에 약 7.5초를 추가하여 체감될 정도의 지연이 발생했습니다.
접근 방식 3: Google로 전환. Google의 무료 티어는 1,000,000 TPM입니다. 문제가 완전히 사라졌습니다. 이제 Groq은 기본 모델이 아닌 폴백 (fallback) 모델이 되었습니다.
진정한 교훈: 기본 제공자 (primary provider)뿐만 아니라 폴백 제공자의 속도 제한 (rate limits)까지 고려하여 설계해야 합니다. Groq은 빠르고 저렴하지만, 무료 티어에서 병렬 멀티 에이전트 워크로드 (parallel multi-agent workloads)를 처리하도록 설계되지 않았습니다.
비용 분석
Google을 운영 기본값으로 전환한 후, 세션당 전체 토큰 및 비용 분석을 수행했습니다:
| 단계 | 입력 토큰 (Input tokens) | 출력 토큰 (Output tokens) |
|---|---|---|
| 대화 (평균 7턴) | ~16,700 | ~3,500 |
| ... |
Google Gemini Flash 가격 기준 (백만 토큰당 입력 $0.075 / 출력 $0.30):
입력 비용: 56,200 / 1,000,000 × $0.075 = $0.0042
출력 비용: 10,100 / 1,000,000 × $0.30 = $0.0030
합계: 세션당 약 $0.007
Socra는 전체 마스터플랜 세션에 대해 ₹499(약 $6)를 청구합니다. 세션당 LLM 비용은 $0.007입니다. 이는 **LLM 비용만 고려했을 때 99.8%의 매출 총이익률 (gross margin)**을 의미합니다.
Railway 호스팅 비용은 월 약 $30의 고정 비용입니다. 손익분기점은 한 달에 약 6회의 유료 세션입니다.
이 계산이 가능한 이유는 제공자(provider) 선택 덕분입니다. 동일한 세션을 Anthropic Haiku로 진행할 경우 비용은 약 $0.085로, 12배 더 비싸며 이 경우 이익률은 약 98.6%가 됩니다. 여전히 괜찮은 수준이지만, 핵심은 다음과 같습니다: 제공자 선택은 단순한 기술적 결정이 아니라 제품 결정(product decision)입니다.
내가 다르게 했을 일들
1. 첫날부터 멀티 제공자 (multi-provider)를 고려하여 설계할 것. 나는 프로덕션 환경이 망가진 후인 3단계에서야 폴백 체인 (fallback chain)을 추가했습니다. 처음부터 아키텍처에 포함되었어야 했습니다. 라우팅 추상화 (routing abstraction, 제공자 감지가 포함된 _call_llm)는 30분 내에 추가할 수 있을 정도로 간단합니다. 처음부터 단일 제공자로 시작할 이유는 없습니다.
2. 병렬 호출 (parallel calls)을 배포하기 전에 속도 제한 (rate limit) 계산을 테스트할 것. 5개의 병렬 에이전트 × 1,900 토큰 = 한 번의 버스트(burst)에 9,500 토큰이 발생합니다. Groq의 무료 티어는 6,000 TPM (Tokens Per Minute)입니다. 이는 사용자들이 에러를 겪기 전까지 내가 수행하지 않았던 아주 기초적인 산수였습니다.
3. 설정 계층 (config layer)에서 API 키의 공백을 제거할 것. 설정 클래스에서 .strip()을 사용하는 것은 5분이면 끝나는 작업이며, 이를 통해 배포 시 발생하는 특정 유형의 버그 전체를 제거할 수 있습니다.
4. 스타트업 로그가 라우팅 로직과 정확히 일치하도록 만들 것. 실제로 Google을 사용하고 있는데 로그에는
실제 앱은 socra-production.up.railway.app에서 확인할 수 있습니다. 여기서 설명한 접근 방식 — OpenAI 호환 엔드포인트 (OpenAI-compatible endpoints), 두 번의 호출을 통한 구조화된 출력 (two-call structured output), 설정 계층에서의 제공자 감지 (provider detection at the config layer) — 은 모두 현재 프로덕션 환경에서 실행되고 있습니다.
저는 HBTU Kanpur의 졸업 전 마지막 학년 학생으로, 프로덕션 머신러닝 (ML) 시스템을 구축하고 있습니다. 비슷한 작업을 하고 계시거나 멀티 에이전트 아키텍처 (multi-agent architecture)에 대해 질문이 있으시다면, LinkedIn과 GitHub을 통해 연락해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기