Phase 11 - Lesson 09

Function Calling & Uso de Ferramentas

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

As LLMs não conseguem fazer nada por si mesmas. Elas apenas geram texto. Essa é toda a sua capacidade. Elas não podem verificar a previsão do tempo, consultar um banco de dados, enviar um e-mail, executar código ou ler um arquivo. Cada "agente de IA" que você já viu é composto por uma LLM gerando um JSON que diz qual função chamar — e então o seu código de fato a chama. O modelo é o cérebro. As ferramentas são as mãos. O function calling é o sistema nervoso que os conecta.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 11 Lição 03 (Structured Outputs) Tempo: ~75 minutos Relacionado: Fase 11 · 14 (Model Context Protocol) — quando uma ferramenta é compartilhada entre hosts, evolua do function-calling inline para um servidor MCP. Esta lição cobre o caso inline; o MCP cobre o caso do protocolo.

Objetivos de Aprendizado

  • Implementar um loop de function calling: definir esquemas de ferramentas, analisar o JSON de chamada de ferramenta do modelo, executar funções e retornar resultados
  • Projetar esquemas de ferramentas com descrições claras e parâmetros tipados que o modelo possa invocar de maneira confiável
  • Construir um loop de agente com múltiplos turnos que encadeia várias chamadas de função para responder a consultas complexas
  • Lidar com casos extremos de chamadas de função: chamadas de ferramentas em paralelo, propagação de erros e prevenção de loops infinitos de ferramentas

O Problema

Você constrói um chatbot. Um usuário pergunta: "Como está o tempo em Tóquio agora?"

O modelo responde: "Não tenho acesso a dados meteorológicos em tempo real, mas com base na estação do ano, Tóquio provavelmente está em torno de 15 graus Celsius..."

Isso é uma alucinação disfarçada de aviso de isenção de responsabilidade. O modelo não sabe a previsão do tempo. Ele nunca saberá. O tempo muda a cada hora. Os dados de treinamento do modelo têm meses de idade.

A resposta correta exige chamar a API do OpenWeatherMap, obter a temperatura atual e retornar o número real. O modelo não pode chamar APIs. O seu código pode. A peça que faltava: um protocolo estruturado que permita ao modelo dizer "Preciso chamar a API de clima com estes argumentos" e permita ao seu código executá-la e enviar o resultado de volta.

Isso é function calling. O modelo gera um JSON estruturado descrevendo qual função invocar e com quais argumentos. Sua aplicação executa a função. O resultado volta para a conversa. O modelo usa o resultado para produzir sua resposta final.

Sem o function calling, as LLMs são enciclopédias. Com ele, elas se tornam agentes.

O Conceito

O Loop de Function Calling

Cada interação de uso de ferramenta segue o mesmo loop de 5 etapas.

sequenceDiagram
    participant U as Usuário
    participant A as Aplicação
    participant M as Modelo
    participant T as Ferramenta

    U->>A: "Como está o tempo em Tóquio?"
    A->>M: mensagens + definições de ferramentas
    M->>A: chamada de ferramenta: get_weather(city="Tokyo")
    A->>T: Executar get_weather("Tokyo")
    T->>A: {"temp": 18, "condition": "cloudy"}
    A->>M: resultado da ferramenta + conversa
    M->>A: "Está 18°C e nublado em Tóquio."
    A->>U: Resposta final

Etapa 1: o usuário envia uma mensagem. Etapa 2: o modelo recebe a mensagem junto com as definições das ferramentas (um JSON Schema que descreve as funções disponíveis). Etapa 3: em vez de responder com texto, o modelo gera uma chamada de ferramenta — um objeto JSON estruturado com o nome da função e os argumentos. Etapa 4: seu código executa a função e captura o resultado. Etapa 5: o resultado é enviado de volta ao modelo, que agora possui dados reais para produzir sua resposta final.

O modelo nunca executa nada de fato. Ele apenas decide o que chamar e com quais argumentos. Seu código é o executor.

Definições de Ferramentas: O Contrato JSON Schema

Cada ferramenta é definida por um JSON Schema que informa ao modelo o que a função faz, quais argumentos ela aceita e quais devem ser os tipos desses argumentos.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get current weather for a city. Returns temperature in Celsius and conditions.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name, e.g. 'Tokyo' or 'San Francisco'"
        },
        "units": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Temperature units"
        }
      },
      "required": ["city"]
    }
  }
}

