03-02-03 — Structured outputs: JSON mode, function calling, tool use

⏱ 15 minFontes validadas em: 2026-04-29

TL;DR

LLMs retornam texto — mas sua aplicação precisa de dados. JSON mode garante JSON válido. Function calling faz o modelo decidir qual função chamar com quais parâmetros. Structured outputs (OpenAI) garante conformidade com um JSON Schema exato. A diferença é garantia: JSON mode evita erros de parse, structured outputs garante o schema. Use function calling/tool use sempre que o LLM precisar se integrar com código real.

O problema: LLMs retornam texto livre

Sem restrições, um LLM pode responder "Claro! Aqui está o resultado: {…}" ou colocar o JSON dentro de um bloco markdown. Isso quebra qualquer json.loads(). As três abordagens a seguir resolvem isso em níveis crescentes de rigidez.

Abordagem 1: JSON mode

Garante que a resposta seja JSON válido. Não garante o schema — o modelo decide a estrutura. Use quando você quer JSON mas tem flexibilidade na estrutura.

from openai import OpenAI
import json

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_object"},  # JSON mode
    messages=[
        {
            "role": "system",
            "content": "Você extrai dados de texto. Sempre responda em JSON válido."
        },
        {
            "role": "user",
            "content": "Extraia os dados: 'Reunião com Pedro Alves (pedro@vale.com) da Vale, diretor de TI, amanhã às 14h.'"
        }
    ]
)

# Garantido ser JSON válido — sem try/except necessário
data = json.loads(response.choices[0].message.content)
print(data)
# {"nome": "Pedro Alves", "email": "pedro@vale.com", "empresa": "Vale", "cargo": "Diretor de TI", "horario": "amanhã às 14h"}
⚠️ JSON mode ≠ schema garantido: O modelo pode inventar campos, omitir campos obrigatórios, ou usar nomes diferentes. Para schema rígido, use Structured Outputs (abordagem 3).

Abordagem 2: Function calling

Você define funções com schemas JSON. O modelo decide quando e como chamar cada função — ele retorna um "tool call" com os parâmetros preenchidos. Você executa a função. É o mecanismo de integração LLM → código.

from openai import OpenAI
import json

client = OpenAI()

# Define as funções disponíveis
tools = [
    {
        "type": "function",
        "function": {
            "name": "criar_ticket_suporte",
            "description": "Cria um ticket de suporte no sistema. Use quando o usuário reportar um problema técnico.",
            "parameters": {
                "type": "object",
                "properties": {
                    "titulo": {
                        "type": "string",
                        "description": "Título conciso do problema"
                    },
                    "prioridade": {
                        "type": "string",
                        "enum": ["baixa", "media", "alta", "critica"],
                        "description": "Prioridade do ticket"
                    },
                    "descricao": {
                        "type": "string",
                        "description": "Descrição detalhada do problema"
                    },
                    "sistema_afetado": {
                        "type": "string",
                        "description": "Nome do sistema ou módulo afetado"
                    }
                },
                "required": ["titulo", "prioridade", "descricao"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "consultar_status_sistema",
            "description": "Consulta o status operacional de um sistema.",
            "parameters": {
                "type": "object",
                "properties": {
                    "sistema": {"type": "string", "description": "Nome do sistema a consultar"}
                },
                "required": ["sistema"]
            }
        }
    }
]

# Simulação: funções reais que seriam chamadas
def criar_ticket_suporte(titulo, prioridade, descricao, sistema_afetado=None):
    return {"ticket_id": "INC-2847", "status": "criado", "titulo": titulo}

def consultar_status_sistema(sistema):
    return {"sistema": sistema, "status": "operacional", "uptime": "99.9%"}

# Conversa com o assistente
messages = [
    {"role": "system", "content": "Você é o assistente de suporte técnico da Impar. Ajude usuários a resolver problemas e criar tickets quando necessário."},
    {"role": "user", "content": "O sistema de autenticação está retornando erro 500 desde as 10h. É urgente, tem 200 usuários bloqueados."}
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"  # modelo decide quando chamar
)

message = response.choices[0].message

# Verifica se o modelo quer chamar uma função
if message.tool_calls:
    messages.append(message)  # adiciona resposta do assistente ao histórico
    
    for tool_call in message.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        
        print(f"Chamando: {func_name}({func_args})")
        
        # Executa a função real
        if func_name == "criar_ticket_suporte":
            result = criar_ticket_suporte(**func_args)
        elif func_name == "consultar_status_sistema":
            result = consultar_status_sistema(**func_args)
        
        # Adiciona resultado da função ao histórico
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result)
        })
    
    # Segunda chamada: modelo processa o resultado e responde ao usuário
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )
    print(final_response.choices[0].message.content)

Abordagem 3: Structured outputs (OpenAI — schema garantido)

A OpenAI garante conformidade exata com o JSON Schema. Zero chance de campos extras, campos faltando, ou tipos errados. Use quando o output alimenta código downstream diretamente.

from pydantic import BaseModel
from openai import OpenAI

client = OpenAI()

# Define o schema via Pydantic — a OpenAI converte para JSON Schema automaticamente
class ContatoExtraido(BaseModel):
    nome: str
    email: str
    empresa: str
    cargo: str
    telefone: str | None = None  # campo opcional

class ListaContatos(BaseModel):
    contatos: list[ContatoExtraido]
    total_encontrados: int

# Structured outputs — schema garantido
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Extraia todos os contatos mencionados no texto."},
        {"role": "user", "content": """
            Reunião confirmada com Ana Lima (ana@tim.com.br), gerente de projetos da TIM,
            e com Roberto Souza (roberto@axia.com, (21) 99999-1234), CTO da Axia.
        """}
    ],
    response_format=ListaContatos  # passa a classe Pydantic diretamente
)

# response.choices[0].message.parsed é um objeto ListaContatos tipado
result: ListaContatos = response.choices[0].message.parsed
print(f"Total: {result.total_encontrados}")
for contato in result.contatos:
    print(f"  {contato.nome} — {contato.email} — {contato.empresa}")

Tool use no Anthropic (Claude)

A Anthropic tem implementação equivalente, mas com sintaxe ligeiramente diferente:

import anthropic
import json

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Retorna a temperatura atual de uma cidade.",
        "input_schema": {
            "type": "object",
            "properties": {
                "cidade": {"type": "string", "description": "Nome da cidade"}
            },
            "required": ["cidade"]
        }
    }
]

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "Qual a temperatura em São Paulo agora?"}
    ]
)

# Verifica se Claude quer usar uma tool
for block in response.content:
    if block.type == "tool_use":
        print(f"Tool: {block.name}")
        print(f"Input: {block.input}")
        
        # Simula execução
        tool_result = {"temperatura": "28°C", "condicao": "ensolarado"}
        
        # Retorna resultado para Claude finalizar a resposta
        final = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            tools=tools,
            messages=[
                {"role": "user", "content": "Qual a temperatura em São Paulo agora?"},
                {"role": "assistant", "content": response.content},
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": json.dumps(tool_result)
                        }
                    ]
                }
            ]
        )
        print(final.content[0].text)

Parallel tool calls

Em GPT-4o, o modelo pode solicitar múltiplas tool calls simultaneamente. Isso é crítico para performance:

# O modelo pode retornar múltiplas tool_calls de uma vez
# Exemplo: "Compare o weather de SP, RJ e BH"
# O modelo retorna 3 tool_calls simultâneos em vez de 3 chamadas sequenciais

if message.tool_calls and len(message.tool_calls) > 1:
    import asyncio
    
    async def execute_tool(tool_call):
        # executa em paralelo
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)
        result = await some_async_function(func_name, func_args)
        return tool_call.id, result
    
    # Executa todas as tools em paralelo
    results = await asyncio.gather(*[execute_tool(tc) for tc in message.tool_calls])
🏢 Azure OpenAI: Function calling e structured outputs estão disponíveis em Azure OpenAI com as mesmas APIs. O Azure adiciona uma camada de segurança: você pode filtrar quais tools são expostas por usuário/grupo via Azure RBAC, garantindo que um usuário não autenticado não possa chamar tools sensíveis.

Como isso se conecta

  • 03-02-02 ReAct: o "Action" do ReAct é implementado via function calling/tool use — agora você sabe o mecanismo técnico
  • 03-03-03 Desafio: o desafio final usa structured outputs para extração de entidades — você vai implementar o que aprendeu aqui
  • → Módulo 04 RAG: retrieval tools são o pattern mais comum de function calling — buscar documentos relevantes antes de responder

Fontes

  1. OpenAI — Structured outputs guide
  2. OpenAI — Function calling guide
  3. Anthropic — Tool use (function calling)
  4. Microsoft Learn — Function calling with Azure OpenAI