Phase 05 - Lesson 20

Salidas Estructuradas y Decodificación Restringida

Pídele JSON a un LLM. Obtén JSON la mayoría de las veces. En producción, "la mayoría" es el problema. La decodificación restringida convierte "la mayoría" en "siempre" editando los logits antes del muestreo.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 5 · 17 (Chatbots), Fase 5 · 19 (Tokenización de Subpalabras) Tiempo: ~60 minutos

El Problema

Un clasificador le solicita a un LLM: "Devuelve uno de {positive, negative, neutral}." El modelo devuelve "El sentimiento es positivo — esta reseña es abrumadoramente favorable porque el cliente afirma explícitamente que ...". Tu parser se cae. El F1 de tu clasificador es 0.0.

La generación libre no es un contrato. Es una sugerencia. Un sistema de producción necesita un contrato.

Existen tres capas en 2026.

  1. Prompting. Pide con amabilidad. "Devuelve solo el objeto JSON." Funciona ~80% en modelos de frontera, menos en los más pequeños.
  2. APIs nativas de salida estructurada. response_format de OpenAI, uso de herramientas de Anthropic, modo JSON de Gemini. Confiable en esquemas soportados. Atado al proveedor.
  3. Decodificación restringida. Modifica los logits en cada paso de generación para que el modelo no pueda emitir tokens inválidos. 100% válido por construcción. Funciona en cualquier modelo local.

Esta lección construye intuición sobre las tres y señala cuándo recurrir a cada una.

El Concepto

Decodificación restringida enmascarando tokens inválidos en cada paso

Cómo funciona la decodificación restringida. En cada paso de generación, el LLM produce un vector de logits sobre todo el vocabulario (~100k tokens). Un procesador de logits se ubica entre el modelo y el muestreador. Calcula qué tokens son válidos dada la posición actual en la gramática objetivo — JSON Schema, regex, gramática libre de contexto — y establece los logits de todos los tokens inválidos en infinito negativo. El softmax sobre los logits restantes coloca masa de probabilidad solo en continuaciones válidas.

Implementaciones en 2026:

  • Outlines. Compila JSON Schema o regex en una máquina de estados finitos. Cada token obtiene una búsqueda O(1) del siguiente token válido. Basado en FSM, así que los esquemas recursivos necesitan aplanarse.
  • XGrammar / llguidance. Motores de gramática libre de contexto. Manejan JSON Schema recursivo. Sobrecarga de decodificación casi nula. OpenAI le dio crédito a llguidance en su implementación de salida estructurada de 2025.
  • vLLM guided decoding. guided_json, guided_regex, guided_choice, guided_grammar integrados mediante backends de Outlines, XGrammar o lm-format-enforcer.
  • Instructor. Wrapper basado en Pydantic sobre cualquier LLM. Reintenta ante fallas de validación. Multiproveedor, pero no modifica los logits — se apoya en reintentos + prompts conscientes de la salida estructurada.

El resultado contraintuitivo

La decodificación restringida suele ser más rápida que la generación sin restricciones. Por dos razones. Primera, reduce el espacio de búsqueda del siguiente token. Segunda, las implementaciones ingeniosas omiten por completo la generación de tokens forzados (andamiaje como {"name": " — cada byte está determinado).

La trampa que te cuesta caro

El orden de los campos importa. Pon answer antes de reasoning, y el modelo se compromete con una respuesta antes de pensar. El JSON es válido. La respuesta es incorrecta. Ninguna validación lo detecta.

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

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

El orden de los campos del esquema es lógica, no formato.

Constrúyelo

Paso 1: generación restringida por regex desde cero

Consulta code/main.py para una implementación autónoma de FSM. La idea central en 30 líneas:

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)

La FSM rastrea qué partes de la gramática hemos satisfecho hasta ahora. valid_tokens(state, tokenizer) calcula qué tokens del vocabulario pueden hacer avanzar la FSM sin abandonar un camino de aceptación.

Paso 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')

Cero errores de validación. Nunca. La FSM vuelve inalcanzable la salida inválida.

Paso 3: Instructor para Pydantic agnóstico de proveedor

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 distinto. Instructor no toca los logits. Da formato al esquema dentro del prompt, parsea la salida y reintenta ante fallas de validación (3 veces por defecto). Funciona con cualquier proveedor. Los reintentos agregan latencia y costo. La portabilidad entre proveedores es su argumento de venta.

Paso 4: APIs nativas de proveedores

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)

Decodificación restringida del lado del servidor. Paridad de confiabilidad con Outlines para esquemas soportados. Sin gestión de modelo local. Te ata al proveedor.

Trampas

  • Esquemas recursivos. Outlines aplana la recursión a una profundidad fija. Las salidas con estructura de árbol (comentarios anidados, AST) necesitan XGrammar o llguidance (basados en CFG).
  • Enums enormes. Un enum de 10.000 opciones compila lento o se queda sin tiempo. Cambia a un retriever: primero predice los top-k candidatos, luego restringe a esos.
  • Gramática demasiado estricta. Fuerza la regex date: "YYYY-MM-DD" y el modelo no podrá producir "unknown" para fechas faltantes. El modelo compensa inventando una fecha. Permite null o un centinela.
  • Compromiso prematuro. Consulta la trampa de orden de campos anterior. Pon siempre el razonamiento primero.
  • Modo JSON del proveedor sin esquema. El modo JSON puro solo garantiza JSON válido, no válido para tu caso de uso. Proporciona siempre un esquema completo.

Úsalo

El stack de 2026:

Situación Elige
Modelo OpenAI/Anthropic/Google, esquema simple Salida estructurada nativa del proveedor
Cualquier proveedor, flujo con Pydantic, tolera reintentos Instructor
Modelo local, necesitas 100% de validez, esquema plano Outlines (FSM)
Modelo local, esquema recursivo XGrammar o llguidance
Servidor de inferencia autoalojado vLLM guided decoding
Procesamiento por lotes con reintentos aceptables Instructor + modelo más barato

Entrégalo

Guarda 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.

Ejercicios

  1. Fácil. Haz un prompt a un modelo pequeño de pesos abiertos (p. ej., Llama-3.2-3B) sin decodificación restringida para Review(sentiment, confidence, evidence_span). Mide la fracción que se parsea como JSON válido en 100 reseñas.
  2. Medio. El mismo corpus con el modo JSON de Outlines. Compara la tasa de conformidad, la latencia y la exactitud semántica.
  3. Difícil. Implementa un decodificador restringido por regex desde cero para números de teléfono (\d{3}-\d{3}-\d{4}). Verifica 0 salidas inválidas en 1000 muestras.

Términos Clave

Término Lo que dice la gente Lo que realmente significa
Decodificación restringida Forzar salida válida Enmascarar los logits de tokens inválidos en cada paso de generación.
Procesador de logits Lo que restringe Función: (logits, state) -> masked_logits.
FSM Máquina de estados finitos Representación compilada de la gramática; búsqueda O(1) del siguiente token válido.
CFG Gramática libre de contexto Gramática que maneja recursión; más lenta pero más expresiva que la FSM.
Orden de campos del esquema ¿Importa? Sí — el primer campo se compromete; pon siempre el razonamiento antes de la respuesta.
Guided decoding El nombre que le da vLLM El mismo concepto, integrado en el servidor de inferencia.
Modo JSON La versión temprana de OpenAI Garantiza la sintaxis JSON; NO garantiza la correspondencia con el esquema.

Lecturas Adicionales

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