목차
- [왜 LLM 업데이트 감시가 Failover 설계의 시작점인가](#왜-llm-업데이트-감시가-failover-설계의-시작점인가)
- [감시해야 할 항목: 모델 성능보다 먼저 봐야 하는 것들](#감시해야-할-항목-모델-성능보다-먼저-봐야-하는-것들)
- [자동 Failover의 핵심 기준: 장애가 아니라 “품질 저하”까지 감지하기](#자동-failover의-핵심-기준-장애가-아니라-품질-저하까지-감지하기)
- [권장 아키텍처: Provider Router와 Model Registry 분리](#권장-아키텍처-provider-router와-model-registry-분리)
- [구현 예시: 상태 점수 기반 라우팅](#구현-예시-상태-점수-기반-라우팅)
- [운영 시 자주 놓치는 함정](#운영-시-자주-놓치는-함정)
- [AI작당 권장 기준](#ai작당-권장-기준)
왜 LLM 업데이트 감시가 Failover 설계의 시작점인가
LLM 장애 대응을 단순히 “API가 죽으면 다른 API로 보낸다” 수준으로 설계하면 실제 운영에서 자주 실패합니다. 최근 LLM 서비스는 완전 장애보다 가격 변경, 레이트리밋 변경, 컨텍스트 길이 조정, 엔드포인트 변경, 모델 alias 변경, 응답 포맷 변화처럼 애매한 형태로 문제가 발생하는 경우가 많습니다.
검색 결과 기준으로 LLM Stats는 OpenAI, Anthropic, Google, Meta, Mistral, DeepSeek 등 주요 LLM 릴리스와 API 변경을 시간 단위로 추적한다고 소개되어 있습니다. 또한 OpenAI, Anthropic, Google Cloud AI, Azure OpenAI, Together AI, Fireworks, Groq 등 제공자별로 가격 변경, 신규 기능, 레이트리밋 업데이트, API 엔드포인트 변경을 추적한다고 알려져 있습니다.
여기서 중요한 점은 “새 모델이 나왔다”보다 “내 서비스의 라우팅 조건이 깨질 수 있는 변경이 발생했는가”입니다. 예를 들어 특정 모델의 분당 요청 제한이 낮아졌다면, 서버는 정상인데 큐가 밀리고 타임아웃이 증가합니다. API 상태 페이지에는 장애가 없다고 표시되지만, 사용자 입장에서는 장애와 같습니다.
현재 상용 API와 오픈소스를 포함해 500개 이상의 LLM 선택지가 존재한다고 알려져 있습니다. 선택지가 많아졌다는 말은 곧, 수동으로 모델별 변경 사항을 따라가는 방식이 오래 버티기 어렵다는 뜻입니다. Failover는 장애 대응 기능이 아니라 업데이트 추적 시스템과 붙어 있어야 하는 운영 기능으로 보는 편이 안전합니다.

감시해야 할 항목: 모델 성능보다 먼저 봐야 하는 것들
모델 벤치마크 점수만 감시하면 운영 장애를 놓칩니다. 실제 자동 Failover에서 먼저 봐야 할 항목은 다음과 같습니다.
1. API 응답 상태
단순히 200 여부만 보면 부족합니다. 429, 500, 502, 503, 504를 분리해서 봐야 합니다. 특히 429는 제공자 장애가 아니라 계정·조직·리전·모델 단위 제한일 수 있어 무조건 다른 제공자로 넘기기보다 “동일 제공자 내 대체 모델”을 먼저 시도할지 결정해야 합니다.
2. P95, P99 지연 시간
평균 응답 시간은 거의 쓸모가 없습니다. 채팅형 서비스에서는 P95가 사용자 체감에 더 가깝습니다. 내부 기준으로는 짧은 질의 기준 P95가 8초를 넘고, 동시에 최근 20개 요청 중 timeout 또는 5xx가 3회 이상이면 degraded 상태로 보는 구성이 실무에서 다루기 좋습니다.
3. 토큰 단가와 최대 출력 토큰
가격 변경은 장애가 아니지만, 자동화 워크플로에서는 비용 장애가 됩니다. 특정 모델이 갑자기 고가 모델로 라우팅되면 월말 비용이 터질 수 있습니다. 따라서 Failover 후보에는 반드시 `max_cost_per_1k_tokens` 또는 `max_cost_per_1m_tokens` 같은 비용 상한을 둬야 합니다.
4. 응답 포맷 호환성
JSON 모드, function calling, tool use, response schema 지원 여부는 모델마다 다릅니다. “텍스트 생성은 되는데 파서가 깨지는 장애”가 여기서 나옵니다. Failover 후보 모델은 성능보다 먼저 출력 계약을 지킬 수 있는지 확인해야 합니다.
5. 모델 alias 변경
`latest`, `preview`, `turbo`, `mini`처럼 alias 성격의 이름을 그대로 프로덕션에 꽂으면 어느 날 응답 성향이 바뀔 수 있습니다. 최신 모델 테스트용 라우팅과 프로덕션 고정 라우팅을 분리하는 것이 좋습니다.
자동 Failover의 핵심 기준: 장애가 아니라 “품질 저하”까지 감지하기
많이 놓치는 부분이 있습니다. Failover 조건을 HTTP 에러에만 걸면 너무 늦습니다. LLM은 다음과 같은 “조용한 실패”가 많습니다.
- 응답은 200이지만 JSON 파싱 실패율이 증가
- 응답 시간이 길어져 사용자 세션이 이탈
- 레이트리밋 직전이라 큐 대기 시간이 급증
- 모델 업데이트 후 특정 시스템 프롬프트 준수율 하락
- tool call 이름은 맞지만 arguments 스키마가 조금씩 틀어짐
따라서 Provider 상태는 `healthy`, `degraded`, `blocked` 정도로 나누는 편이 좋습니다.
예시 기준은 아래처럼 잡을 수 있습니다.
| 상태 | 조건 예시 | 라우팅 정책 |
|---|---|---|

| healthy | 최근 5분 error rate 2% 미만, P95 8초 미만 | 기본 사용 |
| degraded | error rate 2~10% 또는 P95 8~15초 | 신규 요청 일부만 우회 |
| blocked | error rate 10% 초과 또는 429 연속 발생 | 즉시 우회 |
여기서 “429 연속 발생”은 단순 총량보다 연속성이 중요합니다. 예를 들어 최근 60초 안에 429가 5번 발생했더라도 중간에 정상 응답이 많다면 일시적 버스트일 수 있습니다. 반대로 동일 모델에서 429가 3회 연속이면 큐를 더 밀어 넣지 않는 편이 낫습니다.
권장 아키텍처: Provider Router와 Model Registry 분리
Failover 코드를 각 비즈니스 로직 안에 직접 넣으면 나중에 모델을 바꿀 때마다 전체 코드를 수정하게 됩니다. 최소 구성은 다음처럼 나누는 것을 권장합니다.
[Application]
|
v
[LLM Router]
|
+-- [Model Registry]
|
+-- [Provider Health Store]
|
+-- [Cost & Rate Limit Policy]
|
v
[OpenAI / Anthropic / Google / Azure OpenAI / Groq / Fireworks / etc.]핵심은 `Model Registry`입니다. 여기에는 모델 이름만 넣는 것이 아니라, 해당 모델이 어떤 작업에 적합한지, 어떤 기능을 지원하는지, 비용 상한은 얼마인지, JSON 출력이 안정적인지 같은 정보를 함께 둬야 합니다.
예시는 다음과 같습니다.
tasks:
support_chat:
primary: openai:gpt-4.1-mini
fallback:
- anthropic:claude-3-5-haiku-latest
- google:gemini-1.5-flash
requirements:
json_schema: true
tool_calling: true
max_latency_p95_ms: 8000
max_input_tokens: 32000
max_cost_per_request_usd: 0.02
batch_summary:
primary: google:gemini-1.5-flash
fallback:
- openai:gpt-4.1-mini
- mistral:mistral-small
requirements:
json_schema: false
tool_calling: false
max_latency_p95_ms: 30000
max_cost_per_request_usd: 0.005위 예시에서 모델명과 지원 여부는 실제 사용 중인 계정, 리전, API 버전에 따라 달라질 수 있으므로 그대로 복사하기보다 내부 Registry 형식을 설명하는 참고용으로 봐야 합니다.
구현 예시: 상태 점수 기반 라우팅
아래는 단순한 예시입니다. 핵심은 “첫 번째 모델 실패 시 다음 모델 호출”이 아니라, 호출 전 상태 점수를 보고 후보를 정렬한다는 점입니다.
import time
from dataclasses import dataclass
@dataclass
class ProviderState:
name: str
error_rate_5m: float
p95_latency_ms: int
consecutive_429: int
cost_per_request_usd: float
supports_json_schema: bool
last_updated_ts: int
def health_score(state: ProviderState, require_json_schema: bool) -> int:
score = 100
if state.consecutive_429 >= 3:
return 0
if state.error_rate_5m > 0.10:
score -= 60
elif state.error_rate_5m > 0.02:
score -= 25
if state.p95_latency_ms > 15000:
score -= 40
elif state.p95_latency_ms > 8000:
score -= 15
if require_json_schema and not state.supports_json_schema:
return 0
if state.cost_per_request_usd > 0.02:
score -= 20
# 상태 정보가 너무 오래되면 감점
if int(time.time()) - state.last_updated_ts > 300:
score -= 30
return max(score, 0)
def select_provider(candidates, require_json_schema=True):
ranked = sorted(
candidates,
key=lambda p: health_score(p, require_json_schema),
reverse=True
)
selected = ranked[0]
if health_score(selected, require_json_schema) < 50:
raise RuntimeError("No safe LLM provider available")
return selected실제 운영에서는 Redis나 DynamoDB 같은 외부 저장소에 provider health를 기록하고, 애플리케이션 인스턴스들이 같은 상태를 보도록 구성하는 편이 좋습니다. 로컬 메모리만 쓰면 인스턴스 A는 provider를 차단했는데 인스턴스 B는 계속 호출하는 상황이 생깁니다.
상태 TTL은 너무 짧으면 흔들리고, 너무 길면 복구가 늦습니다. 일반적인 채팅 서비스 기준으로는 다음 값을 시작점으로 둘 수 있습니다.
health_check:
window_seconds: 300
min_samples: 20
degraded_ttl_seconds: 120
blocked_ttl_seconds: 60
recovery_probe_ratio: 0.05여기서 `recovery_probe_ratio: 0.05`가 실무적으로 유용합니다. 차단된 provider가 회복됐는지 확인하려면 모든 트래픽을 한 번에 돌려보내면 안 됩니다. 신규 요청의 5%만 probe로 보내고, 정상률이 기준을 넘으면 단계적으로 복구시키는 방식이 안전합니다.

운영 시 자주 놓치는 함정
첫 번째 함정은 스트리밍 응답의 실패 처리입니다. 일반 요청은 timeout이면 다른 provider로 재시도하면 됩니다. 하지만 스트리밍은 일부 토큰이 이미 사용자에게 전달된 뒤 실패할 수 있습니다. 이때 다른 모델로 이어서 생성하면 문체와 맥락이 바뀌어 UX가 더 나빠질 수 있습니다.
스트리밍 Failover는 요청 시작 전 실패에만 자동 재시도하고, 토큰이 1개 이상 전송된 뒤의 실패는 “부분 응답 종료”로 처리하는 정책이 더 안정적입니다. 내부적으로는 `first_token_sent` 플래그를 두면 됩니다.
first_token_sent = False
try:
for chunk in provider.stream(prompt):
first_token_sent = True
send_to_client(chunk)
except TimeoutError:
if not first_token_sent:
retry_with_fallback_provider(prompt)
else:
send_to_client("\n\n응답이 중간에 중단되었습니다. 다시 시도해 주세요.")두 번째 함정은 재시도 폭주입니다. 사용자가 한 번 요청했는데 primary, fallback1, fallback2를 모두 호출하면 장애 상황에서 트래픽이 3배로 늘어납니다. 특히 429 상황에서 무제한 재시도를 걸면 제공자별 제한을 더 빨리 소진합니다.
권장값은 대화형 요청 기준 최대 2회 호출입니다. 즉 primary 1회, fallback 1회까지만 허용합니다. 배치 작업은 큐 기반으로 뒤로 미루는 편이 낫습니다.
retry_policy:
interactive:
max_attempts: 2
retry_on:
- timeout
- 502
- 503
- 504
no_retry_on:
- 400
- 401
- 403
- invalid_request_error
batch:
max_attempts: 4
backoff:
type: exponential
initial_ms: 1000
max_ms: 30000세 번째 함정은 프롬프트 호환성입니다. 같은 시스템 프롬프트라도 provider별 역할 구분, tool schema, safety policy 해석이 다릅니다. 따라서 Failover용 프롬프트를 하나로 통일하기보다 provider adapter에서 변환하는 구조가 필요합니다.
Canonical Prompt
|
+-- OpenAI Adapter
+-- Anthropic Adapter
+-- Gemini Adapter
+-- Open-source Adapter이 구조를 두면 모델 변경이 있을 때 애플리케이션 코드를 바꾸지 않고 adapter만 수정할 수 있습니다.
AI작당 권장 기준
운영 자동화 게시판 기준으로는 다음 기준을 권장합니다.
1. LLM 업데이트 감시는 주 1회 수동 확인이 아니라 자동 수집 대상으로 둡니다.
LLM Stats처럼 주요 제공자의 릴리스, 가격, 레이트리밋, 엔드포인트 변경을 추적한다고 알려진 도구를 참고하거나, 각 provider의 changelog와 status feed를 수집해 내부 알림으로 연결하는 방식이 적합합니다.
2. Failover 후보는 최소 2개 provider, 3개 model tier로 둡니다.
동일 provider 안의 다른 모델만으로는 계정·리전·조직 단위 장애에 취약합니다. 반대로 provider만 다르고 기능 호환성이 없으면 실제 장애 때 쓸 수 없습니다. “주 provider 1개, 보조 provider 1개, 저비용 배치용 모델 1개” 구성이 현실적인 출발점입니다.
3. 자동 전환 기준은 HTTP 상태 코드보다 P95, 연속 429, JSON 파싱 실패율을 우선합니다.
특히 구조화 출력이 필요한 서비스라면 `json_parse_fail_rate`를 반드시 지표로 둬야 합니다. 응답 200이더라도 파싱 실패가 늘면 사용자 기능은 실패한 것입니다.
4. 모델명은 코드에 직접 박지 말고 Registry로 관리합니다.
업데이트가 잦은 환경에서는 이 원칙 하나만 지켜도 장애 대응 속도가 크게 달라집니다. 모델 교체를 배포 없이 설정 변경으로 처리할 수 있어야 합니다.
5. 스트리밍 요청은 “중간 Failover”를 기본값으로 두지 않습니다.
첫 토큰 전 실패는 재시도, 첫 토큰 후 실패는 재시도하지 않는 정책이 더 예측 가능합니다.
LLM Failover는 장애 대응 기능이라기보다, 빠르게 바뀌는 모델 생태계에 맞춘 라우팅 자동화입니다. 업데이트 감시, 비용 상한, 기능 호환성, 상태 점수 기반 라우팅을 한 세트로 설계해야 실제 프로덕션에서 버틸 수 있습니다.
댓글 0