DEV Community

Cover image for Recuperação de Erros em Agentes de IA: Padrões de Retry, Timeout, Backoff e Disjuntor
Lucas
Lucas

Posted on • Originally published at apidog.com

Recuperação de Erros em Agentes de IA: Padrões de Retry, Timeout, Backoff e Disjuntor

Seu agente chama uma API, recebe um 429, tenta novamente imediatamente, recebe outro 429 e entra em um loop que bombardeia um serviço limitado até a execução falhar ou a conta disparar. Ninguém cria esse loop de propósito: ele surge da versão ingênua de “tratar o erro” e é uma das dúvidas mais frequentes no fórum de discussão do Anthropic SDK.

Experimente o Apidog hoje

A recuperação de erros é o que separa uma demonstração funcional de um agente confiável em produção. O problema não é o modelo: é como seu código reage quando uma ferramenta retorna lentamente, sofre rate limit ou está indisponível.

Neste guia, você vai implementar e testar quatro padrões essenciais:

  1. Novas tentativas com backoff exponencial e jitter.
  2. Tempos limite em todas as chamadas.
  3. Disjuntores para dependências indisponíveis.
  4. Chaves de idempotência para evitar efeitos duplicados.

Para uma visão mais ampla, leia também por que os agentes de IA falham em produção.

Você não pode testar recuperação contra uma API saudável

Em desenvolvimento, dependências normalmente respondem rápido e retornam 200. Isso significa que seu código de recuperação pode nunca ser executado antes do deploy.

A primeira execução real de um backoff, timeout ou disjuntor não deve acontecer durante uma indisponibilidade em produção. Para testar recuperação, provoque falhas deliberadamente.

Crie um mock para a API chamada pela ferramenta do agente e programe respostas como:

  • 429 Too Many Requests, com Retry-After;
  • 500 Internal Server Error;
  • resposta lenta até atingir timeout;
  • corpo JSON inválido ou malformado;
  • conexão interrompida.

Em seguida, aponte o agente para o mock e valide o comportamento. O Apidog permite criar mocks e roteirizar respostas para esses cenários.

Novas tentativas com backoff exponencial e jitter

Uma nova tentativa imediata pode transformar uma falha temporária em uma sobrecarga sustentada. Quando vários clientes falham ao mesmo tempo e tentam novamente ao mesmo tempo, eles mantêm o serviço indisponível.

Use duas técnicas juntas:

  • Backoff exponencial: aumenta o intervalo entre tentativas.
  • Jitter: adiciona aleatoriedade para evitar ondas sincronizadas de novas tentativas.

Exemplo em Python:

import random
import time
import requests

RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}

def request_with_retry(url, max_attempts=4, max_delay_seconds=8):
    for attempt in range(max_attempts):
        try:
            response = requests.get(url, timeout=(3, 10))

            if response.status_code not in RETRYABLE_STATUS_CODES:
                response.raise_for_status()
                return response

            retry_after = response.headers.get("Retry-After")

            if response.status_code == 429 and retry_after:
                delay = float(retry_after)
            else:
                base_delay = min(2 ** attempt, max_delay_seconds)
                jitter = random.uniform(0, base_delay * 0.25)
                delay = base_delay + jitter

        except (requests.Timeout, requests.ConnectionError):
            base_delay = min(2 ** attempt, max_delay_seconds)
            jitter = random.uniform(0, base_delay * 0.25)
            delay = base_delay + jitter

        if attempt == max_attempts - 1:
            raise RuntimeError("Limite de tentativas excedido")

        time.sleep(delay)
Enter fullscreen mode Exit fullscreen mode

Regras práticas:

  • Limite a contagem de tentativas, normalmente entre 3 e 5.
  • Limite o atraso máximo para evitar esperas longas demais.
  • Tente novamente apenas erros transitórios, como 429, 500, 502, 503, 504, falhas de conexão e timeouts.
  • Não tente novamente automaticamente erros de validação, como 400 ou 422, sem corrigir a requisição.

