재시도(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}"}],
    )

출처: https://python.useinstructor.com/concepts/retrying/

오류 유형별 재시도

모든 오류에 재시도할 필요는 없어요. 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번·160-120초, 검증 오류는 2-3번·110초, 네트워크 오류는 4번·2~30초 수준이 일반적이에요.

더 알아보기