Os campos description são críticos. O modelo os lê para decidir quando e como usar a ferramenta. Uma descrição vaga como "gets weather" produz uma seleção de ferramentas pior do que "Get current weather for a city. Returns temperature in Celsius and conditions." A descrição funciona como um prompt para a seleção de ferramentas.

Comparação de Provedores

Todos os principais provedores suportam function calling, mas a interface da API difere.

Provedor Parâmetro da API Formato da Chamada de Ferramenta Chamadas em Paralelo Chamada Forçada
OpenAI (GPT-5, o4) tools tool_calls[].function Sim (múltiplas por turno) tool_choice="required"
Anthropic (Claude 4.6/4.7) tools content[].type="tool_use" Sim (múltiplos blocos) tool_choice={"type":"any"}
Google (Gemini 3) function_declarations functionCall Sim function_calling_config
Open-weight (Llama 4, Qwen3, DeepSeek-V3) tools nativo no Llama 4; Hermes ou ChatML em outros Misto Depende do modelo Baseado em prompt ou tool_choice se suportado

Até 2026, os três provedores proprietários convergiram para formatos quase idênticos baseados em JSON-Schema. O Llama 4 é fornecido com um campo tools nativo que corresponde ao formato da OpenAI. Os ajustes finos (fine-tunes) open-weight ainda variam — o formato Hermes (NousResearch) é o mais comum para fine-tunes de terceiros. Para ferramentas compartilhadas entre hosts, prefira o MCP (Fase 11 · 14) em vez do function-calling inline — o servidor é o mesmo para todos eles.

Escolha de Ferramenta: Auto, Required, Específica

Você controla quando o modelo deve usar ferramentas.

Auto (padrão): o modelo decide se deve chamar uma ferramenta ou responder diretamente. "Quanto é 2+2?" -- responde diretamente. "Como está o tempo?" -- chama a ferramenta.

Required: o modelo deve chamar pelo menos uma ferramenta. Use isso quando você sabe que a intenção do usuário exige uma ferramenta. Isso impede que o modelo tente adivinhar em vez de buscar dados reais.

Função específica: força o modelo a chamar uma função específica. tool_choice={"type": "function", "function": {"name": "get_weather"}} garante que a ferramenta de clima seja chamada, independentemente da consulta. Use isso para roteamento — quando a lógica anterior já determinou qual ferramenta é necessária.

Chamada de Função em Paralelo

O GPT-4o e o Claude podem chamar várias funções em um único turno. Um usuário pergunta: "Como está o tempo em Tóquio e em Nova York?" O modelo gera duas chamadas de ferramentas simultaneamente:

[
  {"name": "get_weather", "arguments": {"city": "Tokyo"}},
  {"name": "get_weather", "arguments": {"city": "New York"}}
]

Seu código executa ambas (idealmente de forma concorrente), retorna ambos os resultados e o modelo sintetiza uma única resposta. Isso reduz as viagens de ida e volta (round trips) de 2 para 1. Para agentes com 5 a 10 chamadas de ferramentas por consulta, a chamada paralela reduz a latência em 60-80%.

Structured Outputs vs Function Calling

A Lição 03 cobriu structured outputs (saídas estruturadas). O function calling usa a mesma estrutura de JSON Schema, mas para um propósito diferente.

Structured outputs: forçam o modelo a produzir dados em um formato específico. A saída é o produto final. Exemplo: extrair informações de produtos de um texto como {name, price, in_stock}.

Function calling: o modelo declara a intenção de executar uma ação. A saída é uma etapa intermediária. Exemplo: get_weather(city="Tokyo") -- o modelo está solicitando uma ação, não produzindo a resposta final.

Use structured outputs quando desejar extração de dados. Use function calling quando desejar que o modelo interaja com sistemas externos.

Segurança: As Regras Não Negociáveis

O function calling é a capacidade mais perigosa que você pode conceder a uma LLM. O modelo escolhe o que executar. Se o seu conjunto de ferramentas incluir consultas a bancos de dados, o modelo construirá as consultas. Se incluir comandos de shell, o modelo os escreverá.

Regra 1: Nunca passe SQL gerado pelo modelo diretamente para um banco de dados. O modelo pode e irá gerar DROP TABLE, injeções de UNION ou consultas que retornam todas as linhas. Sempre parametrize. Sempre valide. Sempre use uma lista de permissões (allowlist) de operações.

