PROD · ERROR HANDLING
에러 핸들링
LLM API 오류, 도구 실패, 타임아웃은 피할 수 없습니다. 지수 백오프 재시도, Circuit Breaker, Fallback 체인으로 탄력적인 Agent 시스템을 구축합니다.
에러 분류 체계
| 에러 유형 | 예시 | 재시도 가능 | 처리 방법 |
|---|---|---|---|
| 일시적 오류 | 429 Rate Limit, 503 Service Unavailable | ✅ 재시도 | 지수 백오프 |
| 영구적 오류 | 400 Bad Request, 401 Unauthorized | ❌ 재시도 무의미 | 즉시 실패 처리 |
| 타임아웃 | 네트워크 타임아웃, LLM 응답 지연 | ✅ 재시도 | 더 짧은 타임아웃 + 재시도 |
| LLM 품질 오류 | 잘못된 JSON, 빈 응답, 환각 | ✅ 재시도 | 프롬프트 수정 후 재시도 |
| 도구 실행 오류 | 외부 API 오류, DB 연결 실패 | ✅ 재시도 | Fallback 도구 사용 |
지수 백오프 재시도
pythonretry.py — 타입별 재시도 전략
import asyncio, random
from anthropic import RateLimitError, APIStatusError, APIConnectionError
from functools import wraps
from typing import TypeVar, Callable
T = TypeVar("T")
RETRYABLE_ERRORS = (RateLimitError, APIConnectionError)
PERMANENT_ERRORS = (APIStatusError,)
async def with_retry(
fn: Callable,
*args,
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 60.0,
**kwargs
):
"""지수 백오프 + 지터를 적용한 재시도"""
last_error = None
for attempt in range(max_retries + 1):
try:
return await fn(*args, **kwargs)
except RateLimitError as e:
# Rate limit: Retry-After 헤더 우선 사용
retry_after = float(e.response.headers.get("retry-after", base_delay))
delay = min(retry_after, max_delay)
last_error = e
except APIConnectionError as e:
# 연결 오류: 지수 백오프
delay = min(base_delay * (2 ** attempt), max_delay)
delay += random.uniform(0, delay * 0.1) # 지터
last_error = e
except APIStatusError as e:
if e.status_code in (400, 401, 403):
raise # 영구 오류 → 즉시 실패
delay = min(base_delay * (2 ** attempt), max_delay)
last_error = e
if attempt < max_retries:
print(f"[Retry] {attempt+1}/{max_retries} — {delay:.1f}초 후 재시도: {last_error}")
await asyncio.sleep(delay)
raise last_error
# LangChain LLM에 적용
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(
model="claude-opus-4-7",
max_retries=3, # LangChain 내장 재시도
timeout=60
)
Circuit Breaker 패턴
pythoncircuit_breaker.py — 연쇄 장애 방지
import time
from enum import Enum
class State(Enum):
CLOSED = "closed" # 정상 — 요청 통과
OPEN = "open" # 차단 — 즉시 실패
HALF_OPEN = "half_open" # 탐색 — 일부 요청만
class CircuitBreaker:
def __init__(
self,
failure_threshold: int = 5,
reset_timeout: float = 60.0,
half_open_max_calls: int = 3
):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.half_open_max_calls = half_open_max_calls
self.state = State.CLOSED
self.failure_count = 0
self.last_failure_time = 0
self.half_open_calls = 0
async def call(self, fn, *args, **kwargs):
if self.state == State.OPEN:
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = State.HALF_OPEN
self.half_open_calls = 0
else:
raise RuntimeError("Circuit OPEN — 서비스 일시 중단")
if self.state == State.HALF_OPEN:
self.half_open_calls += 1
if self.half_open_calls > self.half_open_max_calls:
raise RuntimeError("Circuit HALF_OPEN — 제한 초과")
try:
result = await fn(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
self.failure_count = 0
self.state = State.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = State.OPEN
print(f"[CB] Circuit OPEN — {self.failure_count}회 실패")
# 서비스별 Circuit Breaker 등록
breakers = {
"anthropic_api": CircuitBreaker(failure_threshold=5, reset_timeout=60),
"search_api": CircuitBreaker(failure_threshold=3, reset_timeout=30),
"db": CircuitBreaker(failure_threshold=10, reset_timeout=120),
}
Fallback 체인
pythonfallback.py — 단계별 Fallback 체인
from langchain_anthropic import ChatAnthropic
# LangChain .with_fallbacks() 내장 지원
primary_llm = ChatAnthropic(model="claude-opus-4-7")
fallback_llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
llm_with_fallback = primary_llm.with_fallbacks(
[fallback_llm],
exceptions_to_handle=(Exception,)
)
# 다단계 Fallback (검색 → 캐시 → 기본 응답)
async def search_with_fallback(query: str) -> str:
strategies = [
("tavily", lambda q: tavily_search(q)),
("duckduckgo", lambda q: ddg_search(q)),
("cache", lambda q: cache.get(q)),
("default", lambda q: "검색 서비스를 일시적으로 사용할 수 없습니다."),
]
for name, strategy in strategies:
try:
result = await strategy(query)
if result:
print(f"[Fallback] 성공: {name}")
return result
except Exception as e:
print(f"[Fallback] {name} 실패: {e}")
continue
return "검색 결과를 가져올 수 없습니다."