O loop de um agente: devolver os resultados ao modelo e saber quando parar

O loop de um agente: devolver os resultados ao modelo e saber quando parar

O loop é seu, não do modelo

Uma chamada a um modelo de linguagem produz uma resposta e termina. O modelo não consegue executar uma tool, esperar o resultado e continuar raciocinando sozinho: quando pede uma tool, o turno dele acaba ali. Se alguém pergunta “quanto evoluí no supino este mês?”, o modelo pode pedir primeiro o histórico de treinos, mas só verá esse histórico quando alguém enviá-lo em uma nova chamada.

Esse “alguém” é o harness: o programa ao redor do modelo. Ele recebe o pedido, executa a tool, acrescenta o resultado à conversa e chama o modelo de novo. Repete o processo até o modelo responder sem pedir mais nada. O que costumamos chamar de “agente” é, no fundo, esse loop escrito no código do aplicativo. O modelo decide o que pedir; o loop decide se executa, quantas vezes repete e quando para.

O ciclo completo

O loop de tools de um agente O harness chama o modelo. Se a resposta não pede tools, ele sai com a resposta final. Se pede e ainda restam rodadas, executa as tools, acrescenta ao histórico o turno do modelo e os resultados e chama de novo. Se as rodadas acabam, interrompe o loop. 1 Chamar o modelo histórico → novo turno 2 Pediu tools? não → saída 1 Saída 1 resposta final sim 3 Restam rodadas? não → saída 2 Saída 2 fim sem tools sim 4 Executar as tools código local, uma a uma 5 Reinjetar turno do modelo + resultados rodada + 1
Há duas saídas: a normal, quando o modelo para de pedir tools, e a de segurança, quando o limite de rodadas se esgota: uma última chamada sem tools para responder com o que já sabe.

Cada volta do loop é uma rodada: uma chamada ao modelo, a execução das tools que ele pediu e o reenvio dos resultados. O loop tem duas saídas. A normal acontece quando o modelo responde sem pedir tools: essa é a resposta final para a pessoa. A de segurança acontece quando o número máximo de rodadas se esgota, mesmo que o modelo continue pedindo tools.

O loop, passo a passo

Este é o loop em pseudocódigo. A linguagem e a API não importam: o que precisa ser feito é o mesmo em todos os casos. Observe três coisas: quando o loop sai, o que é acrescentado ao histórico em cada volta e quando o modelo é chamado de novo.

turn = call_model(history)

repeat at most MAX_ROUNDS times:
    if the turn asks for no tools:
        exit                           # normal exit: this is the final answer

    results = []
    for each tool request in the turn:
        result = run_tool(request.name, request.arguments)
        add (request.id, result) to results

    append the turn to the history     # what the model asked for
    append the results to the history  # what the app answered
    turn = call_model(history)

return turn

Esse é o núcleo. O repeat at most impõe o limite de rodadas, e o exit é a saída normal. Em cada volta, as tools são executadas uma a uma, na ordem em que o modelo as pediu. É mais lento do que executá-las em paralelo, mas evita que duas escritas sobre os mesmos dados concorram entre si. Mudar essa decisão é mudar o loop, não o modelo. Só falta o que fazer quando o loop termina porque as rodadas acabaram, e isso tem uma seção própria mais abaixo.

O que é reinjetado e em que ordem

O modelo não se lembra de nada entre chamadas. Por isso, em cada chamada ele recebe a conversa inteira: as mensagens anteriores da pessoa e do assistente e, desde que o loop começou, cada pedido de tools com seu resultado. Cada rodada acrescenta duas coisas ao final desse histórico, nesta ordem: primeiro o turno do modelo com seus pedidos (tool_use) e depois uma mensagem com um resultado (tool_result) para cada pedido. O identificador liga cada resultado ao seu pedido.

A ordem não é questão de estilo. A Anthropic rejeita um tool_result que não vem logo após o turno com o seu tool_use, e um resultado sem o pedido que o originou deixa o modelo sem contexto para interpretá-lo. Os três fornecedores resolvem o mesmo problema de formas diferentes:

FornecedorQuem guarda o histórico dentro do loopO que o loop envia em cada rodada
OpenAI (Responses)O fornecedorSó os resultados novos (function_call_output com o seu call_id) e o previous_response_id, que diz à OpenAI qual resposta anterior está sendo continuada.
Anthropic (Messages)O aplicativoO histórico inteiro: a conversa, o turno do modelo intacto e, em seguida, uma mensagem com um tool_result para cada tool_use_id.
Google (Interactions)O aplicativo, porque o Gymnasia não guarda a interação no GoogleO histórico inteiro, com cada function_call seguido do seu function_result com o mesmo call_id.

A OpenAI é a exceção só dentro do loop de uma mesma mensagem: como a API dela consegue lembrar uma resposta pelo identificador, basta enviar o que é novo. Quando a pessoa escreve uma nova mensagem, o Gymnasia começa do zero e envia a conversa completa, igual aos outros. E em todos os casos vale a mesma regra: o pedido vem antes do resultado, e cada resultado leva o identificador do pedido que responde.

Por que é preciso um limite

Nada obriga o modelo a parar de pedir tools. Ele pode repetir a mesma chamada porque o resultado não o convence, encadear buscas sem chegar a uma conclusão ou reagir a um erro pedindo a tool outra vez. Cada rodada é uma chamada de rede, com sua latência e seu custo em tokens, e o histórico cresce a cada volta. Sem um limite, um loop assim pode deixar a pessoa esperando indefinidamente.

O Gymnasia define o limite em uma constante compartilhada pelos três fornecedores: MAX_TOOL_ROUNDS = 10. O número é uma decisão de produto, não uma verdade técnica. Precisa ser alto o bastante para as consultas reais que encadeiam várias tools, como ler o histórico, calcular um recorde e salvar uma meta, e baixo o bastante para cortar um loop descontrolado antes que a espera ou a conta fiquem perceptíveis.

O que acontece quando as rodadas acabam

Interromper o loop é a parte fácil. O difícil é decidir o que dizer à pessoa. Na primeira versão do Gymnasia isso estava mal resolvido, e vale a pena contar porque é fácil repetir em qualquer harness:

  • O Google lançava um erro técnico: a resposta continuava pendente de tools ao atingir o limite.
  • A OpenAI e a Anthropic saíam do loop sem avisar e devolviam o último turno, que ainda pedia tools. Como o texto que o modelo escreve entre rodadas (“vou buscar seu histórico…”) vai se acumulando, a pessoa via essa frase pela metade como se fosse a resposta final, ou um erro genérico do tipo “o fornecedor não devolveu conteúdo”.

Em nenhum caso ela recebia uma resposta útil nem sabia que o orçamento de rodadas tinha acabado. Cortar de uma vez é jogar fora o trabalho feito: o agente talvez já tivesse lido o histórico e só faltasse um passo.

A solução: uma última chamada em que o modelo só pode escrever

Quando as rodadas acabam, o loop não para de repente. Ele faz uma última chamada ao modelo em que ele não pode pedir nenhuma tool, nem as que ficaram pendentes nem outras: só pode responder com texto, usando o que já descobriu.

Antes é preciso resolver um detalhe. O último turno do modelo ainda contém pedidos de tools, e os fornecedores exigem que cada pedido tenha seu resultado. Então o loop responde a eles sem executá-los, com um resultado que só diz o que aconteceu. A instrução sobre o que fazer agora vai à parte, nas instruções de sistema dessa última chamada:

# The rounds ran out and the last turn still asks for tools.
if the turn asks for tools:
    for each pending request:
        result = "Not executed: the step limit for this answer was reached."
    append the turn and those results to the history

    instructions = system_prompt + "You have reached the step limit for this answer.
        Answer with what you already know, explain the limit,
        say what was left undone and ask whether to continue."
    turn = call_model(history, instructions, tools = none)

return turn

Assim a pessoa recebe algo como “Encontrei seu histórico, mas cheguei ao limite de passos desta resposta e não calculei o recorde. Quer que eu continue?”. Se ela responder que sim, o agente segue: o limite é contado por mensagem, então a nova mensagem começa com outras dez rodadas. E se a tool pendente era uma escrita, como salvar uma meta, o modelo sabe que ela não foi feita e não vai dizer o contrário.

