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

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.
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ário | Solução recomendada |
|---|---|
| Chamar a API da OpenAI | L1 + L2, com Pydantic + Instructor |
| Chamar a API do Claude | L1 + L2, porque Claude não oferece Strict Mode confiável |
| Implantar modelo local | L1 + L3, com Outlines ou guided_json do vLLM |
| Exigência altíssima de confiabilidade | L1 + 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
strictparameter 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
| Necessidade | Solução recomendada | Motivo |
|---|---|---|
| Chamada pura de API, com foco em estabilidade | OpenAI + Structured Outputs | Taxa de falha de 0,1%, a opção mais confiável |
| Raciocínio complexo + chamada de ferramentas | Claude + validação L1/L2 | Boa capacidade de raciocínio, mas exige validação |
| Deploy de modelo privado | Qwen/Llama + Outlines | Custo controlável e alta confiabilidade |
| Exigência extrema de formato, como finanças ou saúde | OpenAI Strict ou Outlines | Ambas chegam perto de falha zero |
| Prototipagem rápida | Instructor + qualquer API | Bem 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 erro | Deve tentar de novo? | Motivo |
|---|---|---|
| Erro de formato de parâmetro, como campo ausente ou tipo errado | Sim, com feedback de erro | O LLM consegue se corrigir |
| Erro de serviço da API, como 429 ou 500 | Sim, com backoff | Problema temporário no servidor |
| Falha de validação de negócio, como cidade fora da lista permitida | Não; retorne o erro diretamente | Exige confirmação do usuário |
| Falha na execução da ferramenta, como resultado vazio | Não; use fallback | Problema 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ção | Aumento de latência | Aumento de custo | Confiabilidade |
|---|---|---|---|
| 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 tentativas | Pró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:
- Taxa de falha de formato: proporção de requisições que falham na validação. Acima de 1%, vale investigar.
- 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.
- 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
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
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
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
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
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?
• 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?
Como escolher a solução certa de saída estruturada?
• **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?
Quanto overhead de desempenho a saída estruturada adiciona?
• **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 <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?
**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
Guia de engenharia de AI Agents
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
LangGraph vs AutoGen em rastreamento de estado: checkpoints, recuperação de timeout e decisão técnica
Comparativo aprofundado de rastreamento de estado entre LangGraph e AutoGen: 12 dimensões para avaliar Checkpoint, recuperação de timeout e suporte distribuído, com caso real, árvore de decisão e código executável para escolher o framework de Agent mais adequado.
Parte 11 de 22
Próximo
Benchmark de avaliação de agentes: guia prático de AgentBench a DeepEval
Guia sobre benchmarks de avaliação e testes de desempenho para agentes, comparando AgentBench, WebArena, τ-Bench e outros, com avaliação por componentes no DeepEval e exemplos de código.
Parte 13 de 22



Comentários
Entre com GitHub para comentar