프로덕션 AI API 에러 처리·재시도·레이트리밋·폴백 전략 (Claude)
로컬에서 잘 돌던 AI API 호출이 프로덕션에 올라가는 순간 깨지는 가장 흔한 이유는 모델 품질이 아니라 운영의 빈틈입니다. 트래픽이 몰리면 429 rate limit이 떨어지고, 공급사 부하가 높으면 529 overloaded가 돌아오며, 가끔은 500이 그냥 튀어나옵니다. 이때 재시도를 안 하면 사용자에게 에러가 그대로 노출되고, 무지성으로 즉시 재시도하면 레이트리밋을 더 악화시킵니다. 이 글은 Anthropic Claude를 1차 예제로, 에러 코드 분기 · 타입드 예외 처리 · SDK 자동 재시도와 지수 백오프+지터 · retry-after 헤더 · 모델 폴백 · 스트리밍 타임아웃 회피 · 멱등성과 관측성까지 실전 코드로 정리합니다.
먼저: HTTP 에러 코드와 재시도 가능 여부
프로덕션 코드의 출발점은 "이 에러를 재시도해도 되는가"를 정확히 구분하는 것입니다. 재시도해도 소용없는 에러(잘못된 요청, 인증 실패)를 재시도하면 비용과 지연만 늘고, 재시도해야 할 에러(레이트리밋, 일시적 과부하)를 포기하면 가용성이 떨어집니다. Claude API의 주요 에러 코드는 다음과 같습니다.
| 코드 | 에러 타입 | 재시도 | 흔한 원인 |
|---|---|---|---|
| 400 | invalid_request_error | 아니오 | 잘못된 요청 형식 · 파라미터(빈 messages, role 교대 위반 등) |
| 401 | authentication_error | 아니오 | API 키 누락 · 무효 |
| 403 | permission_error | 아니오 | 키에 권한 없음 |
| 404 | not_found_error | 아니오 | 잘못된 엔드포인트 · 모델 ID 오타 |
| 413 | request_too_large | 아니오 | 요청 크기 초과 |
| 429 | rate_limit_error | 예 | RPM/TPM/TPD 초과 |
| 500 | api_error | 예 | 서버 내부 오류 |
| 529 | overloaded_error | 예 | API 일시 과부하 |
핵심 원칙: 4xx(429 제외)는 재시도 금지, 429와 5xx는 백오프 재시도. 400은 요청 자체가 틀린 것이므로 같은 요청을 반복해봐야 똑같이 실패합니다. 모델 ID 오타로 인한 404도 마찬가지입니다 — 반드시 정확한 모델 ID(claude-opus-4-8 등)를 쓰고, claude-opus-4.8처럼 점을 찍거나 임의 날짜 접미사를 붙이지 마세요.
문자열 매칭 대신 타입드 예외로 분기하라
가장 흔한 안티패턴은 에러 메시지 문자열에 "429"나 "rate_limit"이 들어있는지 검사하는 것입니다. 메시지 포맷은 언제든 바뀔 수 있어 깨지기 쉽습니다. Anthropic SDK는 HTTP 코드별 타입드 예외 클래스를 제공하므로, except/instanceof로 분기하세요.
Python에서의 예외 계층은 다음과 같습니다. 모든 예외는 anthropic.APIError를 상속하므로, 구체적인 것부터 일반적인 것 순서로 잡아야 합니다.
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_API_KEY 환경변수 사용
def call_claude(prompt: str):
try:
msg = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
# 응답 content는 블록 리스트
return "".join(b.text for b in msg.content if b.type == "text")
except anthropic.RateLimitError as e: # 429
# 재시도 대상 — retry-after 헤더를 보고 대기
raise
except anthropic.OverloadedError as e: # 529
# 재시도 대상 — 백오프 또는 모델 폴백
raise
except anthropic.BadRequestError as e: # 400
# 재시도 무의미 — 요청을 고쳐야 함. 로깅 후 즉시 실패.
raise
except anthropic.AuthenticationError as e: # 401
# 키 문제 — 알림 발생, 재시도 금지
raise
except anthropic.APIStatusError as e: # 그 외 4xx/5xx
# e.status_code 와 e.type 으로 세분화
if e.type == "billing_error":
... # 결제 문제는 permission_error(403)와 구분된다
raise
except anthropic.APIConnectionError as e: # 네트워크 단절/타임아웃
# 연결 자체가 실패 — 보통 재시도 대상
raise
각 예외와 HTTP 코드의 매핑은 다음과 같습니다.
| HTTP | Python 예외 클래스 |
|---|---|
| 400 | anthropic.BadRequestError |
| 401 | anthropic.AuthenticationError |
| 403 | anthropic.PermissionDeniedError |
| 404 | anthropic.NotFoundError |
| 413 | anthropic.RequestTooLargeError |
| 429 | anthropic.RateLimitError |
| 500+ | anthropic.InternalServerError |
| 529 | anthropic.OverloadedError |
| 모두 | anthropic.APIError (최상위) |
더 세밀한 분류가 필요하면 APIStatusError의 .type 속성을 쓰세요. 예를 들어 둘 다 403으로 떨어지는 billing_error와 permission_error를 구분할 수 있습니다.
SDK 자동 재시도: 가장 먼저 켜야 할 안전장치
직접 재시도 루프를 짜기 전에, Anthropic SDK는 이미 429와 5xx를 지수 백오프로 자동 재시도한다는 사실을 기억하세요. 기본값은 max_retries=2이며, SDK는 응답의 retry-after 헤더를 읽어 대기 시간을 결정합니다. 대부분의 워크로드는 이 값만 올려도 충분합니다.
import anthropic
# 클라이언트 전역으로 재시도 횟수 설정
client = anthropic.Anthropic(max_retries=5)
# 또는 호출 단위로 오버라이드 (with_options)
msg = client.with_options(max_retries=5, timeout=60.0).messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "안녕하세요"}],
)
중요한 구분: SDK 자동 재시도는 429/5xx/연결 오류에만 적용되며, 400 BadRequestError 같은 비재시도 에러는 재시도하지 않고 즉시 던집니다. 즉 SDK의 기본 동작이 곧 "재시도 가능 여부" 정책을 올바르게 따릅니다. 직접 백오프 루프가 필요한 경우는 (1) 모델 폴백 같은 추가 분기를 넣고 싶을 때, (2) 재시도 횟수·상한을 세밀하게 제어하고 싶을 때, (3) 관측성 훅을 끼워넣고 싶을 때입니다.
지수 백오프 + 지터를 직접 구현하기
SDK 위에 추가 로직(폴백, 커스텀 로깅)을 얹으려면 직접 백오프를 구현합니다. 핵심은 두 가지입니다. 지수 백오프로 재시도 간격을 점점 늘려 부하를 분산하고, 지터(jitter)로 무작위성을 더해 여러 클라이언트가 동시에 깨어나 다시 한꺼번에 몰리는 "thundering herd"를 막습니다. 그리고 429가 retry-after 헤더를 줄 때는 계산한 백오프보다 서버가 알려준 시간을 우선합니다.
import time
import random
import anthropic
client = anthropic.Anthropic(max_retries=0) # 우리가 직접 재시도하므로 SDK 재시도는 끔
# 재시도 대상 예외 (429 / 5xx / 연결오류)
RETRYABLE = (
anthropic.RateLimitError,
anthropic.InternalServerError, # 500+
anthropic.OverloadedError, # 529
anthropic.APIConnectionError, # 네트워크 단절/타임아웃
)
def retry_after_seconds(exc) -> float | None:
"""429/503 응답의 retry-after 헤더(초)를 파싱. 없으면 None."""
resp = getattr(exc, "response", None)
if resp is None:
return None
val = resp.headers.get("retry-after")
if val is None:
return None
try:
return float(val) # 보통 초 단위 정수
except ValueError:
return None
def create_with_backoff(max_attempts: int = 6, base: float = 1.0, cap: float = 60.0, **kwargs):
for attempt in range(max_attempts):
try:
return client.messages.create(**kwargs)
except RETRYABLE as exc:
if attempt == max_attempts - 1:
raise # 마지막 시도까지 실패하면 그대로 던진다
# 1) 서버가 retry-after를 줬으면 그 값을 최우선으로
server_wait = retry_after_seconds(exc)
if server_wait is not None:
delay = server_wait
else:
# 2) 지수 백오프 + "full jitter": 0 ~ min(cap, base*2^attempt)
ceiling = min(cap, base * (2 ** attempt))
delay = random.uniform(0, ceiling)
time.sleep(delay)
# 비재시도 예외(400/401/403/404 등)는 잡지 않으므로 즉시 전파된다
# 사용
msg = create_with_backoff(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "요약해줘: ..."}],
)
print("".join(b.text for b in msg.content if b.type == "text"))
여기서 쓴 "full jitter"(0부터 상한까지 균등 난수)는 AWS 아키텍처 블로그가 권장하는 대표적인 방식으로, 단순 지수 백오프보다 동시성 충돌을 잘 분산합니다. cap으로 상한을 둬서 백오프가 무한정 커지지 않게 하고, max_attempts로 전체 시도 횟수를 제한해 무한 루프를 방지합니다.
모델 폴백: Opus가 막히면 Sonnet으로
529 overloaded는 특정 모델의 수요가 몰릴 때 자주 발생합니다. 이때 동일 모델로 계속 재시도하기보다, 다른 모델로 폴백하면 가용성을 크게 높일 수 있습니다. 흔한 전략은 기본 모델 Opus 4.8이 과부하·레이트리밋이면 더 가볍고 부하가 덜한 Sonnet 4.6(또는 Haiku 4.5)으로 자동 강등하는 것입니다.
import time
import random
import anthropic
client = anthropic.Anthropic(max_retries=0)
# 우선순위 체인: 앞에서부터 시도, 막히면 다음으로
MODEL_CHAIN = ["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"]
RETRYABLE = (
anthropic.RateLimitError,
anthropic.InternalServerError,
anthropic.OverloadedError,
anthropic.APIConnectionError,
)
def create_with_fallback(messages, max_tokens=1024, attempts_per_model=3):
last_exc = None
for model in MODEL_CHAIN:
for attempt in range(attempts_per_model):
try:
msg = client.messages.create(
model=model,
max_tokens=max_tokens,
messages=messages,
)
return model, msg # 어떤 모델이 응답했는지 함께 반환(관측성)
except RETRYABLE as exc:
last_exc = exc
# 마지막 시도가 아니면 백오프 후 같은 모델 재시도
if attempt < attempts_per_model - 1:
delay = random.uniform(0, min(20.0, 2 ** attempt))
time.sleep(delay)
# 마지막 시도면 루프를 빠져나가 다음 모델로 폴백
except (anthropic.BadRequestError,
anthropic.AuthenticationError,
anthropic.PermissionDeniedError):
# 요청/인증 자체가 틀린 것은 모델을 바꿔도 동일 — 즉시 실패
raise
raise last_exc # 모든 모델이 실패
model_used, msg = create_with_fallback(
messages=[{"role": "user", "content": "이 문서를 3줄로 요약해줘: ..."}],
)
print(f"[{model_used}] 응답:")
print("".join(b.text for b in msg.content if b.type == "text"))
폴백 설계 시 주의할 점이 있습니다. 품질이 중요한 작업이라면 폴백은 가용성 보험이지 품질 보장이 아닙니다. Opus 4.8은 입력/출력 $5/$25, Sonnet 4.6은 $3/$15, Haiku 4.5는 $1/$5로 더 저렴하지만, 응답 품질이 작업에 충분한지 확인해야 합니다. 또한 모델마다 컨텍스트 윈도가 다릅니다 — Opus/Sonnet은 1M, Haiku는 200K이므로, 긴 입력을 Haiku로 폴백할 때 413이나 컨텍스트 초과가 날 수 있습니다. 폴백 체인은 "입력 길이가 짧고 품질 허용치가 넓은" 작업에 가장 잘 맞습니다.
참고로 Anthropic은 최상위 모델(예: Claude Fable 5)에 한해 서버사이드 폴백 파라미터를 베타로 제공합니다 — 한 번의 호출 안에서 거절(refusal) 시 폴백 모델이 투명하게 재서빙합니다. 다만 이는 안전 분류기 거절 등 특정 상황에 한정되며, 위에서 본 429/529 과부하 폴백은 여전히 애플리케이션 레벨에서 직접 구현하는 것이 표준입니다.
스트리밍으로 타임아웃 회피하기
출력 토큰이 많은 요청(긴 보고서 생성, 코드 생성 등)은 비스트리밍으로 호출하면 SDK의 HTTP 타임아웃에 걸려 실패하기 쉽습니다. 규칙은 명확합니다. max_tokens가 약 16,000을 넘으면 스트리밍이 사실상 필수입니다. 스트리밍은 첫 토큰부터 연결을 살아 있게 유지하므로 장시간 요청이 끊기지 않습니다.
import anthropic
client = anthropic.Anthropic()
# 긴 출력 — 스트리밍으로 타임아웃 회피
with client.messages.stream(
model="claude-opus-4-8",
max_tokens=64000,
messages=[{"role": "user", "content": "이 사양서를 바탕으로 상세 설계 문서를 작성해줘: ..."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True) # 사용자에게 실시간 표시
final = stream.get_final_message() # 완성된 전체 메시지
print()
print("stop_reason:", final.stop_reason)
print("출력 토큰:", final.usage.output_tokens)
스트리밍 응답에서도 에러는 발생할 수 있으므로(스트림 도중 연결이 끊기면 APIConnectionError 등) with 블록을 앞서 본 백오프/폴백 로직으로 감싸는 것이 안전합니다. 다만 스트리밍 재시도는 처음부터 다시 생성하는 것이므로, 멱등성(아래)을 함께 고려해야 부분 출력이 중복 저장되지 않습니다.
멱등성: 재시도가 중복 부작용을 만들지 않게
재시도의 숨은 함정은 부작용 중복입니다. 예를 들어 "요약을 생성해서 DB에 저장"하는 작업에서, 응답은 성공했는데 네트워크 단절로 클라이언트가 그 응답을 못 받으면, 재시도 시 같은 요약이 두 번 저장될 수 있습니다. 해법은 작업마다 멱등성 키(idempotency key)를 부여하고, 그 키로 중복을 막는 것입니다.
Anthropic SDK는 요청별 헤더를 붙일 수 있어, 운영 측 멱등성 키를 함께 전달하고 자체 저장소에서 중복을 거를 수 있습니다.
import uuid
import anthropic
client = anthropic.Anthropic()
def summarize_and_store(doc_id: str, text: str, store) -> str:
# 1) 작업 단위로 안정적인 멱등성 키 생성 (같은 doc_id면 같은 키)
idem_key = f"summarize:{doc_id}"
# 2) 이미 처리된 작업이면 저장된 결과를 그대로 반환 (재시도 안전)
cached = store.get(idem_key)
if cached is not None:
return cached
# 3) 요청에 추적용 헤더를 부착 (관측성 + 로그 상관관계)
msg = client.with_options(
max_retries=5,
).messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": f"3줄 요약:\n{text}"}],
extra_headers={"X-Idempotency-Key": idem_key,
"X-Request-Trace": str(uuid.uuid4())},
)
summary = "".join(b.text for b in msg.content if b.type == "text")
# 4) 결과를 멱등성 키로 저장 — 동일 작업 재시도 시 LLM 재호출 없이 반환
store.set(idem_key, summary)
return summary
핵심은 LLM 호출 자체보다 "LLM 호출 + 그 결과로 일으키는 부작용"을 하나의 멱등한 단위로 묶는 것입니다. doc_id처럼 작업을 고유하게 식별하는 값으로 키를 만들면, 몇 번을 재시도해도 결과는 한 번만 반영됩니다. response.id(메시지 ID)는 로그 상관관계 추적에 유용하니 함께 기록해 두면 좋습니다.
관측성: 재시도·폴백·레이트리밋을 계측하라
프로덕션에서 가장 위험한 것은 "조용히 잘 돌아가는 것처럼 보이는" 상태입니다. 폴백이 너무 자주 발동해 실제로는 Opus가 거의 안 쓰이고 있거나, 재시도가 누적돼 지연이 폭증하는데 평균값만 보면 안 보일 수 있습니다. 최소한 다음을 계측하세요.
- 에러 코드별 발생률 — 429/529/500을 분리 집계해 어디가 병목인지 파악
- 재시도 횟수 분포 — 평균이 아니라 p95/p99로. 꼬리 지연이 곧 사용자 체감 지연
- 폴백 발동률과 어떤 모델이 실제로 응답했는지 — 위 예제처럼
model_used를 항상 로깅 - 토큰 사용량과 비용 —
msg.usage.input_tokens/output_tokens, 캐시 사용 시cache_read_input_tokens - 레이트리밋 헤더 — 응답의
x-ratelimit-remaining-*로 잔여 쿼터를 모니터링해 한계 도달 전에 대응
import logging
import anthropic
logger = logging.getLogger("llm")
client = anthropic.Anthropic(max_retries=3)
def observed_create(**kwargs):
# raw response를 함께 받아 헤더(레이트리밋 등)에 접근
resp = client.messages.with_raw_response.create(**kwargs)
msg = resp.parse() # 파싱된 Message 객체
logger.info(
"llm_call",
extra={
"model": msg.model,
"request_id": msg.id,
"stop_reason": msg.stop_reason,
"input_tokens": msg.usage.input_tokens,
"output_tokens": msg.usage.output_tokens,
"rl_requests_remaining": resp.headers.get("anthropic-ratelimit-requests-remaining"),
"rl_tokens_remaining": resp.headers.get("anthropic-ratelimit-tokens-remaining"),
},
)
return msg
msg = observed_create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "안녕"}],
)
Laravel(PHP)에서의 프로덕션 패턴
Laravel 백엔드라면 공식 PHP SDK(composer require anthropic-ai/sdk)로 동일한 패턴을 구현합니다. PHP SDK는 APIStatusException의 ->type 속성으로 에러를 분류할 수 있고, 폴백 체인도 자연스럽게 짤 수 있습니다.
<?php
use Anthropic\Client;
use Anthropic\Core\Exceptions\APIStatusException;
$client = new Client(apiKey: getenv('ANTHROPIC_API_KEY'));
// 폴백 체인: 과부하/레이트리밋이면 다음 모델로
$modelChain = ['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
// 재시도 가능한 에러 타입
$retryableTypes = ['rate_limit_error', 'overloaded_error', 'api_error'];
function createWithFallback(Client $client, array $messages, array $modelChain, array $retryableTypes): array
{
$lastError = null;
foreach ($modelChain as $model) {
for ($attempt = 0; $attempt < 3; $attempt++) {
try {
$message = $client->messages->create(
model: $model,
maxTokens: 1024,
messages: $messages,
);
$text = '';
foreach ($message->content as $block) {
if ($block->type === 'text') {
$text .= $block->text;
}
}
return ['model' => $model, 'text' => $text];
} catch (APIStatusException $e) {
$lastError = $e;
$type = $e->type?->value;
// 재시도 불가능한 에러(400/401/403 등)는 즉시 전파
if (!in_array($type, $retryableTypes, true)) {
throw $e;
}
// 지수 백오프 + 지터 (마지막 시도가 아니면)
if ($attempt < 2) {
$ceiling = min(20, 2 ** $attempt);
usleep((int) (mt_rand(0, (int) ($ceiling * 1000)) * 1000)); // 0~ceiling초
}
// 마지막 시도면 다음 모델로 폴백
}
}
}
throw $lastError; // 모든 모델 실패
}
$result = createWithFallback(
$client,
[['role' => 'user', 'content' => '이 문의를 분류해줘: ...']],
$modelChain,
$retryableTypes,
);
echo "[{$result['model']}] {$result['text']}";
Laravel에서는 이런 호출을 큐 잡(Queued Job)으로 감싸는 것을 강력히 권장합니다. 잡의 $tries·backoff()·retryUntil()로 재시도 정책을 프레임워크 레벨에서 관리하고, 실패한 잡은 failed_jobs 테이블에 남아 추적 가능하며, 웹 요청 사이클을 블로킹하지 않습니다. 긴 출력은 PHP에서도 $client->messages->createStream(...)로 스트리밍해 타임아웃을 회피하세요.
다른 공급사(OpenAI)에서의 패턴
OpenAI를 함께 쓰는 경우에도 구조는 동일합니다 — 타입드 예외로 분기하고, SDK 자동 재시도(max_retries)를 켜고, 폴백 체인을 둡니다. 안정적인 표준 패턴만 정리하면 다음과 같습니다(현재 제공 모델 ID는 공급사 공식 문서 확인 권장).
from openai import OpenAI, RateLimitError, APIStatusError, APIConnectionError
client = OpenAI(max_retries=5) # SDK가 429/5xx를 지수 백오프로 자동 재시도
try:
resp = client.chat.completions.create(
model="gpt-4o", # 현행 모델 목록은 공식 문서 확인
messages=[{"role": "user", "content": "안녕"}],
)
print(resp.choices[0].message.content)
except RateLimitError:
... # 429 — 백오프/폴백
except APIConnectionError:
... # 네트워크 — 재시도
except APIStatusError as e:
... # e.status_code 로 분기
공급사 간 폴백(예: Claude → OpenAI)을 구성할 때는 메시지 포맷·도구 스키마·구조화 출력 방식이 서로 다르므로, 폴백 경계에서 요청을 각 SDK 포맷으로 변환해야 한다는 점을 유의하세요.
전체 그림: 프로덕션 호출의 결정 흐름
지금까지의 전략을 하나의 결정 흐름으로 요약하면 다음과 같습니다.
- 1. SDK 자동 재시도부터 켠다 —
max_retries를 충분히(예: 3~5) 설정. 대부분의 일시적 오류는 여기서 흡수됩니다. - 2. 타입드 예외로 분기한다 — 문자열 매칭 금지. 4xx(429 제외)는 즉시 실패, 429·5xx·연결오류는 재시도.
- 3. 추가 제어가 필요하면 직접 백오프 — 지수 백오프 + full jitter,
retry-after헤더 우선,cap·max_attempts로 상한. - 4. 가용성이 중요하면 모델 폴백 — Opus → Sonnet → Haiku. 단, 품질·컨텍스트 윈도 차이를 고려.
- 5. 긴 출력은 스트리밍 —
max_tokens> 16,000이면 필수. - 6. 부작용은 멱등하게 — 작업 키로 중복 방지.
- 7. 모든 것을 계측한다 — 에러율, 재시도 분포(p95/p99), 폴백 발동률, 토큰·비용, 레이트리밋 잔여.
자주 묻는 질문 (FAQ)
Q. SDK가 이미 자동 재시도를 하는데 백오프를 직접 구현할 이유가 있나요?
대부분의 단순 워크로드는 max_retries만 올려도 충분합니다. 직접 구현이 필요한 경우는 (1) 429/529에서 다른 모델로 폴백하는 분기를 넣고 싶을 때, (2) 재시도 횟수·상한·지터 방식을 세밀하게 제어하고 싶을 때, (3) 재시도마다 로깅·메트릭 같은 관측성 훅을 끼워넣고 싶을 때입니다. 이 경우 max_retries=0으로 SDK 재시도를 끄고 직접 루프를 돌립니다.
Q. 429와 529는 어떻게 다르게 처리해야 하나요?
둘 다 재시도 대상이지만 원인이 다릅니다. 429 rate_limit_error는 내 쿼터(RPM/TPM)를 초과한 것이므로 retry-after 헤더를 우선해 대기하고, 지속되면 요청 속도를 낮추거나 큐잉해야 합니다. 529 overloaded_error는 공급사 전체 부하이므로 백오프 재시도와 함께 다른 모델로 폴백하는 것이 특히 효과적입니다(부하가 덜한 Haiku 등).
Q. 에러 메시지 문자열로 분기하면 왜 안 되나요?
메시지 포맷은 SDK·API 버전에 따라 언제든 바뀔 수 있어 msg.includes("429") 같은 검사는 silently 깨집니다. Anthropic SDK는 HTTP 코드별 타입드 예외(RateLimitError, OverloadedError 등)를 제공하므로 반드시 except/instanceof로 분기하세요. 더 세분화가 필요하면 APIStatusError.type(예: billing_error vs permission_error)을 사용합니다.
Q. 폴백으로 Opus에서 Haiku로 내릴 때 주의할 점은?
두 가지입니다. 첫째, 품질 — 폴백은 가용성 보험이지 품질 보장이 아니므로, 강등된 모델의 응답이 해당 작업에 충분한지 검증해야 합니다. 둘째, 컨텍스트 윈도 — Opus/Sonnet은 1M이지만 Haiku는 200K이므로, 긴 입력을 Haiku로 폴백하면 컨텍스트 초과나 413이 날 수 있습니다. 폴백 체인은 입력이 짧고 품질 허용치가 넓은 작업에 가장 안전합니다.
Q. 재시도 때문에 같은 작업이 중복 실행되는 것을 어떻게 막나요?
작업 단위로 멱등성 키를 만들고(예: summarize:{doc_id}), 호출 전 그 키로 이미 처리된 결과가 있는지 확인해 있으면 LLM 재호출 없이 반환합니다. 핵심은 "LLM 호출 + 그로 인한 부작용(DB 저장 등)"을 하나의 멱등한 단위로 묶는 것입니다. 그러면 몇 번을 재시도해도 결과는 한 번만 반영됩니다.
AI 개발이 필요하신가요?
DevTeam은 OpenAI·Claude 등 AI API 연동, 챗봇, RAG, 업무 자동화를 설계부터 배포까지 구현합니다. AI 연동 개발 또는 무료 견적 문의.