Function calling e tool use: como LLMs executam ações no mundo real

Como function calling permite que LLMs executem ações reais: implementação prática, casos de uso e melhores práticas.

Quando a IA finalmente para de só “conversar” e começa a “fazer”

Outubro de 2024. Roberto Fonseca, CTO da FinanceHub (fintech B2B, 95 funcionários), estava animado mas cético.

Ele havia lido sobre “function calling” – a capacidade de LLMs não apenas gerarem texto, mas também executarem ações reais chamando APIs, consultando bancos de dados, enviando emails.

Roberto queria automatizar análise de crédito. O processo manual demorava 2-3 horas por cliente:

  1. Analista consulta 4 sistemas diferentes
  2. Cruza informações
  3. Calcula scores
  4. Gera relatório
  5. Envia recomendação

“Se function calling funciona mesmo, posso automatizar isso,” pensou Roberto.

Mas tinha dúvidas:

  • Como LLM “sabe” qual função chamar?
  • É confiável? E se chamar a função errada?
  • Como lidar com APIs que têm side effects (transferências, por exemplo)?

Roberto decidiu testar.

MVP (2 semanas, R$ 8 mil):

Implementou agent com 5 “tools”:

  1. consultar_serasa() - score de crédito
  2. buscar_historico_cliente() - transações anteriores
  3. calcular_score_interno() - algoritmo proprietário
  4. gerar_relatorio() - monta PDF
  5. enviar_email() - notifica time comercial

Resultado do teste (50 análises):

  • Precisão: 94% (comparado com análise manual)
  • Tempo: 2h30min → 8 minutos (-95%)
  • Custo por análise: R$ 85 → R$ 3,20 (-96%)
  • Erros: 3 casos (6%) - agent chamou função com parâmetro errado

“Funciona, mas precisa de safeguards,” concluiu Roberto.

Após 3 meses em produção (com melhorias):

  • 840 análises automatizadas
  • Precisão: 97%
  • Erros: 0,7% (detectados e revertidos automaticamente)
  • ROI: 1.250% no primeiro ano

Este artigo explica exatamente como function calling funciona, como implementar com segurança, e quando usar (ou não usar).

O que é function calling e por que muda o jogo

Antes do function calling: LLMs só geravam texto

Limitação fundamental dos LLMs (até 2023):

LLMs eram “apenas” geradores de texto. Não importa quão inteligente fosse a resposta, o output era sempre… texto.

Exemplo - o que você PODIA fazer:

Usuário: "Qual o saldo da conta 12345?"

LLM: "Para consultar o saldo, você pode:
1. Acessar [link do sistema]
2. Entrar com suas credenciais
3. Buscar a conta 12345
4. Visualizar o saldo na tela principal"

O LLM gerava instruções, mas você precisava executar.

O que você NÃO PODIA fazer:

Usuário: "Qual o saldo da conta 12345?"
LLM: [consulta banco de dados]
LLM: "O saldo atual da conta 12345 é R$ 14.892,34"

LLM não tinha como executar a consulta.

Workaround (pré-function calling):

Desenvolvedores faziam parsing manual do output:

response = llm.generate("Qual ação tomar para o cliente X?")

if "consultar saldo" in response.lower():
    saldo = api.buscar_saldo(cliente_id)
    # continuar processo...
elif "enviar email" in response.lower():
    # extrair destinatário do texto (regex/outro LLM)
    # enviar email...

Problemas:

  • Frágil (quebrava se LLM mudasse formato da resposta)
  • Impreciso (parsing de texto é ambíguo)
  • Trabalhoso (escrever parser para cada ação)

Com function calling: LLMs podem executar ações

O que mudou:

LLMs modernos (GPT-4, Claude 3, etc.) podem retornar chamadas de função estruturadas ao invés de apenas texto.

Fluxo simplificado:

1. Você: descreve funções disponíveis para o LLM
2. Usuário: faz pergunta
3. LLM: decide qual função chamar (retorna JSON estruturado)
4. Você: executa a função
5. Você: envia resultado de volta pro LLM
6. LLM: processa resultado e responde ao usuário

Exemplo concreto:

# 1. Definir funções disponíveis
tools = [
    {
        "type": "function",
        "function": {
            "name": "buscar_saldo",
            "description": "Consulta saldo de uma conta bancária",
            "parameters": {
                "type": "object",
                "properties": {
                    "conta_id": {
                        "type": "string",
                        "description": "ID da conta (ex: '12345')"
                    }
                },
                "required": ["conta_id"]
            }
        }
    }
]

# 2. Usuário pergunta
messages = [
    {"role": "user", "content": "Qual o saldo da conta 12345?"}
]

# 3. LLM decide chamar função
response = openai.chat.completions.create(
    model="gpt-4",
    messages=messages,
    tools=tools
)

# LLM retorna:
# {
#   "tool_calls": [{
#     "function": {
#       "name": "buscar_saldo",
#       "arguments": '{"conta_id": "12345"}'
#     }
#   }]
# }

# 4. Você executa a função
saldo = database.buscar_saldo("12345")  # R$ 14.892,34

# 5. Envia resultado de volta
messages.append({
    "role": "tool",
    "tool_call_id": response.tool_calls[0].id,
    "content": str(saldo)
})

# 6. LLM processa e responde
final_response = openai.chat.completions.create(
    model="gpt-4",
    messages=messages
)

# LLM: "O saldo atual da conta 12345 é R$ 14.892,34"

Vantagens sobre parsing manual:

  • ✅ Estruturado (JSON confiável)
  • ✅ Parâmetros validados automaticamente
  • ✅ Menos ambiguidade
  • ✅ LLM “entende” o que cada função faz

Como LLM decide qual função chamar

Mistério resolvido:

LLM não “entende” funções magicamente. Você precisa descrever cada função.

Descrição de função bem feita:

{
    "name": "buscar_cliente",
    "description": "Busca informações detalhadas de um cliente no CRM. Use quando precisar de dados como email, telefone, endereço, histórico de compras.",
    "parameters": {
        "type": "object",
        "properties": {
            "cliente_id": {
                "type": "string",
                "description": "ID único do cliente (pode ser CPF/CNPJ ou ID interno)"
            },
            "incluir_historico": {
                "type": "boolean",
                "description": "Se true, inclui histórico de compras dos últimos 12 meses",
                "default": false
            }
        },
        "required": ["cliente_id"]
    }
}

LLM lê essas descrições e decide:

“Usuário perguntou sobre histórico de compras do cliente 123. A função buscar_cliente com parâmetro incluir_historico=true resolve isso.”

Como melhorar decisões do LLM:

  1. Descrições claras e específicas:"description": "Busca cliente""description": "Busca dados de cliente no CRM incluindo nome, email, telefone e endereço. Use quando precisar de informações de contato."

  2. Exemplos no description:

    "description": "Calcula frete. Exemplo: origem='São Paulo', destino='Rio de Janeiro', peso_kg=50 → retorna R$ 85.00"
  3. Enums quando opções são limitadas:

    "status": {
        "type": "string",
        "enum": ["ativo", "inativo", "pendente"],
        "description": "Status do pedido"
    }
  4. Validation hints:

    "cpf": {
        "type": "string",
        "description": "CPF do cliente (formato: 000.000.000-00)",
        "pattern": "^\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}$"
    }

Implementação prática passo a passo

Setup básico com OpenAI

Exemplo completo: Sistema de consulta de estoque

from openai import OpenAI
import json

client = OpenAI()

# 1. Simular banco de dados
def buscar_estoque(produto_id: str) -> dict:
    """Simula consulta ao banco de dados"""
    database = {
        "PROD001": {"nome": "Notebook Dell", "quantidade": 15, "preco": 3500.00},
        "PROD002": {"nome": "Mouse Logitech", "quantidade": 243, "preco": 89.90},
        "PROD003": {"nome": "Teclado Mecânico", "quantidade": 0, "preco": 450.00},
    }
    return database.get(produto_id, {"erro": "Produto não encontrado"})

