Conteúdo da trilha
7 minNarração em inglês, legenda em portuguêsPodcast (RSS)
Capítulos
  1. 0:00Abertura
  2. 0:39A ideia
  3. 1:18Antes e depois
  4. 2:01Um caso real
  5. 2:59Um desafio
  6. 4:06Passo a passo seguro
  7. 5:27Riscos
  8. 6:24Fechamento
Transcrição

Abertura

O assistente agenda um Pix. O banco demora um pouco demais, e a chamada dá timeout. Então o código faz o que sempre faz. Tenta de novo. Cinco vezes.

Mas o banco já tinha registrado a primeira. A resposta só se perdeu na volta. Saem cinco transferências, para um pedido só.

Na aula anterior, você viu esse smell: o retry cego. Agora a gente corrige, passo a passo, com um número antes e um número depois. Você sai sabendo quais erros merecem uma segunda tentativa, e como deixar essa segunda tentativa inofensiva.

A ideia

A ideia cabe numa frase. Antes de repetir, classifique o erro.

Um erro transitório pode sumir sozinho. Rede instável, limite de requisições, um serviço fora do ar por um instante. Esses ganham retry, com espera crescente e um limite.

Um erro permanente não some. Entrada inválida, sem permissão, saldo insuficiente, uma regra de negócio. Esses voltam para quem chamou, com uma explicação que dá para usar.

E toda chamada que muda o mundo carrega uma chave de idempotência. É essa peça que deixa o retry seguro.

Antes e depois

Antes, havia uma seta só. Qualquer erro voltava direto para a chamada. Depois, o tipo do erro escolhe o caminho.

A chamada sai com uma chave. O serviço usa essa chave para reconhecer uma repetição.

O erro é classificado pelo código ou pelo status. Nunca procurando a palavra timeout no texto da mensagem. Mensagem muda. Código não.

Só o erro transitório volta para o começo, com espera crescente e um limite.

O erro permanente vira uma resposta clara. Para o usuário, ou para o modelo, que assim muda de rumo em vez de insistir.

E o sucesso acontece uma vez só, mesmo quando houve retry.

Um caso real

Vamos passar pelo caso perigoso, o timeout que na verdade foi um sucesso.

Um usuário pede para agendar duzentos e cinquenta reais para amanhã.

No momento do pedido, o sistema cria um identificador. Não dentro do loop de retry. No clique.

O banco processa a transferência. Aí a rede derruba a resposta.

O seu código vê um timeout. Isso é transitório, então ele repete. Com a mesma chave.

O banco reconhece a chave e, em vez de agendar de novo, devolve o primeiro resultado.

O trace mostra duas tentativas, e um efeito. É esse o número que você quer ver.

Tratar o timeout como transitório estava certo. O que impediu o duplicado foi uma chave nascida com a intenção do usuário, não com a tentativa. APIs de pagamento sérias aceitam essa chave exatamente por isso.

Um desafio

Uma pergunta para você. Uma API de email pode devolver cinco erros. Requisições demais. Destinatário inválido. Serviço indisponível. Chave de API inválida. E chave duplicada, mensagem já enviada. O que você faz com cada um?

Requisições demais é transitório. Espere o tempo que o serviço pedir e tente de novo, dentro do seu limite.

Destinatário inválido é permanente. Avise o usuário, ou o modelo, que o endereço precisa ser corrigido.

Serviço indisponível é transitório. Repita, com espera crescente. Se continuar falhando depois do limite, pare e use o plano B.

Chave de API inválida é permanente, e é problema de configuração. Nenhuma chamada vai funcionar, então não repita. Avise o time.

E chave duplicada é um sucesso disfarçado. O email já saiu. Trate como feito, e não mande de novo.

Passo a passo seguro

Como em toda refatoração aqui, comece medindo.

Puxe dos logs os erros mais comuns de cada dependência, quantos retries cada um gerou, e qualquer efeito duplicado. Duas transferências para o mesmo pedido é o número que importa.

Classifique os códigos de erro de cada serviço como transitório ou permanente. Na dúvida, trate como permanente. Um não errado sai mais barato que um sim errado.

Coloque uma chave de idempotência em toda chamada com efeito colateral, gerada uma vez por intenção do usuário.

Depois, repita só os erros transitórios, com espera crescente, um limite, e um pouco de aleatoriedade na espera. Essa aleatoriedade se chama jitter, e ela evita que mil clientes repitam no mesmo segundo.

Escreva mensagens úteis para os erros permanentes, para o usuário e para o modelo. Saldo insuficiente na conta é algo que uma pessoa consegue resolver. Erro, não.

No loop do agente, coloque um detector de repetição. A mesma ferramenta, com os mesmos argumentos, depois de um erro permanente, encerra aquele caminho.

E meça de novo. Retries por tipo de erro, tempo de resposta e efeitos duplicados. Esse último deve ser zero.

Riscos

O primeiro risco é o bug mais comum dessa refatoração. A chave é gerada dentro do loop de retry. Cada tentativa ganha uma chave nova, o serviço vê pedidos novos, e a idempotência some. A chave nasce com o clique, o chamado, a conversa, nunca com a tentativa.

O segundo é retry empilhado. O SDK do provedor repete duas vezes. A sua função repete três. O agente chama a ferramenta de novo. São nove tentativas, e ninguém combinou esse número. Desligue o retry do SDK nas chamadas que você mesmo classifica. Um lugar só decide.

E alguns serviços nem aceitam chave. Aí confira o estado antes de repetir. Já existe uma transferência para esse pedido? Se não dá para conferir, não repita um efeito colateral. Passe para uma pessoa.

Fechamento

Então, a ideia para levar. Uma boa política de retry é, quase toda, saber dizer não. Não para erros que o tempo não resolve, não para chave nova em intenção antiga, e não para uma terceira camada de retry. O que sobra é uma segunda tentativa que nunca faz estrago.

Na próxima aula, a gente vê tudo desta trilha funcionando junto num sistema real: extração de notas e contratos. Até lá.

RefatoraçãoSegurança e confiabilidade

Classificar Erro Antes de Tentar de Novo

Classify Errors Before Retry

Classificar Erro Antes de Tentar de Novo (Classify Errors Before Retry) é uma refatoração de IA para sistemas com LLM, do grupo Segurança e confiabilidade. Separar erros temporários de erros definitivos e só repetir o que pode passar.

Elemento
29/35
Nível
Intermediário
Leitura
4 min
Atualizado
Nesta página 9
  1. 01Problema
  2. 02Solução
  3. 03Antes e depois
  4. 04Por que funciona
  5. 05Passo a passo seguro
  6. 06Riscos
  7. 07Desafio
  8. 08Relacionados
  9. 09Teste seu entendimento
  10. ··Perguntas rápidas

Problema

O assistente agenda um Pix. A API do banco devolve timeout. O código tenta de novo, cinco vezes, com a mesma chamada. O banco tinha registrado a primeira. Cinco Pix saem. Todo erro dispara outra tentativa, inclusive os que nunca vão dar certo, e efeitos acabam duplicados. É o smell Retry Cego.

Solução

Antes de tentar de novo, classifique o erro. Erros transitórios (rede, limite de taxa, indisponibilidade momentânea) têm retry com espera crescente e limite. Erros permanentes (validação, permissão, saldo, regra de negócio) voltam para quem chamou com uma explicação. Toda chamada com efeito leva uma chave de idempotência.

Antes e depois

