CTO no Integrar Plus • Publicado em 21 de October de 2026
Quando um agente de inteligência artificial entra em loop e repete cinco vezes a mesma cobrança ou dispara três reembolsos idênticos para o mesmo cliente, a primeira reação da maioria dos desenvolvedores ainda é abrir o editor de texto e mexer no system prompt. Acrescentam instruções em letras maiúsculas exigindo cautela, pedem para o modelo verificar se a ação já foi feita e colocam avisos explícitos para nunca chamar a ferramenta duas vezes.
Isso não funciona. Tratar repetições desastrosas de ferramentas como uma falha de raciocínio do modelo revela uma incompreensão básica da nossa área. O modelo de linguagem gerou a intenção correta, selecionou os parâmetros válidos e entregou a chamada para a camada de execução. O que falhou depois disso pertence à literatura de sistemas distribuídos dos anos setenta, não à engenharia de prompts.
Se a sua requisição HTTP para uma API financeira ou serviço externo leva um timeout de conexão após dez segundos, você não tem uma resposta negativa. Você tem uma ausência completa de observação. O pacote pode ter morrido antes de atingir o servidor de destino, o servidor pode ter processado o débito e travado antes de devolver o código HTTP 200, ou o pacote de volta foi descartado por instabilidade de rede.
Nos frameworks populares de agentes, essa incerteza é achatada em um tipo binário primitivo: sucesso ou erro de execução. A exceção sobe, cai na janela de contexto do modelo como uma mensagem de erro genérica e o modelo, treinado para buscar soluções e cumprir a tarefa atribuída, simplesmente decide tentar de novo. Nenhum ajuste de temperatura ou refinamento semântico altera o fato de que a sua aplicação colocou um sistema estocástico para tomar decisões cegas sobre efeitos colaterais em rede.
Entre os engenheiros que já perceberam esse abismo, duas correntes práticas costumam dominar as discussões sobre como mitigar o estrago.
A primeira corrente defende que a solução definitiva é injetar chaves de idempotência em qualquer chamada que altere estado no mundo exterior. A lógica parece direta: se toda mutação carregar um identificador único, o serviço de destino ignora execuções repetidas e devolve o mesmo resultado anterior sem duplicar o efeito.
A segunda corrente argumenta que confiar cegamente em chaves de idempotência downstream é ingênuo. Serviços de terceiros frequentemente não implementam essas chaves, descartam registros de idempotência após janelas curtas como vinte e quatro horas ou respondem com sucesso silencioso, aceitando a requisição na borda e descartando a operação internamente sem avisar o remetente. Por essa visão, a única proteção confiável é nunca confiar no valor retornado pelo endpoint, forçando o sistema a fazer uma leitura direta no estado durável de destino antes de qualquer nova tentativa.
Eu concordo com a desconfiança da segunda corrente, mas discordo do remédio quando ele é entregue de volta ao agente.
Colocar o agente para ler o estado da API antes de tentar de novo só transfere a fragilidade para a etapa de leitura. Leituras em sistemas externos também sofrem com replicação atrasada, consistência eventual e falhas parciais. Se o modelo lê o saldo logo após um timeout e a replicação de leitura ainda não processou a gravação que de fato ocorreu na ponta, o agente conclui erroneamente que a operação falhou e manda executar outra vez.
O erro de raiz está em deixar a decisão de retry dentro do loop do modelo de linguagem.
A responsabilidade de lidar com chamadas distribuídas, retries, backoff exponencial e reconciliação de estado pertence à aplicação servidora e ao banco de dados relacional que gerencia a fila, nunca à janela de contexto do LLM. O modelo deve apenas registrar a intenção de executar uma ação em uma tabela transitória.
A partir do instante em que a intenção foi registrada, a aplicação assume a máquina de estados. Se você trabalha com um backend tradicional em Ruby on Rails sobre PostgreSQL, por exemplo, o fluxo precisa ser desenhado com garantias transacionais rígidas:
class ExecuteAgentToolCall
def call(agent_run_id, tool_name, payload)
idempotency_key = Digest::SHA256.hexdigest("#{agent_run_id}:#{tool_name}:#{payload.sort.to_json}")
intent = ToolExecution.create_or_find_by!(idempotency_key: idempotency_key) do |record|
record.agent_run_id = agent_run_id
record.state = :pending
record.payload = payload
end
return intent if intent.completed? || intent.unknown?
process_network_call(intent)
end
private
def process_network_call(intent)
response = ExternalGateway.mutate(intent.payload, key: intent.idempotency_key)
intent.update!(state: :completed, response_body: response)
rescue Net::OpenTimeout, Net::ReadTimeout => e
intent.update!(state: :unknown, last_error: e.message)
TriggerReconciliationJob.perform_later(intent.id)
raise AgentExecutionPausedError, "Estado pendente de reconciliação."
end
end
Observe que a geração da chave não pode depender da memória efêmera do framework nem do texto gerado pelo modelo. Ela precisa ser derivada deterministicamente dos parâmetros lógicos da operação e do identificador da execução. Se o agente tentar gerar a chave via prompt, ele mudará caracteres arbitrários a cada turno.
O estado desconhecido, que marquei como unknown no código, é exatamente o ponto onde a maioria das arquiteturas quebra. Quando ocorre um timeout, a tarefa do agente precisa ser pausada imediatamente. O agente não recebe uma mensagem dizendo para tentar de novo, nem recebe autorização para inspecionar o saldo com outro prompt. A execução é congelada até que um job em segundo plano, rodando fora do ciclo conversacional, consulte a API remota, valide a existência da transação via chave persistida e atualize o registro para concluído ou cancelado.
Existe ainda outro problema que vejo ser ignorado solenemente nas discussões de arquitetura: a perda de controle de concorrência sobre a própria execução do agente.
Em infraestruturas operadas com múltiplos workers consumindo filas de background, um agente que coordena passos longos pode facilmente perder a posse do job por estouro de timeout de processamento. A fila assume que o worker morreu e entrega a mesma execução para outro nó. Se o primeiro worker ainda estiver em execução aguardando uma resposta lenta e o segundo worker retomar do último ponto salvo, dois processos passam a atuar concorrentemente sobre os mesmos recursos.
Chaves de idempotência protegem a API de destino contra repetições idênticas, mas não impedem que dois workers ativos tomem decisões divergentes baseadas em estados defasados. Sem travas distribuídas no nível da linha da execução no banco de dados, você tem condições de corrida que nenhum modelo de IA é capaz de prever ou conter.
A indústria gastou décadas aprendendo que a rede é instável, que mensagens são reordenadas e que operações com efeito colateral exigem semântica de entrega controlada por software determinístico. Empacotar chamadas de rede dentro de um json gerado por um modelo de linguagem não suspende as leis da computação distribuída. Se o seu agente precisa interagir com o mundo real além de gerar texto, pare de tentar doutrinar o prompt e construa a camada de tolerância a falhas onde ela sempre precisou estar.
CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.