재시도

재시도 (Retries)

"재시도(Retry)"는 에이전트 런 안에서 서로 다른 7개 레이어, 7가지 다른 의미로 쓰여요. 그리고 이들은 예산을 공유하지 않아요. 이걸 뒤섞으면 런이 생각보다 훨씬 많이(또는 적게) 재시도하는 흔한 원인이 됩니다. 이 페이지가 그 지도예요. 각 레이어는 세부 설정을 다루는 페이지로 연결해 드릴게요.

출처: 공식문서 — Retries

레이어들

레이어 무엇을 재시도하나 설정 방법 메시지 기록에 추가되는 것
Transport 제공자에게 보내는 같은 HTTP 요청 HTTP 클라이언트의 AsyncHTTPX2TenacityTransport 없음 — 에이전트는 그 시도를 전혀 보지 못함
Provider SDK 제공자 SDK 클라이언트가 재발행하는 같은 HTTP 요청 SDK 클라이언트 자체. 기본값·설정은 제공자별로 다름 없음 — 에이전트는 그 시도를 전혀 보지 못함
Durable execution 워크플로 엔진이 재실행하는 모델 요청 전체 — 와이어에 가까운 모든 레이어를 다시 통과. Temporal에선 기본 무제한(maximum_attempts=0) Temporal의 ActivityConfigretry_policy, DBOS의 StepConfigmax_attempts, Prefect의 TaskConfigretries 없음 — 엔진이 그 스텝을 리플레이
Model fallback 다른 모델을 상대로 한 같은 요청 FallbackModel 우승 응답 하나뿐
Tool 도구 호출 하나 — 모델에게 고치라고 요청하면서 retries={'tools': N} 과 도구별 한도 도구 결과 자리에 RetryPromptPart
Output 모델의 최종 답 — 모델에게 고치라고 요청하면서 retries={'output': N}ToolOutput(max_retries=N) RetryPromptPart — 어디에 놓이는지는 아래 참고
Model-request hooks after_model_request, wrap_model_request, on_model_request_error 에서 ModelRetry 를 던진 모델 요청 훅 자체. output 예산을 소비 RetryPromptPart 를 실은 새 요청

마지막 세 개만이 "에이전트 재시도"예요 — 각각 모델 왕복(round trip)을 한 번씩 소비해요. 재시도가 곧 또 다른 요청이기 때문이죠. 나머지 네 개는 모델에 보이지 않아요 — 시도가 실패한 걸 결코 보지 못해요.

재시도 곱셈 (Retry multiplication)

레이어들은 예산을 공유하지 않지만, 쌓여요(stack). 한 레이어의 재시도는 와이어에 더 가까운 모든 레이어의 시도를 감싸요. 논리적 호출 하나가 최대 N 개의 모델 요청을 낼 수 있고 — 최초 시도 + 도구 호출마다 후속 요청 하나 + 도구·출력 재시도 예산이 더하는 만큼 — 각 모델 요청은 최대 M 번 시도가 허용된 제공자 SDK 클라이언트가 보내고, 각 시도는 최대 K 번 시도가 허용된 transport를 타요. 그래서 논리적 호출 하나가 네트워크에 최대 N×M×K 개의 와이어 요청을 올릴 수 있어요. Durable execution 에선 모델 요청을 실행하는 스텝도 재시도하며 매번 MK 를 다시 통과해요 — 그리고 Temporal에선 직접 maximum_attempts 를 설정하지 않으면 그 재시도 횟수가 무제한이에요.

  • N — 논리적 호출당 모델 요청 수: 최초 시도, 도구 호출마다 후속 한 번(성공한 도구 호출도 또 다른 요청을 큐에 넣어요), 그리고 도구·출력 예산이 더하는 모든 재시도 프롬프트
  • M — 제공자 SDK 클라이언트 안에서 모델 요청당 시도 수. 이 예산은 SDK가 정해요. 예를 들어 max_retries=N 으로 설정한 OpenAI 클라이언트는 1 + N 번의 시도를 허용해요. 제공자별 설정은 provider SDK retries 참고
  • K — 와이어에서 요청당 시도 수: transport의 중지 전략이에요. stop_after_attempt(N) 은 총 N 번의 시도(K = N)를 허용하지, 1 + 재시도가 아니에요 — transport retries 참고

