재시도 전략

재시도 전략 (Retry Strategies)

자동 재시도 로직으로 작업과 평가자의 일시적 실패를 처리해요.

LLM 기반 시스템은 일시적 실패를 겪을 수 있어요:

  • 속도 제한
  • 네트워크 타임아웃
  • 일시적 API 중단
  • 컨텍스트 길이 오류

Pydantic Evals는 둘 다에 대해 재시도 구성을 지원해요:

  • 작업 실행 - 평가되는 함수
  • 평가자 실행 - 평가자 자체

출처: 문서

본문

기본 재시도 구성

Tenacity 매개변수를 사용해 evaluate() 또는 evaluate_sync()에 재시도 구성을 전달해요:

from tenacity import stop_after_attempt

from pydantic_evals import Case, Dataset


def my_function(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='basic_retry', cases=[Case(inputs='test')], evaluators=[])

report = dataset.evaluate_sync(
    task=my_function,
    retry_task={'stop': stop_after_attempt(3)},
    retry_evaluators={'stop': stop_after_attempt(2)},
)

재시도 구성 옵션

재시도 구성은 Tenacity를 사용하며 Pydantic AI의 RetryConfig와 같은 옵션을 지원해요:

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset


def my_function(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='retry_config', cases=[Case(inputs='test')], evaluators=[])

retry_config = {
    'stop': stop_after_attempt(3),  # 3번 시도 후 중단
    'wait': wait_exponential(multiplier=1, min=1, max=10),  # 지수 백오프: 1s, 2s, 4s, 8s (10s에서 상한)
    'reraise': True,  # 재시도 소진 후 원래 예외를 다시 던짐
}

dataset.evaluate_sync(
    task=my_function,
    retry_task=retry_config,
)

흔한 매개변수

재시도 구성은 tenacity retry 데코레이터의 어떤 매개변수든 받아요. 흔한 것들:

매개변수 유형 설명
stop StopBaseT 중단 전략 (예: stop_after_attempt(3), stop_after_delay(60))
wait WaitBaseT 대기 전략 (예: wait_exponential(), wait_fixed(2))
retry RetryBaseT 재시도 조건 (예: retry_if_exception_type(TimeoutError))
reraise bool 원래 예외를 다시 던질지 (기본: False)
before_sleep Callable 재시도 사이 잠들기 전 콜백

사용 가능한 모든 옵션은 Tenacity 문서를 참고해요.

작업 재시도

작업 함수가 실패하면 재시도해요:

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset


async def call_llm(inputs: str) -> str:
    return f'LLM response to: {inputs}'


async def flaky_llm_task(inputs: str) -> str:
    """This might hit rate limits or timeout."""
    response = await call_llm(inputs)
    return response


dataset = Dataset(name='task_retry', cases=[Case(inputs='test')])

report = dataset.evaluate_sync(
    task=flaky_llm_task,
    retry_task={
        'stop': stop_after_attempt(5),  # 최대 5번 시도
        'wait': wait_exponential(multiplier=1, min=1, max=30),  # 지수 백오프, 30s에서 상한
        'reraise': True,
    },
)

작업 재시도가 언제 발동하나요?

작업이 예외를 던질 때 재시도가 발동돼요:

class RateLimitError(Exception):
    pass


class ValidationError(Exception):
    pass


async def call_api(inputs: str) -> str:
    return f'API response: {inputs}'


async def my_task(inputs: str) -> str:
    try:
        return await call_api(inputs)
    except RateLimitError:
        # 재시도를 발동시킴
        raise
    except ValidationError:
        # 이것도 재시도를 발동시킴
        raise

지수 백오프

wait_exponential()을 사용하면 지연이 기하급수적으로 증가해요:

Attempt 1: immediate
Attempt 2: ~1s delay (multiplier * 2^0)
Attempt 3: ~2s delay (multiplier * 2^1)
Attempt 4: ~4s delay (multiplier * 2^2)
Attempt 5: ~8s delay (multiplier * 2^3, capped at max)

실제 지연은 wait_exponential()에 전달한 multiplier, min, max 매개변수에 따라 달라져요.

평가자 재시도

평가자가 실패하면 재시도해요:

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge


def my_task(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(
    name='evaluator_retry',
    cases=[Case(inputs='test')],
    evaluators=[
        # LLMJudge는 속도 제한에 걸릴 수 있음
        LLMJudge(rubric='Response is accurate'),
    ],
)

report = dataset.evaluate_sync(
    task=my_task,
    retry_evaluators={
        'stop': stop_after_attempt(3),
        'wait': wait_exponential(multiplier=1, min=0.5, max=10),
        'reraise': True,
    },
)

평가자 재시도가 언제 발동하나요?

평가자가 예외를 던질 때 재시도가 발동돼요:

from dataclasses import dataclass

from pydantic_evals.evaluators import Evaluator, EvaluatorContext


async def external_api_call(output: str) -> bool:
    return len(output) > 0


@dataclass
class APIEvaluator(Evaluator):
    async def evaluate(self, ctx: EvaluatorContext) -> bool:
        # 여기서 예외를 던지면 재시도 로직이 발동함
        result = await external_api_call(ctx.output)
        return result

평가자 실패

모든 재시도 후에도 평가자가 실패하면 EvaluatorFailure로 기록돼요:

from tenacity import stop_after_attempt

from pydantic_evals import Case, Dataset


def task(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='evaluator_failures', cases=[Case(inputs='test')], evaluators=[])

report = dataset.evaluate_sync(task, retry_evaluators={'stop': stop_after_attempt(3)})

# 평가자 실패 확인
for case in report.cases:
    if case.evaluator_failures:
        for failure in case.evaluator_failures:
            print(f'Evaluator {failure.name} failed: {failure.error_message}')
    #> (출력 없음 - 이 케이스에는 평가자 실패 없음)

보고서에서 평가자 실패를 봐요:

from pydantic_evals import Case, Dataset


def task(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='failure_report', cases=[Case(inputs='test')], evaluators=[])
report = dataset.evaluate_sync(task)

report.print(include_evaluator_failures=True)
"""
  Evaluation Summary:
         task
┏━━━━━━━━━━┳━━━━━━━━━━┓
┃ Case ID  ┃ Duration ┃
┡━━━━━━━━━━╇━━━━━━━━━━┩
│ Case 1   │     10ms │
├──────────┼──────────┤
│ Averages │     10ms │
└──────────┴──────────┘
"""
#>
#> ✅ case_0                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% (0/0)

작업과 평가자 재시도 결합하기

둘을 독립적으로 구성할 수 있어요:

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset


def flaky_task(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='combined_retry', cases=[Case(inputs='test')], evaluators=[])

report = dataset.evaluate_sync(
    task=flaky_task,
    retry_task={
        'stop': stop_after_attempt(5),  # 작업을 최대 5번 재시도
        'wait': wait_exponential(multiplier=1, min=1, max=30),
        'reraise': True,
    },
    retry_evaluators={
        'stop': stop_after_attempt(3),  # 평가자를 최대 3번 재시도
        'wait': wait_exponential(multiplier=1, min=0.5, max=10),
        'reraise': True,
    },
)

실용 예시

속도 제한 처리

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge


async def expensive_llm_call(inputs: str) -> str:
    return f'LLM response: {inputs}'


async def llm_task(inputs: str) -> str:
    """Task that might hit rate limits."""
    return await expensive_llm_call(inputs)


dataset = Dataset(
    name='rate_limit_retry',
    cases=[Case(inputs='test')],
    evaluators=[
        LLMJudge(rubric='Quality check'),  # 이것도 속도 제한에 걸릴 수 있음
    ],
)

# 속도 제한에 대한 넉넉한 재시도
report = dataset.evaluate_sync(
    task=llm_task,
    retry_task={
        'stop': stop_after_attempt(10),  # 속도 제한은 여러 번 재시도가 필요할 수 있음
        'wait': wait_exponential(multiplier=2, min=2, max=60),  # 2s에서 시작, 60s까지 지수 증가
        'reraise': True,
    },
    retry_evaluators={
        'stop': stop_after_attempt(5),
        'wait': wait_exponential(multiplier=2, min=2, max=30),
        'reraise': True,
    },
)

네트워크 타임아웃 처리

import httpx
from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset


async def api_task(inputs: str) -> str:
    """Task that calls external API which might timeout."""
    async with httpx.AsyncClient(timeout=10.0) as client:
        response = await client.post('https://api.example.com', json={'input': inputs})
        return response.text


dataset = Dataset(name='timeout_retry', cases=[Case(inputs='test')], evaluators=[])

# 네트워크 문제에 대한 빠른 재시도
report = dataset.evaluate_sync(
    task=api_task,
    retry_task={
        'stop': stop_after_attempt(4),  # 빠른 재시도 몇 번
        'wait': wait_exponential(multiplier=0.5, min=0.5, max=5),  # 빠른 재시도, 5s에서 상한
        'reraise': True,
    },
)

컨텍스트 길이 처리

from tenacity import stop_after_attempt

from pydantic_evals import Case, Dataset


class ContextLengthError(Exception):
    pass


async def llm_call(inputs: str, max_tokens: int = 8000) -> str:
    return f'LLM response: {inputs[:100]}'


async def smart_llm_task(inputs: str) -> str:
    """Task that might exceed context length."""
    try:
        return await llm_call(inputs, max_tokens=8000)
    except ContextLengthError:
        # 더 짧은 컨텍스트로 재시도
        truncated_inputs = inputs[:4000]
        return await llm_call(truncated_inputs, max_tokens=4000)


dataset = Dataset(name='context_length', cases=[Case(inputs='test')], evaluators=[])

# 컨텍스트 길이 오류는 재시도하지 않음 (작업에서 처리)
report = dataset.evaluate_sync(
    task=smart_llm_task,
    retry_task={'stop': stop_after_attempt(1)},  # 재시도 없음, 우리가 처리함
)

재시도 vs 오류 처리

**재시도를 쓸 때: **

  • 일시적 실패 (속도 제한, 타임아웃)
  • 네트워크 문제
  • 일시적 서비스 중단
  • 복구 가능한 오류

오류 처리를 쓸 때:

  • 검증 오류
  • 논리 오류
  • 영구 실패
  • 예상된 오류 조건
class RateLimitError(Exception):
    pass


async def llm_call(inputs: str) -> str:
    return f'LLM response: {inputs}'


def is_valid(result: str) -> bool:
    return len(result) > 0


async def smart_task(inputs: str) -> str:
    """Handle expected errors, let retries handle transient failures."""
    try:
        result = await llm_call(inputs)

        # 출력 검증 (검증 오류는 재시도하지 않음)
        if not is_valid(result):
            return 'ERROR: Invalid output format'

        return result

    except RateLimitError:
        # 재시도 로직이 처리하게 함
        raise

    except ValueError as e:
        # 재시도하지 않음 - 영구 오류
        return f'ERROR: {e}'

문제 해결

"재시도 후에도 계속 실패함"

재시도 횟수를 늘리거나 오류가 재시도 가능한지 확인해요:

import logging

from tenacity import stop_after_attempt

from pydantic_evals import Case, Dataset


def task(inputs: str) -> str:
    return f'Result: {inputs}'


# 무엇이 실패하는지 보려면 로깅 추가
logging.basicConfig(level=logging.DEBUG)

dataset = Dataset(name='troubleshooting', cases=[Case(inputs='test')], evaluators=[])

# Tenacity가 재시도 시도를 로그
report = dataset.evaluate_sync(task, retry_task={'stop': stop_after_attempt(5)})

"평가가 너무 오래 걸림"

재시도 횟수나 대기 시간을 줄여요:

from tenacity import stop_after_attempt, wait_exponential

# 더 빠른 재시도
retry_config = {
    'stop': stop_after_attempt(3),  # 시도 줄임
    'wait': wait_exponential(multiplier=0.1, min=0.1, max=2),  # 빠른 재시도, 2s에서 상한
    'reraise': True,
}

"재시도에도 속도 제한에 걸림"

지연을 늘리거나 max_concurrency를 사용해요:

from tenacity import stop_after_attempt, wait_exponential

from pydantic_evals import Case, Dataset


def task(inputs: str) -> str:
    return f'Result: {inputs}'


dataset = Dataset(name='rate_limit_config', cases=[Case(inputs='test')], evaluators=[])

# 더 긴 지연
retry_config = {
    'stop': stop_after_attempt(5),
    'wait': wait_exponential(multiplier=5, min=5, max=60),  # 5s에서 시작, 60s까지 지수 증가
    'reraise': True,
}

# 동시성도 줄임
report = dataset.evaluate_sync(
    task=task,
    retry_task=retry_config,
    max_concurrency=2,  # 동시 작업 2개만
)

다음 단계

더 알아보기 (Learn more)