Alternar tema

Saída estruturada de LLM: JSON Schema obrigatório e confiabilidade em chamadas de ferramentas

Easton editorial illustration: central JSON Schema gate, three incoming provider-output cards, one validated tool-call object

Alerta em produção: a chamada de ferramenta do Agent falhou. Foram 5 tentativas seguidas, todas por erro de formato de parâmetro. Abri o log e vi que o campo city deveria ser "Beijing", mas o LLM retornou {"name": "Beijing", "id": null}. O parser quebrou, e o pipeline inteiro de processamento de dados parou.

Esse foi um baita problema em que caí no ano passado.

Depois disso comecei a estudar de forma sistemática o problema de saída estruturada em LLMs: de Structured Outputs da OpenAI a Tool Use da Anthropic, de retry automático com Instructor a decodificação restrita com Outlines. No começo, achei que era só uma questão de “escrever melhor o prompt”. Depois ficou claro: isso não é problema de prompt. É problema de arquitetura de confiabilidade.

Este artigo compartilha a tal “arquitetura de confiabilidade em três camadas”: camada de validação de parâmetros, camada de retry em falhas e camada de decodificação restrita. No final, também comparo as soluções da OpenAI, Claude e Gemini, para mostrar como escolher e quando usar cada uma. De quebra, deixo alguns modelos de código de produção para você adaptar direto.

1. Por que saída estruturada é a base de um Agent

Primeiro, deixa eu falar do problema de “deriva de formato” que encontrei. Isso não é um caso isolado. É o tipo de dor que aparece para qualquer pessoa desenvolvendo Agent.

Três formas de deriva de formato

Primeira: campos ausentes. Você pede para o LLM retornar um objeto de usuário com name, age e email. Ele retorna {"name": "João"}. Os outros dois campos somem. Não acontece sempre. Acontece de vez em quando. Só que, em produção, “de vez em quando” vira “inevitável”.

Segunda: tipo errado. A documentação diz claramente: user_id é inteiro. O LLM retorna "user_id": "12345", uma string. A validação do Pydantic em Python falha na hora, e a cadeia inteira de chamada quebra.

Terceira: conteúdo extra. A mais escondida. Você pede JSON, e ele coloca um “Here is the response:” antes e um “I hope this helps!” depois. O parser de JSON vê aquilo e trava.

5-10%
Taxa de falha do JSON Mode
<0,1%
Taxa de falha do Structured Outputs

Os dados oficiais da OpenAI deixam o problema bem visível: o JSON Mode, que só garante JSON válido, tem taxa de falha entre 5% e 10%. Já o Structured Outputs, que força o modelo a seguir um Schema, fica abaixo de 0,1%. É uma diferença de duas ordens de magnitude.

Por que isso importa tanto

Você talvez pense: “é só uma falha de parsing, não? É só adicionar algumas tentativas de retry.”

O problema é que retry não sai de graça.

Custo de chamada de API. Uma chamada ao GPT-4 pode custar alguns centavos. Cinco retries já viram um custo bem maior. Se o seu Agent processa 100 mil requisições por dia e cada requisição tenta de novo, em média, 2 vezes, faça a conta.

Latência acumulada. Uma chamada leva 2 segundos. Três tentativas levam mais de 6 segundos para o usuário. Em cenários de conversa em tempo real, isso é inaceitável.

Experiência do usuário quebrada. A pessoa pergunta a previsão do tempo. Seu Agent fica travado, carregando por 10 segundos, e no fim devolve “erro do sistema”. Na próxima vez, ela não volta.

Por isso, saída estruturada não é um bônus. É a base para um Agent rodar de forma estável. A seguir, mostro como resolver isso. Não com “um prompt melhorzinho”, mas com uma arquitetura confiável.

2. Arquitetura de confiabilidade em três camadas

Essa arquitetura saiu depois de muitos tropeços. Ela não é uma bala de prata, mas consegue reduzir a chance de erro de formato de 5-10% para algo próximo de zero.

L1: camada de validação de parâmetros, a primeira defesa