모든 와이어 요청은 자기만의 지연을 지불하고 — 요청이 모델에 닿으면 토큰도 청구되고 — 그래서 행복 경로가 아니라 최악의 경우를 예산이 흡수해야 해요. UsageLimitsN 만 제한해요: 그 request_limit(기본 50)은 런당 모델 요청을 세고, 그 아래에서 SDK 클라이언트와 transport가 추가하는 와이어 요청은 결코 보지 못해요. ModelSettings.timeout 은 시도마다 적용돼요 — 재시도하는 SDK 클라이언트는 재시도마다 다시 켜죠 — 그리고 포워딩하는 모델 클래스 에서만요. 시간 쪽은 Timeouts 참고.

retries={'output': 2}(최종 답만으로 최대 3개 모델 요청), OpenAI SDK 기본 max_retries=2(요청당 3회 시도), stop_after_attempt(2) 로 중지하는 transport(와이어 요청당 2회 시도)를 가진 런은 네트워크에 3 × 3 × 2 = 18 개의 요청을 올릴 수 있어요. 각각 자기만의 ModelSettings(timeout=10) 마감을 지고 있어요 — 재시도 사이 백오프 대기 전에, 최악의 경우 요청 시간만 180초예요.

Transport 재시도

Transport 재시도는 모델 클라이언트 아래에 살아요: 실패한 HTTP 요청이 에이전트가 전혀 모르는 사이에 다시 보내져요. 제공자에 넘기는 HTTP 클라이언트에 재시도 transport를 설치하지 않으면 이 레이어에선 아무것도 재시도되지 않고, 어떤 오류가 재시도 대상인지도 여러분이 정해요.

이건 속도 제한, 연결 리셋, 5xx 응답에 딱 맞는 레이어예요. transport는 tenacity 위에 만들어지고 httpx2 클라이언트에 꽂히므로, 커스텀 httpx2 클라이언트를 받는 SDK 라면 어떤 제공자에서든 동작해요. AWS Bedrock 이 예외예요 — 그건 boto3로 재시도해요.

transport 밖에서 자체 백오프를 만든다면, ModelHTTPError.retry_after 가 제공자의 Retry-After 헤더를 이미 초 단위로 파싱해 줘요.

설치

재시도 transport를 쓰려면 tenacity 를 설치해야 해요. retries 의존성 그룹으로 설치할 수 있어요:

Terminal

pip install 'pydantic-ai-slim[retries]'

Terminal

uv add 'pydantic-ai-slim[retries]'

재시도하는 클라이언트

똑똑한 재시도 처리로 재시도 기능을 추가하는 예시예요:

smart_retry_example.py

from httpx2 import AsyncClient, ConnectError, HTTPStatusError
from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.retries import (
    AsyncHTTPX2TenacityTransport,
    RetryConfig,
    wait_retry_after,
)


def create_retrying_client():
    """Create a client with smart retry handling for multiple error types."""

    def should_retry_status(response):
        """Raise exceptions for retryable HTTP status codes."""
        if response.status_code in (429, 502, 503, 504):
            response.raise_for_status()  # This will raise HTTPStatusError

    transport = AsyncHTTPX2TenacityTransport(
        config=RetryConfig(
            # Retry on HTTP errors and connection issues
            retry=retry_if_exception_type((HTTPStatusError, ConnectError)),
            # Smart waiting: respects Retry-After headers, falls back to exponential backoff
            wait=wait_retry_after(
                fallback_strategy=wait_exponential(multiplier=1, max=60),
                max_wait=300
            ),
            # Stop after 5 attempts
            stop=stop_after_attempt(5),
            # Re-raise the last exception if all retries fail
            reraise=True
        ),
        validate_response=should_retry_status
    )
    return AsyncClient(transport=transport)

# Use the retrying client with a model
client = create_retrying_client()
model = OpenAIChatModel('gpt-5.2', provider=OpenAIProvider(http_client=client))
agent = Agent(model)

대기 전략 (Wait strategies)

