Pular para o conteúdo
iauaiiauai — portal de tecnologia, IA e Cloud
IAIntermediário

Function calling: dando ferramentas ao LLM

Implemente function calling: defina ferramentas em JSON Schema, execute o loop de chamadas. Código com validação, tratamento de erros e segurança.

Por Equipe iauai · 11 de agosto de 2026 · 12 min de leitura

Nesta página

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:

  1. Você envia: Prompt + lista de ferramentas disponíveis em JSON Schema
  2. Modelo responde: "Quero chamar get_product_price com product_id=123"
  3. Você executa: Chama a função real, obtém resultado
  4. Você realimenta: "Resultado: preço é R$ 99,90"
  5. 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

  1. Integre com agentes de IA em produção — function calling é o coração de agentes.
  2. Leia segurança em aplicações com LLM para validação em profundidade.
  3. 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 é:

  1. Envie prompt + tools em JSON Schema
  2. Modelo responde qual ferramenta chamar
  3. Você executa com validação
  4. Realimenta resultado
  5. 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.

Aprenda jogando

Mestre do Prompt

Monte o prompt certo com os blocos certos — e descarte o ruído.

Jogar Mestre do Prompt

Continue lendo