본문으로 건너뛰기

LLM API 재시도 vs 폴백: 모델을 바꾸기 전 5가지 기준

6 분 소요API 가이드

일시적이고 부작용 없이 다시 실행할 수 있으며 예산이 남았을 때만 같은 경로를 재시도합니다. 폴백은 동일 업무 계약을 통과한 경로에만 허용합니다.

LLM API 실패가 오류 책임, 커밋 상태, 복구 예산, 폴백 등가성, 경로 상태 다섯 기준을 거쳐 재시도, 폴백, 큐, 축소 응답 또는 중단으로 분기되는 지도.

같은 LLM API 경로를 재시도하려면 실패가 일시적이고, 호출을 부작용 없이 다시 실행할 수 있으며, 워크플로 전체의 시간·횟수·비용 예산이 남아 있어야 합니다. 다른 모델로 폴백하려면 그 경로가 동일한 입력, 출력 스키마, 도구, 안전, 데이터, 지연, 비용, 품질 테스트를 미리 통과해야 합니다. 조건을 충족하지 못하면 큐, 검증된 축소 응답, 또는 안전한 중단이 더 낫습니다.

“몇 번 실패했나”보다 먼저 볼 것

기준확인할 질문아니면
오류 책임일시적 네트워크/공급자 문제인가?요청, 인증, 정책, 예산 책임을 수정
커밋 상태사용자에게 보인 출력과 부수 효과 없이 재실행 가능한가?상태를 조회·조정하거나 중단
복구 예산시도 횟수, 경과 시간, 토큰, 비용이 남았나?큐 또는 승인된 축소 모드
폴백 등가성이 워크플로의 테스트 픽스처를 통과했나?자동 전환 금지
경로 상태제한된 상태 점검이 의미 있고 모든 시도가 기록되나?서킷 열기

폴백은 “다른 모델로 재시도”가 아닙니다. 모델을 바꾸면 context window, structured output, tool calling, 안전 필터, 데이터 경계, 가격과 답변 품질이 함께 달라질 수 있습니다.

오류 코드를 행동으로 바로 연결하지 마세요

OpenAI의 현재 error code guide는 429를 요청 속도 제한과 quota/spend 고갈로 나눕니다. 전자는 전송 속도를 낮춰야 하고 후자는 예산 책임자가 해결해야 합니다. 같은 429라고 똑같이 backoff하면 quota 문제를 숨길 뿐입니다.

운영 정책은 최소한 다음을 구분해야 합니다.

  • 공급자 오류 세부 정보가 일시적이라고 분류한 5xx/과부하: 짧고 상한이 있는 backoff+jitter. HTTP 코드만으로 결정하지 않음.
  • 짧은 retry-after가 있는 rate 429: 대기하고 concurrency를 낮춤.
  • quota, credit, spend limit: 동기 재시도 중단. 허용된 백그라운드 작업만 큐로 이동.
  • 400, 잘못된 schema, context overflow: request를 수정.
  • 401/403: credential, permission, route owner 수정.
  • safety/policy: 덜 엄격한 모델로 우회하지 않음.
  • 일부 스트림이나 도구 커밋이 불확실함: 상태를 확인한 뒤 결정.

라이브러리가 이미 무엇을 하는지도 확인해야 합니다. Anthropic API errors는 공식 SDK가 transient connection, rate-limit, 5xx를 기본 두 번 재시도하고 retry-after를 따른다고 설명합니다. Gemini troubleshooting guide도 SDK 자동 retry를 명시합니다. 애플리케이션의 maxRetries만 세면 실제 호출량을 놓칩니다.

복구 예산은 한 장의 장부입니다

text
total workflow attempts = initial + SDK internal + gateway + application retry + fallback + queue redelivery

여기에 최대 경과 시간, 추가 token/비용, 허용 품질 저하를 더합니다. 실시간 상담과 야간 batch는 같은 정책을 공유할 이유가 없습니다.

AWS의 timeouts, retries, backoff with jitter는 여러 계층의 독립 retry가 부하를 곱하고 과부하 복구를 늦출 수 있음을 설명합니다. 한 계층이 다음 행동을 결정하고, 다른 계층은 실제 attempt를 보고하도록 설계하세요.

yaml
workflow: catalog-classification max_total_attempts: 3 max_elapsed_ms: 6000 max_extra_cost_usd: 0.012 replay_safe_until: classification_saved approved_fallback: compact-classifier queue_allowed: true fail_closed_on: [auth, policy, unknown_commit]

이 값은 예시입니다. 서비스 SLO와 장애 주입 결과로 정해야 합니다.

커밋 뒤에는 투명한 재시도가 아닙니다

클라이언트 timeout은 “아무 일도 없었다”는 증거가 아닙니다.

  1. provider가 request를 받기 전이면 replay가 비교적 단순합니다.
  2. provider가 받았지만 응답이 없으면 결과는 unknown입니다.
  3. 사용자가 첫 token을 봤다면 새 모델의 출력을 자연스럽게 이어 붙일 수 없습니다.
  4. tool, DB write, webhook, 메일 같은 작업이 시작됐다면 중복 side effect가 생길 수 있습니다.

상태 변경 작업에는 애플리케이션 operation ID 또는 idempotency key를 두고 requested → started → committed → acknowledged를 저장하세요. unknown이면 실제 상태를 조회하고 조정해야 합니다. 두 번째 모델 호출은 상태 조회가 아닙니다.

스트리밍에서는 사용자에게 보이는 첫 이벤트가 없고 도구·DB 부수 효과도 커밋되지 않았음을 확인한 경우에만 상태를 초기화해 승인된 경로로 전환할 수 있습니다. 그 뒤에는 중단 사실을 알리고 사용자가 다시 시작하게 하거나 검증된 재개 프로토콜을 사용해야 합니다.

폴백 모델의 등가성 계약

같은 테스트 fixture로 여덟 항목을 검증합니다.

  1. 입력: context, image/file, system instruction, locale.
  2. 출력: JSON Schema, required field, refusal, truncation.
  3. 도구: 이름, argument schema, parallel call, result return, idempotency.
  4. 안전: policy, high-impact stop, human escalation.
  5. 데이터: region, retention, tenant, allowed data class.
  6. 운영: p95/p99 latency, streaming, request ID, status.
  7. 비용: 단위, cache, quota owner, maximum spend.
  8. 품질: workflow별 eval과 합격 기준.

분류 작업에 적합한 작은 모델이 정책 안내에도 적합하다는 보장은 없습니다. 생성형 폴백이 합격하지 못하면 cache, deterministic rule, 또는 명확한 retry-later가 낫습니다.

정책 코드를 business catch 밖으로

ts
type Action = "retry" | "fallback" | "queue" | "degrade" | "fail_closed"; function decide(x: { owner: "transient" | "rate" | "quota" | "request" | "auth" | "policy" | "unknown"; commitState: "none" | "committed" | "unknown"; attemptsLeft: number; elapsedMsLeft: number; costLeft: number; retryAfterMs: number; retryExpectedMs: number; retryExpectedCost: number; fallbackExpectedMs: number; fallbackExpectedCost: number; primaryRoute: "healthy" | "degraded" | "open"; fallbackRoute: "healthy" | "unhealthy"; fallbackApproved: boolean; queueAllowed: boolean; degradeApproved: boolean; }): Action { if (["request", "auth", "policy"].includes(x.owner)) return "fail_closed"; if (x.commitState !== "none") return "fail_closed"; if (x.owner === "unknown") return x.queueAllowed ? "queue" : "fail_closed"; const canRetry = x.attemptsLeft > 0 && x.elapsedMsLeft >= x.retryAfterMs + x.retryExpectedMs && x.costLeft >= x.retryExpectedCost; const canFallback = x.attemptsLeft > 0 && x.elapsedMsLeft >= x.fallbackExpectedMs && x.costLeft >= x.fallbackExpectedCost; if (canRetry && ["transient", "rate"].includes(x.owner) && x.primaryRoute !== "open") { return "retry"; } if ( canFallback && ["transient", "rate", "quota"].includes(x.owner) && x.fallbackApproved && x.fallbackRoute === "healthy" ) return "fallback"; if (x.queueAllowed) return "queue"; return x.degradeApproved ? "degrade" : "fail_closed"; }

retryAfterMs는 주 경로 재시도에만 적용합니다. 폴백은 자체 예상 시간·비용으로 판단하고, fallbackApproved에는 대체 경로의 독립 quota 확인도 포함합니다. 따라서 주 경로의 긴 retry-after가 남은 SLO 안에 끝낼 수 있는 정상 폴백을 막지 않습니다. committed 또는 unknown 상태는 자동 분기 밖에서 대조하거나 검증된 재개/멱등성 프로토콜을 사용해야 합니다. 다음 네트워크 전송 전에 시도 레코드를 먼저 저장하세요. 500도 긴 입력 컨텍스트 같은 요청 원인일 수 있으므로 오류 세부 정보 없이 일시적 장애로 분류하면 안 됩니다.

staging에서 실패를 만들어 보세요

속도 제한 429, 할당량 429, 스트림 전 503, 토큰 출력 뒤 연결 끊김, 도구 커밋 뒤 타임아웃, 폴백 스키마 누락, 열린 서킷을 주입합니다. 또한 주 경로의 retry-after가 60초이고 남은 SLO가 8초이지만, 승인된 정상 폴백은 자체 시간·비용 예산으로 2초 안에 끝나는 사례를 넣습니다. 이때 예상 동작은 큐가 아니라 fallback이어야 합니다. 각 테스트 사례는 예상 동작이 하나여야 합니다.

지표는 primary success, retry recovery, fallback recovery, degraded response, fail closed로 분리합니다. 최종 success rate만 보면 고장 난 primary가 폴백 뒤에 숨습니다.

현재 문제가 특정 provider의 429라면 먼저 OpenAI API rate limit, Claude API rate limit, Gemini API rate limits에서 owner를 확인하세요.

승인된 여러 모델을 하나의 compatible endpoint로 시험해야 한다면 staging에서 최신 LaoZhang AI API 문서를 확인할 수 있습니다. 단일 endpoint는 adapter를 줄일 수 있지만 schema, tools, 데이터, 비용, 품질 등가성과 복구 예산을 대신 증명하지 않습니다.

완료 기준은 단순한 200이 아닙니다. 모든 실패가 설명 가능한 하나의 action으로 분기되고, 모든 계층이 하나의 예산을 공유하며, 폴백이 동일 업무 계약을 통과하고, 최종 성공이 앞선 실패 기록을 지우지 않아야 합니다.

#LLM API#재시도#폴백 모델#AI 안정성
Share: