Um desenvolvedor de uma fintech me mostrou este código numa terça-feira:
# Extração de dados de nota fiscal com GPT-4
prompt = "Extraia CNPJ, valor total e data da nota fiscal abaixo: [texto da NF]"
resposta = openai.chat.completions.create(model="gpt-4", messages=[...])
print(resposta)
Output:
"O CNPJ é 12.345.678/0001-99, o valor total foi de R$ 1.234,56 e a data é 15/03/2024."
Ele olhou para mim frustrado:
“Como eu faço parse disso? Às vezes vem ‘R$ 1.234,56’, às vezes ‘1234.56’, às vezes ‘mil duzentos e trinta e quatro reais’. Formato muda toda hora. Preciso de JSON consistente para inserir no banco de dados. Mas o modelo não obedece.”
Este é o problema número 1 de usar LLMs em produção: outputs inconsistentes.
Você precisa de:
{
"cnpj": "12.345.678/0001-99",
"valor_total": 1234.56,
"data_emissao": "2024-03-15"
}
Mas recebe:
- Às vezes JSON válido
- Às vezes texto com JSON dentro
- Às vezes JSON com campos faltando
- Às vezes JSON com tipos errados (string ao invés de number)
Structured outputs e JSON mode resolvem isso: garantem que LLM retorne sempre dados estruturados e validados no formato que você especificou.
O problema: LLMs são probabilísticos, não determinísticos
Por que LLMs são inconsistentes?
LLMs geram texto token por token, baseado em probabilidades.
Quando você pede “retorne em JSON”, o modelo tenta gerar JSON, mas:
- Pode adicionar texto antes/depois do JSON (“Claro! Aqui está: …”)
- Pode usar nomes de campos diferentes (cnpj vs CNPJ vs cnpj_empresa)
- Pode omitir campos opcionais (ou inventar campos extras)
- Pode usar tipos errados (string ao invés de number)
- Pode gerar JSON inválido (vírgula faltando, chave sem fechar)
Exemplo real:
Você pede 100 vezes a mesma extração de dados. Resultados:
Request 1:
{"cnpj": "12.345.678/0001-99", "valor": 1234.56, "data": "2024-03-15"}
Request 2:
{"CNPJ": "12.345.678/0001-99", "valor_total": "1234,56", "data_emissao": "15/03/2024"}
Request 3:
Claro! Aqui estão os dados extraídos:
{"cnpj": "12.345.678/0001-99", "valor": 1234.56}
(faltou "data")
Request 4:
{"cnpj": "12.345.678/0001-99", "valor": 1234.56, "data": "15 de março de 2024", "observacao": "Nota fiscal válida"}
(adicionou campo extra não pedido)
Request 5:
{"cnpj": "12.345.678/0001-99", "valor": "mil duzentos e trinta e quatro reais e cinquenta e seis centavos"}
(valor como texto por extenso)
5 requests, 5 formatos diferentes. Impossível fazer parsing confiável.
Abordagens tradicionais (e por que falham)
Tentativa 1: Pedir “gentilmente” no prompt
prompt = """
Extraia CNPJ, valor total e data.
IMPORTANTE: Retorne APENAS JSON válido, sem texto adicional.
Formato:
{
"cnpj": "string",
"valor_total": number,
"data_emissao": "YYYY-MM-DD"
}
"""
Taxa de sucesso: 85-92% (melhora, mas não é 100%)
Falhas típicas:
- 5-8%: adiciona texto antes/depois do JSON
- 3-5%: usa formato de data errado (DD/MM/YYYY ao invés de YYYY-MM-DD)
- 2-3%: JSON inválido (erro de sintaxe)
Tentativa 2: Few-shot examples
prompt = """
Extraia dados de notas fiscais.
Exemplo 1:
Input: [nota fiscal]
Output: {"cnpj": "11.222.333/0001-44", "valor_total": 580.90, "data_emissao": "2024-01-10"}
Exemplo 2:
Input: [nota fiscal]
Output: {"cnpj": "55.666.777/0001-88", "valor_total": 1205.50, "data_emissao": "2024-02-20"}
Agora extraia desta nota fiscal:
[texto]
"""
Taxa de sucesso: 90-95% (melhor, mas ainda não é 100%)
Tentativa 3: Validação e retry
def extrair_com_retry(texto, max_tentativas=3):
for tentativa in range(max_tentativas):
resposta = chamar_llm(texto)
try:
dados = json.loads(resposta)
validar_schema(dados) # Pydantic
return dados
except:
continue # tenta de novo
raise Exception("Falhou após 3 tentativas")
Taxa de sucesso: 98-99% (quase lá, mas custa 3x mais e é mais lento)
As três soluções: JSON mode, Structured Outputs e Function Calling
Solução 1: JSON mode (OpenAI)
O que é:
Modo especial que garante que o output é JSON válido sintaticamente.
Como usar:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-2024-08-06", # ou gpt-4o-mini
messages=[
{"role": "system", "content": "Você extrai dados de notas fiscais e retorna JSON."},
{"role": "user", "content": f"Extraia CNPJ, valor e data: {texto_nf}"}
],
response_format={"type": "json_object"} # ← JSON mode ativado
)
dados = json.loads(response.choices[0].message.content)
Garantias:
- ✅ Sempre retorna JSON sintaticamente válido
- ✅ Não adiciona texto antes/depois
- ❌ Não garante schema (nomes de campos, tipos)
- ❌ Não garante presença de campos obrigatórios
Exemplo de outputs possíveis:
// Pode retornar:
{"cnpj": "12.345.678/0001-99", "valor": 1234.56, "data": "2024-03-15"}
// Ou pode retornar (schema diferente):
{"CNPJ_EMPRESA": "12.345.678/0001-99", "total": "R$ 1.234,56", "emissao": "15/03/2024"}
// Ou pode retornar (campos faltando):
{"cnpj": "12.345.678/0001-99", "valor": 1234.56}
JSON é válido, mas schema é inconsistente.
Quando usar:
- Você só precisa que seja JSON (não importa o schema exato)
- Você vai validar o schema manualmente depois
- Uso exploratório / prototipagem
Solução 2: Structured Outputs (OpenAI)
O que é:
Extensão do JSON mode que garante conformidade com JSON Schema definido.
Como usar:
from openai import OpenAI
from pydantic import BaseModel
# 1. Definir schema com Pydantic
class NotaFiscal(BaseModel):
cnpj: str
valor_total: float
data_emissao: str # formato: YYYY-MM-DD
client = OpenAI()
# 2. Passar schema para o modelo
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extraia dados de notas fiscais."},
{"role": "user", "content": f"Extraia os dados: {texto_nf}"}
],
response_format=NotaFiscal # ← schema definido
)
# 3. Output é garantido estar conforme schema
dados = response.choices[0].message.parsed # já é objeto Pydantic
print(dados.cnpj) # "12.345.678/0001-99"
print(dados.valor_total) # 1234.56 (float)
print(dados.data_emissao) # "2024-03-15" (string)
Garantias:
- ✅ JSON sintaticamente válido
- ✅ Conforma com schema (campos exatos)
- ✅ Tipos corretos (string, float, int, etc.)
- ✅ Campos obrigatórios sempre presentes
- ✅ Parse automático para objeto Pydantic
Quando usar:
- Produção (confiabilidade é crítica)
- Integração com sistemas (precisa de schema fixo)
- Validação de dados (Pydantic valida automaticamente)
Solução 3: Function Calling (OpenAI e Anthropic)
O que é:
LLM “chama uma função” com parâmetros estruturados. Você define os parâmetros (JSON Schema) e o modelo retorna argumentos no formato correto.
Como usar (OpenAI):
from openai import OpenAI
client = OpenAI()
# 1. Definir "função" (na verdade é schema)
tools = [{
"type": "function",
"function": {
"name": "extrair_nota_fiscal",
"description": "Extrai dados estruturados de nota fiscal",
"parameters": {
"type": "object",
"properties": {
"cnpj": {
"type": "string",
"description": "CNPJ do emissor no formato XX.XXX.XXX/XXXX-XX"
},
"valor_total": {
"type": "number",
"description": "Valor total da nota fiscal em reais"
},
"data_emissao": {
"type": "string",
"description": "Data de emissão no formato YYYY-MM-DD"
}
},
"required": ["cnpj", "valor_total", "data_emissao"]
}
}
}]
# 2. Chamar modelo
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Extraia dados desta NF: {texto_nf}"}],
tools=tools,
tool_choice={"type": "function", "function": {"name": "extrair_nota_fiscal"}}
)
# 3. Extrair argumentos da "chamada de função"
tool_call = response.choices[0].message.tool_calls[0]
argumentos = json.loads(tool_call.function.arguments)
print(argumentos)
# {"cnpj": "12.345.678/0001-99", "valor_total": 1234.56, "data_emissao": "2024-03-15"}
Como usar (Anthropic Claude):
import anthropic
client = anthropic.Anthropic()
# 1. Definir tool
tools = [{
"name": "extrair_nota_fiscal",
"description": "Extrai dados de nota fiscal",
"input_schema": {
"type": "object",
"properties": {
"cnpj": {"type": "string", "description": "CNPJ do emissor"},
"valor_total": {"type": "number", "description": "Valor total em reais"},
"data_emissao": {"type": "string", "description": "Data no formato YYYY-MM-DD"}
},
"required": ["cnpj", "valor_total", "data_emissao"]
}
}]
# 2. Chamar Claude
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": f"Extraia dados: {texto_nf}"}]
)
# 3. Extrair input da tool
tool_use = response.content[0]
argumentos = tool_use.input
print(argumentos)
# {"cnpj": "12.345.678/0001-99", "valor_total": 1234.56, "data_emissao": "2024-03-15"}
Garantias:
- ✅ JSON válido
- ✅ Schema garantido
- ✅ Tipos corretos
- ✅ Funciona com OpenAI e Anthropic
Quando usar:
- Quando precisa que LLM “aja” (não só retorne dados)
- Workflow com múltiplas etapas (LLM decide qual função chamar)
- Agentes (LLM escolhe entre várias ferramentas)
Comparação detalhada: qual abordagem escolher?
Tabela comparativa
| Critério | Prompt tradicional | JSON mode | Structured Outputs | Function Calling |
|---|---|---|---|---|
| Garantia de JSON válido | 90-95% | 100% | 100% | 100% |
| Garantia de schema | ❌ | ❌ | ✅ | ✅ |
| Campos obrigatórios | ❌ | ❌ | ✅ | ✅ |
| Tipos validados | ❌ | ❌ | ✅ | ✅ |
| Facilidade de uso | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| Integração Pydantic | Manual | Manual | Nativa | Manual |
| Suporte multi-modelo | ✅ Todos | OpenAI | OpenAI | OpenAI + Anthropic |
| Custo | Padrão | Padrão | Padrão | Padrão |
| Latência | Padrão | Padrão | Padrão | Padrão |
Quando usar cada abordagem?
Use Prompt tradicional quando:
- Prototipagem rápida
- Output é para humanos (não para parse automático)
- Não importa pequenas inconsistências
Use JSON mode quando:
- Precisa de JSON válido, mas schema pode variar
- Vai validar manualmente depois
- Quer flexibilidade no formato de output
Use Structured Outputs quando:
- Produção (confiabilidade crítica)
- Schema fixo e bem definido
- Integração com sistemas (APIs, banco de dados)
- Usa Pydantic para validação
- OpenAI é seu provider
Use Function Calling quando:
- LLM precisa “agir” (não apenas retornar dados)
- Agentes (LLM escolhe entre múltiplas ferramentas)
- Workflow complexo com decisões
- Quer compatibilidade OpenAI + Anthropic
Validação com Pydantic e Zod
Pydantic (Python)
Setup básico:
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import date
class NotaFiscal(BaseModel):
cnpj: str = Field(..., regex=r'^\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}$')
valor_total: float = Field(..., gt=0) # maior que 0
data_emissao: date
descricao: Optional[str] = None
@validator('cnpj')
def validar_cnpj(cls, v):
# Lógica de validação de CNPJ (digito verificador)
if not verificar_cnpj_valido(v):
raise ValueError('CNPJ inválido')
return v
# Uso:
try:
nf = NotaFiscal(
cnpj="12.345.678/0001-99",
valor_total=1234.56,
data_emissao="2024-03-15"
)
print("Válido!")
except ValidationError as e:
print("Inválido:", e)
Features úteis:
1. Validação de tipos:
class Produto(BaseModel):
codigo: int # auto-converte string para int se possível
preco: float
ativo: bool
# Aceita:
Produto(codigo="123", preco="45.50", ativo="true")
# Converte para:
# codigo=123, preco=45.5, ativo=True
2. Valores padrão:
class Config(BaseModel):
timeout: int = 30 # padrão: 30
retry: bool = True
max_items: int = Field(default=100, ge=1, le=1000)
3. Validadores customizados:
from pydantic import validator
class Usuario(BaseModel):
email: str
idade: int
@validator('email')
def validar_email(cls, v):
if '@' not in v:
raise ValueError('Email inválido')
return v.lower() # normaliza para minúsculas
@validator('idade')
def validar_idade(cls, v):
if v menos de 18:
raise ValueError('Usuário deve ser maior de idade')
return v
4. Modelos aninhados:
class Endereco(BaseModel):
rua: str
cidade: str
cep: str
class Cliente(BaseModel):
nome: str
endereco: Endereco # modelo aninhado
# Uso:
cliente = Cliente(
nome="João Silva",
endereco={
"rua": "Av. Paulista, 1000",
"cidade": "São Paulo",
"cep": "01310-100"
}
)
5. Listas e tipos complexos:
from typing import List
class Pedido(BaseModel):
numero: int
itens: List[str] # lista de strings
valores: List[float]
pedido = Pedido(
numero=123,
itens=["Item A", "Item B"],
valores=[10.50, 25.00]
)
Zod (TypeScript / JavaScript)
Setup básico:
import { z } from 'zod';
const NotaFiscalSchema = z.object({
cnpj: z.string().regex(/^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/),
valorTotal: z.number().positive(),
dataEmissao: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
descricao: z.string().optional()
});
// Validar:
try {
const nf = NotaFiscalSchema.parse({
cnpj: "12.345.678/0001-99",
valorTotal: 1234.56,
dataEmissao: "2024-03-15"
});
console.log("Válido!");
} catch (e) {
console.log("Inválido:", e);
}
Features úteis:
1. Transformações:
const schema = z.object({
email: z.string().email().toLowerCase(), // transforma para minúsculas
idade: z.number().int().positive()
});
2. Validações customizadas:
const cnpjSchema = z.string().refine(
(val) => validarCNPJ(val), // função customizada
{ message: "CNPJ inválido" }
);
3. Schemas aninhados:
const EnderecoSchema = z.object({
rua: z.string(),
cidade: z.string(),
cep: z.string()
});
const ClienteSchema = z.object({
nome: z.string(),
endereco: EnderecoSchema
});
Exemplos práticos completos
Exemplo 1: Extração de dados de contratos (Structured Outputs)
Caso de uso: extrair cláusulas importantes de contratos B2B.
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List
from datetime import date
# 1. Definir schema detalhado
class Clausula(BaseModel):
tipo: str = Field(..., description="Tipo da cláusula (ex: prazo, pagamento, rescisão)")
conteudo: str = Field(..., description="Texto da cláusula")
pagina: int = Field(..., description="Número da página onde aparece")
class ContratoAnalisado(BaseModel):
partes: List[str] = Field(..., description="Nome das partes (contratante e contratado)")
valor_total: float = Field(..., description="Valor total do contrato em reais")
prazo_meses: int = Field(..., description="Prazo de vigência em meses")
data_inicio: date = Field(..., description="Data de início de vigência")
clausulas_importantes: List[Clausula] = Field(..., description="Lista de cláusulas relevantes")
riscos_identificados: List[str] = Field(default=[], description="Possíveis riscos ou pontos de atenção")
# 2. Chamar modelo
client = OpenAI()
texto_contrato = """
[... texto de 30 páginas de contrato ...]
"""
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "Você é um especialista jurídico que analisa contratos B2B."
},
{
"role": "user",
"content": f"Analise este contrato e extraia informações estruturadas:\n\n{texto_contrato}"
}
],
response_format=ContratoAnalisado
)
# 3. Output garantido conforme schema
contrato = response.choices[0].message.parsed
print(f"Partes: {contrato.partes}")
print(f"Valor: R$ {contrato.valor_total:,.2f}")
print(f"Prazo: {contrato.prazo_meses} meses")
print(f"Início: {contrato.data_inicio}")
for clausula in contrato.clausulas_importantes:
print(f"\n{clausula.tipo.upper()} (página {clausula.pagina})")
print(f" {clausula.conteudo}")
if contrato.riscos_identificados:
print("\n⚠️ RISCOS IDENTIFICADOS:")
for risco in contrato.riscos_identificados:
print(f" • {risco}")
Output garantido:
{
"partes": ["Empresa ABC Ltda", "Fornecedor XYZ S/A"],
"valor_total": 450000.00,
"prazo_meses": 24,
"data_inicio": "2024-04-01",
"clausulas_importantes": [
{
"tipo": "pagamento",
"conteudo": "Pagamento em 12 parcelas mensais de R$ 37.500,00",
"pagina": 3
},
{
"tipo": "rescisão",
"conteudo": "Multa de 30% do valor restante em caso de rescisão antecipada pelo contratante",
"pagina": 8
}
],
"riscos_identificados": [
"Multa rescisória alta (30%) pode tornar saída onerosa",
"Não há cláusula de reajuste de preços (risco inflacionário)"
]
}
Benefícios:
- ✅ Campos sempre presentes
- ✅ Tipos corretos (float, int, date)
- ✅ Listas com schema definido
- ✅ Validação automática com Pydantic
Exemplo 2: Classificação multi-label (Function Calling)
Caso de uso: classificar tickets de suporte em múltiplas categorias.
import anthropic
client = anthropic.Anthropic()
# 1. Definir tool
tools = [{
"name": "classificar_ticket",
"description": "Classifica ticket de suporte em categorias predefinidas",
"input_schema": {
"type": "object",
"properties": {
"categoria_principal": {
"type": "string",
"enum": ["tecnico", "financeiro", "comercial", "duvida"],
"description": "Categoria principal do ticket"
},
"subcategorias": {
"type": "array",
"items": {
"type": "string",
"enum": ["bug", "feature_request", "integracao", "pagamento", "fatura", "contrato", "uso_geral"]
},
"description": "Subcategorias aplicáveis (pode ser múltiplas)"
},
"urgencia": {
"type": "string",
"enum": ["baixa", "media", "alta", "critica"],
"description": "Nível de urgência"
},
"requer_especialista": {
"type": "boolean",
"description": "Se requer atenção de especialista técnico"
},
"resumo": {
"type": "string",
"description": "Resumo de 1 linha do problema"
}
},
"required": ["categoria_principal", "subcategorias", "urgencia", "requer_especialista", "resumo"]
}
}]
# 2. Classificar ticket
ticket = """
Bom dia,
Estou tentando integrar o sistema de vocês com nosso ERP via API,
mas estou recebendo erro 401 mesmo usando a chave de API correta.
Já tentei regenerar a chave, mas o erro persiste.
Isso está travando nosso processo de faturamento e precisamos
resolver URGENTE pois temos fechamento hoje.
Aguardo retorno.
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[{
"role": "user",
"content": f"Classifique este ticket:\n\n{ticket}"
}]
)
# 3. Extrair classificação
classificacao = response.content[0].input
print(classificacao)
Output:
{
"categoria_principal": "tecnico",
"subcategorias": ["integracao", "bug"],
"urgencia": "alta",
"requer_especialista": true,
"resumo": "Erro 401 na integração API com ERP bloqueando faturamento"
}
Uso prático:
# Rotear ticket automaticamente baseado na classificação
if classificacao["urgencia"] in ["alta", "critica"]:
notificar_equipe_urgente(classificacao)
if classificacao["requer_especialista"]:
atribuir_para_especialista(ticket, classificacao["categoria_principal"])
else:
atribuir_para_suporte_nivel_1(ticket)
registrar_no_banco(
ticket_id=ticket.id,
categoria=classificacao["categoria_principal"],
subcategorias=classificacao["subcategorias"],
urgencia=classificacao["urgencia"],
resumo=classificacao["resumo"]
)
Exemplo 3: Pipeline completo com validação
Caso de uso: extração de dados + validação + retry + fallback.
from openai import OpenAI
from pydantic import BaseModel, Field, validator
from typing import Optional
import json
# 1. Schema com validações rigorosas
class DadosBancarios(BaseModel):
banco: str = Field(..., min_length=3)
agencia: str = Field(..., regex=r'^\d{4}$')
conta: str = Field(..., regex=r'^\d{5,10}-\d$')
titular: str = Field(..., min_length=5)
cpf_cnpj: str = Field(..., regex=r'(^\d{3}\.\d{3}\.\d{3}-\d{2}$)|(^\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}$)')
@validator('cpf_cnpj')
def validar_documento(cls, v):
# Validação de CPF ou CNPJ (dígitos verificadores)
if not validar_cpf_ou_cnpj(v):
raise ValueError('CPF/CNPJ inválido')
return v
# 2. Função de extração com retry e fallback
def extrair_dados_bancarios(
texto: str,
max_tentativas: int = 3,
fallback_para_json_mode: bool = True
) -> Optional[DadosBancarios]:
client = OpenAI()
# Tentativa 1-N: Structured Outputs (mais confiável)
for tentativa in range(max_tentativas):
try:
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "Você extrai dados bancários de documentos. Seja preciso."
},
{
"role": "user",
"content": f"Extraia dados bancários:\n\n{texto}"
}
],
response_format=DadosBancarios
)
# Parse e validação automática
dados = response.choices[0].message.parsed
return dados # Sucesso!
except Exception as e:
print(f"Tentativa {tentativa + 1} falhou: {e}")
if tentativa < max_tentativas - 1:
continue # Tenta de novo
else:
# Última tentativa falhou
if not fallback_para_json_mode:
raise
# Fallback: JSON mode (menos confiável, mas pode funcionar)
if fallback_para_json_mode:
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": """Extraia dados bancários e retorne JSON com:
- banco (nome do banco)
- agencia (4 dígitos)
- conta (formato: XXXXX-X)
- titular (nome completo)
- cpf_cnpj (com pontuação)"""
},
{
"role": "user",
"content": texto
}
],
response_format={"type": "json_object"}
)
# Parse manual e validação com Pydantic
dados_raw = json.loads(response.choices[0].message.content)
dados = DadosBancarios(**dados_raw) # valida com Pydantic
return dados
except Exception as e:
print(f"Fallback também falhou: {e}")
return None
return None
# 3. Uso
texto_documento = """
Dados para depósito:
Banco: Itaú
Agência: 1234
Conta: 54321-0
Titular: João Silva
CPF: 123.456.789-00
"""
dados = extrair_dados_bancarios(texto_documento)
if dados:
print("✅ Extração bem-sucedida!")
print(dados.dict())
else:
print("❌ Falha na extração")
Features deste pipeline:
- ✅ Structured Outputs como abordagem principal (mais confiável)
- ✅ Retry automático (até 3 tentativas)
- ✅ Fallback para JSON mode se Structured Outputs falhar
- ✅ Validação rigorosa com Pydantic (regex, validadores customizados)
- ✅ Tratamento gracioso de erros
Erros comuns e como evitar
1. Achar que JSON mode garante schema
ERRADO:
# JSON mode não garante campos específicos
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
response_format={"type": "json_object"}
)
dados = json.loads(response.choices[0].message.content)
print(dados["cnpj"]) # ❌ Pode não existir!
CORRETO:
# Use Structured Outputs para garantir schema
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[...],
response_format=NotaFiscal # schema definido
)
dados = response.choices[0].message.parsed
print(dados.cnpj) # ✅ Garantido existir
2. Não validar tipos mesmo com schema
ERRADO:
class Produto(BaseModel):
preco: float
# Confiar cegamente
produto = Produto(preco="mil reais") # ❌ ValueError!
CORRETO:
try:
produto = Produto(preco="1000.50") # ✅ Converte string para float
print(produto.preco) # 1000.5
except ValidationError as e:
print(f"Erro de validação: {e}")
3. Schemas muito complexos (LLM não consegue seguir)
EVITE:
class SchemaMuitoComplexo(BaseModel):
campo1: str
campo2: int
campo3: List[str]
campo4: Dict[str, List[Dict[str, int]]] # ❌ Muito aninhado
campo5: Optional[float]
# ... 20 campos mais
PREFIRA:
# Quebrar em schemas menores
class Item(BaseModel):
nome: str
quantidade: int
class Pedido(BaseModel):
itens: List[Item] # ✅ Mais simples
4. Não ter retry/fallback
ERRADO:
# Uma tentativa só
dados = extrair_dados(texto) # ❌ Se falhar, quebra tudo
CORRETO:
# Retry + fallback
dados = extrair_com_retry(texto, max_tentativas=3)
if not dados:
dados = extrair_com_fallback_json_mode(texto)
if not dados:
log_erro_e_notificar_equipe(texto)
Quando usar cada abordagem: framework de decisão
┌─ Precisa de JSON? ─────────────┐
│ │
│ NÃO → Prompt tradicional │
│ │
│ SIM │
│ ↓ │
│ Schema fixo? │
│ │
│ NÃO → JSON mode │
│ │
│ SIM │
│ ↓ │
│ LLM precisa "agir"? │
│ │
│ SIM → Function Calling │
│ │
│ NÃO │
│ ↓ │
│ Provider? │
│ │
│ OpenAI → Structured Outputs │
│ Anthropic → Function Calling │
│ Outros → JSON mode + validação manual │
└─────────────────────────────────┘
Conclusão
Structured outputs e JSON mode transformam LLMs de “geradores de texto” em “APIs confiáveis”.
Os benefícios:
- 100% de conformidade com schema (vs 85-95% com prompts)
- Zero parsing manual (dados já vêm estruturados)
- Integração nativa com Pydantic/Zod
- Código mais limpo e manutenível
- Menos bugs em produção
Recomendação:
- Produção: sempre use Structured Outputs ou Function Calling
- Prototipagem: JSON mode é suficiente
- Exploração: prompts tradicionais são OK
Seu sistema de IA pode continuar retornando dados inconsistentes que quebram em produção. Ou pode usar structured outputs e ter confiabilidade de API tradicional.
A escolha é sua.
Quer implementar extração de dados com IA na sua empresa?