Lição 2: A ida e volta completa de uma chamada de ferramenta
Objetivos de aprendizado:
- Nomear os campos-chave que a requisição e a resposta carregam em uma ida e volta de chamada de ferramenta
- Dizer se um trecho de código com tool_use / tool_result está corretamente pareado
- Identificar uma dependência de dados entre chamadas do mesmo lote paralelo, e saber quando dividir as chamadas em duas rodadas
- Explicar por que a expressão “o modelo chama uma ferramenta” é, em si, imprecisa
Pré-requisitos: Você leu a Lição 1 e sabe por que agentes precisam de ferramentas | Anterior: Lição 1 << | Próxima: Lição 3 >>
Comece por três blocos de JSON
Você está construindo um bot de atendimento. Um usuário pergunta: “Você consegue verificar onde está meu pedido ORD-2026-8842?”. Seu código envia essa mensagem ao modelo junto com uma definição de ferramenta:
Repare no novo campo tools. Ele não é uma mensagem; é um manifesto que informa ao modelo quais ferramentas ele tem em mãos, como cada uma se parece e quais parâmetros cada uma exige.1 Você precisa enviar esse manifesto em toda requisição — o modelo não “lembra” dele, então o seu código tem que incluí-lo todas as vezes.
O modelo lê o manifesto e, em vez de responder o status do pedido diretamente, retorna algo assim:
Duas coisas novas aparecem aqui: stop_reason virou "tool_use", e o array content ganhou um bloco novo com type: "tool_use". O modelo não consultou informação nenhuma sobre o pedido — ele nem sabe onde fica o sistema de pedidos. Ele está apenas dizendo: “preciso que você chame get_order_status para mim com estes parâmetros, e depois me conte o resultado”.
Seu código assume daqui em diante, consulta de fato o sistema de pedidos, obtém um resultado e empacota esse resultado na próxima requisição a ser enviada de volta:
Repare no que foi acrescentado: a resposta completa do modelo da rodada anterior é jogada de volta em messages na íntegra, seguida de uma nova mensagem user. Essa mensagem não carrega texto digitado pelo usuário — ela carrega um bloco type: "tool_result" cujo tool_use_id corresponde exatamente ao id que o modelo acabou de te entregar.
Só depois de ver esta requisição é que o modelo finalmente diz algo como: “Seu pedido está a caminho, saindo do centro de triagem de Xangai, com entrega prevista para 27 de agosto”. Três blocos de JSON, três trocas de papel: o modelo faz um pedido, o seu código executa, o resultado é devolvido. É isso a ida e volta completa de uma chamada de ferramenta.
stop_reason é um sinal, não um registro de execução
Aqui está o que os iniciantes mais erram: eles supõem que stop_reason: "tool_use" significa que a ferramenta já foi chamada. Não foi. É apenas a razão pela qual o modelo parou ao terminar esta mensagem, o mesmo tipo de campo que "end_turn" (terminou de falar) ou "max_tokens" (ficou sem espaço), só que com um valor diferente.2
O modelo nunca toca num banco de dados, dispara uma requisição HTTP ou roda um comando de shell por conta própria. Tudo o que ele pode fazer é emitir um pedido estruturado; o resto do trabalho recai sobre o seu código ou sobre os servidores da Anthropic.3 É por isso que as ferramentas se dividem em “client tools” (o aplicativo host as executa) e “server tools” (a Anthropic as executa por você) — a diferença é só sobre quem executa este passo, não sobre se o modelo consegue executá-lo sozinho.2
Os três campos de um bloco tool_use, nenhum opcional
Volte àquele bloco tool_use. Só três dos seus campos são obrigatórios:4
id: o identificador único desta chamada, no formato toolu_01XYZ.... Ele tem exatamente uma função — fazer a correspondência quando você devolver o resultado mais tarde.
name: a ferramenta que o modelo escolheu, que tem que corresponder exatamente ao name de uma das ferramentas do seu manifesto tools.
input: um objeto contendo os parâmetros desta chamada, no formato que satisfaz as regras que você definiu em input_schema.
Junte esses três campos e você tem tudo o que o modelo consegue expressar: “quero chamar a ferramenta name com este id, e aqui está o input”. Ele não vai acrescentar lógica do tipo “tente de novo três vezes” — isso você escreve por conta própria no código do host. Como projetar uma interface de ferramenta para que o modelo erre menos parâmetros é território da Lição 3; esta lição só se importa com como esses três campos são empacotados e lidos de volta.
tool_result faz a correspondência pelo tool_use_id
Uma única resposta do modelo pode conter mais de um bloco tool_use. Digamos que o usuário pergunte: “Você consegue verificar onde está meu pedido ORD-2026-8842 e também verificar se o ORD-2026-9001 já foi enviado?”. O modelo coloca dois blocos tool_use no mesmo array content, e stop_reason continua sendo "tool_use".
Seu código tem que consultar os dois pedidos e então, na mesma mensagem user, colocar os dois resultados juntos no array content, com cada tool_result reivindicando a sua própria chamada pelo respectivo tool_use_id:
Se você tomar um atalho e enviar uma rodada só com o primeiro tool_use_id sozinho, o modelo se recusa a continuar a conversa, porque “a rodada anterior tinha um bloco tool_use que nunca recebeu o seu tool_result” — os dois blocos têm que ser reivindicados juntos na próxima mensagem user; você não pode dividi-los em duas requisições e devolvê-los em partes.5 O bloco tool_result também tem um campo opcional is_error: defina-o como true quando a ferramenta falhar, e o modelo saberá que esta chamada esbarrou em um problema.4
Chamadas do mesmo lote não enxergam os resultados umas das outras
Agora que a regra da devolução em lote está resolvida, há uma armadilha mais funda: a dependência de dados entre blocos tool_use do mesmo lote.
Mude de cenário. Um agente de transferência de dinheiro está montado com duas ferramentas: read_balance(account_id) lê o saldo, e withdraw(account_id, amount) tira dinheiro. O usuário diz: “Tire $100 da A001, se houver saldo suficiente”. Em uma única resposta, o modelo devolve dois blocos tool_use: read_balance({"account_id": "A001"}) e withdraw({"account_id": "A001", "amount": 100}).
Olhe o amount do withdraw: 100, copiado direto do número na frase do usuário, sem nenhuma relação com o saldo ser suficiente ou não. Isso não é preguiça do modelo; ele não tem escolha. No momento em que gera esta resposta, read_balance ainda é só “algo que ele planeja fazer” — o valor de retorno dela nem existe ainda, então withdraw não tem como lê-lo. Dentro de um lote de blocos tool_use, nenhuma chamada consegue ver os resultados das outras daquele lote, porque nesse ponto esses resultados não foram executados nem devolvidos.
Então aqui vai uma linha que você mesmo tem que segurar: se o parâmetro de uma operação de escrita deveria, em tese, ser igual ao valor de retorno de uma operação de leitura do mesmo lote, essas duas chamadas não deveriam aparecer na mesma resposta. A abordagem genuinamente segura é dividi-las em duas rodadas: rode só read_balance primeiro, devolva o saldo real como um tool_result, e uma vez que o modelo veja “o saldo é só 60”, deixe que ele decida se chama withdraw e por quanto.
Três táticas que realmente funcionam:
- Escreva a pré-condição na descrição da ferramenta. Acrescente uma linha à
description do withdraw: “só chame depois de ter visto o saldo mais recente retornado por read_balance”. A descrição da ferramenta é ela própria parte do prompt que o modelo consegue ler, o que é muito mais confiável do que torcer para o modelo descobrir a dependência sozinho.6
- Desligue o paralelismo com disable_parallel_tool_use. Defina
{"type": "auto", "disable_parallel_tool_use": true} no tool_choice da requisição, e o modelo chama no máximo uma ferramenta por resposta.5 Aperte primeiro o comportamento para uma de cada vez, deixe claras na sua cabeça as dependências entre os passos, e só então considere afrouxar.
- Coloque uma rede de segurança na camada de execução. Faça o código que roda o
withdraw reconferir ele mesmo o saldo mais recente, recusar-se a rodar se a condição não for satisfeita, e escrever a razão na informação de erro do tool_result para o modelo ver, em vez de fingir que deu certo. Mesmo que o modelo agrupe as duas chamadas de novo desta vez, essa verificação contém o risco.
Desenhe como um diagrama
Desenhe a ida e volta acima e ela fica assim:
O passo em que mais se erra neste diagrama é a seta do “anexar”: enviar só o tool_result sozinho e esquecer de jogar de volta em messages a resposta tool_use completa do modelo daquela rodada. O modelo então recebe um resultado de ferramenta que parece surgir do nada, sem nenhum registro no seu contexto do pedido que ele fez — um non sequitur ou um erro explícito passa a ser provável. O movimento certo é guardar a resposta de cada rodada no histórico na íntegra; messages só cresce e nunca é podado.3
Uma tarefa pode exigir mais de uma ida e volta
O exemplo acima terminou depois de uma única chamada de ferramenta. Em cenários reais, o modelo muitas vezes tem que ir e voltar várias vezes até conseguir concluir. Imagine um bot de deploy. O usuário diz: “Reinicie o serviço para mim e me diga se há algum erro nos logs”:
- O modelo retorna
tool_use na primeira rodada, chamando restart_service; você executa e devolve o resultado
- O modelo retorna
tool_use de novo na segunda rodada, chamando read_logs para verificar se há erros; você executa e devolve os logs
- Na terceira rodada o modelo finalmente retorna
stop_reason: "end_turn", com um resumo em texto
A lógica do código no lado do host é essencialmente um laço: enquanto stop_reason continuar sendo "tool_use", siga executando ferramentas, empacotando os resultados de volta e enviando outra rodada; assim que ele virar "end_turn", entregue o texto final ao usuário.3
Este laço não tem um teto fixo de iterações — para um único pedido do usuário, o modelo pode chamar uma ferramenta só uma vez, ou cinco ou seis vezes até ter reunido o suficiente. A Lição 3 cobre como o projeto da interface de ferramenta consegue cortar o número de idas e voltas; para esta lição, basta lembrar: várias idas e voltas são a norma, não a exceção.
Troque o host, os nomes dos campos mudam, a estrutura não
Se você está em uma API compatível com a da OpenAI, o mesmo mecanismo vem em outra embalagem: a requisição de chamada aparece no array choices[0].message.tool_calls, o sinal de término não se chama stop_reason e sim finish_reason, e o valor dele é "tool_calls" em vez de "tool_use".7 A documentação oficial da OpenAI descreve o processo como "a multi-step conversation between your application and a model via the OpenAI API. When the model calls a function, you must execute it and return the result" (uma conversa de múltiplos passos entre a sua aplicação e um modelo via API da OpenAI; quando o modelo chama uma função, você tem que executá-la e retornar o resultado) — o modelo emite uma requisição de chamada, a aplicação executa e devolve o resultado, exatamente como na Claude.8
Os nomes dos campos mudam conforme a API, mas o esqueleto — “o modelo só envia pedidos, o host cuida da execução, os resultados voltam carregando um identificador, e isso pode repetir por várias rodadas” — é universal.
Recapitulação
- O modelo nunca executa nada diretamente. Ele apenas emite
stop_reason: "tool_use" mais um ou mais blocos tool_use; a execução fica com o aplicativo host
- Um bloco
tool_use tem só três campos obrigatórios: id (para a correspondência), name (a ferramenta escolhida) e input (os parâmetros)
- Os resultados voltam como blocos
tool_result, e o tool_use_id tem que corresponder exatamente ao id do bloco tool_use equivalente
- Uma resposta pode ter vários blocos
tool_use; os blocos tool_result correspondentes têm que ser empacotados na mesma mensagem user, e não divididos em várias requisições
- Blocos
tool_use do mesmo lote não enxergam os resultados de execução uns dos outros: se o parâmetro de uma operação de escrita depende do valor de retorno de uma operação de leitura do mesmo lote, divida-os em duas rodadas, ou force uma de cada vez com disable_parallel_tool_use
- Uma tarefa pode exigir várias idas e voltas: a implementação do lado do host é essencialmente um laço — siga executando e devolvendo enquanto
stop_reason continuar sendo "tool_use", e só termina quando ele virar "end_turn"
>> Lição 3: Cinco tipos comuns de ferramenta: ler, escrever, executar, buscar, chamar