O Anthropic SDK já trata parte disso nas chamadas ao próprio serviço, usando backoff exponencial para erros específicos e uma opção de max-retries. Porém, ele não protege as APIs externas chamadas pelas ferramentas do seu agente.

Em APIs de maior risco, como pagamentos, uma estratégia ruim de retry pode ter impacto real. Veja a análise sobre lógica de nova tentativa para APIs de alto risco.

Defina um tempo limite em cada chamada

Retries só funcionam se a requisição falhar. Uma requisição que fica pendurada é pior: a conexão foi aceita, mas a resposta nunca chega. Sem timeout, a execução do agente fica bloqueada atrás de um socket morto.

Toda chamada de saída precisa de:

  • Timeout de conexão: quanto tempo esperar para estabelecer conexão.
  • Timeout de leitura: quanto tempo esperar pelos dados da resposta.
  • Orçamento total da execução: limite para o fluxo completo do agente.

Exemplo:

response = requests.post(
    "https://api.exemplo.com/processar",
    json={"input": "dados"},
    timeout=(3, 15),  # 3 s para conectar, 15 s para ler a resposta
)
Enter fullscreen mode Exit fullscreen mode

Escolha valores com base na latência real da dependência:

  1. Meça o p95 e o p99 da API.
  2. Defina o timeout acima do p99, com uma margem razoável.
  3. Revise os números quando o comportamento da dependência mudar.

Timeouts muito curtos abortam chamadas que seriam bem-sucedidas. Timeouts muito longos mantêm o agente ocupado além do ponto de utilidade para o usuário.

Para respostas em streaming, use um orçamento específico. Uma resposta longa pode ser legítima, mas uma conexão sem progresso não deve consumir recursos indefinidamente.

Acione um disjuntor quando uma dependência estiver inoperante

Backoff resolve falhas temporárias. Não resolve um serviço completamente indisponível.

Se uma dependência falha continuamente, cada nova chamada provavelmente falhará também. Continuar tentando gera mais carga, mais timeouts e pior experiência para o usuário.

Um disjuntor usa três estados:

Estado Comportamento
Fechado As requisições passam normalmente e as falhas são contabilizadas.
Aberto As chamadas são bloqueadas e falham rapidamente durante um período de resfriamento.
Meio-aberto Uma chamada de teste é liberada para verificar se o serviço se recuperou.

Fluxo recomendado:

Fechado
  └─ falhas acima do limite → Aberto
Aberto
  └─ período de resfriamento termina → Meio-aberto
Meio-aberto
  ├─ sonda bem-sucedida → Fechado
  └─ sonda falha → Aberto
Enter fullscreen mode Exit fullscreen mode

Implemente o disjuntor por dependência, não globalmente. Se a API de busca estiver indisponível, o agente ainda pode usar uma API de faturamento saudável.

Uma versão simplificada em Python:

import time

class CircuitBreaker:
    def __init__(self, failure_threshold=3, reset_timeout=30):
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.failures = 0
        self.opened_at = None

    def allow_request(self):
        if self.opened_at is None:
            return True

        elapsed = time.time() - self.opened_at
        if elapsed >= self.reset_timeout:
            return True  # estado meio-aberto: permite uma sonda

        return False

    def record_success(self):
        self.failures = 0
        self.opened_at = None

    def record_failure(self):
        self.failures += 1

        if self.failures >= self.failure_threshold:
            self.opened_at = time.time()
Enter fullscreen mode Exit fullscreen mode

Antes de chamar uma dependência, verifique allow_request(). Após a chamada, registre sucesso ou falha.

Torne as novas tentativas seguras com chaves de idempotência

Retry não é seguro para toda operação.

Considere este fluxo:

  1. O agente envia POST /charge.
  2. O servidor processa a cobrança.
  3. A resposta expira no caminho de volta.
  4. O agente interpreta a chamada como falha.
  5. O agente tenta novamente.
  6. O cliente é cobrado duas vezes.

