Uma proposta, não execução
Imagine que alguém pergunta: “Quantos carboidratos tem o arroz?” O modelo pode propor uma tool call: search_foods com o argumento {"query":"arroz"}. Essa saída é um dado do provedor, não uma chamada direta a uma função do aplicativo. O modelo ainda não sabe quais tipos de arroz estão no catálogo nem quantos carboidratos cada um contém: o aplicativo precisa fazer a busca.
O harness é o programa que recebe a proposta, decide se a aceita e conecta o nome a uma função local. O identificador da chamada fica separado para associar depois o resultado ao pedido correto. OpenAI, Anthropic e Google transportam tool calls em formatos diferentes, mas, depois do parsing, o despachador trabalha com o mesmo par: name e args.
O percurso completo
Para responder à pergunta sobre o arroz, o aplicativo primeiro verifica se search_foods é uma tool declarada e se a busca é permitida. Depois, o registro encaminha a chamada à função de busca. Essa consulta apenas lê o catálogo: devolve os itens encontrados para “arroz”, incluindo os carboidratos por 100 g, e não altera a dieta da pessoa.
Escolher a função correta
Um registro é uma lista fechada que associa cada nome de tool a uma função do aplicativo. Na pergunta sobre o arroz, o despachador recebe search_foods, encontra esse nome na lista e executa a função que busca alimentos. O registro não escolhe uma variedade de arroz nem calcula seus carboidratos: apenas decide qual função atende ao pedido. Se o nome não estiver na lista, não executa nenhuma função. A lista permanece fixa durante a interação, então o mesmo nome sempre leva à mesma ação.
No artigo sobre como definir as tools, vimos que cada tool é apresentada ao LLM por meio de name, description e inputSchema. Para search_foods, essas informações ajudam o modelo a enviar “arroz” como texto em query. Mas descrever o argumento esperado não garante que o modelo o envie corretamente. O despachador procura o name e passa os argumentos à função escolhida; ele não corrige um argumento incorreto. Por exemplo, antes de adicionar arroz a uma refeição, essa função verifica se o alimento existe e se a quantidade em gramas é válida.
Erros e efeitos
Se o modelo escrever o nome errado e pedir search_foodz, o despachador informa que não reconhece essa tool e não executa nada. Se pedir search_foods, mas a busca não encontrar arroz, o resultado informa isso. Se o catálogo estiver indisponível, o resultado também informa essa situação: “não encontrei arroz” e “não consigo consultar o catálogo” não significam a mesma coisa. Nenhum desses casos justifica inventar um valor de carboidratos.
Uma falha durante a execução de uma função é outro caso. Por exemplo, se o aplicativo tentar adicionar arroz a uma refeição e o acesso ao armazenamento falhar, o despachador devolve um erro controlado ao modelo para que ele explique que não foi possível confirmar o salvamento, sem interromper toda a conversa.
Uma consulta termina ao devolver o dado encontrado ou informar que não foi possível obtê-lo. Uma escrita exige a confirmação do efeito: se a pessoa pedir para adicionar arroz a uma refeição, não basta chamar a função; o aplicativo só deve informar sucesso depois que o alimento for salvo. Se não conseguir determinar se ele foi salvo, não repete a operação automaticamente, pois poderia adicioná-lo duas vezes.
o modelo propõe a ação; o código do aplicativo decide a autorização, executa o handler e verifica o resultado. O nome de uma tool nunca concede permissão por si só.
Devolver o resultado
Suponha que search_foods devolva o item “Arroz blanco (cocido)” (arroz branco cozido), que no catálogo indica 28 g de carboidratos por 100 g. O aplicativo envia esse resultado ao modelo junto com o identificador da tool call original, para que ele saiba a qual busca o resultado corresponde. Só então pode responder: “O arroz branco cozido do catálogo tem 28 g de carboidratos por 100 g”. Se aparecerem várias variedades de arroz, deve distingui-las; se não houver resultados, deve informar isso em vez de inventar um valor.
Cada provedor tem seu próprio formato para devolver o resultado: function_call_output na OpenAI, tool_result na Anthropic e function_result no Google Interactions. Depois de enviá-lo, o aplicativo pede ao modelo que continue e escreva a resposta para a pessoa.
Testes do contrato
Para testar o despachador sem depender de um LLM remoto, os testes simulam suas tool calls. Um teste envia search_foods com {"query":"Arroz"} e verifica se a função de busca devolve o arroz dos dados de teste. Outras chamadas cobrem os demais nomes publicados. Os testes também comparam esses nomes com os handlers registrados: se uma tool for anunciada sem código para executá-la, o teste falha.
Em seguida, testam nomes desconhecidos e verificam que nenhum dado seja escrito. Por fim, repetem cada caso com dados iniciais independentes: a mesma chamada deve seguir o mesmo caminho e produzir o mesmo resultado. Isso detecta mudanças acidentais no despachador sem esperar que um modelo escolha a tool em uma conversa real.
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 reconstruir uma chamada completa a partir do stream; aqui vimos onde ela vira código em execução. A próxima fronteira é validar os argumentos de forma sistemática antes de qualquer efeito.