wait_retry_after

wait_retry_after 함수는 HTTP Retry-After 헤더를 자동으로 존중하는 똑똑한 대기 전략이에요:

wait_strategy_example.py

from tenacity import wait_exponential

from pydantic_ai.retries import wait_retry_after

# Basic usage - respects Retry-After headers, falls back to exponential backoff
wait_strategy_1 = wait_retry_after()

# Custom configuration
wait_strategy_2 = wait_retry_after(
    fallback_strategy=wait_exponential(multiplier=2, max=120),
    max_wait=600  # Never wait more than 10 minutes
)

이 대기 전략은:

  • HTTP 429 응답의 Retry-After 헤더를 자동 파싱해요
  • 초 형식("30")과 HTTP 날짜 형식("Wed, 21 Oct 2015 07:28:00 GMT")을 모두 지원해요
  • 헤더가 없으면 선택한 전략으로 폴백해요
  • 과도한 지연을 막기 위해 max_wait 한도를 존중해요

Transport 클래스

AsyncHTTPX2TenacityTransport

비동기 HTTP 클라이언트용(대부분의 용도에 권장):

async_transport_example.py

from httpx2 import AsyncClient
from tenacity import stop_after_attempt

from pydantic_ai.retries import AsyncHTTPX2TenacityTransport, RetryConfig


def validator(response):
    """Treat responses with HTTP status 4xx/5xx as failures that need to be retried.
    Without a response validator, only network errors and timeouts will result in a retry.
    """
    response.raise_for_status()

# Create the transport
transport = AsyncHTTPX2TenacityTransport(
    config=RetryConfig(stop=stop_after_attempt(3), reraise=True),
    validate_response=validator
)

# Create a client using the transport:
client = AsyncClient(transport=transport)

HTTPX2TenacityTransport

동기 HTTP 클라이언트용:

sync_transport_example.py

from httpx2 import Client
from tenacity import stop_after_attempt

from pydantic_ai.retries import HTTPX2TenacityTransport, RetryConfig


def validator(response):
    """Treat responses with HTTP status 4xx/5xx as failures that need to be retried.
    Without a response validator, only network errors and timeouts will result in a retry.
    """
    response.raise_for_status()

# Create the transport
transport = HTTPX2TenacityTransport(
    config=RetryConfig(stop=stop_after_attempt(3), reraise=True),
    validate_response=validator
)

# Create a client using the transport
client = Client(transport=transport)

흔한 재시도 패턴 (Common retry patterns)

Retry-After 을 지원하는 속도 제한 처리

rate_limit_handling.py

from httpx2 import AsyncClient, HTTPStatusError
from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential

from pydantic_ai.retries import (
    AsyncHTTPX2TenacityTransport,
    RetryConfig,
    wait_retry_after,
)


def create_rate_limit_client():
    """Create a client that respects Retry-After headers from rate limiting responses."""
    transport = AsyncHTTPX2TenacityTransport(
        config=RetryConfig(
            retry=retry_if_exception_type(HTTPStatusError),
            wait=wait_retry_after(
                fallback_strategy=wait_exponential(multiplier=1, max=60),
                max_wait=300  # Don't wait more than 5 minutes
            ),
            stop=stop_after_attempt(10),
            reraise=True
        ),
        validate_response=lambda r: r.raise_for_status()  # Raises HTTPStatusError for 4xx/5xx
    )
    return AsyncClient(transport=transport)

# Example usage
client = create_rate_limit_client()
# Client is now ready to use with any HTTP requests and will respect Retry-After headers

wait_retry_after 함수는 429(속도 제한) 응답의 Retry-After 헤더를 자동 감지해 지정된 시간만큼 기다려요. 헤더가 없으면 지수 백오프로 폴백해요.

네트워크 오류 처리

network_error_handling.py

import httpx2
from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential

from pydantic_ai.retries import AsyncHTTPX2TenacityTransport, RetryConfig