O retry funcionou como programado. O erro está no design da operação.

A solução é uma chave de idempotência. Gere uma chave única por ação lógica e envie-a em todas as tentativas da mesma operação.

import uuid
import requests

idempotency_key = str(uuid.uuid4())

response = requests.post(
    "https://api.exemplo.com/charge",
    json={
        "customer_id": "cus_123",
        "amount": 5000,
    },
    headers={
        "Idempotency-Key": idempotency_key,
    },
    timeout=(3, 15),
)
Enter fullscreen mode Exit fullscreen mode

A regra crítica é simples:

Gere a chave antes do loop de retry, não dentro dele.

Errado:

for attempt in range(3):
    requests.post(
        url,
        headers={"Idempotency-Key": str(uuid.uuid4())},
    )
Enter fullscreen mode Exit fullscreen mode

Correto:

idempotency_key = str(uuid.uuid4())

for attempt in range(3):
    requests.post(
        url,
        headers={"Idempotency-Key": idempotency_key},
    )
Enter fullscreen mode Exit fullscreen mode

O servidor deve registrar a chave recebida na primeira chamada. Se receber a mesma chave novamente, deve devolver o resultado original em vez de executar a operação uma segunda vez.

Use chaves de idempotência em chamadas que criam ou alteram estado:

  • cobranças;
  • pedidos;
  • e-mails;
  • mensagens;
  • registros;
  • transferências;
  • atualizações com efeitos externos.

O guia sobre chaves de idempotência detalha a geração e o tratamento no lado do servidor.

Sobreviva a limites de taxa e ao loop RateLimitError

Rate limits exigem tratamento especial porque o servidor normalmente informa quando você pode tentar novamente.

Uma resposta de limite de taxa excedido geralmente retorna:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Enter fullscreen mode Exit fullscreen mode

Quando receber 429:

  1. Leia o cabeçalho Retry-After.
  2. Espere pelo menos o tempo informado.
  3. Só então execute a nova tentativa.
  4. Use backoff exponencial com jitter se o cabeçalho não estiver presente.
  5. Interrompa após atingir o limite de tentativas.

Exemplo de leitura do cabeçalho:

def get_retry_delay(response, attempt):
    retry_after = response.headers.get("Retry-After")

    if retry_after:
        return float(retry_after)

    base_delay = min(2 ** attempt, 8)
    return base_delay + random.uniform(0, base_delay * 0.25)
Enter fullscreen mode Exit fullscreen mode

Não ignore o Retry-After. Se o servidor pede 30 segundos e seu agente tenta novamente em 2 segundos, ele receberá outro 429 e criará o loop RateLimitError discutido em um tópico separado do SDK.

Além da recuperação, implemente controle proativo de ritmo. Se o provedor permite um número fixo de requisições por minuto, use um bucket de tokens ou uma fila para ficar abaixo do limite antes de receber um 429.

Como testar o caminho de recuperação

Os padrões anteriores só valem se você provar que funcionam. Faça isso com testes que forçam falhas controladas.

O fluxo de teste é sempre parecido:

  1. Simule a dependência. Crie um mock da API chamada pela ferramenta do agente.
  2. Programe uma sequência de respostas. Por exemplo: 429, depois 500, depois 200.
  3. Aponte o agente para o mock. Não use a API real durante o teste.
  4. Valide o comportamento. Verifique atrasos, número de chamadas, cabeçalhos e resultado final.

Um cenário útil:

Chamada 1 → 429 + Retry-After: 2
Chamada 2 → 500
Chamada 3 → 200 + corpo válido
Enter fullscreen mode Exit fullscreen mode

Afirmações esperadas:

  • o agente aguardou pelo menos 2 segundos após o 429;
  • o agente tentou novamente após o 500;
  • a terceira chamada foi bem-sucedida;
  • o número de tentativas não excedeu o máximo configurado.

Crie também estes cenários:

1. Caminho de desistência

Faça o mock retornar falha em todas as chamadas.

Valide que o agente:

  • para ao atingir o limite de tentativas;
  • retorna um erro claro;
  • não fica em loop;
  • não aguarda indefinidamente.

2. Disjuntor aberto

Faça várias chamadas consecutivas falharem.

Valide que, após o limite configurado:

  • o disjuntor abre;
  • novas chamadas falham rapidamente;
  • o agente não paga um timeout completo para cada tentativa.

3. Idempotência

Configure o mock para:

  1. aceitar uma operação mutável;
  2. processar a ação;
  3. interromper a resposta;
  4. receber a nova tentativa.

Valide que ambas as requisições usam a mesma chave:

Idempotency-Key: mesma-chave-em-todas-as-tentativas
Enter fullscreen mode Exit fullscreen mode

Além disso, valide que o mock registra uma única ação lógica, e não duas operações independentes.

O guia sobre como testar agentes que chamam APIs mostra como estruturar esse harness de ponta a ponta.

Lista de verificação para recuperação de erros

Antes de enviar um agente para produção, confirme:

  • [ ] Toda chamada de saída possui timeout de conexão e de leitura.
  • [ ] A execução total possui um orçamento de tempo.
  • [ ] Retries usam backoff exponencial com jitter.
  • [ ] Retries têm limite de atraso e de tentativas.
  • [ ] Respostas 429 respeitam Retry-After.
  • [ ] Dependências críticas possuem disjuntores independentes.
  • [ ] Chamadas que alteram estado usam uma chave de idempotência estável.
  • [ ] O caminho de desistência retorna um erro limpo.
  • [ ] Os cenários de falha são testados contra mocks, não presumidos.

Se todos os itens estiverem marcados, seu agente se recuperará por design, não por sorte.

Onde o Apidog se encaixa — e onde não

O Apidog não é um framework de agentes, host de modelo ou runtime de orquestração. Ele não constrói, executa ou avalia seu agente.

Seu papel está na camada de API: a camada em que retries, timeouts, rate limits, idempotência e validação de requisições precisam ser testados.

Use o Apidog para:

  • simular dependências externas;
  • roteirizar respostas como 429, 500, timeout e corpos malformados;
  • validar cabeçalhos como Idempotency-Key;
  • conferir contagem de chamadas;
  • testar se o agente respeita Retry-After;
  • detectar envios duplicados antes que afetem usuários.

Esse é o encaixe prático: o Apidog simula as falhas que seu agente precisa sobreviver e verifica o que ele envia de volta.

Perguntas frequentes

O Anthropic SDK não lida com retries para mim?

Para as chamadas ao próprio serviço, sim: o SDK tenta novamente certos erros usando backoff exponencial, respeita Retry-After e permite configurar o limite com max-retries.

Mas ele não protege as APIs externas chamadas pelas ferramentas do seu agente. Você precisa aplicar os mesmos padrões nessas integrações.

Quando preciso de uma chave de idempotência?

Em qualquer chamada que crie ou altere estado, como cobranças, pedidos, mensagens, e-mails ou novos registros.

Chamadas somente leitura geralmente podem ser repetidas sem uma chave. Para operações mutáveis, gere uma chave por ação lógica e reutilize-a em todas as tentativas.

Ensaie uma falha esta semana

Você não precisa implementar todos os padrões de uma vez.

Comece pelo risco mais caro para seu caso:

  • loop de rate limit;
  • cobrança duplicada;
  • timeout infinito;
  • dependência externa indisponível.

Programe um 429, descarte uma resposta ou simule um 500. Em seguida, observe o que o agente realmente faz: quanto espera, quantas vezes chama a API e se reutiliza a mesma chave de idempotência.

Quando você vir um backoff limpo e uma única chave onde antes poderia haver uma cobrança duplicada, terá um motivo melhor do que uma demonstração impecável para confiar no agente.

Use o Apidog para simular falhas, roteirizar sequências de respostas e validar como seu agente se comporta quando uma API resiste.

Top comments (0)