01 - O problema do output não estruturado

TL;DR

LLMs são funções estocásticas com saída não tipada. Você pede JSON, vem markdown com ```json em volta, campo faltando, vírgula sobrando, chave alucinada que nunca foi pedida. Pedir JSON via prompt funciona em 95% dos casos. O sistema quebra nos outros 5%. Estruturar output não é detalhe de UX, é a forma de recriar contrato de tipo na borda entre LLM e código. Esta nota explica por que o problema existe, por que ele é mais grave do que parece, e quando é OK conviver com texto livre.

O problema em uma linha

Você pede JSON. O modelo gera texto que parece JSON. O parser discorda — às vezes, nos inputs mais complexos, nos momentos mais inconvenientes.

A distinção que importa: “parece JSON” é probabilidade. “É JSON válido dentro do schema esperado” é garantia. Sistemas de produção precisam de garantias.

O cenário real

Você pediu pra extrair uma fatura em JSON. O prompt diz:

“Extraia os dados da fatura abaixo e retorne em JSON com os campos valor, vencimento, fornecedor. Retorne apenas o JSON, sem explicações.”

E o que vem de volta, ao longo de mil execuções, inclui variações como:

Aqui está o JSON com os dados extraídos:
 
```json
{
  "valor": "R$ 1.234,56",
  "vencimento": "15/03/2026",
  "fornecedor": "Acme S.A.",
}

Espero ter ajudado!


Esse exemplo tem cinco problemas técnicos empilhados, e nenhum é raro:

1. **Wrapper de markdown**: o JSON vem cercado de ```` ```json ```` apesar do prompt pedir "apenas". Seu parser quebra na primeira linha.
2. **Texto em volta**: saudação antes, despedida depois. Você pode aparar, mas precisa de regex robusto.
3. **Vírgula sobrando**: JSON estrito não aceita trailing comma. Python aceita; JS não. Seu serviço falha em ambiente diferente do dev.
4. **Tipos errados**: `valor` veio como string formatada (`"R$ 1.234,56"`), não número. Você esperava `1234.56`. Operações aritméticas explodem.
5. **Data ambígua**: `15/03/2026` é 15 de março (PT-BR) ou 3 de maio (EN-US)? Sem schema, qualquer um.

E em 1 em cada 200 execuções, ainda aparece a categoria mais perversa:

```json
{
  "valor": 1234.56,
  "vencimento": "2026-03-15",
  "fornecedor": "Acme S.A.",
  "observacao": "Pagamento via boleto"
}

Veio um campo observacao que ninguém pediu. O modelo alucinou um campo que ele achou útil. Seu schema downstream não tem esse campo, e dependendo do consumidor, isso quebra silenciosamente — ou pior, é ignorado e dados relevantes somem.

Por que “pedir JSON no prompt” não basta

A causa raiz é a natureza do próprio LLM:

  • Geração é probabilística. Mesmo com temperature=0, mudanças mínimas no prompt, no modelo, ou no contexto deslocam a distribuição. O modelo tende a obedecer “retorne apenas JSON”, mas a probabilidade de obedecer não é 1.
  • O modelo foi treinado em texto humano. Textos humanos quase sempre vêm com introdução, conclusão, hedge. “Apenas JSON” contraria milhões de exemplos do corpus de treino.
  • A instrução compete com outras instruções. Quanto mais coisas você pede no prompt, menor a probabilidade de cada uma ser respeitada. “Apenas JSON” entra na fila com “campos completos”, “datas em ISO”, “valores numéricos”.
  • Não há mecanismo de validação interna. O modelo não roda um parser antes de emitir. Ele emite token por token; se na metade ele “lembrou” de explicar, ele explica.

Resultado: dá pra subir a taxa de sucesso de 95% pra 99% afiando o prompt. Mas pra chegar nos 99.99% que pipeline de produção exige, você precisa de um mecanismo que force o modelo a aderir ao schema, não convença ele educadamente. Isso é structured outputs (notas 04-06).

A diferença entre teste e produção

Esse problema é quase invisível em testes manuais porque o desenvolvedor escolhe os casos de teste e, inconscientemente, testa os inputs que o modelo gerencia bem. A taxa de falha real aparece em produção, com inputs de usuários reais — mais ambíguos, mais longos, mais inesperados. Por isso o problema frequentemente é “descoberto” quando o sistema já está em produção e alguém reclama de um erro estranho.

Taxonomia de falhas de output

Os cinco problemas do exemplo de fatura não são aleatórios — eles pertencem a categorias estáveis de falha. Conhecer a taxonomia ajuda a escolher a solução certa:

CategoriaExemploSolução
Envelope indesejado```json em volta do JSONEnforcement de output format
Texto decorativoSaudação antes e depois do JSONOutput format + constraint declarativa
JSON malformadoTrailing comma, aspas quebradasSchema enforcement com retry
Tipo erradoNúmero como string formatatadaJSON Schema com type: number
Campo alucinadoCampo extra não pedidoSchema com additionalProperties: false
Campo faltandoCampo obrigatório ausenteSchema com required: [...]
Semântica incorretaValor valid dentro do shape mas erradoValidação pós-schema (nota 07)

As categorias 1-6 são problemas de forma — o schema resolve. A categoria 7 é problema de conteúdo — precisa de lógica de validação semântica. As notas seguintes cobrem o schema (02) e depois o mecanismo por provider (03-06) e depois a validação semântica (07).

O custo de parsers defensivos

A reação comum é blindar o parser. Algo como:

def parse_llm_json(text: str) -> dict:
    # Tenta direto
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass
 
    # Extrai bloco markdown
    match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group(1))
        except json.JSONDecodeError:
            pass
 
    # Remove trailing commas
    cleaned = re.sub(r",(\s*[}\]])", r"\1", text)
    try:
        return json.loads(cleaned)
    except json.JSONDecodeError:
        pass
 
    # Última tentativa — extrai primeiro `{...}`
    match = re.search(r"\{.*\}", text, re.DOTALL)
    if match:
        return json.loads(match.group(0))
 
    raise ValueError(f"Não consegui parsear: {text[:200]}")

Esse parser existe em quase todo projeto LLM legado. Os problemas:

  • Cresce sem parar. Cada bug em produção vira uma nova heurística. Em seis meses tem 200 linhas e ninguém entende.
  • Esconde o sintoma. Quando o parser conserta sozinho, você nunca vê que o modelo está se desviando. Drift silencioso vira regressão de qualidade.
  • Não resolve o problema de chave alucinada. Você parsea com sucesso e passa adiante um objeto com campo a mais.
  • Custo computacional. Em pipelines de alta vazão, regex e múltiplos json.loads custam.

Structured outputs eliminam essa categoria de código. Em vez de consertar a saída ruim, força o modelo a emitir saída boa.

Por que o problema piora com temperatura e volume

Dois multiplicadores do problema que engenheiros descobrem na prática:

Temperatura: com temperature=0, os modelos seguem as instruções de formato com mais consistência. Mas temperature=0 não é 100% determinístico em todos os provedores, e não é adequado para todas as tarefas (geração criativa, diversidade, etc.). Em sistemas que precisam de temperatura alta, a taxa de falha de formato sobe.

Volume: uma taxa de 2% de falha parece aceitável. Em 100 chamadas, são 2 erros — fácil de tratar com retry manual. Em 100.000 chamadas/dia, são 2.000 erros/dia. Cada erro que não é capturado é dado corrompido ou request que falhou silenciosamente. A matemática de falha em escala muda o que “aceitável” significa.

O enforcement via API resolve os dois: o modelo não tem liberdade de emitir tokens fora do schema, independente de temperatura. E o enforcement acontece antes da resposta chegar ao código chamador, então o volume não afeta a taxa de erro visível ao sistema.

Quando o problema é crítico vs quando é OK

Nem todo uso de LLM precisa de structured output. Heurística:

CenárioOutput não estruturado é OK?
Resposta de chatbot mostrada ao humanoSim, texto é o produto
Brainstorming, drafts, exploraçãoSim, estrutura atrapalha
Sumarização que vai pra pessoaSim, markdown serve
Fatura extraída pra ir pro bancoNão: precisa de schema
Classificação que vira label em ML pipelineNão: precisa de enum validado
Decisão de roteamento de agenteNão: precisa de tipo confiável
Resposta de função de tool (agente)Não: precisa de schema
Input pra outro LLM downstreamTalvez, depende se downstream parsea ou consome texto

A regra: se o output vai ser consumido por código, structured. Se vai ser consumido por humano, livre. Cinza só na fronteira (output que humano lê e sistema parsea — relatórios, dashboards). Nesses casos, normalmente vale gerar duas versões — uma livre pro humano, uma estruturada pro sistema — ou um structured com um campo markdown_summary dentro.

Armadilhas comuns

Parser defensivo cresce sem parar e esconde drift

A reação natural quando o modelo não respeita “retorne apenas JSON” é escrever um parser defensivo que conserta a saída. O parser começa com 10 linhas e em seis meses tem 200, com heurísticas empilhadas para cada bug de produção. O problema mais sério: quando o parser conserta sozinho, você nunca vê que o modelo está se desviando. Drift silencioso de qualidade vira regressão que você descobre tarde. Parser defensivo é gambiarra; structured output enforcement é a solução.

"Apenas JSON" no prompt não é garantia em produção

“Retorne apenas JSON, sem explicações” funciona em 95-98% dos casos com modelos bons. O sistema quebra nos outros 2-5% — que em produção com 10.000 chamadas/dia é 200-500 falhas. E as falhas acontecem nos casos mais complexos de input, exatamente onde você mais precisa de confiabilidade. Pedir “apenas JSON” melhora a taxa; enforcement via API (strict mode, tool use, response schema) é o que faz a taxa chegar perto de 100%.

Campo alucinado é mais grave do que campo faltando

Quando um campo obrigatório está faltando, o erro é visível — o parser quebra com KeyError. Quando o modelo adiciona um campo que não foi pedido (um observacao que você não esperava), o objeto parsea com sucesso mas carrega dado não solicitado. Dependendo do consumidor downstream, isso quebra silenciosamente (campo ignorado) ou contamina (campo injetado em local errado). Validação de schema que apenas verifica a presença de campos obrigatórios não captura essa categoria de erro; você precisa de validação que rejeita campos extras também.

A reframing certa

A pergunta não é “como eu faço o LLM retornar JSON?“. É:

“Como eu transformo a saída textual e probabilística do LLM num objeto tipado e validado antes do próximo passo?”

A resposta tem três partes:

  1. Schema como contrato declarativo (nota 02).
  2. Mecanismo de enforcement que o provider oferece — strict mode, tool use, response schema (notas 03-06).
  3. Validação semântica em cima do shape garantido, com retry quando algo está fora (nota 07).

O resto da trilha é isso, ramificado.

Uma forma alternativa de enquadrar: a borda entre LLM e código é sempre uma borda de desconfiança. Do lado do LLM, tudo é texto probabilístico. Do lado do código, tudo precisa ser tipado e previsível. O mecanismo de structured output é a alfândega nessa fronteira — o que passa precisa satisfazer o schema, ou é retido.

Por que isso importa mais do que parece

Sistemas LLM que “funcionam bem em teste mas quebram em produção” quase sempre têm um parser defensivo insuficiente ou ausência de enforcement de schema. A diferença entre 98% de sucesso e 99.9% de sucesso não é uma melhora incremental — é a diferença entre “tolerável com monitoramento” e “pode rodar sem supervisão constante”.

Detectar o problema em produção

Diagnóstico antes de refatorar: se você já tem um sistema com LLM e quer saber se o problema de output não estruturado está presente, os sinais estão no código e nos logs.

Sinais no código:

  • Presença de funções como clean_json(), extract_json(), parse_llm_response() — parser defensivo em ação
  • Blocos try/except json.JSONDecodeError com múltiplas tentativas aninhadas
  • Regex para extrair texto entre ``` e ```
  • Código que acessa campos com .get("campo", valor_default) em vez de acesso direto
  • Validação manual de tipo: if isinstance(result["valor"], str): result["valor"] = float(result["valor"].replace(",", ".")

Sinais em logs:

  • Erros de JSON parsing que somem após retry automático — sinal de falha intermitente mascarada
  • Logs de “campo ausente, usando default” — dado estrutural sendo ignorado silenciosamente
  • Aumento de erros associado a inputs mais longos ou complexos — correlação típica do problema de geração

Métricas de baseline: Antes de migrar para enforcement de schema, meça: taxa de falha de parse, distribuição de falhas por tipo (envelope, malformed, campo faltando), e tempo gasto em retry. Essas métricas são o argumento para a mudança e o baseline para confirmar melhora depois.

Por que o problema piora com agentes

Em pipelines LLM simples (usuário → LLM → usuário), output não estruturado é inconveniente. Em sistemas agênticos, é falha em cascata.

Quando um agente chama uma ferramenta, o output da chamada (o resultado do LLM) vira o input da ferramenta. Se esse output não tem o formato certo, a ferramenta falha. Em loops agênticos, o agente pode tentar de novo, mas sem enforcement, tentativas repetidas produzem a mesma distribuição de outputs — incluindo os malformados. O loop entra em ciclo de falha.

As notas 03-06 cobrem como os provedores resolvem isso no nível de API: a resposta do LLM é validada contra um schema antes de ser devolvida ao chamador. Se não validar, o modelo tenta de novo internamente — sem que o código de chamada precise saber. Esse é o mecanismo que torna structured outputs uma solução real, não uma melhora marginal.

O contraste com o pipeline simples fica claro na matemática de propagação de erro. Se cada etapa de uma pipeline de 3 LLMs tem 98% de sucesso de formato, a pipeline completa tem 0.98 × 0.98 × 0.98 ≈ 94.1% de sucesso. Com enforcement, cada etapa fica próxima de 100%, e a pipeline composta mantém essa confiabilidade. Agentes complexos com 5-10 steps de LLM precisam de enforcement em cada ponto, não só na entrada ou na saída final.

Sinal de alerta em design agêntico: se você está planejando escrever código de retry em cima do output do agente porque “às vezes não vem no formato certo”, o problema está no nível errado. O retry deveria acontecer dentro do modelo via enforcement, não em lógica de aplicação.

Quando structured outputs não resolvem tudo

Enforcement de schema resolve o problema de forma, não o problema de conteúdo. O campo vencimento vai existir e vai ser uma string no formato ISO 8601 — mas se o modelo extraiu a data errada, o schema não captura isso. Você vai receber {"vencimento": "2026-04-15"} quando a data correta é 2026-03-15, e o schema vai considerar válido.

Isso não é uma limitação a esconder — é onde entra a validação semântica (nota 07). O pipeline completo é:

texto livre → [schema enforcement] → output tipado → [validação semântica] → output confiável

Saltar a segunda etapa porque a primeira dá conforto é um equívoco comum. Schema enforcement é necessário mas não suficiente. A nota 07 cobre o que fazer com output que passou no schema mas suspeito de erro semântico: validação com regras de negócio, cross-check com dados externos, e retry com feedback direcionado ao modelo.

Como explicar em inglês

Em entrevistas sobre sistemas de IA, a questão de structured outputs aparece tanto em design de sistemas quanto em debugging de produção:

“The fundamental problem is that LLMs are probabilistic text generators, not typed functions. When you need the output to be consumed by code — a parser, an agent, a downstream pipeline — you can’t rely on ‘please return only JSON’ in the prompt. That gets you 95-98% compliance, and 2-5% failures that happen precisely when inputs are complex. Structured output enforcement via provider APIs — strict mode, tool use, response schemas — is what closes that gap to near-100%, without brittle regex parsers.”

PortuguêsInglês
output não estruturadounstructured output / free-text output
saída tipadatyped output
schema de outputoutput schema
campo alucinadohallucinated field / extra field
parser defensivodefensive parser / error-handling parser
enforcement de schemaschema enforcement
validação de outputoutput validation
trailing commatrailing comma
tipo erradowrong type / type mismatch
consumidor downstreamdownstream consumer

O que vem a seguir

Você já sabe por que pedir JSON no prompt não basta. A próxima nota resolve a linguagem: como escrever o contrato que o modelo vai respeitar — JSON Schema, a forma padrão de descrever shape, tipos, restrições e defaults.

Ver 02 - JSON Schema como contrato.

Checklist antes de seguir

Antes de continuar para JSON Schema (nota 02), certifique-se de que você:

  • Consegue nomear pelo menos 3 categorias de falha de output (taxonomia da nota)
  • Entende por que temperature=0 não elimina o problema
  • Sabe identificar parser defensivo no código (os sinais de diagnóstico)
  • Conhece a distinção entre falha de forma (schema resolve) e falha de conteúdo (validação semântica)
  • Sabe em quais cenários output não estruturado é OK

Se algum item ficou obscuro, releia a seção correspondente antes de avançar.

Fontes

  • @hooeemBecome an AI Engineer, cap #6. Posição “structured outputs is the boundary that turns text into types”.
  • Eugene YanPatterns for Building LLM-based Systems & Products (eugeneyan.com). Seção sobre “Guardrails” cobre validação de output.
  • AnthropicTool use overview (docs.anthropic.com). Posicionamento de tool use como mecanismo de structured output.

Veja também