Regra 2: Use uma lista de permissões para as funções. O modelo só deve ser capaz de chamar funções que você definiu explicitamente. Nunca crie uma ferramenta genérica do tipo "executar qualquer função por nome". Se você tem 50 funções internas, exponha apenas as 5 que o usuário realmente precisa.

Regra 3: Valide os argumentos. O modelo pode passar um nome de cidade como "; DROP TABLE users; --". Valide cada argumento em relação aos tipos, intervalos e formatos esperados antes da execução.

Regra 4: Sanitize os resultados das ferramentas. Se uma ferramenta retornar dados confidenciais (chaves de API, informações pessoalmente identificáveis - PII, erros internos), filtre-os antes de enviá-los de volta ao modelo. O modelo incluirá os resultados das ferramentas em sua resposta de forma literal.

Regra 5: Limite a taxa de chamadas de ferramentas. Um modelo em loop pode chamar ferramentas centenas de vezes. Defina um limite máximo (10-20 chamadas por conversa é razoável). Interrompa loops infinitos.

Tratamento de Erros

As ferramentas falham. As APIs sofrem timeout. Os bancos de dados ficam fora do ar. Os arquivos não existem. O modelo precisa saber quando uma ferramenta falhou e o porquê.

Retorne os erros como resultados estruturados de ferramentas, não como exceções:

{
  "error": true,
  "message": "City 'Toky' not found. Did you mean 'Tokyo'?",
  "code": "CITY_NOT_FOUND"
}

O modelo lê isso, ajusta seus argumentos e tenta novamente. Os modelos são bons em se autocorrigir a partir de mensagens de erro estruturadas. Eles são ruins em se recuperar de respostas vazias ou erros genéricos de "algo deu errado".

MCP: Model Context Protocol

O MCP é o padrão aberto da Anthropic para interoperabilidade de ferramentas. Em vez de cada aplicação definir suas próprias ferramentas, o MCP fornece um protocolo universal: as ferramentas são disponibilizadas por servidores MCP e consumidas por clientes MCP (como Claude Code, Cursor ou sua própria aplicação).

Um único servidor MCP pode expor ferramentas para qualquer cliente compatível. Um servidor MCP Postgres concede acesso ao banco de dados para qualquer agente compatível com MCP. Um servidor MCP do GitHub concede acesso a repositórios para qualquer agente. As ferramentas são definidas uma vez e usadas em qualquer lugar.

O MCP está para o function calling assim como o HTTP está para a rede. Ele padroniza a camada de transporte para que as ferramentas se tornem portáteis.

Build It

Passo 1: Definir o Registro de Ferramentas

Construa um registro que armazene as definições das ferramentas e suas implementações. Cada ferramenta possui uma definição em JSON Schema (o que o modelo vê) e uma função Python (o que seu código executa).

import json
import math
import time
import hashlib


TOOL_REGISTRY = {}


def register_tool(name, description, parameters, function):
    TOOL_REGISTRY[name] = {
        "definition": {
            "type": "function",
            "function": {
                "name": name,
                "description": description,
                "parameters": parameters,
            },
        },
        "function": function,
    }

Passo 2: Implementar 5 Ferramentas

Construa uma calculadora, busca de clima, simulador de busca web, leitor de arquivos e executor de código.

def calculator(expression, precision=2):
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return {"error": True, "message": f"Invalid characters in expression: {expression}"}
    try:
        result = eval(expression, {"__builtins__": {}}, {"math": math})
        return {"result": round(float(result), precision), "expression": expression}
    except Exception as e:
        return {"error": True, "message": str(e)}


WEATHER_DB = {
    "tokyo": {"temp_c": 18, "condition": "cloudy", "humidity": 72, "wind_kph": 14},
    "new york": {"temp_c": 22, "condition": "sunny", "humidity": 45, "wind_kph": 8},
    "london": {"temp_c": 12, "condition": "rainy", "humidity": 88, "wind_kph": 22},
    "san francisco": {"temp_c": 16, "condition": "foggy", "humidity": 80, "wind_kph": 18},
    "sydney": {"temp_c": 25, "condition": "sunny", "humidity": 55, "wind_kph": 10},
}


def get_weather(city, units="celsius"):
    key = city.lower().strip()
    if key not in WEATHER_DB:
        suggestions = [c for c in WEATHER_DB if c.startswith(key[:3])]
        return {
            "error": True,
            "message": f"City '{city}' not found.",
            "suggestions": suggestions,
            "code": "CITY_NOT_FOUND",
        }
    data = WEATHER_DB[key].copy()
    if units == "fahrenheit":
        data["temp_f"] = round(data["temp_c"] * 9 / 5 + 32, 1)
        del data["temp_c"]
    data["city"] = city
    return data


SEARCH_DB = {
    "python function calling": [
        {"title": "OpenAI Function Calling Guide", "url": "https://platform.openai.com/docs/guides/function-calling", "snippet": "Learn how to connect LLMs to external tools."},
        {"title": "Anthropic Tool Use", "url": "https://docs.anthropic.com/en/docs/tool-use", "snippet": "Claude can interact with external tools and APIs."},
    ],
    "MCP protocol": [
        {"title": "Model Context Protocol", "url": "https://modelcontextprotocol.io", "snippet": "An open standard for connecting AI models to data sources."},
    ],
    "weather API": [
        {"title": "OpenWeatherMap API", "url": "https://openweathermap.org/api", "snippet": "Free weather API with current, forecast, and historical data."},
    ],
}


def web_search(query, max_results=3):
    key = query.lower().strip()
    for db_key, results in SEARCH_DB.items():
        if db_key in key or key in db_key:
            return {"query": query, "results": results[:max_results], "total": len(results)}
    return {"query": query, "results": [], "total": 0}


FILE_SYSTEM = {
    "data/config.json": '{"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096}',
    "data/users.csv": "name,email,role\nAlice,alice@example.com,admin\nBob,bob@example.com,user",
    "README.md": "# My Project\nA tool-use agent built from scratch.",
}


def read_file(path):
    if ".." in path or path.startswith("/"):
        return {"error": True, "message": "Path traversal not allowed.", "code": "FORBIDDEN"}
    if path not in FILE_SYSTEM:
        available = list(FILE_SYSTEM.keys())
        return {"error": True, "message": f"File '{path}' not found.", "available_files": available, "code": "NOT_FOUND"}
    content = FILE_SYSTEM[path]
    return {"path": path, "content": content, "size_bytes": len(content), "lines": content.count("\n") + 1}


def run_code(code, language="python"):
    if language != "python":
        return {"error": True, "message": f"Language '{language}' not supported. Only 'python' is available."}
    forbidden = ["import os", "import sys", "import subprocess", "exec(", "eval(", "__import__", "open("]
    for pattern in forbidden:
        if pattern in code:
            return {"error": True, "message": f"Forbidden operation: {pattern}", "code": "SECURITY_VIOLATION"}
    try:
        local_vars = {}
        exec(code, {"__builtins__": {"print": print, "range": range, "len": len, "str": str, "int": int, "float": float, "list": list, "dict": dict, "sum": sum, "min": min, "max": max, "abs": abs, "round": round, "sorted": sorted, "enumerate": enumerate, "zip": zip, "map": map, "filter": filter, "math": math}}, local_vars)
        result = local_vars.get("result", None)
        return {"success": True, "result": result, "variables": {k: str(v) for k, v in local_vars.items() if not k.startswith("_")}}
    except Exception as e:
        return {"error": True, "message": f"{type(e).__name__}: {e}"}

Passo 3: Registrar Todas as Ferramentas

