O que o function calling realmente faz em um LLM
Pergunte a um modelo “qual é a previsão do tempo em Austin agora” sem nenhuma conexão com o mundo externo, e ele tem duas opções: dizer que não sabe, ou inventar alguma coisa. O function calling dá a ele uma terceira opção. Ele descreve o que precisa, passa essa descrição para o seu código e espera uma resposta real antes de responder.
O nome é um pouco enganoso. O modelo não chama nada. Ele emite um bloco de dados estruturados dizendo qual tool ele quer e com quais argumentos, e a sua aplicação decide se executa isso e o que fazer com o resultado. “Tool use” é o termo mais preciso, e é o que a documentação da Anthropic usa; OpenAI e Google costumam dizer function calling. O mecanismo é o mesmo nos dois nomes. O modelo só produz um pedido, nunca o efeito colateral.
Por que o modelo pede tools mas nunca os executa
Imagine um cliente em um balcão que pode apontar para itens no cardápio e dizer quantos quer, mas nunca entra atrás do balcão. O modelo é o cliente: ele lê o cardápio que você deu a ele (as definições dos tools), escolhe um item e diz quantos quer (os argumentos). O seu código é a pessoa atrás do balcão que prepara o pedido, e que pode recusar um pedido que não faz sentido.
Essa separação sustenta todo o modelo de segurança. Uma tool call é uma sugestão: o seu código pode validar ou rejeitá-la exatamente como faria com qualquer outra entrada vinda de uma fonte não confiável, porque é isso que ela é. Um modelo que sofreu prompt injection de uma página web hostil ainda só consegue pedir uma tool call. Ele não consegue pular a sua validação e chegar direto ao banco de dados.
Como definir um tool com JSON Schema
Cada provedor quer as mesmas três coisas para cada tool: um nome, uma descrição que o modelo usa para decidir quando o tool se aplica, e um JSON Schema para os argumentos. Aqui está um tool de clima para a OpenAI Chat Completions API:
| |
Dedique mais tempo à descrição do que ao schema. O modelo escolhe um tool comparando o pedido do usuário com as descrições da lista que você enviou. Uma descrição vaga (“coisas de clima”) fica de fora quando existe um tool melhor descrito que faz outra coisa, ou é ativada em pedidos que não deveria tratar. Escreva como se tivesse que explicar a função para um colega que vai ler exatamente uma frase sobre ela.
Como funciona o ciclo de requisição e resposta do function calling
Uma tool call exige pelo menos duas requisições. Aqui está o percurso completo com a API da OpenAI:
| |
A primeira resposta não responde à pergunta. Ela chega com finish_reason
igual a tool_calls e sem conteúdo de texto, porque o modelo decidiu que
precisa de get_weather antes de conseguir dizer algo útil.
call.function.arguments chega como uma string JSON, não como um objeto
já interpretado, então json.loads não é opcional. Você mesmo executa
get_weather (é uma função Python comum; o modelo não tem como
executá-la), e depois adiciona o resultado como uma mensagem com role: "tool" e o tool_call_id correspondente.
Só a segunda requisição, que agora carrega esse resultado, devolve uma
frase de verdade. Se você adicionar a mensagem do assistente com seu
tool_calls mas deixar de fora o resultado do tool correspondente, a API
rejeita a próxima requisição em vez de tentar adivinhar.
Em que o tool use da Anthropic difere do function calling da OpenAI
Mesmo mecanismo, nomes de campos diferentes. Se você troca de provedor ou dá suporte aos dois, é aqui que vêm os seus bugs:
| Conceito | OpenAI | Anthropic |
|---|---|---|
| Campo com a lista de tools | tools, cada tool envolto em {"type": "function", "function": {...}} | tools, cada tool direto: {"name", "description", "input_schema"} |
| Chave do schema dos argumentos | parameters | input_schema |
| Pedido do modelo | array tool_calls na mensagem, arguments como string JSON | bloco de conteúdo tool_use, input já como objeto interpretado |
| Envio do resultado | nova mensagem, role: "tool", tool_call_id | nova mensagem de usuário, bloco de conteúdo type: "tool_result", tool_use_id |
| Sinalizar uma chamada que falhou | sem campo dedicado: coloque o erro em content | is_error: true no bloco tool_result |
A armadilha está no formato dos argumentos. A Anthropic entrega input já
interpretado como um dict Python ou um objeto JS; a OpenAI entrega uma
string que você mesmo precisa passar por json.loads. Código escrito para
um provedor e apontado para o outro importa sem problemas e depois falha na
primeira tool call. Se você dá suporte aos dois, escreva uma única função
de normalização por onde os dois caminhos passam antes de as suas
implementações de tools verem os argumentos.
Como funcionam as chamadas paralelas e os loops de várias etapas
As duas APIs podem retornar mais de uma tool call em uma única resposta.
Peça a previsão do tempo de três cidades e você recebe três entradas em
tool_calls, não três idas e voltas separadas. Execute-as (em paralelo, se
forem independentes) e adicione os três resultados antes da requisição
seguinte. Cada ID de chamada que o modelo emitiu precisa de um resultado;
envie apenas um dos três e a requisição falha.
Algumas tarefas exigem várias rodadas: buscar um usuário, depois o histórico de pedidos dele, depois sinalizar o pedido atrasado. Escreva isso como um loop que continua chamando o modelo e executando o que ele pede até chegar uma resposta sem tool calls. Depois, coloque um limite. Nada na API impede um modelo de pedir o mesmo tool nove vezes seguidas quando os resultados não o aproximam de uma resposta. Um limite fixo, digamos 10 ou 20 iterações, transforma isso em um erro claro em vez de uma conta fora de controle.
Erros comuns no function calling
Tratar os argumentos como entrada confiável. args["city"] vem de um
modelo, o que significa que vem de qualquer texto que alimentou a
conversa, incluindo uma página web raspada ou um documento que o modelo
deveria resumir. Valide antes que isso chegue a uma consulta de banco de
dados ou um comando de shell. Um tool que constrói SQL interpolando os
argumentos do modelo tem a mesma superfície de injeção que um que constrói
SQL a partir de um formulário web.
Confiar no schema como se fosse uma garantia. O modelo é bom em seguir
required e enum, mas não é perfeito. Em algum momento ele vai enviar
"unit": "F" em vez de "fahrenheit", ou vai pular um campo obrigatório.
Valide, e devolva uma mensagem de erro clara como resultado do tool em vez
de quebrar. Diga ao modelo o que estava errado e, muitas vezes, ele
consegue tentar de novo com argumentos corrigidos na rodada seguinte.
Deixar o modelo escolher tools destrutivos por conta própria. Se um
tool apaga dados ou envia um e-mail, deixar tool_choice no automático é o
padrão errado. Mantenha os tools destrutivos fora da lista até que uma
pessoa aprove a ação específica, ou torne o efeito reversível (um rascunho
em vez de um e-mail enviado) e deixe uma etapa separada confirmá-lo.
Quando não usar function calling
Cada tool call adiciona uma ida e volta ao modelo, mais a latência que o próprio tool tiver. Se o “tool” só fosse retornar um fato estático, coloque esse fato no prompt ou no contexto recuperado e evite a ida e volta; é para isso que serve o retrieval do RAG. E se a decisão é na verdade uma classificação com um conjunto pequeno e fixo de resultados (encaminhar este ticket para uma de quatro filas), uma chamada tipada e não generativa responde mais rápido e mais barato do que pedir a um modelo de chat genérico que escolha um tool. É para isso que os modelos system one são feitos.
O function calling compensa o custo quando a ação depende de dados em tempo de execução que o modelo não pode ter no prompt, e o conjunto de ações possíveis é muito aberto para codificar como uma máquina de estados. Também é o loop por trás de todo agente CLI de código: ler um arquivo, executar um teste, decidir o que fazer depois, chamar outro tool.
Comece com um tool, uma função Python fixa por trás dele, e um loop que para depois de uma única ida e volta. Acerte a validação de argumentos e o caminho de erro antes de adicionar um segundo tool. A maioria dos bugs em uma configuração com vários tools acaba sendo o primeiro tool lidando mal com argumentos que não esperava.