DEV Community

Cover image for Idempotência de Agentes de IA: Pare Cobranças Duplicadas em Retentativas
Lucas
Lucas

Posted on Originally published at apidog.com

Idempotência de Agentes de IA: Pare Cobranças Duplicadas em Retentativas

Idempotência para agentes de IA: evite cobranças duplicadas

Seu agente chamou o endpoint de pagamento. A requisição foi processada, a cobrança foi efetuada e, em seguida, a resposta expirou no retorno. O agente nunca viu um 200, então fez o que você instruiu em caso de falha: tentou novamente. O cliente foi cobrado duas vezes, mas nada nos logs pareceu um erro.

Experimente o Apidog hoje

Esse é um modo de falha que diferencia agentes de clientes de API comuns. Um humano clica em Pagar, vê um spinner e espera. Um agente em um loop de repetição interpreta o silêncio como falha e tenta novamente — às vezes três ou quatro vezes, em uma velocidade impossível para uma pessoa.

Quanto mais agressiva a política de repetição, maior o risco de gravações duplicadas. A solução é a idempotência: fazer com que uma requisição repetida produza o mesmo efeito que uma única requisição.

Neste guia, você verá:

  • o significado de idempotência no HTTP;
  • como gerar chaves reutilizáveis por agentes;
  • o que o servidor precisa armazenar;
  • como testar duplicatas antes de colocar uma API de pagamento em produção.

Se você ainda não leu nosso conteúdo sobre por que agentes de IA falham em produção, considere as gravações duplicadas como um dos modos de falha mais comuns por trás de “o agente fez isso duas vezes”.

O Apidog entra na etapa de testes. A idempotência deve ser implementada na API e na camada de ferramentas do agente. Depois, você precisa disparar a mesma requisição duas vezes e provar que a segunda chamada não alterou o estado — um cenário que pode ser salvo e executado no CI.

Diagrama de idempotência para agentes

Por que agentes quebram a idempotência com mais frequência

Três características do tráfego de agentes tornam as duplicatas mais prováveis.

1. Repetições agressivas

Frameworks de agentes repetem requisições por padrão, porque falhas transientes de rede são uma causa frequente de execuções interrompidas. Técnicas como backoff e circuit breakers aumentam a confiabilidade, mas também podem aumentar o número de vezes que uma requisição chega ao servidor.

Veja também o guia de recuperação de erros de agentes.

2. Timeouts são ambíguos

Quando uma requisição expira, o cliente não sabe se o servidor processou a operação.

Um 504 de um proxy pode significar que:

  • a gravação nunca aconteceu; ou
  • a gravação aconteceu, mas a resposta foi perdida.

Humanos normalmente verificam o estado antes de tentar novamente. Agentes podem não fazer isso, pois “verificar primeiro” exige uma chamada de ferramenta adicional que o modelo precisa decidir executar.

3. Loops reiniciam mais do que uma etapa

Um agente que falha em uma tarefa pode reiniciar a tarefa inteira, não apenas a etapa com erro. Se a primeira etapa cria um pedido e a quarta falha, um reinício ingênuo pode criar um segundo pedido.

Em um script tradicional, o limite da repetição costuma estar explícito no código. Em um agente multi-etapas, o modelo pode decidir onde a execução recomeça.

O resultado é simples: agentes não precisam enviar requisições incorretas para causar problemas. Basta enviarem requisições corretas mais de uma vez.

O que a idempotência realmente garante

Uma operação é idempotente quando executá-la várias vezes produz o mesmo efeito que executá-la uma vez.

GET, PUT e DELETE são definidos como idempotentes pela RFC 9110. POST não é, por isso operações como criar um pedido, enviar uma mensagem ou iniciar uma transferência normalmente usam esse método.

Idempotência não é segurança

Um método seguro não altera o estado do servidor. DELETE é idempotente, mas destrutivo: chamá-lo cinco vezes deixa o recurso excluído, assim como uma única chamada — mas o recurso continua desaparecido.

Os agentes precisam tratar segurança e idempotência como propriedades separadas. O mesmo princípio aparece no uso de chaves de API de privilégio mínimo para agentes.

Idempotência não exige respostas idênticas

A segunda chamada pode retornar o resultado armazenado da primeira e até usar um código de status diferente. O ponto essencial é que o estado do servidor não seja alterado novamente:

  • uma cobrança;
  • um pedido;
  • um e-mail;
  • uma transferência.

Chaves de idempotência: tornando POST seguro

O padrão mais comum é uma chave gerada pelo cliente e enviada com a requisição. O servidor registra a chave, a requisição e o resultado. Chamadas posteriores com a mesma chave retornam o resultado registrado, sem executar o trabalho novamente.

A Stripe popularizou esse padrão. Consulte a documentação de idempotência da Stripe e o rascunho do IETF sobre o campo de cabeçalho Idempotency-Key.

A requisição pode ser:

POST /v1/payments HTTP/1.1
Host: api.yourservice.com
[REDACTED CREDENTIAL] [REDACTED]...
Idempotency-Key: 9f2b7c14-6d3a-4b18-9d55-1e2a7c0b4f31
Content-Type: application/json

{
  "amount": 4900,
  "currency": "usd",
  "customer_id": "cus_8812",
  "description": "Pro plan, August"
}
Enter fullscreen mode Exit fullscreen mode

A chave pode ser um UUID. Para o servidor, ela significa apenas:

Esta requisição representa a mesma operação lógica.

Armazene a chave junto com uma impressão digital do corpo da requisição e a resposta produzida.

Gerando uma chave reutilizável pelo agente

Um erro comum é gerar um novo UUID em cada chamada do wrapper. Nesse caso, cada tentativa recebe uma chave diferente e a idempotência deixa de funcionar.

A chave deve representar a operação lógica, não a tentativa HTTP.

Gere a chave quando o agente decidir executar a ação e reutilize-a em todas as tentativas dessa decisão:

import uuid

class PaymentTool:
    def __init__(self, client):
        self.client = client
        self._keys = {}

    def charge(self, task_id, step_id, amount, customer_id):
        # One key per (task, step). Retries of the same step reuse it.
        op = f"{task_id}:{step_id}"
        if op not in self._keys:
            self._keys[op] = str(uuid.uuid4())

        return self.client.post(
            "/v1/payments",
            headers={"Idempotency-Key": self._keys[op]},
            json={"amount": amount, "customer_id": customer_id},
        )
Enter fullscreen mode Exit fullscreen mode

Uma chave determinística também funciona e sobrevive a reinícios do processo, ao contrário de um dicionário em memória:

import hashlib

def idempotency_key(task_id: str, step_id: str, payload: dict) -> str:
    raw = f"{task_id}|{step_id}|{sorted(payload.items())}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]
Enter fullscreen mode Exit fullscreen mode

Derive a chave a partir de:

  • ID da execução ou tarefa;
  • ID da etapa;
  • payload, quando necessário.

Não use um timestamp ou um valor aleatório regenerado a cada tentativa.

Se o agente reiniciar a tarefa inteira e realmente quiser criar uma nova cobrança, o ID da tarefa deverá mudar — e a chave também. Esse é o comportamento esperado.

O que o servidor precisa fazer

Uma implementação funcional deve:

  1. Reivindicar a chave antes do trabalho. Insira a chave em uma tabela com restrição de unicidade. Se a inserção falhar, outra tentativa já é a proprietária.
  2. Validar o payload. Se a chave existir, mas a impressão digital do corpo for diferente, retorne 422. A mesma chave com outro payload indica um bug do cliente.
  3. Controlar chamadas concorrentes. Se a primeira tentativa ainda estiver em andamento, retorne 409 para que o cliente recue em vez de competir.
  4. Armazenar o resultado. Ao terminar, guarde o status e o corpo da resposta. As chamadas posteriores devem reutilizar esses dados.

Um esquema mínimo:

CREATE TABLE idempotency_records (
  key             TEXT PRIMARY KEY,
  request_hash    TEXT NOT NULL,
  state           TEXT NOT NULL,      -- in_progress | completed
  response_status INT,
  response_body   JSONB,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at      TIMESTAMPTZ NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

Defina uma expiração. Vinte e quatro horas cobrem a maioria das janelas de repetição realistas. Manter chaves para sempre transforma a tabela em um passivo operacional. A Stripe expira chaves após 24 horas, um padrão razoável para adotar.

Testando se a segunda chamada não altera nada

Implementar idempotência é apenas metade do trabalho. A outra metade é provar que ela funciona.

Um teste básico:

  1. envie a requisição;
  2. capture a resposta;
  3. envie exatamente a mesma requisição novamente;
  4. confirme que o servidor não executou o trabalho duas vezes.

A resposta, sozinha, não basta. Duas cobranças bem-sucedidas podem retornar 200. Portanto, valide o estado do servidor:

  • o corpo da segunda resposta corresponde ao da primeira, incluindo o ID do recurso;
  • uma chamada GET na coleção retorna um registro, não dois;
  • qualquer contador ou saldo foi alterado apenas uma vez.

No Apidog, configure um cenário com três etapas:

  1. envie o POST com uma Idempotency-Key fixa;
  2. repita o POST com a mesma chave;
  3. liste o recurso e valide a contagem.

Salve o ID retornado pela primeira etapa em uma variável e confirme que a segunda resposta retorna o mesmo valor. Como o cenário fica armazenado, ele pode ser executado no CI a cada alteração no fluxo de pagamentos. A mesma estratégia se aplica ao teste de contrato de API.

Casos adicionais

Cubra também estes cenários:

  • Mesma chave, corpo diferente: espere 422, não um sucesso silencioso.
  • Duplicatas concorrentes: dispare duas requisições ao mesmo tempo e confirme que exatamente uma executa o trabalho. Isso detecta a ausência de uma restrição de unicidade que um teste sequencial não encontraria.

Mocks ajudam a testar a lógica antes de a API de pagamento existir. Simule uma resposta que reconheça a chave de idempotência para exercitar cedo o comportamento de repetição do agente. Veja também por que agentes de IA devem usar mocks em vez de produção.

Teste de repetição de uma requisição idempotente

Quando você não pode adicionar uma chave

Se a API não é sua e não oferece suporte a idempotência, considere estas opções:

Torne a operação naturalmente idempotente

Use um PUT com um caminho de recurso escolhido pelo cliente:

PUT /orders/{client_order_id}
Enter fullscreen mode Exit fullscreen mode

Se você controla o design da API, essa abordagem pode ser preferível a POST com cabeçalho. Ela não exige uma tabela adicional de chaves.

Verifique antes de escrever

Peça ao agente para consultar um registro com a mesma chave natural antes de criar um novo.

Essa abordagem é mais fraca: uma corrida entre a verificação e a gravação ainda pode produzir dois registros. Porém, ela elimina o caso comum em que uma operação foi concluída, mas o cliente recebeu um timeout.

Faça a deduplicação a jusante

Para mensagens e eventos:

  1. anexe um ID de mensagem estável;
  2. armazene os IDs processados no consumidor;
  3. descarte repetições.

Esse é um padrão comum em sistemas orientados a eventos e também aparece no guia de webhooks confiáveis.

Coloque um humano no controle

Para operações irreversíveis que não podem ser tornadas idempotentes, adicione uma aprovação humana. Esse é o padrão de portão de aprovação descrito no guia sobre guardrails de agentes de IA.

Quando o custo de uma duplicata é alto, a aprovação é a opção correta.

Saiba qual execução fez o quê

A idempotência impede duplicações, mas não informa qual tentativa criou o registro. Essa informação é essencial durante um incidente.

Mantenha a identidade da execução anexada ao trabalho. Em um serviço próprio, registre em cada tentativa:

  • ID da tarefa;
  • ID da etapa;
  • chave de idempotência;
  • estado da execução;
  • resultado da ferramenta.

Quando o agente roda em um ambiente de execução de código, a plataforma pode manter esse vínculo. No Sharkly, cada execução é associada à tarefa de origem, com estado e resultado armazenados junto ao thread de comentários. Assim, uma gravação repetida pode ser rastreada até uma execução específica, em vez de aparecer como uma tentativa anônima.

Checklist antes do lançamento

  • Toda ferramenta não idempotente exige uma chave.
  • O wrapper se recusa a enviar a requisição sem uma chave.
  • As chaves são derivadas da tarefa e da etapa, não da tentativa.
  • O servidor reivindica a chave antes de executar o trabalho.
  • A mesma chave com payload diferente retorna um erro.
  • Duplicatas concorrentes são controladas por uma restrição de banco de dados.
  • Um teste salvo prova que a segunda chamada não altera o estado.
  • O teste é executado no CI.
  • As chaves expiram e a tabela é limpa regularmente.

Com essa proteção, sua política de repetição pode ser mais agressiva sem tornar o agente perigoso. Esse é o principal benefício: idempotência permite criar agentes resilientes sem aceitar efeitos duplicados.

Perguntas frequentes

Preciso de chaves de idempotência para ferramentas somente leitura?

Não. Requisições GET já são idempotentes e seguras. Repeti-las custa principalmente latência. Reserve chaves para chamadas que criam, cobram, enviam ou alteram o estado de outra forma.

Onde a chave deve ser gerada?

No wrapper da ferramenta, usando os identificadores da tarefa e da etapa do agente.

Deixar o modelo gerar a chave é um erro: em novas tentativas, modelos podem regenerar valores ou reutilizar uma chave em tarefas diferentes.

Qual status uma requisição repetida deve retornar?

Retorne o status armazenado da chamada original. Se o primeiro POST retornou 201, a repetição deve retornar 201 novamente, com o mesmo corpo.

Algumas APIs adicionam um cabeçalho como Idempotent-Replay: true para indicar uma resposta reutilizada. Isso ajuda na depuração e é inofensivo para clientes que ignoram o cabeçalho.

Por quanto tempo devo manter as chaves?

Vinte e quatro horas cobrem quase todas as janelas de repetição. Uma retenção mais longa raramente ajuda e faz a tabela crescer sem limites.

Após a expiração, uma nova tentativa deve ser tratada como uma nova operação.

Idempotência substitui transações?

Não.

Chaves de idempotência impedem que requisições duplicadas produzam efeitos duplicados. Transações garantem que uma única requisição seja atômica. Você precisa das duas.

Sempre que o banco permitir, a reivindicação da chave deve ser gravada na mesma transação que o trabalho.

Como testar sem um provedor de pagamentos real?

Aponte o agente para um mock que implemente a semântica da chave, incluindo o retorno 422 para payloads incompatíveis.

O guia do Apidog sobre testes de agentes de IA contra APIs mockadas aborda essa configuração. Você também pode baixar o Apidog e manter o mock e o teste de repetição no mesmo projeto.

Referências

Top comments (0)