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.
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:
- Novas tentativas com backoff exponencial e jitter.
- Tempos limite em todas as chamadas.
- Disjuntores para dependências indisponíveis.
- 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, comRetry-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)
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
400ou422, 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
)
Escolha valores com base na latência real da dependência:
- Meça o p95 e o p99 da API.
- Defina o timeout acima do p99, com uma margem razoável.
- 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
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()
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:
- O agente envia
POST /charge. - O servidor processa a cobrança.
- A resposta expira no caminho de volta.
- O agente interpreta a chamada como falha.
- O agente tenta novamente.
- 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),
)
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())},
)
Correto:
idempotency_key = str(uuid.uuid4())
for attempt in range(3):
requests.post(
url,
headers={"Idempotency-Key": idempotency_key},
)
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
Quando receber 429:
- Leia o cabeçalho
Retry-After. - Espere pelo menos o tempo informado.
- Só então execute a nova tentativa.
- Use backoff exponencial com jitter se o cabeçalho não estiver presente.
- 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)
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:
- Simule a dependência. Crie um mock da API chamada pela ferramenta do agente.
-
Programe uma sequência de respostas. Por exemplo:
429, depois500, depois200. - Aponte o agente para o mock. Não use a API real durante o teste.
- 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
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:
- aceitar uma operação mutável;
- processar a ação;
- interromper a resposta;
- receber a nova tentativa.
Valide que ambas as requisições usam a mesma chave:
Idempotency-Key: mesma-chave-em-todas-as-tentativas
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
429respeitamRetry-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)