Phase 13 - Lesson 04

Saída Estruturada — JSON Schema, Pydantic, Zod, Decodificação Restrita

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

"Ask the model nicely to return JSON" fails 5 to 15 percent of the time, even on frontier models. Structured outputs close that gap with constrained decoding: the model is literally prevented from emitting a token that would violate the schema. OpenAI's strict mode, Anthropic's schema-typed tool use, Gemini's responseSchema, Pydantic AI's output_type, and Zod's .parse are five surface forms of the same idea. This lesson builds the schema validator and the strict-mode contract learners will use for every production extraction pipeline.

Tipo: Build Idiomas: Python (stdlib, subconjunto de JSON Schema 2020-12) Pré-requisitos: Phase 13 · 02 (aprofundamento em chamada de função) Tempo: ~75 minutos

Objetivos de Aprendizado

  • Escrever um JSON Schema 2020-12 para um alvo de extração usando as restrições corretas (enum, min/max, required, pattern).
  • Explicar por que o strict mode e a decodificação restrita oferecem garantias diferentes de "validar após a geração".
  • Distinguir os três modos de falha: erro de parse, violação de schema, recusa do modelo.
  • Publicar um pipeline de extração com reparo tipado e tratamento de recusa tipado.

O Problema

Um agente que lê um e-mail de pedido de compra precisa transformar texto livre em {customer, line_items, total_usd}. Três abordagens.

Abordagem um: solicitar JSON via prompt. "Responda em JSON com os campos customer, line_items, total_usd." Funciona de 85 a 95 por cento das vezes em modelos de fronteira. Falha de seis maneiras: chave ausente, vírgula pendente, tipos incorretos, campos alucinados, truncamento no limite de tokens, prosa vazada como "Aqui está seu JSON:".

Abordagem dois: validar após a geração. Gerar livremente, analisar, validar contra o schema, tentar novamente em caso de falha. Confiável, mas caro — você paga por cada tentativa e bugs de truncamento custam um turno extra por ocorrência.

Abordagem três: decodificação restrita. O provedor impõe o schema no momento da decodificação. Tokens inválidos são mascarados da distribuição de amostragem. A saída tem garantia de ser analisada e validada. A falha colapsa em um único modo: recusa (o modelo decide que a entrada não se ajusta ao schema).

Cada provedor de fronteira em 2026 envia alguma forma de abordagem três.

  • OpenAI. response_format: {type: "json_schema", strict: true} mais refusal na resposta se o modelo recusar.
  • Anthropic. Imposição de schema em entradas de tool_use; stop_reason: "refusal" não existe, mas end_turn sem chamada de ferramenta é o sinal.
  • Gemini. responseSchema no nível da requisição; em 2026, o Gemini envia restrições gramaticais no nível de token para tipos selecionados.
  • Pydantic AI. output_type=InvoiceModel emite um RunResult estruturado tipado para InvoiceModel.
  • Zod (TypeScript). Parser de tempo de execução que valida a saída do provedor em relação a um schema Zod; combina com o beta.chat.completions.parse da OpenAI.

O ponto em comum: declarar o schema uma vez, impô-lo de ponta a ponta.

O Conceito

JSON Schema 2020-12 — a língua franca

Cada provedor aceita o JSON Schema 2020-12. Os construtos que você mais usa:

  • type: um de object, array, string, number, integer, boolean, null.
  • properties: mapa do nome do campo para o subschema.
  • required: lista de nomes de campos que devem aparecer.
  • enum: conjunto fechado de valores permitidos.
  • minimum / maximum (números), minLength / maxLength / pattern (strings).
  • items: subschema aplicado a cada elemento do array.
  • additionalProperties: false proíbe campos extras (o padrão varia de acordo com o modo).

O strict mode da OpenAI adiciona três requisitos: cada propriedade deve ser listada em required, additionalProperties: false em todos os lugares e nenhuma $ref não resolvida. Se você violar isso, a API retornará 400 no momento da requisição.

Pydantic, a integração em Python

O Pydantic v2 gera o JSON Schema a partir de modelos em formato de dataclass via model_json_schema(). O Pydantic AI encapsula isso para que você escreva:

class Invoice(BaseModel):
    customer: str
    line_items: list[LineItem]
    total_usd: Decimal

e o framework do agente traduz o schema para o strict mode da OpenAI, input_schema da Anthropic ou responseSchema do Gemini na ponta. A saída do modelo retorna como uma instância tipada de Invoice. Erros de validação lançam um ValidationError com caminhos de erro tipados.

Zod, a integração em TypeScript

Zod (z.object({customer: z.string(), ...})) é o equivalente em TS. O SDK de Node da OpenAI expõe zodResponseFormat(Invoice), que se traduz no payload JSON Schema da API.

Recusas (Refusals)

O strict mode não pode forçar o modelo a responder. Se a entrada não puder ser ajustada ao schema ("o e-mail era um poema, não uma fatura"), o modelo emite um campo refusal contendo o motivo. Seu código deve tratar isso como um resultado de primeira classe, não como uma falha. A recusa também é útil como um sinal de segurança: um modelo solicitado a extrair um número de cartão de crédito de um e-mail de conteúdo protegido retorna uma recusa com o motivo de segurança anexado.

Decodificação restrita no mundo open-source

Implementações de pesos abertos (open-weights) usam três técnicas.

  1. Decodificação baseada em gramática (outlines, guidance, lm-format-enforcer): constrói um autômato finito determinístico a partir do schema; a cada passo, mascara os logits dos tokens que violariam a FSM.
  2. Mascaramento de logits com um parser JSON: executa um parser JSON em fluxo contínuo em sincronia com o modelo; a cada passo, calcula o conjunto de próximos tokens válidos.
  3. Decodificação especulativa com um verificador: um modelo de rascunho barato propõe tokens, e o verificador impõe o schema.

Provedores comerciais escolhem uma dessas técnicas nos bastidores. O estado da arte em 2026 é mais rápido do que a geração de texto comum para saídas estruturadas curtas e tem aproximadamente a mesma velocidade para saídas longas.

Os três modos de falha

  1. Erro de parse. A saída não é um JSON válido. Não pode acontecer sob o strict mode. Ainda pode acontecer em provedores não estritos.
  2. Violação de schema. A saída é analisada com sucesso, mas viola o schema. Não pode acontecer sob o strict mode. Comum fora dele.
  3. Recusa. O modelo recusa a resposta. Deve ser tratada como um resultado tipado.

Estratégia de tentativa de recuperação (Retry strategy)

Quando você está fora do strict mode (uso de ferramentas da Anthropic, OpenAI não estrita, Gemini mais antigo), o padrão de recuperação é:

generate -> parse -> validate -> if fail, inject error and retry, max 3x

Uma nova tentativa geralmente é suficiente. Três tentativas capturam falhas de modelos mais fracos. Além de três é um sinal de um schema ruim: o modelo não consegue satisfazê-lo para algumas entradas, e o prompt ou o schema precisa ser corrigido.

Suporte a modelos pequenos

A decodificação restrita funciona em modelos pequenos. Um modelo aberto de 3B parâmetros com imposição de gramática supera um modelo de 70B parâmetros com prompting puro em tarefas estruturadas. Esta é a principal razão pela qual saídas estruturadas são importantes para produção: elas desacoplam a confiabilidade do tamanho do modelo.

Use Isso

code/main.py envia um validador JSON Schema 2020-12 mínimo em stdlib (types, required, enum, min/max, pattern, items, additionalProperties). Ele envolve um schema de Invoice e executa uma saída falsa de LLM através do validador, demonstrando caminhos de erro de parse, violação de schema e recusa. Substitua a saída falsa por qualquer resposta real do provedor em produção.

O que observar:

  • O validador retorna uma lista [ValidationError] tipada com o caminho e a mensagem. Essa é a estrutura que você deseja expor no prompt de nova tentativa.
  • O ramo de recusa NÃO realiza novas tentativas. Ele registra o erro e retorna uma recusa tipada. A Phase 14 · 09 usa recusas como um sinal de segurança.
  • A verificação de additionalProperties: false é acionada na entrada de teste adversarial, mostrando por que o strict mode fecha as portas para campos alucinados.

Envie Isso

Esta lição produz outputs/skill-structured-output-designer.md. Dado um alvo de extração de texto livre (faturas, tickets de suporte, currículos, etc.), a skill produz um JSON Schema 2020-12 que é compatível com strict-mode e um modelo Pydantic correspondente, com tratamento de recusa e nova tentativa tipados esboçados.

Exercícios

  1. Execute code/main.py. Adicione um quarto caso de teste onde total_usd seja um número negativo. Confirme que o validador o rejeita com o caminho de restrição minimum.

  2. Estenda o validador para suportar oneOf com um discriminador. O caso comum: line_item é um produto ou um serviço, marcado por kind. O strict mode tem regras sutis aqui; consulte o guia de saídas estruturadas da OpenAI.

  3. Escreva o mesmo schema de fatura como um BaseModel do Pydantic e compare a saída do model_json_schema() com o seu schema feito à mão. Identifique o único campo que o Pydantic define por padrão e que a versão feita à mão omite.

  4. Meça as taxas de recusa. Construa dez entradas que não deveriam ser extraíveis (uma letra de música, uma prova matemática, um e-mail em branco) e execute-as em um provedor real com strict mode. Conte as recusas versus as saídas alucinadas. Esta é a sua verdade básica (ground truth) para novas tentativas cientes de recusa.

  5. Leia o guia de saídas estruturadas da OpenAI de ponta a ponta. Identifique o único construto que ele proíbe explicitamente no strict mode que o JSON Schema comum permite. Em seguida, projete um schema que use o construto proibido de forma não essencial e reatore-o para ser compatível com strict mode.

Key Terms

Termo O que as pessoas dizem O que realmente significa
JSON Schema 2020-12 "A especificação do schema" Dialeto de rascunho da IETF de schema que todo provedor moderno fala
Strict mode "Schema garantido" Flag da OpenAI que impõe o schema via decodificação restrita
Constrained decoding "Mascaramento de logit" Imposição em tempo de decodificação que mascara os próximos tokens inválidos
Refusal "Modelo recusa" Resultado tipado quando a entrada não cabe no schema
Parse error "JSON inválido" A saída não foi analisada como JSON; impossível sob strict
Schema violation "Formato incorreto" Analisado com sucesso, mas violou tipos / obrigatórios / enums / intervalos
additionalProperties: false "Sem extras permitidos" Proíbe campos desconhecidos; obrigatório no strict da OpenAI
Pydantic BaseModel "Saída tipada" Classe Python que emite e valida JSON Schema
Zod schema "Tipo de saída em TypeScript" Schema de tempo de execução do TS para validação de saída do provedor
Grammar enforcement "Decodificação restrita open-source" Mascaramento de logit baseado em FSM, como em outlines / guidance

Leituras Adicionais

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).