def reservar_produto(produto_id: str, quantidade: int) -> dict:
    """Simula reserva de produto"""
    estoque = buscar_estoque(produto_id)
    if "erro" in estoque:
        return {"sucesso": False, "mensagem": "Produto não existe"}
    if estoque["quantidade"] < quantidade:
        return {"sucesso": False, "mensagem": f"Estoque insuficiente. Disponível: {estoque['quantidade']}"}
    return {"sucesso": True, "mensagem": f"Reservado {quantidade} unidades de {estoque['nome']}"}

# 2. Definir tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "buscar_estoque",
            "description": "Consulta quantidade disponível e preço de um produto no estoque",
            "parameters": {
                "type": "object",
                "properties": {
                    "produto_id": {
                        "type": "string",
                        "description": "ID do produto (formato: PROD###)"
                    }
                },
                "required": ["produto_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "reservar_produto",
            "description": "Reserva uma quantidade específica de produto para um cliente",
            "parameters": {
                "type": "object",
                "properties": {
                    "produto_id": {"type": "string", "description": "ID do produto"},
                    "quantidade": {"type": "integer", "description": "Quantidade a reservar"}
                },
                "required": ["produto_id", "quantidade"]
            }
        }
    }
]

# 3. Função para executar tool calls
def executar_funcao(nome_funcao: str, argumentos: dict):
    if nome_funcao == "buscar_estoque":
        return buscar_estoque(**argumentos)
    elif nome_funcao == "reservar_produto":
        return reservar_produto(**argumentos)
    else:
        return {"erro": "Função não encontrada"}

# 4. Loop principal
def processar_pedido(mensagem_usuario: str):
    messages = [{"role": "user", "content": mensagem_usuario}]

    while True:
        response = client.chat.completions.create(
            model="gpt-4-turbo",
            messages=messages,
            tools=tools
        )

        message = response.choices[0].message

        # Se LLM retornou resposta final (não tool call)
        if not message.tool_calls:
            return message.content

        # LLM quer chamar função(ões)
        messages.append(message)

        for tool_call in message.tool_calls:
            nome_funcao = tool_call.function.name
            argumentos = json.loads(tool_call.function.arguments)

            print(f"🔧 Chamando: {nome_funcao}({argumentos})")

            resultado = executar_funcao(nome_funcao, argumentos)

            print(f"✅ Resultado: {resultado}")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(resultado)
            })

# 5. Testar
print(processar_pedido("Tem notebook Dell disponível? Quero reservar 5 unidades"))

Output esperado:

🔧 Chamando: buscar_estoque({'produto_id': 'PROD001'})
✅ Resultado: {'nome': 'Notebook Dell', 'quantidade': 15, 'preco': 3500.0}

🔧 Chamando: reservar_produto({'produto_id': 'PROD001', 'quantidade': 5})
✅ Resultado: {'sucesso': True, 'mensagem': 'Reservado 5 unidades de Notebook Dell'}

Sim, temos 15 unidades de Notebook Dell disponíveis. Reservei 5 unidades para você!

Implementando com Claude (Anthropic)

Claude usa sintaxe ligeiramente diferente (“tools” ao invés de “functions”):

import anthropic

client = anthropic.Anthropic()

# Definir tools (similar mas formato diferente)
tools = [
    {
        "name": "buscar_estoque",
        "description": "Consulta quantidade e preço de produto",
        "input_schema": {
            "type": "object",
            "properties": {
                "produto_id": {
                    "type": "string",
                    "description": "ID do produto"
                }
            },
            "required": ["produto_id"]
        }
    }
]

# Usar
response = client.messages.create(
    model="claude-3-opus-20240229",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Consulta estoque PROD001"}]
)

