재시도(Retry) 로직
재시도(Retry) 로직
LLM 호출은 안정적이지만 때로는 API 오류, 속도 제한(rate limit), 검증 실패가 발생해요. Instructor는 Tenacity라는 파이썬 라이브러리와 결합해 이런 실패를 재시도로 우아하게 처리해요. 재시도를 잘 설계하면 일시적인 오류와 잘못된 출력 모두를 자동으로 넘길 수 있어요.
기본 재시도와 지수 백오프
가장 흔한 패턴은 지수 백오프로 재시도 사이를 지연시키는 거예요. @retry 데코레이터에 중지 조건과 대기 전략을 지정하면 돼요.
import instructor
from pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential
client = instructor.from_provider("openai/gpt-4.1-mini")
class UserInfo(BaseModel):
name: str
age: int
email: str
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def extract_user_info(text: str) -> UserInfo:
return client.create(
response_model=UserInfo,
messages=[{"role": "user", "content": f"Extract user info: {text}"}],
)
오류 유형별 재시도
모든 오류에 재시도할 필요는 없어요. retry_if_exception_type으로 특정 오류에만 재시도하게 만들 수 있어요. 속도 제한·API 오류는 지연을 길게, 검증 오류는 짧게 재시도하는 식으로 나누면 좋아요.
from openai import APIError, RateLimitError
from pydantic import ValidationError
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
@retry(
retry=retry_if_exception_type((RateLimitError, APIError)),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=1, max=60),
)
def handle_api_errors(text: str) -> UserInfo:
return client.create(response_model=UserInfo, messages=[{"role": "user", "content": text}])
@retry(
retry=retry_if_exception_type(ValidationError),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
)
def handle_validation_errors(text: str) -> UserInfo:
return client.create(response_model=UserInfo, messages=[{"role": "user", "content": text}])
커스텀 재시도 조건
예외가 아니라 결과의 내용을 보고 재시도하고 싶을 때는 retry_if_result를 써요. 추출한 값이 기준을 못 넘는 경우에 다시 시도하는 식이에요.
from tenacity import retry, retry_if_result, stop_after_attempt
def should_retry(result: UserInfo) -> bool:
return result.age < 0 or result.age > 150 or not result.email
@retry(retry=retry_if_result(should_retry), stop=stop_after_attempt(3))
def extract_valid_user(text: str) -> UserInfo:
return client.create(response_model=UserInfo, messages=[{"role": "user", "content": text}])
로깅과 토큰 예산
- 로깅:
before_log/after_log를 넣으면 재시도 시점을 로그로 남길 수 있어요. - 토큰 예산:
token_budget을 지정하면 검증 재시도 비용을 제한할 수 있어요. 누적 사용량이 예산에 도달하면TokenBudgetExceeded예외가 나와요. 이건 하드한 요청당 제한이 아니라 "재시도 예산"이란 점에 주의해요.
from instructor.core import TokenBudgetExceeded
try:
user = client.create(
response_model=UserInfo,
messages=[{"role": "user", "content": "Extract: Jason is 25"}],
max_retries=3,
token_budget=2_000,
)
except TokenBudgetExceeded as error:
print(error.total_usage)
Instructor 내장 재시도
Tenacity와 별개로 Instructor에도 내장 재시도가 있어요. from_provider(..., max_retries=3, retry_delay=1)처럼 설정할 수 있고, 두 방식을 함께 써서 더 튼튼하게 만들 수도 있어요.
실패 이력 추적
여러 번 재시도해도 실패하면 InstructorRetryException이 발생하고, e.failed_attempts로 각 시도의 번호와 예외를 확인할 수 있어요. 이런 실패 이력은 reask 핸들러로 전파되어 문맥에 맞는 오류 메시지와 점진적 수정을 가능하게 해요.
베스트 프랙티스
- 항상 중지 조건을 설정하세요.
stop_after_attempt(3)처럼 바운드해야 해요. 조건 없는@retry()는 무한 재시도로 이어질 수 있어요. - 오류 유형별로 재시도 횟수와 지연을 다르게 잡는 걸 권장해요. 속도 제한은 5번·1
60-120초, 검증 오류는 2-3번·110초, 네트워크 오류는 4번·2~30초 수준이 일반적이에요.
더 알아보기
- Tenacity 문서: tenacity.readthedocs.io
- 검증: Validation
- 오류 처리: Error Handling