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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
weather_tool = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current temperature and conditions for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'Austin, TX'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from openai import OpenAI
import json

client = OpenAI()
messages = [{"role": "user", "content": "What's the weather in Austin?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=[weather_tool],
)
message = response.choices[0].message
messages.append(message)

if message.tool_calls:
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(args["city"], args.get("unit", "celsius"))
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

    final = client.chat.completions.create(model="gpt-4o", messages=messages)
    print(final.choices[0].message.content)

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:

ConceitoOpenAIAnthropic
Campo com a lista de toolstools, cada tool envolto em {"type": "function", "function": {...}}tools, cada tool direto: {"name", "description", "input_schema"}
Chave do schema dos argumentosparametersinput_schema
Pedido do modeloarray tool_calls na mensagem, arguments como string JSONbloco de conteúdo tool_use, input já como objeto interpretado
Envio do resultadonova mensagem, role: "tool", tool_call_idnova mensagem de usuário, bloco de conteúdo type: "tool_result", tool_use_id
Sinalizar uma chamada que falhousem campo dedicado: coloque o erro em contentis_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.

Artigos relacionados