# Processar tool use (similar ao OpenAI)
if response.stop_reason == "tool_use":
    tool_use = response.content[1]  # Claude retorna em content
    nome = tool_use.name
    argumentos = tool_use.input
    # executar função...

Parallel function calling: múltiplas chamadas simultâneas

Cenário:

Usuário pergunta: “Quero saber estoque de PROD001, PROD002 e PROD003”

Sem parallel calling:

LLM chama: buscar_estoque("PROD001")  [espera]
LLM chama: buscar_estoque("PROD002")  [espera]
LLM chama: buscar_estoque("PROD003")  [espera]
Total: 3 round-trips

Com parallel calling:

LLM chama simultaneamente:
- buscar_estoque("PROD001")
- buscar_estoque("PROD002")
- buscar_estoque("PROD003")
Total: 1 round-trip

Implementação:

response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=messages,
    tools=tools,
    parallel_tool_calls=True  # habilita chamadas paralelas
)

# Executar todas em paralelo (usando asyncio ou threading)
import concurrent.futures

with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = []
    for tool_call in response.choices[0].message.tool_calls:
        nome = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
        future = executor.submit(executar_funcao, nome, args)
        futures.append((tool_call.id, future))

    # Coletar resultados
    for tool_call_id, future in futures:
        resultado = future.result()
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call_id,
            "content": json.dumps(resultado)
        })

Vantagem: Reduz latência significativamente quando múltiplas chamadas são necessárias.

Casos de uso práticos

1. Assistente de análise financeira

Tools implementadas:

tools = [
    {
        "name": "buscar_transacoes",
        "description": "Retorna transações de uma conta em período específico",
        "parameters": {
            "conta_id": str,
            "data_inicio": str,  # "YYYY-MM-DD"
            "data_fim": str
        }
    },
    {
        "name": "calcular_gastos_categoria",
        "description": "Soma gastos por categoria (alimentação, transporte, etc)",
        "parameters": {
            "transacoes": list,
            "categoria": str
        }
    },
    {
        "name": "gerar_grafico",
        "description": "Gera gráfico de gastos",
        "parameters": {
            "dados": dict,
            "tipo": str  # "pizza", "barras", "linha"
        }
    },
    {
        "name": "comparar_com_mes_anterior",
        "description": "Compara gastos atuais com mês anterior",
        "parameters": {
            "conta_id": str,
            "mes_atual": str
        }
    }
]

Exemplo de uso:

Usuário: "Quanto gastei com alimentação em janeiro e como compara com dezembro?"

Agent (autonomamente):
1. buscar_transacoes(conta_id="...", data_inicio="2026-01-01", data_fim="2026-01-31")
2. calcular_gastos_categoria(transacoes=..., categoria="alimentação") → R$ 1.240
3. buscar_transacoes(conta_id="...", data_inicio="2025-12-01", data_fim="2025-12-31")
4. calcular_gastos_categoria(transacoes=..., categoria="alimentação") → R$ 980
5. gerar_grafico(dados={"Jan": 1240, "Dez": 980}, tipo="barras")

Resposta: "Você gastou R$ 1.240 em alimentação em janeiro, um aumento de 26,5%
em relação a dezembro (R$ 980). [gráfico anexado]"

2. Agent de customer success

Tools:

tools = [
    {
        "name": "buscar_cliente",
        "description": "Dados do cliente (nome, empresa, plano, etc)",
        "parameters": {"cliente_id": str}
    },
    {
        "name": "buscar_tickets_abertos",
        "description": "Lista tickets de suporte ainda abertos",
        "parameters": {"cliente_id": str}
    },
    {
        "name": "buscar_uso_produto",
        "description": "Métricas de uso (logins, features usadas, etc)",
        "parameters": {"cliente_id": str, "periodo_dias": int}
    },
    {
        "name": "calcular_health_score",
        "description": "Calcula score de saúde do cliente (0-100)",
        "parameters": {"dados_uso": dict, "dados_suporte": dict}
    },
    {
        "name": "criar_tarefa_cs",
        "description": "Cria tarefa para equipe de CS",
        "parameters": {
            "cliente_id": str,
            "prioridade": str,  # "baixa", "média", "alta"
            "descricao": str
        }
    },
    {
        "name": "enviar_email",
        "description": "Envia email proativo ao cliente",
        "parameters": {
            "destinatario": str,
            "assunto": str,
            "corpo": str
        }
    }
]

Exemplo: Análise proativa de churn risk

# Executado automaticamente toda segunda-feira
for cliente in clientes_ativos:
    prompt = f"""
    Analise a saúde do cliente {cliente.id} e tome ações proativas se necessário.

    Critérios de alerta:
    - Health score menos de 50: criar tarefa ALTA para CS
    - Tickets abertos mais de 5 dias: escalar
    - Uso caiu 40%+ vs mês anterior: enviar email check-in
    """

    agent.processar(prompt)
    # Agent autonomamente:
    # 1. Busca dados do cliente
    # 2. Calcula health score
    # 3. Decide ação apropriada
    # 4. Executa (cria tarefa OU envia email OU escala ticket)

3. Automação de RFPs (Request for Proposal)

Tools:

tools = [
    {
        "name": "buscar_em_rfps_anteriores",
        "description": "Busca respostas em RFPs anteriores por palavra-chave ou tópico",
        "parameters": {"query": str, "top_k": int}
    },
    {
        "name": "buscar_em_documentacao",
        "description": "Busca informação técnica na documentação do produto",
        "parameters": {"query": str}
    },
    {
        "name": "consultar_equipe_tecnica",
        "description": "Envia pergunta para equipe técnica via Slack (para perguntas que não têm resposta documentada)",
        "parameters": {"pergunta": str, "canal": str}
    },
    {
        "name": "gerar_secao_rfp",
        "description": "Gera rascunho de seção do RFP baseado em contexto fornecido",
        "parameters": {
            "secao": str,
            "contexto": str,
            "tom": str  # "técnico", "comercial", "executivo"
        }
    }
]

Exemplo:

RFP pergunta: "Descreva suas capacidades de integração com sistemas ERP"

Agent:
1. buscar_em_rfps_anteriores(query="integração ERP", top_k=5)
   → encontra 3 respostas anteriores
2. buscar_em_documentacao(query="integrações disponíveis")
   → encontra lista de APIs
3. gerar_secao_rfp(
     secao="Integrações ERP",
     contexto=[respostas anteriores + documentação],
     tom="técnico"
   )
   → gera rascunho de resposta

Output: Rascunho pronto para revisão humana em 2 minutos (vs 45 minutos manual)

Segurança e safeguards: evitando desastres

Perigos de function calling sem proteção

Cenários de risco:

  1. Ações destrutivas:
# Função perigosa SEM proteção
def deletar_cliente(cliente_id: str):
    database.delete(f"DELETE FROM clientes WHERE id = {cliente_id}")
    return {"sucesso": True}

Risco: LLM pode interpretar mal e deletar cliente errado.

  1. Side effects financeiros:
# Função com impacto financeiro
def processar_reembolso(cliente_id: str, valor: float):
    payment_api.refund(cliente_id, valor)
    return {"sucesso": True}

Risco: Bug pode custar milhares de reais.

  1. Acesso não autorizado:
# Função sem validação de permissões
def buscar_dados_cliente(cliente_id: str):
    return database.query(f"SELECT * FROM clientes WHERE id = {cliente_id}")

Risco: Agent pode acessar dados que usuário não deveria ver.

  1. Injection attacks:
# Vulnerável a SQL injection
def buscar_produtos(nome: str):
    return database.query(f"SELECT * FROM produtos WHERE nome LIKE '%{nome}%'")

Risco: Agent poderia ser manipulado a executar SQL malicioso.

Safeguards obrigatórios

1. Classificação de funções por risco:

class RiskLevel:
    READ_ONLY = 1      # Sem side effects (consultas)
    LOW_RISK = 2       # Side effects reversíveis (criar rascunho)
    MEDIUM_RISK = 3    # Side effects importantes (enviar email)
    HIGH_RISK = 4      # Side effects críticos (transferência)
    DESTRUCTIVE = 5    # Irreversível (deletar dados)

tools_config = {
    "buscar_cliente": {"risk": RiskLevel.READ_ONLY, "requires_approval": False},
    "enviar_email": {"risk": RiskLevel.MEDIUM_RISK, "requires_approval": False},
    "processar_reembolso": {"risk": RiskLevel.HIGH_RISK, "requires_approval": True},
    "deletar_cliente": {"risk": RiskLevel.DESTRUCTIVE, "requires_approval": True},
}

2. Sistema de aprovação para ações críticas:

def executar_funcao_com_safeguard(nome_funcao, argumentos):
    config = tools_config[nome_funcao]

    # Funções de alto risco requerem aprovação
    if config["risk"] >= RiskLevel.HIGH_RISK:
        if not config["requires_approval"]:
            raise SecurityError("Função de alto risco requer aprovação")

        # Solicitar aprovação humana
        aprovado = solicitar_aprovacao_humana(
            funcao=nome_funcao,
            argumentos=argumentos,
            contexto=obter_contexto_decisao()
        )

        if not aprovado:
            return {"erro": "Ação não aprovada por humano"}

    # Executar função
    return executar_funcao(nome_funcao, argumentos)

3. Dry-run mode:

DRY_RUN = True  # Habilitar para testes

def enviar_email(destinatario, assunto, corpo):
    if DRY_RUN:
        print(f"[DRY RUN] Enviaria email:")
        print(f"  Para: {destinatario}")
        print(f"  Assunto: {assunto}")
        print(f"  Corpo: {corpo[:100]}...")
        return {"sucesso": True, "dry_run": True}

    # Execução real
    email_api.send(destinatario, assunto, corpo)
    return {"sucesso": True}

4. Validação rigorosa de parâmetros:

def processar_reembolso(cliente_id: str, valor: float):
    # Validações
    if not re.match(r"^CLI\d{6}$", cliente_id):
        raise ValueError("cliente_id inválido")

    if valor <= 0 or valor mais de 10000:
        raise ValueError("Valor deve estar entre R$ 0,01 e R$ 10.000")

    # Verificar se cliente existe
    cliente = database.buscar_cliente(cliente_id)
    if not cliente:
        raise ValueError("Cliente não encontrado")

    # Verificar se tem saldo para reembolso
    if cliente.saldo_disponivel < valor:
        raise ValueError(f"Saldo insuficiente. Disponível: R$ {cliente.saldo_disponivel}")

    # Tudo OK, executar
    payment_api.refund(cliente_id, valor)
    audit_log.registrar("reembolso", cliente_id, valor)
    return {"sucesso": True, "valor": valor}

5. Audit logging completo:

def executar_funcao_com_audit(nome_funcao, argumentos, contexto):
    # Log antes de executar
    audit_id = audit_log.iniciar(
        funcao=nome_funcao,
        argumentos=argumentos,
        usuario=contexto.get("usuario"),
        timestamp=datetime.now(),
        conversation_id=contexto.get("conversation_id")
    )

    try:
        resultado = executar_funcao(nome_funcao, argumentos)

        # Log de sucesso
        audit_log.finalizar(audit_id, sucesso=True, resultado=resultado)

        return resultado
    except Exception as e:
        # Log de erro
        audit_log.finalizar(audit_id, sucesso=False, erro=str(e))
        raise

6. Rate limiting:

from collections import defaultdict
import time

call_counts = defaultdict(list)

def executar_funcao_com_rate_limit(nome_funcao, argumentos):
    # Limite: máximo 10 chamadas por minuto por função
    now = time.time()
    minute_ago = now - 60

    # Limpar chamadas antigas
    call_counts[nome_funcao] = [
        ts for ts in call_counts[nome_funcao] if ts > minute_ago
    ]

    # Verificar limite
    if len(call_counts[nome_funcao]) >= 10:
        raise RateLimitError(f"{nome_funcao} atingiu limite de 10 chamadas/min")

    # Registrar chamada
    call_counts[nome_funcao].append(now)

    # Executar
    return executar_funcao(nome_funcao, argumentos)

Caso real: FinanceHub automatiza análise de crédito

Implementação e desafios

Contexto:

  • Análise de crédito manual: 2h30min por cliente
  • Volume: 40-60 análises/semana
  • Custo: R$ 85 por análise (analista sênior)

Tools implementadas (6 tools):

  1. consultar_serasa(cpf) - Score de crédito externo
  2. buscar_historico_transacoes(cliente_id, meses) - Transações passadas
  3. calcular_score_interno(dados) - Algoritmo proprietário
  4. buscar_protestos(cpf) - Consulta Serasa/SPC
  5. gerar_relatorio_pdf(dados) - Monta relatório formatado
  6. enviar_notificacao(destinatario, mensagem) - Notifica time comercial

Desafios encontrados:

1. LLM chamava funções desnecessárias:

Problema: Para CPF inválido, LLM chamava todas as funções mesmo assim,
gerando custo (APIs de Serasa custam R$ 0,80 por consulta)

Solução: Validar CPF ANTES de chamar qualquer API externa
def validar_cpf_antes(cpf: str):
    if not validar_cpf(cpf):
        return {
            "erro": "CPF inválido",
            "instrucao_para_llm": "Informe ao usuário que CPF é inválido e solicite correção. NÃO chame outras funções."
        }
    return {"cpf_valido": True}

2. Ordem de chamadas importava:

Problema: LLM às vezes chamava calcular_score_interno() ANTES de buscar_historico_transacoes(),
resultando em score baseado em dados incompletos

Solução: Adicionar dependências explícitas nas descrições
{
    "name": "calcular_score_interno",
    "description": """
    Calcula score interno de crédito.

    IMPORTANTE: Só chame DEPOIS de:
    1. consultar_serasa()
    2. buscar_historico_transacoes()
    3. buscar_protestos()

    O cálculo depende desses dados estarem disponíveis.
    """
}

3. Falhas em APIs externas quebravam processo:

Problema: Se Serasa API falhava (timeout, erro 500), agent travava

Solução: Fallback e tratamento de erros explícito
def consultar_serasa_com_fallback(cpf: str):
    try:
        return serasa_api.consultar(cpf)
    except TimeoutError:
        return {
            "erro": "Serasa timeout",
            "instrucao_para_llm": "Serasa indisponível. Prossiga com análise baseada apenas em dados internos e indique que score externo não foi possível."
        }
    except Exception as e:
        return {
            "erro": str(e),
            "instrucao_para_llm": "Erro ao consultar Serasa. Baseie análise apenas em dados internos."
        }

Resultados mensurados

Após 3 meses em produção:

MétricaManualCom AgentVariação
Tempo médio2h 30min8 min-95%
Custo por análiseR$ 85R$ 3,20-96%
Precisão96%*97%+1%
Análises/semana50180+260%
Erros críticos2%0,7%-65%

*Precisão manual medida em auditoria de 200 análises

Erros identificados e corrigidos:

Tipo de ErroOcorrênciasCausaSolução
Parâmetro errado em API12 casosLLM passava CPF formatado (com pontos) quando API esperava só númerosFunção wrapper faz sanitização
Chamada desnecessária de API paga8 casosLLM consultava Serasa mesmo com dados recentes em cacheAdicionar tool verificar_cache()
Score calculado com dados incompletos5 casosOrdem de chamadas incorretaDependências explícitas em descriptions
Timeout não tratado3 casosAPI externa lentaTimeout de 5s + fallback

ROI detalhado:

Investimento:

  • Desenvolvimento: R$ 38.000 (3 semanas, 2 devs)
  • Testes e ajustes: R$ 12.000 (2 semanas)
  • APIs (Serasa, etc): R$ 420/mês
  • Infra (servidores): R$ 280/mês
  • Total ano 1: R$ 58.400

Retorno ano 1:

  • Economia em analistas: 130 análises/mês × R$ 81,80 = R$ 10.634/mês = R$ 127.608/ano
  • Aumento de capacidade permite fechar mais clientes: estimados R$ 85.000/ano adicionais
  • Total: R$ 212.608/ano

ROI: 264% Payback: 4,4 meses

Checklist: quando function calling faz sentido

Sinais de que você precisa de function calling

  • Processo envolve consultar múltiplos sistemas/APIs
  • Tarefas requerem dados em tempo real (não podem ser pré-processados)
  • Decisões dependem de informações que mudam frequentemente
  • Humanos gastam tempo significativo “colhendo dados” antes de analisar
  • Processo tem etapas claras mas ordem pode variar
  • Volume é alto o suficiente para justificar automação (mais de 100/mês)

Sinais de que você NÃO precisa (ainda)

  • Processo é 100% linear e padronizado (automação tradicional basta)
  • Dados necessários são estáticos/raramente mudam
  • Ações são todas de alto risco (muito perigoso automatizar)
  • Volume é muito baixo (menos de 20/mês)
  • Processo muda toda semana (função calling requer estabilidade)

Perguntas de segurança obrigatórias

Antes de implementar function calling em produção:

  • Todas as funções têm validação de parâmetros?
  • Funções de alto risco requerem aprovação humana?
  • Há audit logging de todas as chamadas?
  • Funções com side effects têm dry-run mode para testes?
  • Rate limiting está implementado?
  • Há rollback para ações críticas?
  • Monitora ção alerta sobre comportamento anômalo?

Se qualquer resposta é “não”, NÃO vá para produção ainda.

Conclusão: function calling é o que torna IA realmente útil

Function calling é a diferença entre IA que “conversa sobre” e IA que “faz”.

Três aprendizados principais:

  1. Descrições claras são 80% do sucesso

    • LLM decide baseado nas descriptions
    • Invista tempo escrevendo descriptions detalhadas
    • Inclua dependências, exemplos, constraints
  2. Safeguards não são opcionais

    • Funções de alto risco DEVEM ter aprovação humana
    • Audit logging é obrigatório
    • Validação rigorosa de parâmetros salva de desastres
  3. Comece read-only, evolua gradualmente

    • Primeiras funções: apenas consultas (sem side effects)
    • Depois: ações reversíveis (criar rascunhos)
    • Por último: ações críticas (com aprovação)

Framework de implementação:

Semana 1-2: Identificar processo e mapear funções necessárias Semana 3-4: Implementar funções read-only + testes extensivos Semana 5-6: Adicionar funções com side effects (low risk) Semana 7-8: Implementar safeguards completos Semana 9-10: Testes em staging + dry-run Semana 11-12: Produção gradual (10% → 50% → 100%)

O que fazer agora:

  1. Escolha 1 processo repetitivo e time-consuming
  2. Mapeie as “funções” que humano executa
  3. Implemente versão MVP (3-5 funções read-only)
  4. Teste exaustivamente
  5. Adicione funções com side effects gradualmente
  6. Meça impacto objetivamente

Quer ajuda para implementar function calling com segurança?

Na Orient.me, implementamos agents com function calling seguindo best practices:

  • Mapeamento de processos e funções
  • Implementação com safeguards completos
  • Testes extensivos (100+ cenários)
  • Rollout gradual e monitorado

Tempo típico: 6-12 semanas ROI médio: 380% no primeiro ano

Agende conversa gratuita para avaliar seu caso.

Pronto para sair do manual?

Agende o diagnóstico gratuito. Vamos mapear o gargalo, estimar o impacto e definir o primeiro resultado mensurável.

Você sai com clareza — não com um pitch de vendas.