O que essa camada faz é simples: usar Pydantic para definir a estrutura de dados esperada, forçar conversão de tipos e filtrar por lista permitida.

from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import datetime

class ToolCallParams(BaseModel):
    """Modelo de parâmetros para chamada de ferramenta"""
    city: str = Field(..., min_length=1, max_length=50, description="Nome da cidade")
    date: Optional[datetime] = Field(None, description="Data da consulta")
    units: str = Field("metric", pattern="^(metric|imperial)$")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        # Validação por lista permitida
        allowed_cities = {"Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Hangzhou"}
        if v not in allowed_cities:
            raise ValueError(f"Cidade não suportada: {v}. Atualmente suportadas: {allowed_cities}")
        return v

O Pydantic ajuda em três coisas: conversão forçada de tipos, como string "123" para inteiro 123; detecção de campos ausentes; e validação personalizada. Essa é a camada mais básica e também a mais importante.

L2: camada de retry em falhas, com autocorreção por feedback

Quando os dados retornados pelo LLM falham na validação, você não deve simplesmente tentar de novo. O caminho melhor é devolver a mensagem de erro para o modelo e pedir que ele corrija. A biblioteca Instructor faz isso muito bem e já encapsula essa lógica.

import instructor
from openai import OpenAI
from pydantic import ValidationError

client = instructor.patch(OpenAI())

def get_weather_with_retry(user_query: str, max_retries: int = 3):
    """Mecanismo de retry com feedback de erro"""
    messages = [{"role": "user", "content": user_query}]

    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o",
                response_model=ToolCallParams,  # Modelo Pydantic
                messages=messages,
                temperature=0.1  # Baixa temperatura para saída estruturada
            )
            return response  # Validação automática aprovada

        except ValidationError as e:
            # Devolve o erro ao LLM para que ele corrija
            error_msg = f"Falha na validação de parâmetros: {str(e)}\nCorrija e retorne novamente no formato JSON correto."
            messages.append({"role": "assistant", "content": "Gerando parâmetros..."})
            messages.append({"role": "user", "content": error_msg})

            if attempt == max_retries - 1:
                raise Exception(f"Ainda falhou após {max_retries} tentativas: {e}")

# Exemplo de uso
result = get_weather_with_retry("Consulte a previsão do tempo de Beijing para amanhã")

A ideia central é: o LLM não está chutando às cegas. Ele consegue entender onde errou e por quê. Quando recebe feedback, consegue corrigir. Nos meus testes, depois de adicionar esse mecanismo, a taxa de sucesso após retry subiu de 60% para mais de 95%.

L3: camada de decodificação restrita, evitando erro na origem

As duas primeiras camadas são “resgate depois do erro”. A L3 é prevenção antes do erro.

O princípio da decodificação restrita é este: quando o LLM gera cada token, uma máquina de estados finitos (FSM) limita o espaço de escolha e força o modelo a gerar apenas sequências compatíveis com o Schema. É como instalar um freio no LLM: mesmo que ele queira sair do formato, não consegue.

Há duas opções principais de implementação:

Outlines (solução open source, boa para modelos locais):

from outlines import models, generate
import json

# Carrega um modelo local
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Define o Schema
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"}
    },
    "required": ["name", "age"]
}

# Cria o gerador com restrição
generator = generate.json(model, schema)
result = generator("Extraia as informações do usuário: João tem 28 anos")
# 100% compatível com o Schema, sem precisar de retry

guided_json do vLLM (boa opção para deploy de modelos grandes):

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-72B-Instruct")
sampling_params = SamplingParams(
    temperature=0.0,
    guided_decoding_backend="outlines",
    guided_json={  # Passa o JSON Schema diretamente
        "type": "object",
        "properties": {
            "tool_name": {"type": "string"},
            "arguments": {"type": "object"}
        }
    }
)

O custo da L3 é um overhead extra de compilação: a FSM precisa ser construída previamente a partir do Schema. Se o seu Schema muda com frequência, reconstruir a FSM a cada vez adiciona latência. Mas, para a maioria das aplicações com Agent, o Schema é relativamente estável, então esse custo costuma ser aceitável.

Como escolher entre as três camadas

CenárioSolução recomendada
Chamar a API da OpenAIL1 + L2, com Pydantic + Instructor
Chamar a API do ClaudeL1 + L2, porque Claude não oferece Strict Mode confiável
Implantar modelo localL1 + L3, com Outlines ou guided_json do vLLM
Exigência altíssima de confiabilidadeL1 + L2 + L3 juntas

3. Comparativo entre fornecedores: OpenAI, Claude e Gemini

Nesta seção, vamos comparar as diferenças de implementação entre os fornecedores. Se você não fizer essa comparação, é fácil cair em armadilhas, porque o conceito e a implementação de “saída estruturada” mudam bastante entre eles.

OpenAI: Strict Mode, conformidade obrigatória

A OpenAI lançou Structured Outputs em agosto de 2024. Hoje, é uma das soluções mais confiáveis entre APIs comerciais.

O mecanismo central é o parâmetro strict: true. Quando ativado, a saída do LLM é restringida ao JSON Schema definido por você, garantindo 100% de conformidade. Por baixo, a técnica usada é decodificação restrita, baseada em Grammar-based Constrained Decoding, parecida com o princípio do Outlines.

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Extraia as informações do usuário"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "user_info",
            "strict": True,  # Parâmetro chave
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"}
                },
                "required": ["name", "age"]
            }
        }
    }
)
# Saída 100% compatível com o Schema

Os dados oficiais da OpenAI indicam que a taxa de falha do Strict Mode fica abaixo de 0,1%. Na prática, também não encontrei erro de formato usando essa abordagem. Mas há limites: Schema recursivo não é suportado, e algumas estruturas aninhadas mais complexas exigem contornos.

Anthropic Claude: Tool Use, sem garantia de conformidade

A saída estruturada do Claude segue outro caminho: Tool Use, ou chamada de ferramentas.

Você define uma ferramenta, e Claude a chama passando parâmetros. Só que há uma armadilha: embora o parâmetro strict exista, a documentação oficial deixa claro que ele é ignorado. Claude não garante que os parâmetros vão obedecer ao Schema definido.

Esta é a frase da documentação oficial da Anthropic, atualizada em abril de 2026:

“The strict parameter is currently ignored for tool definitions. Claude will make a best effort to provide valid arguments, but does not guarantee schema compliance.”

Em português direto: ele vai tentar, mas não garante. Por isso, ao usar Claude para chamada de ferramentas, sempre adicione L1, validação de parâmetros, e L2, retry em falhas.

import anthropic

client = anthropic.Anthropic()

# Definição de ferramenta no Claude
tools = [{
    "name": "get_weather",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
}]

response = client.messages.create(
    model="claude-3.5-sonnet",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Clima em Beijing"}]
)

# Importante: valide manualmente os parâmetros de tool_use
for block in response.content:
    if block.type == "tool_use":
        # Aqui entra a validação com Pydantic
        validated_params = ToolCallParams.model_validate(block.input)

Google Gemini: Controlled Generation

A solução do Gemini se chama Controlled Generation e usa o parâmetro response_schema para especificar a estrutura da saída.

import google.generativeai as genai

model = genai.GenerativeModel('gemini-1.5-pro')

response = model.generate_content(
    "Extraia as informações do usuário",
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"}
            },
            "required": ["name", "age"]
        }
    }
)

A confiabilidade do Gemini fica entre OpenAI e Claude: existe restrição, mas não com a mesma força de “conformidade obrigatória” da OpenAI. Nos meus testes, a taxa de falha ficou por volta de 1-2%. É melhor que JSON Mode, mas não chega ao nível do Strict Mode.

Modelos open source: apoio em Outlines/vLLM

Modelos open source, como Qwen, Llama e Mistral, não oferecem saída estruturada por conta própria. Você precisa de ferramentas externas. As opções mais comuns são Outlines e guided_json do vLLM, citadas acima.

Aqui há um ponto interessante: com Outlines, a confiabilidade da saída estruturada em modelos open source pode superar a de algumas APIs comerciais. A razão é simples: a FSM é uma restrição dura. Não existe o cenário “vou tentar, mas não garanto”.

Tabela rápida de escolha

NecessidadeSolução recomendadaMotivo
Chamada pura de API, com foco em estabilidadeOpenAI + Structured OutputsTaxa de falha de 0,1%, a opção mais confiável
Raciocínio complexo + chamada de ferramentasClaude + validação L1/L2Boa capacidade de raciocínio, mas exige validação
Deploy de modelo privadoQwen/Llama + OutlinesCusto controlável e alta confiabilidade
Exigência extrema de formato, como finanças ou saúdeOpenAI Strict ou OutlinesAmbas chegam perto de falha zero
Prototipagem rápidaInstructor + qualquer APIBem empacotado, com retry automático

4. Modelos de código práticos

Esta seção traz alguns modelos de código de produção. Eu validei esses padrões em ambiente real, então você pode adaptar direto.

Modelo 1: exemplo completo de OpenAI Structured Outputs

"""
Exemplo completo de OpenAI Structured Outputs
Indicado para: chamada de ferramentas, extração de dados, geração de relatórios e cenários parecidos
"""
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
import json

# 1. Define o modelo Pydantic
class SearchQuery(BaseModel):
    """Parâmetros de busca"""
    keywords: List[str] = Field(
        ...,
        min_length=1,
        max_length=5,
        description="Lista de palavras-chave de busca"
    )
    filters: Optional[dict] = Field(
        default=None,
        description="Filtros opcionais"
    )
    limit: int = Field(
        default=10,
        ge=1,
        le=100,
        description="Quantidade de resultados retornados"
    )

# 2. Converte o modelo Pydantic em JSON Schema
def model_to_schema(model: type[BaseModel]) -> dict:
    """Converte um modelo Pydantic em JSON Schema"""
    schema = model.model_json_schema()
    # Remove metadados adicionados pelo Pydantic
    schema.pop("title", None)
    for prop in schema.get("properties", {}).values():
        prop.pop("title", None)
    return schema

# 3. Chamada com saída estruturada
client = OpenAI()

def extract_search_params(user_input: str) -> SearchQuery:
    """Extrai parâmetros de busca a partir da entrada do usuário"""
    schema = model_to_schema(SearchQuery)

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Você é um assistente de busca que ajuda o usuário a extrair parâmetros de pesquisa."
            },
            {"role": "user", "content": user_input}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "search_query",
                "strict": True,
                "schema": schema
            }
        },
        temperature=0.1  # Baixa temperatura para saída estruturada
    )

    # 4. Analisa e valida novamente
    raw_content = response.choices[0].message.content
    data = json.loads(raw_content)
    return SearchQuery.model_validate(data)

# Exemplo de uso
if __name__ == "__main__":
    query = extract_search_params(
        "Quero encontrar artigos sobre programação assíncrona em Python, apenas do último mês, no máximo 20 resultados"
    )
    print(query)
    # SearchQuery(keywords=['Python', 'programação assíncrona'], filters={'date_range': 'last_month'}, limit=20)

Modelo 2: exemplo de retry automático com Instructor

"""
Exemplo de retry automático com Instructor
Indicado para: Claude API, OpenAI JSON Mode sem Strict e cenários que exigem tolerância a erro
"""
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, ValidationError

class AgentAction(BaseModel):
    """Decisão de ação do Agent"""
    action_type: str = Field(
        ...,
        pattern="^(search|execute|respond|clarify)$"
    )
    parameters: dict = Field(default_factory=dict)
    reasoning: str = Field(..., min_length=10)

# Aplica patch no cliente OpenAI
client = instructor.patch(OpenAI())

def get_agent_decision(
    context: str,
    user_request: str,
    max_retries: int = 3
) -> AgentAction:
    """
    Obtém a decisão de ação do Agent, com retry automático

    Args:
        context: contexto atual da conversa
        user_request: solicitação do usuário
        max_retries: número máximo de tentativas

    Returns:
        AgentAction: decisão de ação validada
    """
    messages = [
        {"role": "system", "content": "Você é um assistente inteligente que analisa a necessidade do usuário e decide o próximo passo."},
        {"role": "user", "content": f"Contexto: {context}\n\nSolicitação do usuário: {user_request}"}
    ]

    try:
        response = client.chat.completions.create(
            model="gpt-4o",
            response_model=AgentAction,  # Validação automática do Instructor
            messages=messages,
            max_retries=max_retries,  # Retry embutido
            temperature=0.1
        )
        return response

    except ValidationError as e:
        # O Instructor já tentou max_retries vezes
        raise Exception(f"Não foi possível corrigir o formato. Verifique a definição do modelo: {e}")

# Exemplo de uso
decision = get_agent_decision(
    context="O usuário está consultando informações de clima",
    user_request="Consulte a previsão de Beijing para amanhã e recomende atividades ao ar livre se estiver ensolarado"
)
print(f"Tipo de ação: {decision.action_type}")
print(f"Parâmetros: {decision.parameters}")
print(f"Raciocínio: {decision.reasoning}")

Modelo 3: saída estruturada com modelo local usando Outlines

"""
Exemplo de saída estruturada com modelo local usando Outlines
Indicado para: deploy privado, sensibilidade a custo e alta exigência de privacidade
"""
from outlines import models, generate
from pydantic import BaseModel
from typing import List
import json

# Define a estrutura de dados
class ProductInfo(BaseModel):
    """Informações do produto"""
    name: str
    price: float
    category: str
    tags: List[str]

# Carrega o modelo. A primeira carga pode levar alguns segundos.
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Cria o gerador estruturado
# Observação: o schema será compilado em FSM na primeira chamada, com overhead aproximado de 1-2 segundos
schema_str = json.dumps(ProductInfo.model_json_schema())
generator = generate.json(model, schema_str)

def extract_product_info(description: str) -> ProductInfo:
    """
    Extrai informações estruturadas de uma descrição de produto

    Args:
        description: texto de descrição do produto

    Returns:
        ProductInfo: informações estruturadas do produto
    """
    prompt = f"Extraia as informações-chave da seguinte descrição de produto e retorne em JSON:\n{description}"

    # Gera resultado 100% compatível com o Schema
    result = generator(prompt)

    # Converte para modelo Pydantic, com segunda validação para garantir consistência
    return ProductInfo.model_validate(result)

# Exemplo de uso
description = """
Este fone Bluetooth usa tecnologia recente de redução de ruído, custa 299 reais
e pertence à categoria de acessórios digitais, adequado para corrida, deslocamento diário e outros cenários.
"""
product = extract_product_info(description)
print(product)
# ProductInfo(name='fone Bluetooth', price=299.0, category='acessórios digitais', tags=['corrida', 'deslocamento diário'])

Modelo 4: fluxo completo de chamada de ferramenta

"""
Fluxo completo de validação de parâmetros para chamada de ferramenta
Inclui: definição de Schema → chamada ao LLM → validação de parâmetros → retry em falha → execução da ferramenta
"""
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator, ValidationError
from typing import Callable, Dict, Any
import json

# 1. Define o modelo de parâmetros da ferramenta
class WeatherQueryParams(BaseModel):
    """Parâmetros da ferramenta de consulta de clima"""
    city: str = Field(..., min_length=1, max_length=50)
    date_offset: int = Field(default=0, ge=-7, le=7, description="Deslocamento de data; 0 indica hoje")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        allowed = {"Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Hangzhou", "Chengdu", "Wuhan"}
        if v not in allowed:
            raise ValueError(f"Cidade não suportada. Opções: {allowed}")
        return v

# 2. Gerenciador de chamadas de ferramenta
class ToolCallManager:
    """Gerencia o fluxo completo de chamada de ferramenta"""

    def __init__(self):
        self.client = OpenAI()
        self.tools: Dict[str, Callable] = {}

    def register_tool(self, name: str, func: Callable, param_model: type[BaseModel]):
        """Registra uma ferramenta"""
        self.tools[name] = {
            "function": func,
            "param_model": param_model
        }

    def execute_with_retry(
        self,
        tool_name: str,
        user_request: str,
        max_retries: int = 3
    ) -> Any:
        """Executa uma chamada de ferramenta com retry"""

        tool_config = self.tools[tool_name]
        param_model = tool_config["param_model"]
        schema = param_model.model_json_schema()

        messages = [
            {"role": "system", "content": f"Extraia os parâmetros de chamada da ferramenta '{tool_name}'"},
            {"role": "user", "content": user_request}
        ]

        for attempt in range(max_retries):
            try:
                # Chama o LLM para obter os parâmetros
                response = self.client.chat.completions.create(
                    model="gpt-4o",
                    messages=messages,
                    response_format={
                        "type": "json_schema",
                        "json_schema": {
                            "name": tool_name,
                            "strict": True,
                            "schema": schema
                        }
                    },
                    temperature=0.1
                )

                # Valida os parâmetros
                params = param_model.model_validate_json(
                    response.choices[0].message.content
                )

                # Executa a ferramenta
                return tool_config["function"](params)

            except ValidationError as e:
                # Devolve o erro para o LLM corrigir
                messages.append({
                    "role": "user",
                    "content": f"Falha na validação de parâmetros: {e}\nCorrija o formato dos parâmetros."
                })
                continue

        raise Exception(f"Falha na chamada da ferramenta. Ainda não passou na validação após {max_retries} tentativas")

# 3. Exemplo de uso
def get_weather(params: WeatherQueryParams) -> str:
    """Simula uma consulta de clima"""
    # Aqui ficaria a chamada real à API
    return f"{params.city} terá tempo ensolarado nos próximos {params.date_offset} dias"

manager = ToolCallManager()
manager.register_tool("get_weather", get_weather, WeatherQueryParams)

result = manager.execute_with_retry(
    "get_weather",
    "Consulte a previsão do tempo de Beijing para amanhã"
)
print(result)  # Beijing terá tempo ensolarado nos próximos 1 dias

Esses modelos cobrem os cenários mais comuns. Na prática, você pode combinar e ajustar de acordo com a necessidade.

5. Boas práticas para ambiente de produção

Depois que o código está pronto, ainda há vários detalhes de produção para cuidar. Aqui vão alguns problemas que já encontrei e as respectivas formas de resolver.

Configuração de Temperature: não exagere

Em saída estruturada, recomendo definir Temperature entre 0.0 e 0.2. Esse intervalo aparece na documentação oficial da OpenAI e também foi o mais estável nos meus testes.

Qual é o problema de temperatura alta? O LLM fica mais “divergente”, e a saída fica mais aleatória. Aleatoriedade é inimiga de saída estruturada. O que você quer é determinismo, não criatividade. Antes, configurei Temperature como 0.7, e a taxa de erro de formato subiu para 15%. Depois mudei para 0.1, e praticamente parei de ver esse tipo de problema.

Estratégia de retry: nem todo erro merece nova tentativa

Antes de tentar de novo, classifique o tipo de erro:

Tipo de erroDeve tentar de novo?Motivo
Erro de formato de parâmetro, como campo ausente ou tipo erradoSim, com feedback de erroO LLM consegue se corrigir
Erro de serviço da API, como 429 ou 500Sim, com backoffProblema temporário no servidor
Falha de validação de negócio, como cidade fora da lista permitidaNão; retorne o erro diretamenteExige confirmação do usuário
Falha na execução da ferramenta, como resultado vazioNão; use fallbackProblema da própria ferramenta

Já vi gente repetir todo tipo de erro sem limite. Resultado: um nome de cidade fora da lista permitida fez o LLM tentar adivinhar 10 vezes, sem acertar, até estourar timeout. Separar os tipos de erro é o que torna o tratamento eficiente.

Comparativo de overhead de desempenho

SoluçãoAumento de latênciaAumento de custoConfiabilidade
Restrição por prompt, sem parâmetro especial+0 ms+0%5-10% de falha
JSON Mode, apenas OpenAI+50 ms+0%2-5% de falha
Structured Outputs, com Strict+100 ms+0%<0,1% de falha
Retry com Instructor+200-500 ms por tentativa+custo × número de tentativasPróxima de 0%
FSM do Outlines+1-2 s na primeira compilação+0%100% de conformidade

Na escolha, pese o objetivo: se você busca estabilidade extrema, use Structured Outputs ou Outlines. Se busca prototipagem rápida, use retry automático com Instructor. Se o orçamento é limitado, JSON Mode + validação manual também pode ser suficiente.

Métricas de monitoramento: três indispensáveis

Depois de colocar em produção, monitore pelo menos estas métricas:

  1. Taxa de falha de formato: proporção de requisições que falham na validação. Acima de 1%, vale investigar.
  2. Número médio de retries: normalmente deveria ficar entre 0,5 e 1,5. Acima de 2, o modelo ou o Schema provavelmente tem problema.
  3. Latência média: saída estruturada adiciona 50-200 ms em relação à saída comum, mas precisa ficar dentro de um intervalo aceitável.

Eu uso Prometheus + Grafana para monitoramento e olho os relatórios toda semana. Uma vez, o número de retries saltou de 0,8 para 2,5. Investigando, descobri que o Schema tinha mudado, mas o código não tinha sido sincronizado. Ainda bem que o monitoramento pegou a tempo.

Conclusão

Depois de tudo isso, a ideia central é simples: em 2026, saída estruturada já não é um problema difícil, desde que você use o método certo.

A arquitetura em três camadas, com validação de parâmetros, retry em falhas e decodificação restrita, cobre quase todos os cenários, do “consegue rodar” ao “roda de forma estável”. Na hora de escolher fornecedor, lembre: OpenAI Strict Mode é o mais estável; Claude exige validação própria; modelos open source com Outlines podem ser surpreendentemente confiáveis.

Os modelos de código estão na quarta seção. Dá para pegar e adaptar. Se você está começando com desenvolvimento de Agent, eu recomendo começar pelo Instructor: ele é bem empacotado e já traz retry automático e feedback de erro embutidos. Depois, quando estiver mais confortável, avalie se vale subir para Outlines e buscar 100% de conformidade obrigatória.

Se tiver dúvidas, deixe um comentário ou fale comigo diretamente. Este texto é um pouco mais técnico, mas espero que ajude você a evitar alguns problemas antes que eles cheguem à produção.

Fluxo completo para implementar OpenAI Structured Outputs

Passo a passo completo, da definição do modelo Pydantic até a chamada com saída estruturada

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Definir o modelo de dados com Pydantic

    Crie uma classe de modelo Pydantic e use Field para definir restrições de campos:

    • Use `Field(..., min_length=1, max_length=50)` para definir o intervalo de tamanho de strings
    • Use `Field(default=10, ge=1, le=100)` para definir intervalos numéricos
    • Use `@field_validator` para adicionar lógica de validação personalizada, como filtro por lista permitida
    • Use `Optional[T]` para definir campos opcionais
  2. 2

    Step 2: Converter o modelo Pydantic em JSON Schema

    Use o método `model.model_json_schema()` para converter:

    ```python
    schema = SearchQuery.model_json_schema()
    schema.pop("title", None) # Remove metadados do Pydantic
    ```

    Garanta que o Schema atenda aos requisitos do OpenAI Structured Outputs.
  3. 3

    Step 3: Chamar a API da OpenAI e ativar o Strict Mode

    Defina o parâmetro `response_format` na requisição da API:

    • `type: "json_schema"` — especifica o tipo de saída estruturada
    • `strict: True` — ativa o modo de conformidade obrigatória
    • `json_schema.name` — nome do Schema, definido por você
    • `json_schema.schema` — JSON Schema convertido no passo anterior
  4. 4

    Step 4: Analisar a resposta e fazer uma segunda validação

    Embora o Strict Mode garanta 100% de conformidade, ainda vale fazer uma segunda validação:

    • Use `json.loads()` para analisar a string de resposta
    • Use `model.model_validate(data)` para validar com Pydantic
    • Capture exceções `ValidationError` e trate casos de borda
  5. 5

    Step 5: Configurar o parâmetro Temperature

    Em cenários de saída estruturada, use baixa temperatura:

    ```python
    temperature=0.1 # Recomendado: 0.0-0.2
    ```

    Evite temperaturas altas, que aumentam a aleatoriedade da saída e afetam a estabilidade do formato.