-for attempt in range(5):
-    try:
-        return bank.schedule_pix(key, amount, date)
-    except Exception:
-        time.sleep(2 ** attempt)
+TRANSIENT = {"TIMEOUT", "CONNECTION_RESET", "RATE_LIMITED", "SERVICE_UNAVAILABLE"}
+
+def schedule_pix(key, amount, date, request_id):
+    for attempt in range(3):
+        try:
+            return bank.schedule_pix(key, amount, date, idempotency_key=request_id)
+        except BankError as e:
+            if e.code not in TRANSIENT:
+                return {"ok": False, "error": e.code, "retryable": False,
+                        "message": HUMAN_MESSAGES.get(e.code, e.message)}
+            time.sleep(min(2 ** attempt + random.random(), 10))   # espera com jitter
+    return {"ok": False, "error": "UNAVAILABLE", "retryable": True,
+            "message": "O banco está instável. Tente de novo em alguns minutos."}

Antes: qualquer erro, mesma resposta.

flowchart LR
  C["1. Chamada"]:::tool --> E{"2. Erro?"}
  E -->|"qualquer"| C

Depois: o tipo do erro decide.

flowchart LR
  C["1. Chamada com chave de idempotência"]:::tool --> E{"2. Tipo do erro"}
  E -->|"transitório"| W["3. Esperar e tentar, com limite"]:::tool
  W --> C
  E -->|"permanente"| R(["4. Explicar a quem chamou"]):::io
  E -->|"ok"| OK(["5. Resultado"]):::io
  1. Toda chamada com efeito carrega uma chave que o serviço usa para não repetir o efeito.
  2. O erro é classificado pelo código ou status, nunca pelo texto da mensagem.
  3. Só erros transitórios voltam a ser tentados, com espera crescente e teto.
  4. Erros permanentes viram uma resposta clara, que o modelo ou o usuário pode usar.
  5. O sucesso acontece uma vez, mesmo com retries.

Por que funciona

Repetir só ajuda quando a causa do erro pode desaparecer sozinha. Rede instável e limite de taxa passam; saldo insuficiente e chave Pix inválida, não. Classificar evita gastar tentativas no que não vai funcionar, e evita que uma tentativa “dê certo” na hora errada.

A chave de idempotência resolve o caso mais perigoso: a chamada que deu certo no servidor, mas cuja resposta se perdeu na rede. Sem a chave, o retry repete o efeito. Com ela, o servidor reconhece a repetição e devolve o resultado da primeira vez. APIs de pagamento sérias (a do Stripe, as de Pix dos bancos) aceitam essa chave por esse motivo.

Num agente, o erro permanente bem explicado também ajuda o modelo: em vez de repetir schedule_pix com os mesmos argumentos, ele informa o usuário ou muda de caminho. Classifique pelo código (INSUFFICIENT_FUNDS, 400, 429), nunca por um if "timeout" in str(e). Mensagens mudam; códigos não.

Exemplo trabalhado: timeout que era sucesso

Entrada: agendar R$ 250 para amanhã, request_id=chk_9f3a. O banco processa, a rede cai antes da resposta. Sem chave, o retry cria chk novo e agenda de novo. Com chave, o segundo POST devolve o mesmo agendamento. Saída no trace: attempts=2, effect=1.

Classificar o timeout como transitório está certo. O que impede o duplicado é a chave nascida com a intenção do usuário, não com a tentativa.

Checagem +20 XP de primeiraA API devolve 400 com INSUFFICIENT_FUNDS. Seu loop faz retry com espera crescente, porque ‘às vezes o saldo muda’. O que isso causa?

Passo a passo seguro

  1. Meça. Levante nos logs os erros mais comuns de cada dependência e quantos retries cada um gerou. Procure efeitos duplicados (dois Pix com o mesmo pedido).
  2. Classifique os códigos de erro de cada serviço em transitórios e permanentes. Na dúvida, trate como permanente.
  3. Acrescente chave de idempotência a todas as chamadas com efeito, gerada uma vez por intenção do usuário.
  4. Implemente o retry só para transitórios, com espera crescente, jitter (um pouco de acaso na espera, para mil clientes não baterem no mesmo segundo) e teto. Respeite Retry-After quando o serviço mandar.
  5. Escreva mensagens úteis para erros permanentes, para o modelo e para o usuário.
  6. Detecte repetição no agente: a mesma ferramenta com os mesmos argumentos depois de erro permanente encerra o caminho.
  7. Meça de novo: retries por erro, efeitos duplicados (devem ir a zero) e tempo até a resposta.

Armadilha: três camadas de retry

O SDK do provedor repete duas vezes. A sua função repete três. O loop do agente chama a ferramenta de novo. Nove tentativas, e ninguém combinou o teto. Desligue o retry do SDK (max_retries=0) nas chamadas em que você classifica. Um só lugar decide.

Outra armadilha: gerar uuid4() dentro do for attempt. Cada volta é uma chave nova, e a idempotência some. A chave nasce com o clique, o ticket, o request_id da conversa.

Se o serviço não aceita chave, consulte o estado antes de repetir (“já existe um Pix com este request_id?”). Sem consulta, não repita efeito.

Checagem +20 XP de primeiraVocê gera idempotency_key=str(uuid4()) no início de cada volta do for attempt. O banco aceita a chave. Por que os Pix ainda duplicam num timeout?

Riscos

  • Classificação errada. Um erro permanente tratado como transitório volta ao problema original. Revise a lista quando o serviço mudar.
  • Chave de idempotência gerada a cada tentativa. Assim ela não protege nada.
  • Serviço que não suporta idempotência. Nesse caso, consulte o estado antes de repetir.
  • Retries empilhados. O SDK repete, a sua função repete e o agente repete. Defina um só lugar responsável por retry. Combine com Adicionar Caminho de Fallback quando o transitório persiste: depois do teto, disjuntor, não mais uma volta.

Desafio

Classifique estes erros de uma API de envio de email e diga o que fazer com cada um.

429 Too Many Requests
400 Invalid recipient address
503 Service Unavailable
401 Invalid API key
409 Duplicate idempotency key (message already sent)
Ver solução
  • 429: transitório. Esperar (respeitando Retry-After) e tentar de novo, com limite.
  • 400 endereço inválido: permanente. Informar ao modelo ou ao usuário que o endereço precisa ser corrigido.
  • 503: transitório. Retry com espera; se persistir, abrir o disjuntor e usar o fallback.
  • 401: permanente e de configuração. Não repetir; alertar o time, porque nenhuma chamada vai funcionar.
  • 409 chave duplicada: sucesso disfarçado. O email já foi enviado; tratar como resultado positivo, sem reenviar.

Relacionados

Teste seu entendimento

Checagem +20 XP de primeiraO agente chama schedule_pix, recebe INVALID_PIX_KEY, e chama de novo com os mesmos argumentos no passo seguinte. Qual peça desta refatoração faltou no agente?

Checagem +20 XP de primeiraO provedor devolve 429 com cabeçalho Retry-After: 20. Seu backoff estava em 2s, 4s, 8s. O que você faz nesta tentativa?

Checagem +20 XP de primeiraUm 409 Duplicate idempotency key chega depois de um timeout. O email já foi enviado. Como o código trata?

Perguntas rápidas

O que é Classificar Erro Antes de Tentar de Novo?

Classificar Erro Antes de Tentar de Novo (Classify Errors Before Retry) é uma refatoração de IA para sistemas com LLM, do grupo Segurança e confiabilidade. Separar erros temporários de erros definitivos e só repetir o que pode passar.

Que problema Classificar Erro Antes de Tentar de Novo resolve?

Trata Retry Cego.

Aonde Classificar Erro Antes de Tentar de Novo leva?

Ao padrão de projeto Disjuntor e Ação Idempotente.

Para ir além

Relações

Smells que trata 1
Padrões de destino 2
Aparece nas trilhas 1
Buscar