Por que a instrução não vai no resultado da tool

A primeira versão colocava tudo no resultado: “Não executada… responda com o que sabe e pergunte se quer que você continue”. Com o Google funcionava, mas com a OpenAI não. O modelo tratava esse texto como um dado devolvido por uma tool, não como uma ordem: não perguntava e às vezes até dizia que não tinha acesso às ferramentas.

Faz sentido que seja assim. O resultado de uma tool pode conter texto que você não controla, como o de uma página web ou de um documento, e um modelo que obedecesse às ordens escritas ali seria fácil de manipular. Ao passar a instrução para as instruções de sistema da chamada de encerramento, os três testes com a OpenAI explicaram o limite e terminaram perguntando se deviam continuar.

Regra de design:

o resultado de uma tool conta o que aconteceu; o que o modelo deve fazer em seguida vai nas instruções de sistema.

Como dizer ao modelo que não use tools

Todas essas APIs têm um parâmetro, tool_choice, que decide o que o modelo pode fazer com as tools em uma chamada específica. Com auto, o valor habitual, o modelo decide se pede alguma. Com none, as tools continuam declaradas, mas o modelo não pode pedir nenhuma e precisa responder com texto. A chamada de encerramento usa none. Cada fornecedor escreve isso de um jeito, e há duas armadilhas:

FornecedorComo pedir que não use tools na chamada de encerramento
OpenAI (Responses) e compatíveistool_choice: "none".
Anthropic (Messages)tool_choice: { type: "none" }, sem remover a lista de tools: se o histórico contém pedidos de tools, a API exige que as tools continuem declaradas.
Google (Interactions)tool_choice: "none" dentro de generation_config. Na raiz da requisição, a API responde com um erro 400 Unknown parameter.

Se mesmo assim não houver resposta

A chamada de encerramento também pode falhar: a rede cai, ou o modelo ignora a instrução e volta a pedir uma tool. Só nesse caso a pessoa vê um erro. Mas não o genérico “o fornecedor não devolveu conteúdo”, que não explica nada e convida a repetir a mesma pergunta. Em vez disso, ela vê um que diz o que aconteceu e o que pode fazer: “Esta consulta precisava de mais passos do que o assistente pode dar em uma única resposta. Tente dividi-la em perguntas menores”.

Regra de design:

o agente ficar sem rodadas não é só mais uma falha. Normalmente ele já descobriu algo útil, então dê a ele uma última chance de contar isso sem deixá-lo pedir mais tools, que diga com clareza o que não chegou a fazer e que pergunte se deve continuar. Reserve a mensagem de erro para quando nem isso funcionar, e que ela explique o que aconteceu.

Testes do loop

O loop pode ser testado inteiro sem rede e sem modelo. Basta um fornecedor falso: uma função que, em vez de chamar a API, devolve turnos preparados de antemão, um por rodada. Assim cada teste fixa exatamente o que o modelo pede em cada volta e verifica o que o loop faz. A mesma bateria se repete para OpenAI, Anthropic e Google.

Duas rodadas e resposta final

O fornecedor falso pede uma tool, depois outra e então responde com texto. O loop deve executar as duas tools, chamar o modelo duas vezes e devolver a resposta final.

A ordem do histórico

Na segunda chamada, o histórico deve conter cada turno do modelo seguido dos seus resultados, rodada a rodada. Se alguém inverter as duas entradas, o teste falha antes do fornecedor real.

Sem tools, sem voltas

Se a primeira resposta já é texto, o loop sai na primeira volta: não executa nenhuma tool e não chama o modelo de novo.

Um modelo que não para

O fornecedor falso pede tools em todos os turnos, sem fim. O loop deve executar exatamente o limite de rodadas, responder ao pedido pendente sem executá-lo e fazer a chamada de encerramento sem tools.

O limite recebe mais três verificações. Primeira: ao atingir o limite, os pedidos pendentes recebem o resultado “Não executada” e não são executados. Segunda: a chamada de encerramento leva tool_choice com o valor none, no lugar que cada fornecedor exige, e a instrução de encerramento nas instruções de sistema, só nessa chamada. Terceira: se o encerramento falhar, a pessoa vê a mensagem que explica o que aconteceu, não a genérica.

Como sabemos que esses testes servem para alguma coisa? Quebrando o código de propósito e verificando que eles falham. Invertemos a ordem do histórico, subimos o limite para 11 e removemos o tool_choice da chamada de encerramento, e em cada caso vários testes ficaram vermelhos. Um teste que continua verde com o código quebrado não protege nada.

Aprender a construir um agente completo

Este artigo faz parte de uma série sobre como construir um agente ou harness usando um aplicativo de academia como caso prático. O passo anterior foi transformar uma tool call em código executável; aqui fechamos o ciclo, devolvendo o resultado ao modelo e colocando um limite no número de voltas.

Veja o índice completo da série sobre o agente do Gymnasia.

Continuar lendo

Últimos posts -->

Você viu esses projetos?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

App de fitness com dois agentes que são executados integralmente no dispositivo, sem backend, de forma que os dados do usuário nunca saem do celular. Um coach conversacional BYOK com tools locais e system prompt remoto com fallback offline, e um estimador de refeições que tira os macronutrientes de uma foto do prato, com leitura de códigos de barras contra o OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Sistema multiagente de pesquisa construído com LangGraph. Um supervisor decompõe sua pergunta em tópicos e lança subagentes de busca em paralelo; cada um comprime seus achados antes de repassá-los a um agente redator que escreve o relatório final em markdown com suas fontes. Streaming ao vivo por WebSockets, modelo configurável por papel e chaves de API próprias que nunca são armazenadas no servidor.

Tau

Tau Tau
Python
LangChain

Sistema multiagente de tutoria para estudantes do ensino médio, com um agente por disciplina e material de curso elaborado e validado por uma equipe de professores. Chegou a ser usado com alunos reais em um colégio privado na Espanha e em uma escola de ensino médio na Colômbia.

Ver todos os projetos -->
>_ Disponível para projetos

Tem um projeto com IA?

Vamos conversar.

maximofn@gmail.com

Especialista em Machine Learning e Inteligência Artificial. Desenvolvo soluções com IA generativa, agentes inteligentes e modelos personalizados.

Quer assistir alguma palestra?

Últimas palestras -->

Quer melhorar com essas dicas?

Últimos tips -->

Use isso localmente

Os espaços do Hugging Face nos permitem executar modelos com demos muito simples, mas e se a demo quebrar? Ou se o usuário a deletar? Por isso, criei contêineres docker com alguns espaços interessantes, para poder usá-los localmente, aconteça o que acontecer. Na verdade, se você clicar em qualquer botão de visualização de projeto, ele pode levá-lo a um espaço que não funciona.

Flow edit

Flow edit Flow edit

Edite imagens com este modelo de Flow. Baseado em SD3 ou FLUX, você pode editar qualquer imagem e gerar novas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos os contêineres -->
>_ Disponível para projetos

Tem um projeto com IA?

Vamos conversar.

maximofn@gmail.com

Especialista em Machine Learning e Inteligência Artificial. Desenvolvo soluções com IA generativa, agentes inteligentes e modelos personalizados.

Você quer treinar seu modelo com esses datasets?

short-jokes-dataset

HuggingFace

Dataset com piadas em inglês

Uso: Fine-tuning de modelos de geração de texto humorístico

231K linhas 2 colunas 45 MB
Ver no HuggingFace →

opus100

HuggingFace

Dataset com traduções de inglês para espanhol

Uso: Treinamento de modelos de tradução inglês-espanhol

1M linhas 2 colunas 210 MB
Ver no HuggingFace →

netflix_titles

HuggingFace

Dataset com filmes e séries da Netflix

Uso: Análise de catálogo Netflix e sistemas de recomendação

8.8K linhas 12 colunas 3.5 MB
Ver no HuggingFace →
Ver mais datasets -->