FAQ

O que fazer quando um LLM retorna JSON em formato errado?
Use uma arquitetura de confiabilidade em três camadas:

• L1, camada de validação de parâmetros: use Pydantic para definir modelos de dados, conversão automática de tipos e validação de campos
• L2, camada de retry em falhas: use a biblioteca Instructor para tentar de novo automaticamente, devolvendo o erro ao LLM para autocorreção
• L3, camada de decodificação restrita: use Outlines ou `guided_json` do vLLM para garantir conformidade na origem
Qual é a diferença entre as saídas estruturadas da OpenAI e do Claude?
O Strict Mode do OpenAI Structured Outputs garante 100% de conformidade de formato, com taxa de falha &lt;0,1%. Já o Tool Use do Claude não garante conformidade; o parâmetro strict é ignorado oficialmente, então você precisa adicionar validação L1/L2 por conta própria. Se a prioridade for estabilidade extrema, escolha OpenAI. Se você precisa de raciocínio complexo, Claude + validação própria costuma ser uma opção melhor.
Como escolher a solução certa de saída estruturada?
Escolha de acordo com o cenário e a exigência:

• **Chamadas à API da OpenAI**: Structured Outputs + Strict Mode, a opção mais estável
• **Chamadas à API do Claude**: validação com Pydantic + retry com Instructor, pois exige validação própria
• **Deploy de modelo local**: Outlines ou `guided_json` do vLLM, com custo controlável e alta confiabilidade
• **Prototipagem rápida**: biblioteca Instructor, bem empacotada e pronta para usar
• **Cenários rigorosos, como finanças e saúde**: OpenAI Strict ou Outlines, com falha próxima de zero
Como configurar o parâmetro Temperature?
Para saída estruturada, recomendo definir Temperature entre 0.0 e 0.2. Esse é o intervalo recomendado pela documentação da OpenAI e também foi o mais estável nos testes. Temperaturas altas aumentam a aleatoriedade da saída e elevam a taxa de erro de formato. Nos meus testes, com Temperature 0.7 a taxa de erro chegou a 15%; depois de mudar para 0.1, os problemas de formato praticamente desapareceram.
Quanto overhead de desempenho a saída estruturada adiciona?
O overhead varia bastante entre as soluções:

• **Restrição por prompt**: +0 ms de latência, taxa de falha de 5-10%
• **JSON Mode**: +50 ms de latência, taxa de falha de 2-5%
• **Structured Outputs**: +100 ms de latência, taxa de falha &lt;0,1%
• **Retry com Instructor**: +200-500 ms por tentativa, taxa de falha próxima de 0%
• **FSM do Outlines**: +1-2 s na primeira compilação, 100% de conformidade

Escolha equilibrando exigência de confiabilidade e orçamento.
Quais erros devem ser repetidos? Quais não devem?
A estratégia de retry precisa diferenciar o tipo de erro:

**Deve tentar de novo**:
• Erro de formato de parâmetro, como campo ausente ou tipo errado — o LLM consegue se corrigir
• Erro de serviço da API, como 429 ou 500 — problema temporário no servidor

**Não deve tentar de novo**:
• Falha de validação de negócio, como cidade fora da lista permitida — exige confirmação do usuário
• Falha na execução da ferramenta, como retorno vazio — é problema da ferramenta em si e deve ir para fallback

Retry infinito causa timeout. Só dá para tratar isso bem quando você separa os tipos de erro.

19 min de leitura · Publicado em: 6 mai 2026 · Atualizado em: 14 jul 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog