La mayoría de los equipos choca con el mismo problema aproximadamente en la tercera semana de trabajo con LLM: el modelo responde casi correctamente. El JSON lleva un comentario de más. O le falta la llave de cierre. O — lo más traicionero — devuelve un JSON válido, pero con el campo status fijado en "ok" en lugar de "approved", una diferencia que la lógica downstream no distingue. La aplicación cae cuatro horas después, cuando esa rama del código finalmente se ejecuta. En producción.
El problema no es que los LLM sean poco fiables. El problema es que el formateo del texto y la corrección semántica son dos cosas distintas, y la mayoría de las implementaciones las mezcla. Este artículo explica dónde está la frontera entre ambas, qué herramientas existen para garantizar la corrección sintáctica y cómo construir una capa de validación que capture el resto.
Por qué «devuelve JSON» en el prompt no es suficiente
Cuando escribes "Respond in JSON format" en el prompt, le estás dando una instrucción al modelo, no una garantía. Los modelos de lenguaje autoregresivos generan token a token según probabilidades — cada token siguiente es el resultado de una distribución condicionada a todo lo anterior. El modelo no «ve» una llave abierta que debe cerrar. Ve probabilidades de los tokens siguientes.
De esto se derivan tres categorías de problemas:
- Error sintáctico — JSON inválido, texto sobrante antes o después, bloques markdown `
`json ...`` envolviendo la salida, caracteres de control, errores de escape unicode - Error de esquema — JSON válido, pero falta un campo obligatorio, un campo tiene el tipo de dato incorrecto (string en lugar de int), un valor enum fuera del listado permitido, una estructura anidada aparece aplanada
- Error semántico — JSON válido, esquema correcto, pero el contenido es incorrecto (clasificación errónea, valor inventado, incoherencia lógica entre campos)
El prompt engineering puede suprimir parcialmente la primera categoría. Para la segunda y la tercera no es suficiente por sí solo.
JSON mode vs. constrained decoding: la diferencia que importa
Cuando los proveedores hablan de «JSON mode», generalmente se refieren a una de dos cosas que son técnicamente muy distintas.
JSON mode (sesgo a nivel de token): A través de un parámetro de API se aumenta la probabilidad de los tokens que pertenecen a la gramática JSON y se suprimen los que violarían la validez. El resultado es casi siempre un JSON sintácticamente válido, pero el modelo sigue decidiendo la estructura por su cuenta. No es posible garantizar que el resultado tenga el campo invoice_number — solo que será parseable.
Constrained decoding / generación basada en gramática: El modelo genera únicamente los tokens que están permitidos en cada estado según una gramática formal (por ejemplo, JSON Schema, RegEx, gramática libre de contexto). La implementación usa típicamente un autómata finito (FSM — finite state machine) o una estructura similar que rastrea el estado del parsing y en cada paso restringe la selección de tokens a los permitidos. El resultado garantiza conformidad con la gramática — incluyendo un esquema concreto con campos obligatorios y tipos de datos.
La diferencia en la práctica: JSON mode te da un resultado parseable, la generación basada en gramática te da un resultado conforme a tu esquema de Pydantic o zod.
XGrammar: el estándar actual para constrained decoding
Para la mayoría de los frameworks de serving en producción — vLLM, SGLang, TensorRT-LLM — desde principios de 2026 el backend por defecto para la decodificación con restricciones es XGrammar. Respecto al enfoque anterior (la librería Outlines, que fue pionera en el enfoque FSM pero tenía problemas con los tiempos de compilación en esquemas complejos), XGrammar alcanza un overhead inferior a 40 microsegundos por token — un impacto prácticamente nulo en la latencia. Si utilizas un stack de serving moderno self-hosted, el constrained decoding está disponible sin configuración adicional.
Para las API en la nube (OpenAI, Anthropic, Google), el constrained decoding no suele estar expuesto directamente — se dispone de JSON mode o de un parámetro «response format» con esquema, que funciona de forma similar, pero la implementación corre del lado del proveedor.
Más sobre serving self-hosted: vLLM vs SGLang vs Ollama — cuándo usar cada uno.
Definición del esquema: Pydantic y zod como fuente de verdad
Si el constrained decoding depende de un esquema formal, necesitas definir ese esquema en algún lugar y mantenerlo. En la práctica hay dos caminos dominantes:
Python — Pydantic v2:
from pydantic import BaseModel, Field
from enum import Enum
class RiskLevel(str, Enum):
low = "low"
medium = "medium"
high = "high"
class SupplierAssessment(BaseModel):
supplier_name: str
risk_level: RiskLevel
score: int = Field(ge=0, le=100)
flags: list[str]
summary: str = Field(max_length=300)El modelo de Pydantic se serializa directamente en JSON Schema, que la mayoría de los frameworks LLM acepta. La validación en el parsing (model.model_validate(json_data)) detecta los errores de esquema con la ubicación exacta.
TypeScript / Node.js — zod:
import { z } from "zod";
const SupplierAssessment = z.object({
supplier_name: z.string(),
risk_level: z.enum(["low", "medium", "high"]),
score: z.number().int().min(0).max(100),
flags: z.array(z.string()),
summary: z.string().max(300),
});El principio clave: el esquema es la única fuente de verdad. No lo definas una vez en el prompt y otra en el código — acabarán divergiendo. Genera la descripción del esquema para el prompt dinámicamente a partir de la propia definición de Pydantic o zod.
Validación y retry: la capa de producción
Ni el constrained decoding ni el JSON mode protegen contra errores semánticos. Y con las API en la nube pueden producirse anomalías sintácticas a pesar del JSON mode (interrupciones de red, truncamiento por max_tokens). El pipeline de producción necesita por tanto una capa de validación con lógica de retry.
El patrón básico:
import json
from pydantic import ValidationError
async def call_with_validation(prompt: str, schema: type[BaseModel], max_retries: int = 3):
for attempt in range(max_retries):
raw = await llm_call(prompt)
try:
data = json.loads(raw)
return schema.model_validate(data)
except (json.JSONDecodeError, ValidationError) as e:
if attempt == max_retries - 1:
raise
# Devolver el error al prompt para corrección
prompt = repair_prompt(prompt, raw, str(e))
raise RuntimeError("Max retries exceeded")Algunos puntos prácticos:
- Retry con el error en contexto: En el siguiente intento, incluye en el prompt la salida exacta del validador. El modelo suele corregir el error cuando sabe dónde está — no cuando solo recibe un «inténtalo de nuevo».
- Exponential backoff: Ante rate limits de la API o una red inestable.
- Limitar los reintentos: En la práctica bastan 2–3 intentos. Si el modelo sigue devolviendo una salida inválida tras tres intentos, el problema está en tu esquema o en el prompt, no en el azar.
- Registra cada intento fallido: Sin log no sabrás con qué frecuencia ocurre ni por qué.
La fiabilidad del tool calling — un problema estrechamente relacionado — la abordamos en el artículo Tool calling fiable en producción.
Por qué incluso una alta fiabilidad no es suficiente y qué hacer al respecto
La fiabilidad de un prompt "Respond in JSON" sin infraestructura adicional depende mucho del modelo, la complejidad del esquema y el prompt — y con esquemas exigentes cae con frecuencia por debajo del nivel utilizable en producción. El JSON mode (sesgo de token) mejora notablemente la situación. La combinación de generación basada en gramática + validación con Pydantic + capa de retry eleva la fiabilidad a un nivel en el que la mayoría de los equipos deja de lidiar con errores sintácticos y puede centrarse en la corrección semántica.
Para muchos casos de uso esto suena bien. Pero imagina un pipeline que se ejecuta mil veces al día: con una fiabilidad del 99 % tienes 10 fallos diarios. Si cada fallo implica intervención manual de un operador o un documento procesado incorrectamente, esos 10 casos diarios se convierten rápidamente en un problema.
Medidas prácticas más allá del retry:
- Esquemas simples: Cada nivel de anidamiento y cada campo nuevo reduce la fiabilidad. Si necesitas un esquema complejo, divídelo en varias llamadas secuenciales con salidas más simples.
- Enum en lugar de texto libre: Donde sea posible, sustituye el texto libre por un listado fijo de valores. El modelo debe elegir entre
["approved", "rejected", "pending"], no inventarse el formato. - Temperaturas bajas:
temperature=0o cerca de cero para tareas de extracción. La creatividad no es deseable aquí. - Ejemplos explícitos en el prompt: Los ejemplos few-shot de salida JSON válida siguen siendo uno de los trucos más efectivos.
Corrección semántica: la frontera donde la tecnología no llega
El constrained decoding garantiza que la salida sea sintáctica y estructuralmente correcta. No garantiza que su contenido sea correcto.
Ejemplos de errores semánticos que pasan por todas las capas técnicas:
- Clasificar una factura como
"approved"a pesar de que el importe supera el límite aprobado — porque el modelo no fue entrenado sobre tus reglas internas - Extraer una fecha incorrecta de un documento (campo confundido)
- Sentiment
"positive"para un texto que es irónicamente crítico - El valor
score: 87que no se desprende del texto, sino que se genera como «un número razonable»
Estos errores se resuelven de otra manera: evals, pruebas sobre una muestra representativa de salidas y, donde el riesgo es alto, una capa de human-in-the-loop. Cómo medir sistemáticamente la calidad de las salidas de un LLM, incluida la dimensión semántica, lo describimos en el artículo Cómo medir la calidad de una aplicación LLM (evals).
Structured outputs con proveedores de API vs. self-hosted
Si usas API en la nube, tienes a tu disposición:
- OpenAI: Parámetro
response_formatcontype: "json_schema"y la definición de JSON Schema en línea. Desde cierta versión de la API garantiza conformidad con el esquema a nivel de gramática (no solo sesgo de token). - Anthropic Claude: Salidas estructuradas nativas mediante el parámetro
output_format— decodificación con restricciones directamente a nivel de token, garantiza conformidad con JSON Schema. También disponible con tool use estricto con esquema de entrada definido con precisión. - Google Gemini:
responseMimeType: "application/json"+ parámetroresponseSchema.
Los tres enfoques funcionan bien para casos de uso típicos. La diferencia práctica aparece en:
- Esquemas muy complejos (anidamiento profundo,
oneOf/anyOf) — las API en la nube pueden tener limitaciones sobre las features de JSON Schema soportadas - Alto throughput — con miles de peticiones por minuto,
vLLM+XGrammarself-hosted es económicamente más ventajoso; véase la comparativa en vLLM vs SGLang vs Ollama - Datos sensibles — si los documentos de entrada contienen PII o secretos comerciales, las API en la nube generan un riesgo de data egress; el self-hosted on-prem lo elimina
Para sectores regulados (sanidad, finanzas, derecho) la vía on-prem es casi siempre relevante — más sobre esto en LLM locales vs. cloud: cuándo tiene sentido cada opción.
Salida estructurada en pipelines RAG y de agentes
Los structured outputs no son solo para tareas de extracción. En cualquier pipeline donde un LLM genera una salida intermedia que consume otro componente, el formato estructurado es una condición de fiabilidad.
En un pipeline RAG normalmente necesitas salida estructurada para:
- Query planning — el modelo decide qué fuentes consultar, con qué filtro, cuántos resultados
- Decisión de reranking — en RAG agentic, el modelo evalúa la relevancia del chunk y devuelve una puntuación
- Citas — dónde en el documento fuente se encuentra la respuesta (chunk ID, offset, confianza)
- Salida final para downstream — cuando el pipeline RAG lo consume otro sistema y no solo muestra texto
En un pipeline de agentes aplica lo mismo a cada tool call — el agente debe devolver una selección de acción estructurada (nombre del tool + parámetros) para que el orquestador sepa qué ejecutar. Un fallo de estructura aquí no significa solo un error de parsing — puede significar ejecutar la acción incorrecta.
Experiencia práctica: en pipelines con más de tres herramientas merece la pena validar cada entrada de tool mediante un modelo Pydantic, no solo un dict libre. Los errores se detectan así en la frontera de entrada al tool, no en las profundidades de su lógica.
Monitoring y degradación en producción
La salida estructurada tiende a funcionar bien tras el despliegue y a degradarse gradualmente — cuando el modelo cambia del lado del proveedor (las API en la nube actualizan el modelo en silencio), cuando cambia la distribución de los datos de entrada, o cuando el esquema crece con nuevos campos sin actualizar el prompt.
El monitoring mínimo que recomendamos:
- Tasa de error de parsing: ¿Qué porcentaje de llamadas termina con un error de validación en el primer intento? Si este número crece, algo ha cambiado.
- Tasa de retry: ¿Qué porcentaje de llamadas necesitó más de un intento? Por encima del 5 % es una señal.
- Tasa de null/default por campo: Si el campo
invoice_numberes null en el 30 % de los casos, o bien las entradas no lo contienen, o bien el modelo ha dejado de extraerlo. - Seguimiento de versión del esquema: Cada cambio de esquema es un evento de despliegue — registra la versión del esquema en cada llamada para poder correlacionar cambios de calidad.
Sin este monitoring, la degradación solo se detectará cuando el sistema downstream falle en producción — no cuando ocurra.
Preguntas frecuentes
¿Cuál es la diferencia entre JSON mode y structured outputs?
JSON mode (sesgo a nivel de token) garantiza solo la validez sintáctica — el resultado será un JSON parseable, pero sin garantía de una estructura concreta. Los structured outputs (basados en gramática / restringidos por esquema) garantizan conformidad con el esquema definido, incluyendo campos obligatorios y tipos de datos. Para pipelines de producción necesitas el segundo enfoque.
¿Afecta el constrained decoding a la calidad o la velocidad del modelo?
Con implementaciones modernas (XGrammar) el overhead es inferior a 40 microsegundos por token — en la práctica inapreciable. La calidad de la respuesta puede variar ligeramente, ya que el modelo no puede formular libremente — con esquemas estrechos que tienen campos de texto largo es despreciable; con esquemas muy restrictivos (por ejemplo, un enum pequeño para una respuesta en lenguaje natural) la salida puede resultar menos natural.
¿Puedo usar structured outputs con un modelo ejecutándose localmente?
Sí, con vLLM o SGLang XGrammar está disponible out-of-the-box. Ollama soporta JSON mode, pero no constrained decoding basado en gramática en toda su extensión — para pipelines de producción con un esquema preciso, vLLM es la mejor opción. Más en vLLM vs SGLang vs Ollama.
¿Qué hacer cuando el modelo sigue devolviendo valores incorrectos a pesar del formato correcto?
Se trata de un error semántico, no sintáctico — el constrained decoding no lo resolverá. Soluciones: ejemplos few-shot con patrones correctos e incorrectos, reglas explícitas en el system prompt, fine-tuning sobre una muestra del dominio, o una capa de human-in-the-loop para decisiones críticas.
¿Cómo definir el esquema para que el modelo no se equivoque en los tipos de datos?
Sé explícito: en lugar de score: number usa score: integer, minimum: 0, maximum: 100. En lugar de date: string usa date: string, format: date (ISO 8601). Define siempre los valores enum — un string libre es una invitación a las variaciones. Cada precisión en el esquema reduce la probabilidad de error.
*MP Industrial Solutions ayuda a las empresas a diseñar e implementar pipelines LLM en producción — desde la validación de salidas hasta el monitoring en entornos reales. Si estás resolviendo la fiabilidad de las salidas estructuradas en tu sistema, estaremos encantados de analizar la arquitectura concreta contigo.*