def create_network_resilient_client():
    """Create a client that handles network errors with retries."""
    transport = AsyncHTTPX2TenacityTransport(
        config=RetryConfig(
            retry=retry_if_exception_type((
                httpx2.TimeoutException,
                httpx2.ConnectError,
                httpx2.ReadError
            )),
            wait=wait_exponential(multiplier=1, max=10),
            stop=stop_after_attempt(3),
            reraise=True
        )
    )
    return httpx2.AsyncClient(transport=transport)

# Example usage
client = create_network_resilient_client()
# Client will now retry on timeout, connection, and read errors

커스텀 재시도 로직

custom_retry_logic.py

import httpx2
from tenacity import retry_if_exception, stop_after_attempt, wait_exponential

from pydantic_ai.retries import (
    AsyncHTTPX2TenacityTransport,
    RetryConfig,
    wait_retry_after,
)


def create_custom_retry_client():
    """Create a client with custom retry logic."""
    def custom_retry_condition(exception):
        """Custom logic to determine if we should retry."""
        if isinstance(exception, httpx2.HTTPStatusError):
            # Retry on server errors but not client errors
            return 500 <= exception.response.status_code < 600
        return isinstance(exception, httpx2.TimeoutException | httpx2.ConnectError)

    transport = AsyncHTTPX2TenacityTransport(
        config=RetryConfig(
            retry=retry_if_exception(custom_retry_condition),
            # Use wait_retry_after for smart waiting on rate limits,
            # with custom exponential backoff as fallback
            wait=wait_retry_after(
                fallback_strategy=wait_exponential(multiplier=2, max=30),
                max_wait=120
            ),
            stop=stop_after_attempt(5),
            reraise=True
        ),
        validate_response=lambda r: r.raise_for_status()
    )
    return httpx2.AsyncClient(transport=transport)

client = create_custom_retry_client()
# Client will retry server errors (5xx) and network errors, but not client errors (4xx)

httpx2 호환 제공자와 함께 쓰기

재시도 transport는 http_client 인자가 httpx2.AsyncClient 를 받는 어떤 제공자에서든 동작해요. 각 제공자 문서 에서 어떤 클라이언트 타입을 받는지 보세요. Bedrock 은 boto3를 쓰고 자기 방식대로 재시도를 설정해요.

SDK가 여전히 레거시 httpx.AsyncClient 를 요구하는 제공자(Groq, Cohere)는 Pydantic AI v2 동안 그 클라이언트에 폐기 예정인 TenacityTransportAsyncTenacityTransport 를 쓸 수 있어요. 둘 다 레거시 클라이언트 지원과 함께 v3에서 제거돼요.

OpenAI

openai_with_retries.py

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

from smart_retry_example import create_retrying_client

client = create_retrying_client()
model = OpenAIChatModel('gpt-5.2', provider=OpenAIProvider(http_client=client))
agent = Agent(model)

OpenAI 호환 제공자 아무거나

openai_compatible_with_retries.py

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

from smart_retry_example import create_retrying_client

client = create_retrying_client()
model = OpenAIChatModel(
    'your-model-name',  # Replace with actual model name
    provider=OpenAIProvider(
        base_url='https://api.example.com/v1',  # Replace with actual API URL
        api_key='your-api-key',  # Replace with actual API key
        http_client=client
    )
)
agent = Agent(model)

Anthropic

anthropic_with_retries.py

from pydantic_ai import Agent
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.providers.anthropic import AnthropicProvider

from smart_retry_example import create_retrying_client

client = create_retrying_client()
model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(http_client=client))
agent = Agent(model)

모범 사례 (Best practices)

  1. 보수적으로 시작: 재시도를 적게(3-5회) 두고 합리적인 대기 시간을 쓰세요.
  2. 지수 백오프 사용: 장애 중 서버에 과부하가 걸리는 걸 피하는 데 도움이 돼요.
  3. 최대 대기 시간 설정: 합리적인 최대 대기 시간으로 무한 지연을 막으세요.
  4. 속도 제한을 올바르게 처리: 가능하면 Retry-After 헤더를 존중하세요.
  5. 재시도 시도 기록: 운영에서 재시도 동작을 모니터링하도록 로깅을 추가하세요. (httpx2 를 계측하면 Logfire가 자동으로 잡아줘요.)
  6. 서킷 브레이커 고려: 트래픽이 많은 애플리케이션은 서킷 브레이커 패턴 구현을 고려하세요.

운영에서 재시도 모니터링 (Monitoring Retries in Production)

과도한 재시도는 근본 문제를 가리키고 비용을 늘릴 수 있어요. Logfire 가 재시도 패턴 추적을 도와줘요:

  • 어떤 요청이 재시도를 유발했는지 확인
  • 재시도 원인(속도 제한, 서버 오류, 타임아웃) 이해
  • 시간에 따른 재시도 빈도 모니터링
  • 재시도를 줄일 기회 식별

HTTPX 계측 을 켜면 재시도 시도가 트레이스에 자동으로 잡혀요.

오류 처리 (Error handling)

재시도 transport는 모든 재시도 시도가 실패하면 마지막 예외를 다시 던져요. 애플리케이션에서 적절히 처리하세요:

error_handling_example.py

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

from smart_retry_example import create_retrying_client

client = create_retrying_client()
model = OpenAIChatModel('gpt-5.2', provider=OpenAIProvider(http_client=client))
agent = Agent(model)

성능 고려 (Performance considerations)

  • 재시도는 특히 지수 백오프에서 요청에 지연을 더해요
  • 재시도 동작을 설정할 때 애플리케이션의 전체 타임아웃을 고려하세요
  • 시스템적 문제를 감지하도록 재시도 비율을 모니터링하세요
  • 여러 요청을 처리할 땐 더 나은 동시성을 위해 async transport를 쓰세요

더 고급 재시도 설정은 tenacity 문서 를 참고하세요.

AWS Bedrock

AWS Bedrock 제공자는 httpx2 대신 boto3의 내장 재시도 메커니즘을 써요. Bedrock 재시도를 설정하려면 boto3의 Config 를 쓰세요:

from botocore.config import Config

config = Config(retries={'max_attempts': 5, 'mode': 'adaptive'})

완전한 예시는 Bedrock: Configuring Retries 참고.

Provider SDK 재시도

transport와 모델 사이에 에이전트가 결코 보지 못하는 레이어가 하나 더 있어요: 제공자 SDK의 자체 클라이언트예요. 여러분의 코드가 듣기 전에 실패한 요청을 재발행해요. 기본값, 재시도 대상 오류, 설정이 제공자마다 달라서 M 은 여러분이 쓰는 클라이언트에서 정해야 해요. 재시도 transport 는 이 클라이언트 아래 에 자리해서 둘은 서로 교체하는 게 아니라 쌓여요: 하나를 설정해도 다른 하나가 비활성화되지 않아요.

제공자별 설정은 OpenAI, Anthropic, Google, Groq, Cohere, AWS Bedrock 을 보세요.

모델 폴백은 재시도가 아니에요

FallbackModel 은 현재 모델이 실패하면 다음 모델로 이동해요. 같은 모델을 다시 시도하진 않아요. 재시도의 대용품으로 다루지 말고 transport 재시도와 짝을 지으세요: 일시적 실패엔 같은 제공자를 재시도하고, 정말 다운됐을 때만 다른 제공자로 폴백해요. Fallback Model 참고.

도구 재시도 (Tool retries)

도구 재시도는 모델에게 보내는 메시지예요: 그 호출이 안 됐고, 이유는 이렇고, 다시 시도해. 이건 도구 인자에 대한 Pydantic ValidationError, 도구(또는 그 args_validator, 또는 도구 훅)가 ModelRetry 를 던지는 것, 도구 타임아웃, 그리고 모델이 존재하지 않는 도구를 호출하는 것으로 촉발돼요.

Tool Execution, Retries, and Failures 가 설정을 다뤄요: 기본 예산 1, 도구별 / 툴셋별 / 런별 / 에이전트 전체의 우선순위 사다리, 그리고 ModelRetryToolFailed 간 선택. 런을 추론할 때 중요한 카운터 의 속성 세 개가 있어요:

  • 카운터는 도구 이름 키로 하고 성공하면 리셋돼요. 각 도구는 자기만의 카운트를 가져요. 런 전체의 도구 재시도 예산은 없어요. 도구가 성공하면 카운트가 지워져요 — 그래서 실패와 성공을 번갈아 하는 도구는 예산 1 을 결코 소진하지 않고 한 런에서 여러 번 실패할 수 있어요.
  • max_retries=N 은 N번의 재시도를 허용하므로 N+1번의 시도. max_retries=0 은 재시도 프롬프트를 보내지 않고 첫 실패에서 바로 예외를 던져요.
  • 모델이 지어낸 도구 이름은 자기만의 예산을 받아요. 알 수 없는 도구 이름은 사용 가능한 도구를 나열하는 재시도 프롬프트를 만들고, 지어낸 이름 키로 된 예산을 소비하며, 에이전트 전체 tools 예산에 묶여요. 그래서 매번 다른 이름을 환각하는 모델은 계속 새 예산을 받아요.

도구 예산을 소진하면 UnexpectedModelBehavior 가 발생해요.

메시지 기록에서 재시도는 어떻게 보이나

재시도된 도구 호출엔 ToolReturnPart 이 없어요 — 그 자리를 RetryPromptPart 가 차지하고 같은 tool_call_id 를 지녀요. 둘 다 있는 경우는 없어요:

retry_prompt_history.py

from pydantic_ai import (
    Agent,
    ModelMessage,
    ModelResponse,
    ModelRetry,
    TextPart,
    ToolCallPart,
)
from pydantic_ai.models.function import AgentInfo, FunctionModel


def lookup_then_answer(
    messages: list[ModelMessage], info: AgentInfo
) -> ModelResponse:
    if len(messages) == 1:
        return ModelResponse(parts=[ToolCallPart('lookup_user', {'name': 'John'})])
    elif len(messages) == 3:
        return ModelResponse(
            parts=[ToolCallPart('lookup_user', {'name': 'John Doe'})]
        )
    return ModelResponse(parts=[TextPart('John Doe is user 123.')])


agent = Agent(FunctionModel(lookup_then_answer))


@agent.tool_plain
def lookup_user(name: str) -> int:
    if ' ' not in name:
        raise ModelRetry('Provide the full name.')
    return 123


result = agent.run_sync('Who is John?')
print([type(p).__name__ for m in result.all_messages() for p in m.parts])
"""
[
    'UserPromptPart',
    'ToolCallPart',
    'RetryPromptPart',
    'ToolCallPart',
    'ToolReturnPart',
    'TextPart',
]
"""

(이 예시는 완결돼 있어 그대로 실행할 수 있어요.)

RetryPromptPart 는 실패를 문자열(ModelRetry 에서) 또는 Pydantic 오류 상세 목록(ValidationError 에서)으로 지니고, 모델에는 'Fix the errors and try again.' 이 붙어 렌더링돼요. 그 tool_name 은 재시도가 특정 도구 호출에 속하면 설정되고, 런의 출력에 속하면 None 이에요.

재시도 프롬프트가 기록에 남으므로, 나중 런에서 그 기록을 재사용 하면 실패를 모델에 다시 재생해요. 모델이 자기 이전 실수를 못 보게 하려면 ProcessHistory capability로 걸러내세요.

ToolFailed 는 의도적인 반대예요: outcome='failed' 를 가진 ToolReturnPart 를 기록하고 재시도 예산을 소비하지 않아 반복 실패는 재시도 횟수가 아니라 UsageLimits 에 묶여요. Reporting a Failed Tool Result 참고.

출력 재시도 (Output retries)

출력 예산은 도구 예산과 분리돼 있고, 그 시행 방식은 모델이 최종 답을 돌려주는 방식에 따라 달라요. How output retries are enforced 가 두 경로를 모두 다뤄요. 메시지 기록에 중요한 차이는:

  • 텍스트 경로 (output_type=str, TextOutput, NativeOutput, PromptedOutput, 그리고 쓸 수 있는 출력이 없는 응답): 런 전체에 걸쳐 예산 하나를 공유해요. 재시도는 유일한 부분이 tool_name=NoneRetryPromptPart 인 새 ModelRequest 가 돼요.
  • 도구 경로 (ToolOutput): 출력 예산이 출력 도구 당 기본 한도로 작용하고, ToolOutput(max_retries=N) 로 재정의할 수 있어요. 재시도 프롬프트는 함수 도구의 것과 정확히 똑같이 출력 도구의 tool_call_id 에 묶여요.

둘 다 검증 실패, 출력 함수출력 검증기ModelRetry 를 던지는 것, 그리고 할 수 있는 게 아무것도 없는 모델 응답으로 촉발돼요. 둘 다 예산이 다 떨어지면 UnexpectedModelBehavior 를 던져요.

마지막 촉발 조건엔 예외가 있어요: 출력 타입이 None 을 허용하면 — 예를 들어 output_type=str | None — 빈 응답이나 thinking-only 응답은 재시도가 아니라 유효한 최종 결과 None 이에요. 도구 호출에서 일을 끝내고 thinking만 내놓는 모델은 그렇지 않으면 채우기용 텍스트를 만들도록 밀려날 거예요. 그 None 에도 출력 검증기는 여전히 돌아서, 스스로 ModelRetry 를 던져 재시도를 강제할 수 있어요.

두 예산은 하나의 인자로 설정돼요:

retry_budgets.py

from pydantic_ai import Agent

agent = Agent('openai:gpt-5.2', retries=3)  # (1)

strict_output = Agent('openai:gpt-5.2', retries={'tools': 5, 'output': 1})  # (2)

int 는 도구·출력 예산 둘 다 설정해요.

AgentRetries dict는 이름 붙인 키만 설정하고, 이름 없는 키는 기본 1 을 유지해요.

같은 인자는 런마다 — agent.run(..., retries=...) 등 — 그리고 일련의 런에 대해 agent.override() 로 받아들여져요. Which retry limit wins 에 완전한 우선순위 표가 있어요.

결코 재시도되지 않는 것 (What is never retried)

  • prepare 콜백. 도구별 prepare=, PrepareTools, 또는 동적 툴셋 이 던진 예외는 변경 없이 런 밖으로 전파돼요 — ModelRetry 조차도 거기선 재시도 프롬프트로 바뀌지 않아요. 한 턴 동안 도구를 숨기려면 예외를 던지는 대신 콜백에서 None 을 반환하세요.
  • before_model_request 훅. 이건 요청이 아직 조립되는 동안, 모델이 호출되기 전에 돌아요. 그래서 거기서 던진 ModelRetry 는 재시도 프롬프트가 되는 대신 런 밖으로 전파돼요 — 재시도할 응답이 아직 없으니까요. 다른 model-request hooks 중 하나에서 던지세요: hooks.on.after_model_request(모델이 만든 응답을 거부할 때 — 거부된 응답은 메시지 기록에 남아 모델이 자기 말을 볼 수 있어요), hooks.on.model_request (wrap_model_request), 또는 hooks.on.model_request_error (on_model_request_error).
  • ModelRetryToolFailed 외의 예외들. 도구가 던진 다른 것은 뭐든 재시도가 되는 대신 런 밖으로 전파돼요 — , on_tool_execute_error 를 구현한 capability 가 예외를 먼저 보고 대체 도구 결과를 돌려주거나 ModelRetry 를 던져 런을 계속하게 할 수 있을 때는 예외예요. ApprovalRequiredCallDeferred 는 둘 다 아닌 예외들이에요: 오류가 아니라 제어 흐름이라 전파 대신 DeferredToolRequests 출력으로 런을 끝내요 — 단, 멈출 수 없어 대신 모델에게 '이 도구는 세션 중에 완료할 수 없다'는 설명으로 답하는 리얼타임 세션 은 예외예요. Ending a run from inside a tool 에 완전한 표가 있어요.
  • 에이전트 런 전체. 아무것도 여러분 대신 에이전트를 재실행해 주지 않아요. Pydantic Evals 는 평가 중에 전체 태스크·이벨류에이터를 재시도하는 자체 retry_taskretry_evaluators 옵션이 있어요 — Retry Strategies 참고. 그건 에이전트 밖에 있으므로, 재시도된 태스크는 새 도구·출력 예산으로 시작해요.

더 알아보기 (Learn more)