Function calling (também chamado de tool use) é o recurso que permite a um modelo de linguagem pedir a execução de uma função do seu sistema, como consultar um frete, buscar um pedido ou agendar um horário, devolvendo o nome da função e os argumentos em JSON. Structured outputs é o recurso irmão: obriga a resposta do modelo a seguir exatamente um JSON Schema que você definiu. Os dois existem para o mesmo fim: trocar texto livre, difícil de processar, por dados estruturados que o seu código consegue usar sem adivinhação.
A regra prática é simples. Use function calling quando o modelo precisa agir ou buscar informação que não tem. Use structured outputs quando você precisa que a resposta final venha num formato fixo, como extrair dados de um documento. E prefira o modo estrito (strict) oferecido pelas APIs a “pedir JSON no prompt”, que funciona na maior parte das vezes, mas não sempre. Este guia mostra o fluxo de mensagens, exemplos com os SDKs da OpenAI e da Anthropic, validação com Pydantic e os erros que mais aparecem em produção.
Resumo rápido
- No function calling, o modelo só pede a chamada; quem executa é o seu código, que devolve o resultado ao modelo.
- Ferramentas e formatos de saída são descritos em JSON Schema: tipos, campos obrigatórios e descrições.
- Pedir JSON no prompt não garante formato válido. Structured outputs e strict mode restringem a geração ao esquema.
- Mesmo com modo estrito, valide no seu código (Pydantic, por exemplo): esquema correto não significa conteúdo correto.
- Os modos estritos aceitam só um subconjunto do JSON Schema. Confira as limitações na documentação de cada fornecedor.
O conceito: o modelo pede, o seu código executa
Um modelo de linguagem não tem acesso ao seu banco de dados, à internet ou ao relógio do servidor. O que ele faz bem é entender um pedido em linguagem natural e decidir o que seria necessário para atendê-lo. Function calling formaliza isso: você descreve as funções disponíveis, e o modelo, quando achar útil, responde com um pedido estruturado em vez de texto.
Esse desenho tem uma consequência de segurança importante: nada acontece sem passar pelo seu código. Você pode validar os argumentos, recusar chamadas, pedir confirmação humana ou registrar tudo em log. É o mesmo mecanismo que está por trás dos agentes de IA, que basicamente repetem esse ciclo várias vezes até concluir uma tarefa.
O fluxo de mensagens, passo a passo
Independentemente do fornecedor, o ciclo tem quatro etapas:
- Definição: você envia a mensagem do usuário junto com a lista de ferramentas. Cada uma tem nome, descrição e parâmetros em JSON Schema.
- Pedido do modelo: o modelo responde com uma ou mais chamadas de ferramenta, cada uma com um identificador, o nome e os argumentos.
- Execução: seu código executa a função real e monta uma mensagem de resultado, ligada à chamada pelo identificador.
- Resposta final: você envia o histórico mais os resultados. O modelo responde ao usuário ou pede outra ferramenta.
Os nomes mudam entre APIs. Na API Responses da OpenAI, o pedido chega como item function_call (com call_id e arguments em string JSON), e você responde com um item function_call_output. Na API Messages da Anthropic, a resposta tem stop_reason igual a "tool_use" e blocos tool_use (com id e input já como objeto); você devolve blocos tool_result numa mensagem do usuário, referenciando o tool_use_id.
Os dois fornecedores também permitem controlar a escolha: deixar o modelo decidir (auto), obrigá-lo a chamar alguma ferramenta ou uma ferramenta específica, ou desativar chamadas paralelas quando a ordem importa.
JSON Schema: o contrato entre você e o modelo
JSON Schema é um padrão para descrever a forma de um JSON: quais campos existem, seus tipos, quais são obrigatórios e quais valores são permitidos. O modelo usa o esquema e, principalmente, as descrições para decidir quando chamar a ferramenta e como preencher os argumentos.
Um esquema bem feito para uma ferramenta de frete:
{
"type": "object",
"properties": {
"cep": {"type": "string", "description": "CEP de destino, só dígitos, 8 caracteres"},
"peso_kg": {"type": "number", "description": "Peso total do pedido em quilos"},
"modalidade": {"type": "string", "enum": ["economica", "expressa"]}
},
"required": ["cep", "peso_kg", "modalidade"],
"additionalProperties": false
}
Três detalhes fazem diferença: enum para valores fechados (evita “Expressa”, “express”, “rápida”), descrições com formato esperado e additionalProperties: false, que os modos estritos de ambos os fornecedores exigem em todo objeto.
Pedir JSON no prompt vs. structured outputs
Muita aplicação ainda faz assim: “Responda apenas com um JSON no formato {…}”. Funciona na maioria das chamadas, mas às vezes o modelo adiciona um texto antes, esquece um campo, inventa outro ou gera um JSON quebrado. Em escala, esse “às vezes” vira dezenas de erros por dia.
Structured outputs resolvem isso restringindo a geração: o modelo só consegue produzir tokens que mantenham a saída válida segundo o esquema. Há um meio-termo que vale conhecer, o “JSON mode”, que garante JSON sintaticamente válido, mas não garante aderência ao seu esquema.
| Abordagem | JSON válido | Segue o esquema | Quando usar |
|---|---|---|---|
| Pedir JSON no prompt | Não garantido | Não garantido | Protótipos, modelos sem suporte a modo estrito |
| JSON mode | Sim | Não garantido | Legado; prefira structured outputs quando disponível |
| Structured outputs (resposta) | Sim | Sim, dentro do subconjunto suportado | Extração de dados, classificação, respostas para outro sistema |
| Function calling com strict | Sim | Sim, nos argumentos da ferramenta | Quando o modelo precisa acionar funções com argumentos confiáveis |
Duas ressalvas. Primeiro, os modos estritos aceitam só parte do JSON Schema: a documentação da Anthropic, por exemplo, lista que restrições numéricas (minimum, maximum) e de tamanho de string não são suportadas, e esquemas recursivos também não; a OpenAI exige que todos os campos estejam em required, representando campos opcionais com união com null. Segundo, a garantia tem exceções documentadas: recusas do modelo e respostas cortadas pelo limite de tokens podem sair fora do esquema. Trate esses casos no código.
Exemplos de código: OpenAI e Anthropic
Os exemplos usam Python e leem o ID do modelo de uma variável de ambiente. Os modelos disponíveis mudam com frequência; confira a página de modelos de cada fornecedor e se o modelo escolhido suporta structured outputs.
Function calling com o SDK da OpenAI (API Responses)
Cenário: um e-commerce de cosméticos em Belo Horizonte quer que o assistente informe o frete.
import json, os
from openai import OpenAI
client = OpenAI()
MODELO = os.environ["MODELO_OPENAI"]
def calcular_frete(cep, peso_kg, modalidade):
return {"valor": 23.90, "prazo_dias": 4} # aqui entra sua API de frete
tools = [{
"type": "function",
"name": "calcular_frete",
"description": "Calcula valor e prazo do frete para um CEP brasileiro.",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"cep": {"type": "string", "description": "8 dígitos, sem hífen"},
"peso_kg": {"type": "number"},
"modalidade": {"type": "string", "enum": ["economica", "expressa"]},
},
"required": ["cep", "peso_kg", "modalidade"],
"additionalProperties": False,
},
}]
entrada = [{"role": "user", "content": "Frete expresso de 2 kg para 30140071?"}]
resp = client.responses.create(model=MODELO, tools=tools, input=entrada)
entrada += resp.output
for item in resp.output:
if item.type == "function_call":
args = json.loads(item.arguments)
entrada.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(calcular_frete(**args)),
})
final = client.responses.create(model=MODELO, tools=tools, input=entrada)
print(final.output_text)
Para extrair dados com um formato fixo, a OpenAI oferece client.responses.parse, que aceita um modelo Pydantic em text_format e devolve o objeto pronto em response.output_parsed.
Structured outputs e strict tool use com o SDK da Anthropic
Cenário: uma clínica odontológica em Recife recebe pedidos de agendamento por mensagem e quer extrair os dados num formato fixo para gravar no sistema.
import os
from anthropic import Anthropic
from pydantic import BaseModel
client = Anthropic()
MODELO = os.environ["MODELO_CLAUDE"]
class PedidoAgendamento(BaseModel):
nome: str
procedimento: str
periodo_preferido: str
telefone: str | None
msg = ("Oi, aqui é a Marina Souza, queria marcar uma limpeza, "
"de preferência de manhã. Meu número é 81 99999-0000.")
resp = client.messages.parse(
model=MODELO,
max_tokens=1024,
messages=[{"role": "user", "content": f"Extraia os dados do pedido: {msg}"}],
output_format=PedidoAgendamento,
)
pedido = resp.parsed_output
print(pedido.nome, pedido.procedimento)
O helper messages.parse converte o modelo Pydantic em esquema, envia no parâmetro output_config.format da API, valida a resposta e devolve o objeto em parsed_output. Para ferramentas, basta adicionar "strict": True à definição (ao lado de name e input_schema) para que os argumentos sigam o esquema. Se você tiver código antigo usando o parâmetro output_format em chamadas cruas à API, a documentação o marca como obsoleto em favor de output_config.
Validação com Pydantic: a camada que não pode faltar
Pydantic é a biblioteca Python mais usada para validar dados a partir de classes com anotações de tipo. Ela serve para três coisas neste contexto: gerar o JSON Schema, validar a saída e expressar regras que o modo estrito não cobre.
Mesmo com structured outputs, o esquema garante forma, não verdade. O modelo pode devolver um CEP com 7 dígitos dentro de um campo string perfeitamente válido. Regras de negócio ficam no seu código:
from pydantic import BaseModel, ValidationError, field_validator
class Frete(BaseModel):
cep: str
peso_kg: float
@field_validator("cep")
@classmethod
def cep_valido(cls, v):
digitos = "".join(c for c in v if c.isdigit())
if len(digitos) != 8:
raise ValueError("CEP precisa ter 8 dígitos")
return digitos
try:
frete = Frete.model_validate_json(texto_do_modelo)
except ValidationError as erro:
# devolva o erro ao modelo e peça correção, ou escale para um humano
print(erro)
No function calling, uma boa prática é devolver o erro de validação como resultado da ferramenta (na Anthropic, o bloco tool_result aceita is_error). O modelo lê a mensagem e, em geral, corrige os argumentos na tentativa seguinte. Limite o número de tentativas para não entrar em loop.
Erros comuns
- Executar argumentos sem validar. Nunca passe argumentos do modelo direto para SQL, comandos de shell ou APIs de pagamento. Valide tipo, faixa e permissão do usuário.
- Descrições vagas. “Busca dados” não ajuda. Diga o que a ferramenta faz, o que retorna e quando não deve ser usada.
- Esquema com recurso não suportado. Usar
minimumoumaxLengthno modo estrito pode gerar erro na requisição. Mova essas regras para o Pydantic. - Esquecer
additionalProperties: false. É exigido pelos modos estritos em cada objeto, inclusive nos aninhados. - Ignorar chamadas paralelas. O modelo pode pedir várias ferramentas numa resposta. Seu código precisa processar todas e devolver um resultado para cada identificador.
- Não tratar recusa e corte por tokens. Verifique o motivo de parada antes de confiar no JSON.
- Ferramentas demais. Cada definição ocupa tokens em toda chamada e aumenta a chance de escolha errada. Se as integrações precisam ser compartilhadas entre aplicações, considere o Model Context Protocol (MCP).
Quando usar cada abordagem
| Situação | Abordagem recomendada |
|---|---|
| Extrair campos de e-mails, notas ou mensagens | Structured outputs na resposta |
| Classificar tickets em categorias fixas | Structured outputs com enum |
| Assistente que consulta pedidos, estoque ou agenda | Function calling com strict |
| Fluxo com várias ações encadeadas e decisões | Function calling em loop (agente), com limites |
| Modelo local ou sem suporte a modo estrito | JSON no prompt + validação Pydantic + nova tentativa |
| Texto livre para humanos (resposta de atendimento) | Nenhuma das duas; texto comum basta |
Se o canal for WhatsApp, combine function calling com as regras de segurança do guia de integração de IA no WhatsApp: mensagens de clientes são entrada não confiável e podem tentar manipular o modelo para chamar ferramentas indevidas.
Perguntas frequentes
Function calling e tool use são a mesma coisa?
Sim, no essencial. “Function calling” é o nome popularizado pela OpenAI; “tool use” é o termo usado pela Anthropic. Ambos descrevem o modelo pedindo a execução de uma função descrita por você, com argumentos em JSON.
Structured outputs dispensam validação no meu código?
Não. Eles garantem que o JSON segue o esquema, dentro das limitações documentadas, mas não que os valores estão corretos. CEP, CPF, datas e regras de negócio precisam de validação própria.
Modelos open source suportam function calling?
Muitos modelos abertos são treinados para isso, e ferramentas de execução local oferecem suporte a ferramentas e saídas com esquema. A confiabilidade varia bastante entre modelos e tamanhos, então teste com seus próprios casos antes de colocar em produção.
O modelo pode chamar uma função que eu não defini?
Ele só recebe as ferramentas que você enviou, mas pode errar nome ou argumentos sem o modo estrito. Por segurança, seu código deve aceitar apenas nomes de uma lista fixa e ignorar o resto.
Usar ferramentas deixa a chamada mais cara?
Sim. As definições das ferramentas contam como tokens de entrada em toda requisição, e cada ida e volta de ferramenta é uma nova chamada. Mantenha poucas ferramentas, descrições objetivas e resultados enxutos.