O problema real
Você quer que um LLM execute ações — buscar dados do banco, chamar API, calcular coisa. A solução óbvia é colocar tudo no prompt: "se o usuário pedir o preço, chama a API de produtos". Funciona uma vez. Duas semanas depois, o modelo alucinava qual API chamar, os parâmetros estavam errados, e você passou um dia debugando. Descobre que o modelo precisa de "ferramentas" declaradas explicitamente em JSON Schema, um loop de executar-e-realimentar, e validação em cada passo.
Function calling (tool use) é a forma correta. O LLM decide qual ferramenta chamar, você executa, e realimenta com o resultado. Controle total, segurança, auditoria.
Como funciona o ciclo de function calling?
É um loop com 4 passos:
- Você envia: Prompt + lista de ferramentas disponíveis em JSON Schema
- Modelo responde: "Quero chamar
get_product_pricecomproduct_id=123" - Você executa: Chama a função real, obtém resultado
- Você realimenta: "Resultado: preço é R$ 99,90"
- Modelo conclui: Formata resposta final para o usuário
Repete até o modelo terminar (sem mais chamadas).
Mão na massa
1. Declarar ferramentas em JSON Schema
Schemas definem: nome, descrição, parâmetros, tipos, obrigatoriedade.
import json
from typing import Any
# Ferramentas disponíveis
tools = [
{
"type": "function",
"function": {
"name": "get_product_price",
"description": "Retorna o preço de um produto. Use quando o usuário perguntar 'quanto custa' ou 'qual o preço'.",
"parameters": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"description": "ID único do produto (ex: 'PROD-001')"
},
"currency": {
"type": "string",
"enum": ["BRL", "USD"],
"description": "Moeda desejada. Padrão: BRL"
}
},
"required": ["product_id"]
}
}
},
{
"type": "function",
"function": {
"name": "check_stock",
"description": "Verifica se um produto está em estoque e quantidade disponível.",
"parameters": {
"type": "object",
"properties": {
"product_id": {
"type": "string",
"description": "ID único do produto"
},
"warehouse": {
"type": "string",
"enum": ["SP", "RJ", "MG"],
"description": "Região do armazém. Padrão: SP"
}
},
"required": ["product_id"]
}
}
},
{
"type": "function",
"function": {
"name": "create_order",
"description": "Cria um pedido. Requer confirmação do usuário antes de executar.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"customer_email": {"type": "string"}
},
"required": ["product_id", "quantity", "customer_email"]
}
}
}
]
print(json.dumps(tools, indent=2))
Regra de ouro: A descrição é um prompt. Escreva claro, com exemplos. Aqui 90% das pessoas erra — descrevem mal, e o modelo nunca chama a ferramenta.
2. Loop completo com OpenAI
from openai import OpenAI
def process_tool_call(tool_name: str, tool_args: dict) -> str:
"""Executa a ferramenta solicitada. VALIDA SEMPRE."""
# Validação de segurança: rejeiça IDs suspeitos
if "product_id" in tool_args:
product_id = tool_args["product_id"]
if not product_id.startswith("PROD-"):
return json.dumps({"error": f"Invalid product_id: {product_id}"})
# Simula execução das ferramentas
if tool_name == "get_product_price":
product_id = tool_args["product_id"]
currency = tool_args.get("currency", "BRL")
# Aqui você chamaria a API real
prices = {
"PROD-001": {"BRL": 99.90, "USD": 19.99},
"PROD-002": {"BRL": 199.90, "USD": 39.99}
}
if product_id in prices:
price = prices[product_id][currency]
return json.dumps({"product_id": product_id, "price": price, "currency": currency})
else:
return json.dumps({"error": f"Product not found: {product_id}"})
elif tool_name == "check_stock":
product_id = tool_args["product_id"]
warehouse = tool_args.get("warehouse", "SP")
# Simula estoque
stock = {
"PROD-001": {"SP": 10, "RJ": 5, "MG": 0},
"PROD-002": {"SP": 0, "RJ": 3, "MG": 2}
}
if product_id in stock:
qty = stock[product_id][warehouse]
return json.dumps({
"product_id": product_id,
"warehouse": warehouse,
"in_stock": qty > 0,
"quantity_available": qty
})
else:
return json.dumps({"error": f"Product not found: {product_id}"})
elif tool_name == "create_order":
# ATENÇÃO: antes de criar, SEMPRE confirma
return json.dumps({
"error": "Order creation requires explicit user confirmation. Use natural language to ask the user to confirm."
})
else:
return json.dumps({"error": f"Unknown tool: {tool_name}"})
def chat_with_tools(user_message: str, conversation_history: list = None) -> str:
"""Chat loop com function calling."""
client = OpenAI()
if conversation_history is None:
conversation_history = []
# Adiciona mensagem do usuário
conversation_history.append({
"role": "user",
"content": user_message
})
# Loop até o modelo terminar (sem mais tool calls)
max_iterations = 10
iteration = 0
while iteration < max_iterations:
iteration += 1
# Chama modelo com tools disponíveis
response = client.chat.completions.create(
model="gpt-4",
messages=conversation_history,
tools=tools,
tool_choice="auto" # O modelo decide se chama tool
)
# Verifica se modelo quer usar tool
if response.stop_reason == "tool_calls":
# Processa cada tool call
for tool_call in response.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f"🔧 Chamando: {tool_name}({tool_args})")
# Executa ferramenta
tool_result = process_tool_call(tool_name, tool_args)
print(f"📊 Resultado: {tool_result}")
# Realimenta o modelo
conversation_history.append({
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_name,
"arguments": json.dumps(tool_args)
}
}
]
})
conversation_history.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
})
else:
# Modelo terminou (stop_reason == "end_turn")
final_response = response.choices[0].message.content
conversation_history.append({
"role": "assistant",
"content": final_response
})
return final_response
return "Máximo de iterações atingido"
# Teste
response = chat_with_tools("Qual é o preço do produto PROD-001?")
print(f"\n💬 Assistente: {response}")
response = chat_with_tools("E qual é o estoque em São Paulo?")
print(f"\n💬 Assistente: {response}")
Output esperado:
🔧 Chamando: get_product_price({'product_id': 'PROD-001'})
📊 Resultado: {"product_id": "PROD-001", "price": 99.9, "currency": "BRL"}
💬 Assistente: O produto PROD-001 custa R$ 99,90.
3. Parallelização: múltiplas ferramentas
# Se o modelo quer chamar várias ferramentas, processa em paralelo
from concurrent.futures import ThreadPoolExecutor
def process_tool_calls_parallel(tool_calls: list) -> dict:
"""Executa múltiplas tool calls em paralelo."""
results = {}
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {}
for tool_call in tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
future = executor.submit(process_tool_call, tool_name, tool_args)
futures[tool_call.id] = (tool_name, tool_args, future)
for tool_id, (tool_name, tool_args, future) in futures.items():
result = future.result()
results[tool_id] = {"tool_name": tool_name, "result": result}
print(f"✓ {tool_name} concluído")
return results
# No loop anterior, substitui por:
if response.stop_reason == "tool_calls":
results = process_tool_calls_parallel(response.tool_calls)
4. Tratamento de erro: retry com feedback
def chat_with_tools_robust(user_message: str) -> str:
"""Chat com retry automático quando ferramenta falha."""
client = OpenAI()
conversation_history = [{
"role": "user",
"content": user_message
}]
max_iterations = 10
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.chat.completions.create(
model="gpt-4",
messages=conversation_history,
tools=tools,
tool_choice="auto"
)
if response.stop_reason == "tool_calls":
has_error = False
for tool_call in response.tool_calls:
tool_name = tool_call.function.name
try:
tool_args = json.loads(tool_call.function.arguments)
# Validação de negócio
if tool_name == "create_order" and "quantity" in tool_args:
if tool_args["quantity"] < 1:
raise ValueError("Quantidade deve ser > 0")
result = process_tool_call(tool_name, tool_args)
# Se resultado é erro, marca para retry
result_obj = json.loads(result)
if "error" in result_obj:
has_error = True
result = json.dumps({
"error": result_obj["error"],
"retry": True,
"suggestion": "Por favor, tente com outro product_id ou parâmetro."
})
except (json.JSONDecodeError, ValueError) as e:
has_error = True
result = json.dumps({"error": f"Erro ao processar: {str(e)}"})
# Realimenta
conversation_history.append({
"role": "assistant",
"content": ""
})
conversation_history.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
else:
# Sucesso
return response.choices[0].message.content
return "Erro: máximo de retentativas atingido"
Armadilhas comuns
Armadilha 1: Descrição ruim = modelo nunca chama
# ❌ Ruim
"name": "get_price"
# ✅ Bom
"name": "get_product_price",
"description": "Retorna o preço atual de um produto. Use SEMPRE que o usuário perguntar 'quanto custa', 'qual o preço', 'preço do produto', etc."
A descrição é um prompt — ela determina quando o modelo chama. Se estiver vaga, ele ignora a ferramenta.
Armadilha 2: Confiar no que o modelo mandou
Nunca execute product_id = tool_args["product_id"] diretamente. O modelo pode enviar valores inválidos, injections, etc.
# ❌ Perigoso
product_id = tool_args["product_id"]
run_query(f"SELECT * FROM products WHERE id = {product_id}")
# ✅ Seguro
product_id = tool_args.get("product_id", "")
if not product_id.startswith("PROD-"):
raise ValueError("Invalid product_id")
# Só depois usa
Armadilha 3: Esquecer context limit
Se o loop roda 20 vezes com muitas tool calls, o histórico cresce e estora o context window.
# Limpa histórico antigo se passar de 50 mensagens
if len(conversation_history) > 50:
conversation_history = conversation_history[-40:]
Armadilha 4: Tool call sem confirmação de negócio
Criar pedido, deletar dados, alterar permissões — SEMPRE pede confirmação explícita antes de executar. Não deixa o modelo chamar sozinho.
# Na ferramenta create_order, retorna:
{"confirmation_required": True, "message": "Confirma criar pedido para customer@email.com?"}
Quando não usar function calling
- Lógica muito complexa: Se a decisão de qual ferramenta usar exige análise profunda, better fazer isso em código normal.
- Latência crítica: Cada iteração adiciona ~200-500ms. Se precisa < 100ms, chama a ferramenta diretamente em código.
- Segurança extrema: Function calling é mais difícil de auditar. APIs síncronas são mais previsíveis.
- Modelo pequeno (< 7B): Mistral 7B funciona mas comete erros. Use GPT-4 ou Llama 2 13B.
Próximos passos
- Integre com agentes de IA em produção — function calling é o coração de agentes.
- Leia segurança em aplicações com LLM para validação em profundidade.
- Monte seus evals com avaliação de LLM com evals — mede taxa de chamadas corretas.
Resumo prático
Function calling é essencial quando o LLM precisa fazer coisas (chamar APIs, bancos, ferramentas). O loop é:
- Envie prompt + tools em JSON Schema
- Modelo responde qual ferramenta chamar
- Você executa com validação
- Realimenta resultado
- Repete até terminar
Sempre valide argumentos, sempre pede confirmação para ações críticas, sempre limita iterações. Com essas três regras, fica seguro e auditável.