Phase 05 - Lesson 20

Saídas Estruturadas e Decodificação Restrita

Peça JSON a um LLM. Receba JSON na maioria das vezes. Em produção, "maioria" é o problema. A decodificação restrita transforma "maioria" em "sempre" ao editar os logits antes da amostragem.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 17 (Chatbots), Fase 5 · 19 (Tokenização de Subpalavras) Tempo: ~60 minutos

O Problema

Um classificador solicita a um LLM: "Retorne um de {positive, negative, neutral}." O modelo retorna "O sentimento é positivo — esta avaliação é extremamente favorável porque o cliente afirma explicitamente que ...". Seu parser quebra. O F1 do seu classificador é 0.0.

Geração livre não é um contrato. É uma sugestão. Um sistema de produção precisa de um contrato.

Existem três camadas em 2026.

  1. Prompting. Peça com educação. "Retorne apenas o objeto JSON." Funciona ~80% em modelos de fronteira, menos em modelos menores.
  2. APIs nativas de saída estruturada. response_format da OpenAI, uso de ferramentas da Anthropic, modo JSON do Gemini. Confiável em schemas suportados. Preso ao fornecedor.
  3. Decodificação restrita. Modifica os logits a cada passo de geração de modo que o modelo não possa emitir tokens inválidos. 100% válido por construção. Funciona em qualquer modelo local.

Esta lição constrói intuição para as três e indica quando recorrer a cada uma.

O Conceito

Decodificação restrita mascarando tokens inválidos em cada passo

Como a decodificação restrita funciona. A cada passo de geração, o LLM produz um vetor de logits sobre todo o vocabulário (~100k tokens). Um processador de logits fica entre o modelo e o amostrador. Ele calcula quais tokens são válidos dada a posição atual na gramática alvo — JSON Schema, regex, gramática livre de contexto — e define os logits de todos os tokens inválidos como infinito negativo. O softmax sobre os logits restantes coloca massa de probabilidade apenas em continuações válidas.

Implementações em 2026:

  • Outlines. Compila JSON Schema ou regex em uma máquina de estados finitos. Cada token recebe uma busca O(1) do próximo token válido. Baseado em FSM, então schemas recursivos precisam ser achatados.
  • XGrammar / llguidance. Motores de gramática livre de contexto. Lidam com JSON Schema recursivo. Overhead de decodificação quase nulo. A OpenAI creditou o llguidance em sua implementação de saída estruturada de 2025.
  • vLLM guided decoding. guided_json, guided_regex, guided_choice, guided_grammar integrados via backends Outlines, XGrammar ou lm-format-enforcer.
  • Instructor. Wrapper baseado em Pydantic sobre qualquer LLM. Tenta novamente em caso de falha de validação. Multi-fornecedor, mas não modifica logits — ele depende de retentativas + prompts cientes de saída estruturada.

O resultado contraintuitivo

A decodificação restrita é frequentemente mais rápida do que a geração irrestrita. Duas razões. Primeira, ela encolhe o espaço de busca do próximo token. Segunda, implementações inteligentes pulam totalmente a geração de tokens forçados (estrutura fixa como {"name": " — cada byte é determinado).

A armadilha que te custa caro

A ordem dos campos importa. Coloque answer antes de reasoning, e o modelo se compromete com uma resposta antes de pensar. O JSON é válido. A resposta está errada. Nenhuma validação detecta isso.

// BAD
{"answer": "yes", "reasoning": "because ..."}

// GOOD
{"reasoning": "... therefore ...", "answer": "yes"}

A ordem dos campos do schema é lógica, não formatação.

Construa

Passo 1: geração restrita por regex do zero

Veja code/main.py para uma implementação autônoma de FSM. A ideia central em 30 linhas:

def mask_logits(logits, valid_token_ids):
    mask = [float("-inf")] * len(logits)
    for tid in valid_token_ids:
        mask[tid] = logits[tid]
    return mask


def generate_constrained(model, tokenizer, prompt, fsm):
    ids = tokenizer.encode(prompt)
    state = fsm.initial_state
    while not fsm.is_accept(state):
        logits = model.next_token_logits(ids)
        valid = fsm.valid_tokens(state, tokenizer)
        logits = mask_logits(logits, valid)
        tok = sample(logits)
        ids.append(tok)
        state = fsm.transition(state, tok)
    return tokenizer.decode(ids)

A FSM rastreia quais partes da gramática satisfizemos até agora. valid_tokens(state, tokenizer) calcula quais tokens do vocabulário podem avançar a FSM sem sair de um caminho de aceitação.

Passo 2: Outlines para JSON Schema

from pydantic import BaseModel
from typing import Literal
import outlines


class Review(BaseModel):
    sentiment: Literal["positive", "negative", "neutral"]
    confidence: float
    evidence_span: str


model = outlines.models.transformers("meta-llama/Llama-3.2-3B-Instruct")
generator = outlines.generate.json(model, Review)

result = generator("Classify: 'The wait staff was attentive and the food arrived hot.'")
print(result)
# Review(sentiment='positive', confidence=0.93, evidence_span='attentive ... hot')

Zero erros de validação. Nunca. A FSM torna a saída inválida inalcançável.

Passo 3: Instructor para Pydantic agnóstico de fornecedor

import instructor
from anthropic import Anthropic
from pydantic import BaseModel, Field


class Invoice(BaseModel):
    vendor: str
    total_usd: float = Field(ge=0)
    line_items: list[str]


client = instructor.from_anthropic(Anthropic())
invoice = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    response_model=Invoice,
    messages=[{"role": "user", "content": "Extract from: 'Acme Corp $420. Widget, Gizmo.'"}],
)

Mecanismo diferente. O Instructor não toca nos logits. Ele formata o schema dentro do prompt, faz o parsing da saída e tenta novamente em caso de falha de validação (3 vezes por padrão). Funciona com qualquer fornecedor. Retentativas adicionam latência e custo. A portabilidade entre fornecedores é o argumento de venda.

Passo 4: APIs nativas de fornecedores

from openai import OpenAI

client = OpenAI()
response = client.responses.create(
    model="gpt-5",
    input=[{"role": "user", "content": "Classify: 'The food was cold.'"}],
    text={"format": {"type": "json_schema", "name": "sentiment",
          "schema": {"type": "object", "required": ["sentiment"],
                     "properties": {"sentiment": {"type": "string",
                                                  "enum": ["positive", "negative", "neutral"]}}}}},
)
print(response.output_parsed)

Decodificação restrita no lado do servidor. Paridade de confiabilidade com o Outlines para schemas suportados. Sem gerenciamento de modelo local. Prende você ao fornecedor.

Armadilhas

  • Schemas recursivos. O Outlines achata a recursão até uma profundidade fixa. Saídas em árvore (comentários aninhados, AST) precisam de XGrammar ou llguidance (baseados em CFG).
  • Enums gigantes. Um enum de 10.000 opções compila lentamente ou estoura o tempo limite. Mude para um retriever: primeiro preveja os top-k candidatos, depois restrinja a eles.
  • Gramática estrita demais. Force a regex date: "YYYY-MM-DD" e o modelo não conseguirá produzir "unknown" para datas ausentes. O modelo compensa inventando uma data. Permita null ou um sentinela.
  • Comprometimento prematuro. Veja a armadilha de ordem dos campos acima. Sempre coloque o raciocínio primeiro.
  • Modo JSON do fornecedor sem schema. O modo JSON puro só garante JSON válido, não válido para o seu caso de uso. Sempre forneça um schema completo.

Use

A stack de 2026:

Situação Escolha
Modelo OpenAI/Anthropic/Google, schema simples Saída estruturada nativa do fornecedor
Qualquer fornecedor, fluxo com Pydantic, tolera retentativas Instructor
Modelo local, precisa de 100% de validade, schema plano Outlines (FSM)
Modelo local, schema recursivo XGrammar ou llguidance
Servidor de inferência auto-hospedado vLLM guided decoding
Processamento em lote com retentativas aceitáveis Instructor + modelo mais barato

Entregue

Salve como outputs/skill-structured-output-picker.md:

---
name: structured-output-picker
description: Choose a structured output approach, schema design, and validation plan.
version: 1.0.0
phase: 5
lesson: 20
tags: [nlp, llm, structured-output]
---

Given a use case (provider, latency budget, schema complexity, failure tolerance), output:

1. Mechanism. Native vendor structured output, Instructor retries, Outlines FSM, or XGrammar CFG. One-sentence reason.
2. Schema design. Field order (reasoning first, answer last), nullable fields for "unknown", enum vs regex, required fields.
3. Failure strategy. Max retries, fallback model, graceful `null` handling, out-of-distribution refusal.
4. Validation plan. Schema compliance rate (target 100%), semantic validity (LLM-judge), field-coverage rate, latency p50/p99.

Refuse any design that puts `answer` or `decision` before reasoning fields. Refuse to use bare JSON mode without a schema. Flag recursive schemas behind an FSM-only library.

Exercícios

  1. Fácil. Faça um prompt a um pequeno modelo de pesos abertos (ex.: Llama-3.2-3B) sem decodificação restrita para Review(sentiment, confidence, evidence_span). Meça a fração que faz parsing como JSON válido em 100 avaliações.
  2. Médio. Mesmo corpus com o modo JSON do Outlines. Compare taxa de conformidade, latência e acurácia semântica.
  3. Difícil. Implemente um decodificador restrito por regex do zero para números de telefone (\d{3}-\d{3}-\d{4}). Verifique 0 saídas inválidas em 1000 amostras.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Decodificação restrita Força saída válida Mascara os logits de tokens inválidos a cada passo de geração.
Processador de logits A coisa que restringe Função: (logits, state) -> masked_logits.
FSM Máquina de estados finitos Representação compilada da gramática; busca O(1) do próximo token válido.
CFG Gramática livre de contexto Gramática que lida com recursão; mais lenta, porém mais expressiva que a FSM.
Ordem dos campos do schema Isso importa? Sim — o primeiro campo se compromete; sempre coloque o raciocínio antes da resposta.
Guided decoding Nome dado pelo vLLM Mesmo conceito, integrado ao servidor de inferência.
Modo JSON Versão inicial da OpenAI Garante a sintaxe JSON; NÃO garante correspondência ao schema.

Leitura Adicional

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