def register_all_tools():
    register_tool(
        "calculator", "Evaluate a mathematical expression. Supports +, -, *, /, parentheses, and decimals. Returns the numeric result.",
        {"type": "object", "properties": {"expression": {"type": "string", "description": "Math expression, e.g. '(10 + 5) * 3'"}, "precision": {"type": "integer", "description": "Decimal places in result", "default": 2}}, "required": ["expression"]},
        calculator,
    )
    register_tool(
        "get_weather", "Get current weather for a city. Returns temperature, condition, humidity, and wind speed.",
        {"type": "object", "properties": {"city": {"type": "string", "description": "City name, e.g. 'Tokyo' or 'San Francisco'"}, "units": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units, defaults to celsius"}}, "required": ["city"]},
        get_weather,
    )
    register_tool(
        "web_search", "Search the web for information. Returns a list of results with title, URL, and snippet.",
        {"type": "object", "properties": {"query": {"type": "string", "description": "Search query"}, "max_results": {"type": "integer", "description": "Maximum results to return", "default": 3}}, "required": ["query"]},
        web_search,
    )
    register_tool(
        "read_file", "Read the contents of a file. Returns the file content, size, and line count.",
        {"type": "object", "properties": {"path": {"type": "string", "description": "Relative file path, e.g. 'data/config.json'"}}, "required": ["path"]},
        read_file,
    )
    register_tool(
        "run_code", "Execute Python code in a sandboxed environment. Set a 'result' variable to return output.",
        {"type": "object", "properties": {"code": {"type": "string", "description": "Python code to execute"}, "language": {"type": "string", "enum": ["python"], "description": "Programming language"}}, "required": ["code"]},
        run_code,
    )

Passo 4: Construir o Loop de Function Calling

Este é o motor principal. Ele simula o modelo decidindo qual ferramenta chamar, executa a ferramenta e envia os resultados de volta.

def simulate_model_decision(user_message, tools, conversation_history):
    msg = user_message.lower()

    if any(word in msg for word in ["weather", "temperature", "forecast"]):
        cities = []
        for city in WEATHER_DB:
            if city in msg:
                cities.append(city)
        if not cities:
            for word in msg.split():
                if word.capitalize() in [c.title() for c in WEATHER_DB]:
                    cities.append(word)
        if not cities:
            cities = ["tokyo"]
        calls = []
        for city in cities:
            calls.append({"name": "get_weather", "arguments": {"city": city.title()}})
        return calls

    if any(word in msg for word in ["calculate", "compute", "math", "what is", "how much"]):
        for token in msg.split():
            if any(c in token for c in "+-*/"):
                return [{"name": "calculator", "arguments": {"expression": token}}]
        if "+" in msg or "-" in msg or "*" in msg or "/" in msg:
            expr = "".join(c for c in msg if c in "0123456789+-*/.() ")
            if expr.strip():
                return [{"name": "calculator", "arguments": {"expression": expr.strip()}}]
        return [{"name": "calculator", "arguments": {"expression": "0"}}]

    if any(word in msg for word in ["search", "find", "look up", "google"]):
        query = msg.replace("search for", "").replace("look up", "").replace("find", "").strip()
        return [{"name": "web_search", "arguments": {"query": query}}]

    if any(word in msg for word in ["read", "file", "open", "cat", "show"]):
        for path in FILE_SYSTEM:
            if path.split("/")[-1].split(".")[0] in msg:
                return [{"name": "read_file", "arguments": {"path": path}}]
        return [{"name": "read_file", "arguments": {"path": "README.md"}}]

    if any(word in msg for word in ["run", "execute", "code", "python"]):
        return [{"name": "run_code", "arguments": {"code": "result = 'Hello from the sandbox!'", "language": "python"}}]

    return []


def execute_tool_call(tool_call):
    name = tool_call["name"]
    args = tool_call["arguments"]

    if name not in TOOL_REGISTRY:
        return {"error": True, "message": f"Unknown tool: {name}", "code": "UNKNOWN_TOOL"}

    tool = TOOL_REGISTRY[name]
    func = tool["function"]
    start = time.time()

    try:
        result = func(**args)
    except TypeError as e:
        result = {"error": True, "message": f"Invalid arguments: {e}"}

    elapsed_ms = round((time.time() - start) * 1000, 2)
    return {"tool": name, "result": result, "execution_time_ms": elapsed_ms}


def run_function_calling_loop(user_message, max_iterations=5):
    conversation = [{"role": "user", "content": user_message}]
    tool_definitions = [t["definition"] for t in TOOL_REGISTRY.values()]
    all_tool_results = []

    for iteration in range(max_iterations):
        tool_calls = simulate_model_decision(user_message, tool_definitions, conversation)

        if not tool_calls:
            break

        results = []
        for call in tool_calls:
            result = execute_tool_call(call)
            results.append(result)

        conversation.append({"role": "assistant", "content": None, "tool_calls": tool_calls})

        for result in results:
            conversation.append({"role": "tool", "content": json.dumps(result["result"]), "tool_name": result["tool"]})

        all_tool_results.extend(results)
        break

    return {"conversation": conversation, "tool_results": all_tool_results, "iterations": iteration + 1 if tool_calls else 0}

Passo 5: Validação de Argumentos

Construa um validador que verifica os argumentos da chamada da ferramenta em relação ao JSON Schema antes da execução.

def validate_tool_arguments(tool_name, arguments):
    if tool_name not in TOOL_REGISTRY:
        return [f"Unknown tool: {tool_name}"]

    schema = TOOL_REGISTRY[tool_name]["definition"]["function"]["parameters"]
    errors = []

    if not isinstance(arguments, dict):
        return [f"Arguments must be an object, got {type(arguments).__name__}"]

    for required_field in schema.get("required", []):
        if required_field not in arguments:
            errors.append(f"Missing required argument: {required_field}")

    properties = schema.get("properties", {})
    for arg_name, arg_value in arguments.items():
        if arg_name not in properties:
            errors.append(f"Unknown argument: {arg_name}")
            continue

        prop_schema = properties[arg_name]
        expected_type = prop_schema.get("type")

        type_checks = {"string": str, "integer": int, "number": (int, float), "boolean": bool, "array": list, "object": dict}
        if expected_type in type_checks:
            if not isinstance(arg_value, type_checks[expected_type]):
                errors.append(f"Argument '{arg_name}': expected {expected_type}, got {type(arg_value).__name__}")

        if "enum" in prop_schema and arg_value not in prop_schema["enum"]:
            errors.append(f"Argument '{arg_name}': '{arg_value}' not in {prop_schema['enum']}")

    return errors

Passo 6: Executar a Demonstração

def run_demo():
    register_all_tools()

    print("=" * 60)
    print("  Function Calling & Tool Use Demo")
    print("=" * 60)

    print("\n--- Registered Tools ---")
    for name, tool in TOOL_REGISTRY.items():
        desc = tool["definition"]["function"]["description"][:60]
        params = list(tool["definition"]["function"]["parameters"].get("properties", {}).keys())
        print(f"  {name}: {desc}...")
        print(f"    params: {params}")

    print(f"\n--- Argument Validation ---")
    validation_tests = [
        ("get_weather", {"city": "Tokyo"}, "Valid call"),
        ("get_weather", {}, "Missing required arg"),
        ("get_weather", {"city": "Tokyo", "units": "kelvin"}, "Invalid enum value"),
        ("calculator", {"expression": 123}, "Wrong type (int for string)"),
        ("unknown_tool", {"x": 1}, "Unknown tool"),
    ]
    for tool_name, args, label in validation_tests:
        errors = validate_tool_arguments(tool_name, args)
        status = "VALID" if not errors else f"ERRORS: {errors}"
        print(f"  {label}: {status}")

    print(f"\n--- Tool Execution ---")
    direct_tests = [
        {"name": "calculator", "arguments": {"expression": "(10 + 5) * 3 / 2"}},
        {"name": "get_weather", "arguments": {"city": "Tokyo"}},
        {"name": "get_weather", "arguments": {"city": "Mars"}},
        {"name": "web_search", "arguments": {"query": "python function calling"}},
        {"name": "read_file", "arguments": {"path": "data/config.json"}},
        {"name": "read_file", "arguments": {"path": "../etc/passwd"}},
        {"name": "run_code", "arguments": {"code": "result = sum(range(1, 101))"}},
        {"name": "run_code", "arguments": {"code": "import os; os.system('rm -rf /')"}},
    ]
    for call in direct_tests:
        result = execute_tool_call(call)
        print(f"\n  {call['name']}({json.dumps(call['arguments'])})")
        print(f"    -> {json.dumps(result['result'], indent=None)[:100]}")
        print(f"    time: {result['execution_time_ms']}ms")

    print(f"\n--- Full Function Calling Loop ---")
    test_queries = [
        "What's the weather in Tokyo?",
        "Calculate (100 + 250) * 0.15",
        "Search for MCP protocol",
        "Read the config file",
        "Run some Python code",
        "Tell me a joke",
    ]
    for query in test_queries:
        print(f"\n  User: {query}")
        result = run_function_calling_loop(query)
        if result["tool_results"]:
            for tr in result["tool_results"]:
                print(f"    Tool: {tr['tool']} ({tr['execution_time_ms']}ms)")
                print(f"    Result: {json.dumps(tr['result'], indent=None)[:90]}")
        else:
            print(f"    [No tool called -- direct response]")
        print(f"    Iterations: {result['iterations']}")

    print(f"\n--- Parallel Tool Calls ---")
    multi_city_query = "What's the weather in tokyo and london?"
    print(f"  User: {multi_city_query}")
    result = run_function_calling_loop(multi_city_query)
    print(f"  Tool calls made: {len(result['tool_results'])}")
    for tr in result["tool_results"]:
        city = tr["result"].get("city", "unknown")
        temp = tr["result"].get("temp_c", "N/A")
        print(f"    {city}: {temp}C, {tr['result'].get('condition', 'N/A')}")

    print(f"\n--- Security Checks ---")
    security_tests = [
        ("read_file", {"path": "../../etc/passwd"}),
        ("run_code", {"code": "import subprocess; subprocess.run(['ls'])"}),
        ("calculator", {"expression": "__import__('os').system('ls')"}),
    ]
    for tool_name, args in security_tests:
        result = execute_tool_call({"name": tool_name, "arguments": args})
        blocked = result["result"].get("error", False)
        print(f"  {tool_name}({list(args.values())[0][:40]}): {'BLOCKED' if blocked else 'ALLOWED'}")

Use It

OpenAI Function Calling

# from openai import OpenAI
#
# client = OpenAI()
#
# tools = [{
#     "type": "function",
#     "function": {
#         "name": "get_weather",
#         "description": "Get current weather for a city",
#         "parameters": {
#             "type": "object",
#             "properties": {
#                 "city": {"type": "string"},
#                 "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
#             },
#             "required": ["city"]
#         }
#     }
# }]
#
# response = client.chat.completions.create(
#     model="gpt-4o",
#     messages=[{"role": "user", "content": "Weather in Tokyo?"}],
#     tools=tools,
#     tool_choice="auto",
# )
#
# tool_call = response.choices[0].message.tool_calls[0]
# args = json.loads(tool_call.function.arguments)
# result = get_weather(**args)
#
# final = client.chat.completions.create(
#     model="gpt-4o",
#     messages=[
#         {"role": "user", "content": "Weather in Tokyo?"},
#         response.choices[0].message,
#         {"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result)},
#     ],
# )
# print(final.choices[0].message.content)

A OpenAI retorna chamadas de ferramentas como response.choices[0].message.tool_calls. Cada chamada possui um id que você deve incluir ao retornar o resultado. O modelo usa esse ID para associar os resultados às chamadas correspondentes. O GPT-4o pode retornar múltiplas chamadas de ferramentas em uma única resposta — itere e execute todas elas.

Anthropic Tool Use

# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-sonnet-4-20250514",
#     max_tokens=1024,
#     tools=[{
#         "name": "get_weather",
#         "description": "Get current weather for a city",
#         "input_schema": {
#             "type": "object",
#             "properties": {
#                 "city": {"type": "string"},
#                 "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
#             },
#             "required": ["city"]
#         }
#     }],
#     messages=[{"role": "user", "content": "Weather in Tokyo?"}],
# )
#
# tool_block = next(b for b in response.content if b.type == "tool_use")
# result = get_weather(**tool_block.input)
#
# final = client.messages.create(
#     model="claude-sonnet-4-20250514",
#     max_tokens=1024,
#     tools=[...],
#     messages=[
#         {"role": "user", "content": "Weather in Tokyo?"},
#         {"role": "assistant", "content": response.content},
#         {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tool_block.id, "content": json.dumps(result)}]},
#     ],
# )

A Anthropic retorna chamadas de ferramentas como blocos de conteúdo com type: "tool_use". O resultado da ferramenta é enviado em uma mensagem de usuário com type: "tool_result". Observe a principal diferença: a Anthropic usa input_schema para definições de parâmetros de ferramentas, enquanto a OpenAI usa parameters.

MCP Integration

# MCP servers expose tools over a standardized protocol.
# Any MCP-compatible client can discover and call these tools.
#
# Example: connecting to a Postgres MCP server
#
# from mcp import ClientSession, StdioServerParameters
# from mcp.client.stdio import stdio_client
#
# server_params = StdioServerParameters(
#     command="npx",
#     args=["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
# )
#
# async with stdio_client(server_params) as (read, write):
#     async with ClientSession(read, write) as session:
#         await session.initialize()
#         tools = await session.list_tools()
#         result = await session.call_tool("query", {"sql": "SELECT count(*) FROM users"})

O MCP desacopla a implementação da ferramenta de seu consumo. O servidor Postgres conhece o SQL. O servidor GitHub conhece a API. Seu agente apenas descobre e chama as ferramentas — ele não precisa de código específico do provedor para cada integração.

Ship It

Esta lição produz outputs/prompt-tool-designer.md — um template de prompt reutilizável para projetar definições de ferramentas. Dê a ele uma descrição do que você deseja que a ferramenta faça e ele produzirá a definição JSON Schema completa com descrições, tipos e restrições.

Ela também produz outputs/skill-function-calling-patterns.md — um framework de decisão para implementar chamadas de função em produção, cobrindo o design de ferramentas, tratamento de erros, segurança e padrões específicos de provedores.

Exercises

  1. Adicione uma 6ª ferramenta: consulta de banco de dados. Implemente uma ferramenta SQL simulada com uma tabela em memória. A ferramenta aceita um nome de tabela e condições de filtro (não SQL bruto). Valide se o nome da tabela está em uma lista de permissões (allowlist) e se os operadores de filtro estão restritos a =, >, <, >=, <=. Retorne as linhas correspondentes como JSON.

  2. Implemente a repetição com feedback de erro. Quando uma chamada de ferramenta falhar (por exemplo, cidade não encontrada), envie a mensagem de erro de volta para a função de decisão do modelo e permita que ele corrija seus argumentos. Acompanhe quantas tentativas cada chamada leva. Defina um máximo de 3 tentativas por chamada de ferramenta.

  3. Construa um agente de múltiplas etapas. Algumas consultas exigem o encadeamento de chamadas de ferramentas: "Leia o arquivo de configuração e me diga qual modelo está configurado, depois pesquise na web pelo preço desse modelo." Implemente um loop que seja executado até que o modelo decida que nenhuma outra ferramenta é necessária, passando os resultados acumulados em cada etapa de decisão. Limite a 10 iterações para evitar loops infinitos.

  4. Meça a precisão da seleção de ferramentas. Crie 30 consultas de teste com os nomes de ferramentas esperados. Execute sua função de decisão em todas as 30 e meça qual porcentagem de vezes ela seleciona a ferramenta correta. Identifique quais consultas causam mais confusão entre as ferramentas.

  5. Implemente o cache de chamadas de ferramentas. Se a mesma ferramenta for chamada com argumentos idênticos dentro de 60 segundos, retorne o resultado do cache em vez de reexecutar. Use um dicionário chaveado por (tool_name, frozenset(args.items())). Meça as taxas de acerto do cache em uma conversa com 20 consultas.

Key Terms

Termo O que as pessoas dizem O que realmente significa
Function calling "Uso de ferramentas" (Tool use) O modelo gera um JSON estruturado descrevendo uma função a ser invocada com argumentos específicos — o seu código executa a função, não o modelo
Definição de ferramenta "Esquema da função" Um objeto JSON Schema que descreve o nome, propósito, parâmetros e tipos de uma ferramenta — o modelo lê isso para decidir quando e como usar a ferramenta
Escolha de ferramenta "Modo de chamada" Controla se o modelo deve chamar uma ferramenta (required), pode chamar uma ferramenta (auto) ou deve chamar uma ferramenta específica (named)
Chamada paralela "Multi-tool" O modelo gera várias chamadas de ferramenta em um único turno, reduzindo o número de idas e vindas (round trips) — tanto o GPT-4o quanto o Claude suportam isso
Resultado da ferramenta "Saída da função" O valor de retorno da execução de uma ferramenta, enviado de volta ao modelo como uma mensagem para que ele possa usar dados reais em sua resposta
Validação de argumentos "Checagem de entrada" A verificação se os argumentos gerados pelo modelo correspondem aos tipos, intervalos e restrições esperados antes de executar a ferramenta
MCP "Protocolo de ferramentas" Model Context Protocol — o padrão aberto da Anthropic para expor ferramentas por meio de servidores que qualquer cliente compatível pode descobrir e chamar
Loop do agente "Loop ReAct" O ciclo iterativo de o-modelo-decide-a-ferramenta, o-código-executa-a-ferramenta, o-resultado-alimenta-o-modelo até que o modelo tenha informações suficientes para responder
Envenenamento de ferramenta "Injeção de prompt via ferramentas" Um ataque em que os resultados da ferramenta contêm instruções que manipulam o comportamento do modelo — sanitize todas as saídas de ferramentas
Limite de taxa "Orçamento de chamadas" A definição de um número máximo de chamadas de ferramenta por conversa para evitar loops infinitos e custos descontrolados de API

Further Reading

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).