O problema real
Você quer que um LLM faça coisas, como consultar o estoque, buscar um preço ou abrir um chamado. A ideia ingênua é descrever tudo no prompt e pedir que o modelo "responda no formato certo" para você interpretar depois. Funciona em demonstração. Em produção, o modelo inventa nomes de função, esquece parâmetros e você gasta dias depurando texto livre.
Function calling, também chamado de tool use, resolve isso de forma estruturada. Você declara as ferramentas com JSON Schema, o modelo diz qual quer usar e com quais argumentos, o seu código executa e devolve o resultado. O modelo nunca executa nada. Quem executa, valida e decide o que é permitido é você.
Como funciona o ciclo
O ciclo se repete até o modelo parar de pedir ferramentas.
- Você envia a mensagem do usuário e a lista de ferramentas.
- O modelo responde com um pedido de ferramenta, por exemplo
consultar_precocom{"produto_id": "PROD-001"}. - Seu código valida os argumentos e executa a função de verdade.
- Você devolve o resultado ao modelo, ligado ao identificador do pedido.
- O modelo usa o resultado para responder ao usuário, ou pede outra ferramenta.
Os exemplos usam o SDK da OpenAI (pip install openai) e depois mostram a diferença para o SDK da Anthropic. Os nomes de campos mudam entre provedores, mas a lógica é a mesma. Defina a variável MODELO com o nome de um modelo que suporte ferramentas, conforme a documentação do seu provedor.
Mão na massa
1. Declarar as ferramentas
A descrição de cada ferramenta funciona como um prompt, porque é ela que ajuda o modelo a decidir quando chamar. Descrições vagas levam a ferramentas ignoradas ou mal usadas.
TOOLS = [
{
"type": "function",
"function": {
"name": "consultar_preco",
"description": "Retorna o preço atual de um produto. Use quando o usuário perguntar quanto custa.",
"parameters": {
"type": "object",
"properties": {
"produto_id": {"type": "string", "description": "Código do produto, como PROD-001"},
"moeda": {"type": "string", "enum": ["BRL", "USD"], "description": "Padrão BRL"},
},
"required": ["produto_id"],
},
},
},
{
"type": "function",
"function": {
"name": "consultar_estoque",
"description": "Informa a quantidade disponível de um produto em um armazém.",
"parameters": {
"type": "object",
"properties": {
"produto_id": {"type": "string", "description": "Código do produto, como PROD-001"},
"armazem": {"type": "string", "enum": ["SP", "RJ", "MG"], "description": "Padrão SP"},
},
"required": ["produto_id"],
},
},
},
]
2. Implementar as funções com validação
Trate os argumentos como entrada de usuário, porque na prática são. O modelo pode errar, ou ser induzido por um texto malicioso a enviar valores perigosos.
import json
PRECOS = {"PROD-001": {"BRL": 99.90, "USD": 19.99}, "PROD-002": {"BRL": 199.90, "USD": 39.99}}
ESTOQUE = {"PROD-001": {"SP": 10, "RJ": 5, "MG": 0}, "PROD-002": {"SP": 0, "RJ": 3, "MG": 2}}
def executar_ferramenta(nome: str, args: dict) -> str:
produto = args.get("produto_id", "")
if not isinstance(produto, str) or produto not in PRECOS:
return json.dumps({"erro": f"produto não encontrado: {produto!r}"})
if nome == "consultar_preco":
moeda = args.get("moeda", "BRL")
if moeda not in ("BRL", "USD"):
return json.dumps({"erro": "moeda inválida"})
return json.dumps({"produto_id": produto, "preco": PRECOS[produto][moeda], "moeda": moeda})
if nome == "consultar_estoque":
armazem = args.get("armazem", "SP")
if armazem not in ESTOQUE[produto]:
return json.dumps({"erro": "armazém inválido"})
return json.dumps({"produto_id": produto, "armazem": armazem, "quantidade": ESTOQUE[produto][armazem]})
return json.dumps({"erro": f"ferramenta desconhecida: {nome}"})
Aqui os dados são fixos para o exemplo rodar sozinho. Na vida real, a função consulta seu banco ou sua API, sempre com consultas parametrizadas.
3. O loop completo
import os
from openai import OpenAI
client = OpenAI()
MODELO = os.environ["MODELO"]
def conversar(mensagem: str, historico: list | None = None, max_voltas: int = 8) -> str:
historico = historico if historico is not None else []
historico.append({"role": "user", "content": mensagem})
for _ in range(max_voltas):
resp = client.chat.completions.create(
model=MODELO, messages=historico, tools=TOOLS, tool_choice="auto"
)
msg = resp.choices[0].message
historico.append(msg) # mantém a mensagem do assistente, com os tool_calls
if not msg.tool_calls:
return msg.content
for chamada in msg.tool_calls:
try:
args = json.loads(chamada.function.arguments)
resultado = executar_ferramenta(chamada.function.name, args)
except json.JSONDecodeError:
resultado = json.dumps({"erro": "argumentos não são um JSON válido"})
historico.append({
"role": "tool",
"tool_call_id": chamada.id,
"content": resultado,
})
return "Não consegui concluir dentro do limite de passos."
print(conversar("Quanto custa o PROD-001 e tem estoque em SP?"))
Três detalhes costumam quebrar implementações. O fim do ciclo é detectado pela ausência de tool_calls na mensagem, e não por um campo de motivo de parada. A mensagem do assistente com os pedidos de ferramenta precisa entrar no histórico antes dos resultados. E toda chamada pede uma resposta com o mesmo tool_call_id, mesmo quando deu erro. Devolver o erro como resultado deixa o modelo corrigir o argumento ou explicar o problema ao usuário.
Quando o modelo pede várias ferramentas de uma vez, o loop acima executa uma por vez. Se elas forem lentas e independentes, dá para rodá-las em paralelo com concurrent.futures, mantendo a ordem das respostas no histórico.
4. A mesma ideia com a API da Anthropic
No SDK da Anthropic (pip install anthropic), as ferramentas usam input_schema, o pedido chega como um bloco tool_use dentro de content, e o resultado volta numa mensagem do usuário com bloco tool_result. O sinal de que o modelo quer ferramentas é stop_reason == "tool_use".
import anthropic
claude = anthropic.Anthropic()
TOOLS_CLAUDE = [{
"name": "consultar_preco",
"description": "Retorna o preço atual de um produto. Use quando o usuário perguntar quanto custa.",
"input_schema": {
"type": "object",
"properties": {"produto_id": {"type": "string"}},
"required": ["produto_id"],
},
}]
mensagens = [{"role": "user", "content": "Quanto custa o PROD-001?"}]
while True:
resp = claude.messages.create(
model=os.environ["MODELO_CLAUDE"], max_tokens=1024,
tools=TOOLS_CLAUDE, messages=mensagens,
)
mensagens.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
break
resultados = [
{"type": "tool_result", "tool_use_id": b.id,
"content": executar_ferramenta(b.name, b.input)}
for b in resp.content if b.type == "tool_use"
]
mensagens.append({"role": "user", "content": resultados})
print("".join(b.text for b in resp.content if b.type == "text"))
Em produção, adicione o mesmo limite de voltas do exemplo anterior.
Ações com efeito: confirmação e permissões
Consultar é seguro. Criar pedido, apagar dados ou mudar permissões não é. Para esse tipo de ferramenta, não deixe o modelo executar sozinho. Retorne uma proposta e peça confirmação do usuário na interface, fora da conversa com o modelo.
def propor_pedido(produto_id: str, quantidade: int) -> dict:
# Não cria nada. A interface mostra o resumo e só um clique do usuário chama criar_pedido().
return {"status": "aguardando_confirmacao", "resumo": f"{quantidade} x {produto_id}"}
Dê a cada ferramenta o menor poder possível, como usuário de banco somente leitura para consultas, e use as permissões do usuário final, não as do sistema. Para os riscos de injeção de prompt, leia Segurança em aplicações com LLM.
Armadilhas comuns
Descrição vaga
Se o nome é get_price e a descrição está vazia, o modelo não sabe quando chamar. Descreva o que a ferramenta faz, quando usar e o formato dos argumentos.
Confiar nos argumentos
Valide tipo, formato e domínio de cada argumento, e nunca monte SQL ou comandos de shell concatenando o que veio do modelo.
Histórico que cresce sem controle
Cada volta adiciona mensagens, e os resultados das ferramentas podem ser grandes. Resuma ou corte resultados longos antes de devolvê-los, e acompanhe o tamanho do contexto.
Ferramentas demais
Com muitas ferramentas parecidas, o modelo erra mais na escolha. Mantenha poucas, com nomes distintos e propósito claro, e meça a taxa de acerto com evals.
Quando não usar function calling
- O fluxo é fixo e conhecido. Se você já sabe que sempre vai chamar A e depois B, escreva em código, sem pedir ao modelo para decidir.
- A latência é crítica. Cada volta é uma chamada ao modelo a mais.
- A tarefa exige garantias fortes sobre o que será executado. Nesse caso, restrinja as ferramentas ao mínimo ou use um fluxo determinístico com o modelo só na interpretação do texto.
Próximos passos
Function calling é a base dos agentes. Veja Agentes de IA em produção para orquestração, limites e custos. Para saber se o modelo escolhe a ferramenta certa, monte casos de teste com Avaliação de LLM com evals.
Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria