Razonamiento guiado por esquemas: vLLM, XGrammar y Pydantic
Traducción automática Este artículo se tradujo automáticamente a partir de la versión original en inglés.
Este artículo está dirigido a ingenieros de Python que necesitan que el código posterior pueda validar la salida del modelo. Aprenderás a definir un esquema de Pydantic, solicitar structured output a vLLM y añadir comprobaciones de aplicación para el significado y las políticas.
Reintentar una llamada a un LLM no garantiza un JSON válido. La siguiente muestra puede fallar de la misma forma, y las llamadas repetidas añaden latencia y coste.
Schema-Guided Reasoning (SGR) impone un esquema mientras el modelo genera cada token. Defines los campos obligatorios con Pydantic y el motor de inferencia bloquea los tokens que infringirían esa estructura. El resultado es sintácticamente válido por construcción, en lugar de depender de reintentos.
TL;DR. SGR utiliza constrained decoding para mantener la salida de un LLM dentro de un esquema de Pydantic. vLLM puede usar XGrammar para restringir la estructura generada. Valida las reglas semánticas en el código de la aplicación.
¿Qué es Schema-Guided Reasoning?
Schema-Guided Reasoning es una técnica que Rinat Abdullin describió en julio de 2025. En lugar de permitir que el modelo complete texto libremente —lo que puede resultar incoherente o ambiguo—, le proporcionas una plantilla estricta que define:
- qué pasos debe representar la respuesta
- el orden previsto de esos pasos, para que un revisor pueda inspeccionar el recorrido desde los datos hasta la decisión
- dónde debe concentrar la atención
Piensa en ello como una lista de comprobación cognitiva que el modelo debe seguir.
Qué controla el esquema
Campos como churn_analysis, margin_math y max_discount_percent hacen explícitas las salidas intermedias previstas. Un esquema restringe la forma de la respuesta. Por sí solo, no hace que un campo dependa de otro ni demuestra que la decisión sea correcta.
Esto proporciona:
- razonamiento reproducible en ejecuciones repetidas
- salidas auditables en las que cada paso se puede inspeccionar
- campos intermedios que puedes evaluar frente a un dataset de pruebas
- modelos más pequeños que pasan a ser viables, ya que el esquema aporta la estructura que, de otro modo, el modelo tendría que aprender
- Abdullin escribe que una mejora de precisión del 5–10 % «no es infrecuente» en los casos que ha observado; se trata de una observación de un profesional, no de un resultado de benchmark, así que debes medirla en tu propia carga de trabajo
SGR frente a Chain of Thought y prompt engineering
Los tres enfoques se diferencian principalmente por el grado de restricción que imponen al modelo.
| Característica | Prompt Engineering | Chain of Thought | Schema-Guided Reasoning |
|---|---|---|---|
| Estructura de salida | Texto variable | Prosa libre | JSON/Pydantic rígido |
| Mecanismo de control | Persuasión semántica («Please output JSON») | Prompting heurístico («Let’s think step by step») | Constrained decoding (basado en gramática) |
| Flujo de razonamiento | Lo determina el modelo | Lo determina el modelo | El desarrollador describe una topología prevista |
| Auditabilidad | Baja (requiere parsing) | Baja (requiere leer la prosa) | Alta (inspección a nivel de campo) |
| Integración | Difícil (parsing con regex) | Difícil (formato variable) | Requiere compatibilidad con esquemas y validación |
| Tasa de errores | Alta (variabilidad de formato) | Moderada (alucinación del formato) | Se bloquean las salidas no válidas según el esquema; permanecen los errores semánticos |
| Requisitos del modelo | Seguir instrucciones de forma sólida | Gran capacidad de razonamiento | También funciona con modelos más pequeños |
Prompt engineering: persuasión semántica
Please analyze the customer data and output your response as valid JSON
with the following structure: {"discount": <number>, "reason": <string>}
Be careful with the formatting!
Confías en que la comprensión del modelo sobre «output JSON» pese más que su tendencia a conversar. Una actualización del modelo, un cambio de temperatura o un ejemplo few-shot diferente pueden romper tu parser.
Chain of Thought: traza de razonamiento útil, mismo problema de estructura
Let's think step by step:
1. First, I'll analyze the customer's churn risk...
2. Then I'll calculate the margin...
3. Therefore, I recommend a 15% discount.
CoT puede mejorar la precisión de la tarea cuando el prompt, el modelo, la tarea y la evaluación lo permiten, pero deja el resultado en forma de prosa, difícil de parsear de manera fiable. Es posible que acabes haciendo una segunda llamada al LLM solo para extraer datos estructurados.
SGR: chain of thought estructurado
SGR puede poner los campos intermedios a disposición para su inspección y evaluación. Que eso mejore la precisión de la tarea depende del modelo, el prompt, la tarea y el uso que se haga de esos campos; el esquema solo formaliza su forma:
class PricingLogic(BaseModel):
# 1. Data Analysis (must complete before decision)
churn_analysis: str = Field(..., description="Analyze churn_probability")
financial_analysis: str = Field(..., description="Analyze cart_value and margin")
# 2. Math Enforcement (explicit calculation)
margin_math: str = Field(..., description="Calculate: 'Cart $X * Y% = $Z'")
# 3. Decision Constraint (bounded by prior analysis)
max_discount_percent: float = Field(..., description="Max allowed discount")
# 4. Final Output
offer_code: str
customer_message: str
El esquema describe estos campos en ese orden. Un esquema de objeto único no crea un paso de validación independiente entre ellos. Usa llamadas separadas o comprobaciones de aplicación cuando las decisiones posteriores deban depender de resultados anteriores.
Patrones de SGR
SGR tiene tres patrones principales que se pueden combinar para crear workflows más grandes.
1. Cascade: pasos de razonamiento secuenciales
Cascade representa un orden de razonamiento dentro de una única respuesta estructurada. No impone una transición de estado entre campos.
from pydantic import BaseModel
from typing import Literal, Annotated
from annotated_types import Ge, Le
class CandidateEvaluation(BaseModel):
"""Evaluate a job candidate with enforced reasoning order."""
# Step 1: Summarize (forces context awareness)
brief_candidate_summary: str
# Step 2: Rate (bounded integer)
rate_skill_match: Annotated[int, Ge(1), Le(10)]
# Step 3: Decide (constrained choices)
final_recommendation: Literal["hire", "reject", "hold"]
Encaja bien en: evaluación de candidatos, clasificación de documentos, análisis de cumplimiento y diagnóstico médico.
Se pide al modelo que devuelva brief_candidate_summary, rate_skill_match y final_recommendation en ese orden. Si el orden es un requisito de la política, impónlo mediante llamadas separadas o lógica determinista en la aplicación.
2. Routing: una sentencia switch semántica
Routing hace que el modelo se comprometa con una ruta de entre varias opciones, implementadas con tipos Union.
from pydantic import BaseModel
from typing import Literal, Union
class FeatureLookup(BaseModel):
"""Route to database lookup."""
rationale: str
tool_name: Literal["fetch_user_features"] = "fetch_user_features"
user_id: str
class GeneralResponse(BaseModel):
"""Standard response for non-pricing queries."""
tool_name: Literal["respond"] = "respond"
content: str
class RouterSchema(BaseModel):
"""The model must pick exactly ONE branch."""
action: Union[FeatureLookup, GeneralResponse]
Encaja bien en: clasificación de intención, selección de herramientas, triaje de soporte y dispatch multi-agent.
Los valores Literal específicos de cada rama ayudan a la validación a distinguir los miembros de la unión. No hacen que el routing sea correcto. Valida el resultado y haz el dispatch en el código de la aplicación. Para usar un discriminador explícito de Pydantic, configura y prueba una unión discriminada.
3. Cycle: razonamiento repetido con listas
Cycle obliga al modelo a producir varios elementos, con límites sobre su cantidad.
from pydantic import BaseModel
from typing import List, Literal, Annotated
from annotated_types import MinLen, MaxLen
class RiskFactor(BaseModel):
explanation: str
severity: Literal["low", "medium", "high"]
class RiskAssessment(BaseModel):
"""Generate 2-4 risk factors."""
factors: Annotated[List[RiskFactor], MinLen(2), MaxLen(4)]
Encaja bien en: evaluación de riesgos, extracción de incidencias, llamadas paralelas a herramientas y planificación en varios pasos.
Los límites MinLen y MaxLen fuerzan un mínimo de 2 y un máximo de 4 elementos. Combinado con Routing, así es como se despacha un batch de tool calls de ancho fijo.
Cómo hacer que SGR funcione: constrained decoding
Los patrones anteriores no son más que esquemas de Pydantic. Lo que hace que sean vinculantes es constrained decoding, también llamado Structured Output.
Constrained decoding modifica el paso de generación de tokens. En lugar de permitir que el modelo muestree libremente de su vocabulario, el motor aplica una máscara de gramática que bloquea los tokens que infringirían el esquema. Esto ocurre en el motor de inferencia, no en el código de la aplicación.
[!TIP] SGR no requiere «reasoning models» como o1 o DeepSeek-R1. Funciona correctamente con modelos ajustados para seguir instrucciones y, especialmente bien, con modelos destilados a partir de modelos de razonamiento.
Proveedores cloud compatibles
La documentación de los siguientes proveedores anunciaba compatibilidad con structured output cuando se actualizó este artículo, el 2026-06-07. La compatibilidad, los subconjuntos de esquemas, el nivel de strictness y el comportamiento ante fallos varían según el modelo y el endpoint:
| Proveedor | Compatibilidad |
|---|---|
| OpenAI | Structured Outputs (incluido Azure) |
| Google/Gemini | Compatibilidad con JSON Schema desde noviembre de 2025 (Pydantic y Zod) |
| Mistral | Custom Structured Output |
| Grok | Structured Outputs para varios modelos |
| Fireworks AI | JSON Schema |
| Cerebras | Structured Outputs |
| OpenRouter | Depende del proveedor downstream y de la ruta seleccionada |
Motores de inferencia compatibles
Para modelos self-hosted, los principales motores disponen de un backend de constrained decoding:
| Motor | Backend |
|---|---|
| vLLM | xgrammar o guidance |
| SGLang | Outlines, XGrammar o llguidance |
| TensorRT-LLM | GuidedDecoding |
| Ollama | Structured Outputs |
Por qué este artículo se centra en vLLM y XGrammar
Hay varios motivos:
- vLLM es uno de los motores de inferencia de LLM open source más utilizados en producción, por lo que lo que construyas aquí se puede portar fácilmente.
- XGrammar está implementado en C++ y los benchmarks citados miden su overhead en condiciones concretas. Mídelo con tu modelo, esquema, hardware y configuración de serving.
- La API de vLLM es compatible con OpenAI, lo que hace que la migración desde proveedores cloud tenga un coste bajo.
- XGrammar gestiona esquemas anidados complejos, uniones y estructuras recursivas.
Cómo impone los esquemas XGrammar
Dónde se aplica la máscara
XGrammar modifica los logits de salida después del forward pass del modelo y antes del muestreo. No cambia el modelo en sí, sino que filtra los tokens que se pueden seleccionar.
Un agent loop de inferencia estándar tiene este aspecto:
1. Input tokens → GPU Forward Pass → Logits (probability scores for all ~128K tokens)
2. Logits → Sampling (temperature, top-p, etc.) → Next Token
3. Repeat until done
XGrammar se intercala entre los pasos 1 y 2:
1. Input tokens → GPU Forward Pass → Raw Logits
2. Raw Logits → XGrammar Logits Processor → Masked Logits
3. Masked Logits → Sampling → Next Token (guaranteed valid)
4. Repeat until done
El modelo sigue calculando su distribución de probabilidad completa en la GPU. El GrammarMatcher de XGrammar genera la bitmask en la CPU; después, el motor de serving mueve esa bitmask al device de los logits y la aplica in place antes del muestreo. Para logits en la GPU, XGrammar utiliza un kernel de GPU para aplicar la máscara. Los tokens no válidos reciben logits con valor -∞, lo que hace que su probabilidad sea exactamente 0 después de softmax.
Dos fases
XGrammar divide el trabajo entre compilación y ejecución. Este diseño reduce el trabajo repetido de la gramática.
Fase 1: compilación de la gramática, una vez por esquema
# This happens once per schema
tokenizer_info = xgr.TokenizerInfo.from_huggingface(tokenizer)
grammar_compiler = xgr.GrammarCompiler(tokenizer_info)
compiled_grammar = grammar_compiler.compile_json_schema(schema_json)
Durante la compilación, XGrammar:
- Convierte el JSON Schema en una Context-Free Grammar.
- Construye un Pushdown Automaton (PDA), una máquina de estados con una pila que puede gestionar estructuras anidadas como
{"a": {"b": {"c": ...}}}. - Precalcula qué tokens son válidos en cada posición de la gramática. El resultado es la «adaptive token mask cache».
- Clasifica los tokens como «context-independent» (se pueden almacenar en caché) o «context-dependent» (deben comprobarse en runtime frente al estado de la pila).
[!NOTE] El artículo de XGrammar informa de que aproximadamente el 99 % de los tokens eran context-independent en sus mediciones (artículo). Considera esa cifra específica de un benchmark, no una proporción universal.
Fase 2: generación de la máscara en runtime, en cada token
En cada paso de generación:
- El
GrammarMatcherrealiza el seguimiento de la posición actual en la gramática. - Consulta la máscara precalculada para los tokens context-independent.
- Ejecuta el PDA para comprobar los tokens context-dependent restantes.
- Combina ambos conjuntos en una bitmask final, la mueve al device de los logits y la aplica allí.
Por qué se utilizan pushdown automata y no regex
Por el anidamiento. Una expresión regular —una máquina de estados finita— no puede procesar de forma fiable estructuras como:
{ "user": { "profile": { "settings": { "theme": "dark" } } } }
La parte difícil son las llaves de cierre }}}: necesitas recordar cuántas llaves has abierto. Un Pushdown Automaton tiene una pila que realiza ese seguimiento, por lo que puede gestionar una profundidad de anidamiento arbitraria. Por eso XGrammar también puede imponer Union types, objetos anidados y esquemas recursivos, ámbitos en los que los enfoques basados en regex se quedan cortos.
Un ejemplo concreto: generar un campo float
Cuando el modelo genera "max_discount_percent":, XGrammar sabe por el esquema que a continuación debe aparecer un float. El conjunto de tokens válidos depende del estado del parser y del tokenizer. Al principio del número, la máscara puede admitir un dígito o un signo menos; después de un dígito, puede admitir continuaciones como otro dígito, un punto decimal o un marcador de exponente.
- Una comilla,
{,[,true,falseonullno puede iniciar este número, por lo que la máscara bloquea sus tokens en ese estado. - El forward pass puede haber asignado una probabilidad alta al token correspondiente a
"fifteen". Como ese token no puede continuar este campo numérico, la máscara lo elimina y el modelo debe elegir una continuación numérica válida.
Qué afecta al overhead
Hay tres motivos:
- Generación y transferencia de la máscara. XGrammar genera las máscaras en la CPU y el motor de serving las transfiere al device de los logits para aplicarlas in place. El grado de solapamiento depende de la implementación del serving y de la carga de trabajo.
- Caching. La mayor parte del trabajo de validación se realiza en tiempo de compilación. En runtime, el trabajo consiste principalmente en consultas a caché.
- Implementación en C++. El hot path está escrito en C++, no en Python, y la máscara se aplica in place a los logits.
Los benchmarks citados informan de un overhead bajo para las gramáticas, tokenizers, hardware y cargas de trabajo que probaron. Estos resultados no establecen ninguna garantía general de latencia o throughput.
Implementación práctica con vLLM
El proyecto sgr-discount-manager es una demo externa de carácter ilustrativo. Este artículo no fija su commit ni afirma que los fragmentos siguientes se hayan ejecutado en ese repositorio.
Estructura del proyecto
sgr/
├── agent.py # Main orchestration
├── models/
│ └── schemas.py # Pydantic SGR schemas
├── prompts/
│ ├── routing.py # Phase 1 prompts
│ └── pricing.py # Phase 3 prompts
├── store/
│ └── hybrid_store.py # Hot/Cold data retrieval
└── utils/
└── llm_client.py # LLM client wrapper with xgrammar
Paso 1: definir los esquemas
# sgr/models/schemas.py
from pydantic import BaseModel, Field
from typing import Literal, Union
# --- Phase 1: Routing (Union for branching) ---
class FeatureLookup(BaseModel):
"""Route to DB lookup if pricing context is needed."""
rationale: str
tool_name: Literal["fetch_user_features"] = "fetch_user_features"
user_id: str
class GeneralResponse(BaseModel):
"""Standard response for non-pricing queries."""
tool_name: Literal["respond"] = "respond"
content: str
class RouterSchema(BaseModel):
action: Union[FeatureLookup, GeneralResponse]
# --- Phase 2: Pricing Logic (Cascade for sequential reasoning) ---
class PricingLogic(BaseModel):
"""
Structured response for dynamic pricing. The fields record an intended analysis→decision flow.
"""
# 1. Data Analysis (Reflection)
churn_analysis: str = Field(...,
description="Analyze churn_probability (High > 0.7).")
financial_analysis: str = Field(...,
description="Analyze cart_value and profit_margin.")
# 2. Hard Math Enforcement
margin_math: str = Field(...,
description="Calculate absolute profit: 'Cart $200 * 0.20 Margin = $40'.")
# 3. Model-proposed decision; application code approves it.
max_discount_percent: float = Field(...,
description="Proposed discount percentage. Application code enforces policy.")
Paso 2: un cliente de LLM que activa XGrammar
# sgr/utils/llm_client.py
import json
from typing import TypeVar
from openai import OpenAI
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
class LLMClient:
"""Wrapper for vLLM with XGrammar-enforced structured generation."""
def __init__(self, base_url: str = "http://localhost:8000/v1"):
# Local vLLM commonly has no authentication. EMPTY is not authentication.
self.client = OpenAI(base_url=base_url, api_key="EMPTY")
self.model = self._get_available_model()
def _get_available_model(self) -> str:
"""Auto-detect the model running on vLLM server."""
try:
models = self.client.models.list()
if models.data:
return models.data[0].id
except Exception:
pass
return "Qwen/Qwen2.5-7B-Instruct"
def run_sgr(self, messages: list[dict], schema_class: type[T]) -> T:
"""Run inference with Schema-Guided Response constraints.
Uses vLLM structured outputs to constrain the JSON shape at generation time.
"""
schema_dict = schema_class.model_json_schema()
# Enhance system message with schema for model guidance
enhanced_messages = messages.copy()
if enhanced_messages and enhanced_messages[0]["role"] == "system":
schema_json = json.dumps(schema_dict, indent=2)
enhanced_messages[0] = {
"role": "system",
"content": (
enhanced_messages[0]["content"]
+ f"\n\nRespond with JSON matching this schema:\n{schema_json}"
),
}
# vLLM v0.12+ structured outputs. Configure the backend on the server.
completion = self.client.chat.completions.create(
model=self.model,
messages=enhanced_messages,
temperature=0.1, # Reduces sampling variation; it is not deterministic.
extra_body={"structured_outputs": {"json": schema_dict}},
)
raw_response = completion.choices[0].message.content
return schema_class.model_validate_json(raw_response)
[!NOTE] En las versiones actuales de vLLM,
structured_outputs: {"json": schema_dict}solicita JSON que coincida con el esquema. Configura el backend de structured output con la opción--structured-outputs-config.backenddel servidor cuando sea necesario. Se trata de una imposición por software en el servidor de inferencia, no de una imposición por hardware.
Paso 3: orquestar el agente
# sgr/agent.py
from decimal import Decimal
from .models.schemas import PricingLogic, RouterSchema
from .prompts.routing import build_routing_prompt
from .prompts.pricing import build_pricing_context_prompt, ASSISTANT_FETCH_MESSAGE
from .store.hybrid_store import HybridFeatureStore
from .utils.llm_client import LLMClient
def approve_discount(offer: PricingLogic, context: dict) -> Decimal:
"""Enforce the pricing policy independently of the model's explanation."""
cart_value = Decimal(str(context["current_cart_value"]))
margin = Decimal(str(context["cart_profit_margin"]))
proposed = Decimal(str(offer.max_discount_percent))
if cart_value <= 0 or not Decimal("0") <= margin <= Decimal("1"):
raise ValueError("Invalid pricing context")
gross_profit = cart_value * margin
discount_cost = cart_value * proposed / Decimal("100")
policy_cap = min(margin * Decimal("100"), Decimal("20"))
if not Decimal("0") <= proposed <= policy_cap or discount_cost > gross_profit:
raise ValueError("Proposed discount violates pricing policy")
return proposed.quantize(Decimal("0.01"))
def pricing_agent(user_query: str, user_id: str) -> str:
"""Process a pricing query with three-phase SGR workflow."""
llm = LLMClient()
feature_store = HybridFeatureStore()
# Build conversation history
history = [
{"role": "system", "content": build_routing_prompt(user_id)},
{"role": "user", "content": user_query},
]
# --- Phase 1: Routing (Uses RouterSchema) ---
print(f"🤖 Processing: '{user_query}' for {user_id}")
decision = llm.run_sgr(history, RouterSchema)
print(f"📍 Routing decision: {decision.action.tool_name}")
if decision.action.tool_name == "respond":
return decision.action.content
# --- Phase 2: Context Retrieval ---
if decision.action.tool_name == "fetch_user_features":
print(f"🔍 Fetching features for {user_id}...")
context = feature_store.get_user_context(user_id)
if not context:
return "Error: User profile not found."
print(f" [Data] LTV: ${context.get('user_ltv')} | "
f"Margin: {context.get('cart_profit_margin', 0) * 100}%")
# Inject context into conversation
history.append({"role": "assistant", "content": ASSISTANT_FETCH_MESSAGE})
history.append({
"role": "user",
"content": build_pricing_context_prompt(
churn_prob=context.get("churn_probability", 0.5),
cart_val=context.get("current_cart_value", 100),
margin=context.get("cart_profit_margin", 0.2),
user_ltv=context.get("user_ltv", 0),
),
})
# --- Phase 3: model proposal, then deterministic policy enforcement ---
print("🧠 Proposing Offer (Schema Enforced)...")
offer = llm.run_sgr(history, PricingLogic)
approved_discount = approve_discount(offer, context)
# Audit log: reasoning is inspectable; pricing is application-enforced.
print(f" [Audit] Math: {offer.margin_math}")
print(f" [Audit] Approved Discount: {approved_discount}%")
return (
"We value your loyalty! Here's a special "
f"{approved_discount}% discount with code SAVE{approved_discount:.0f}."
)
return "I'm sorry, I couldn't process your request."
if __name__ == "__main__":
response = pricing_agent("I want a discount or I'm leaving!", "user_102")
print(f"\n💬 Final Reply: {response}")
Paso 4: ejecutar vLLM con XGrammar
# Start vLLM server with XGrammar backend
vllm serve \
--model Qwen/Qwen2.5-7B-Instruct \
--port 8000 \
--structured-outputs-config.backend xgrammar
# Run the agent
uv run python -m sgr.agent
Salida ilustrativa
🤖 Processing: 'I want a discount or I'm leaving!' for user_102
📍 Routing decision: fetch_user_features
🔍 Fetching features for user_102...
[Data] LTV: $1,500 | Margin: 20%
🧠 Proposing Offer (Schema Enforced)...
[Audit] Math: Cart $200 * 0.20 Margin = $40
[Audit] Approved Discount: 15.00%
💬 Final Reply: We value your loyalty! Here's a special 15.00% discount
with code SAVE15.
Esta salida es ilustrativa. El esquema restringe la forma, pero la aplicación debe verificar que la aritmética y la política de descuentos sean correctas antes de utilizar la oferta.
Checklist de producción para el esquema, vLLM y SGR
Diseño del esquema
- Ordena los campos según el flujo de razonamiento previsto. Trata ese orden como documentación, salvo que llamadas separadas o comprobaciones de aplicación lo impongan.
- Escribe descripciones
Fielddescriptivas. Guían la atención del modelo tanto como el nombre del campo. - Restringe mediante
LiteralyAnnotated. UsaLiteral["a", "b"]para enums yAnnotated[int, Ge(1), Le(10)]para los límites. - Mantén los esquemas centrados. Un esquema por fase de razonamiento y, después, compón el flujo con varias llamadas.
Configuración de vLLM
- Usa una temperatura baja (0,1–0,3) para reducir la variación del muestreo. Para pruebas repetibles, fija el modelo y la configuración de serving, utiliza el modo determinista del servidor si ofrece uno y verifica el resultado.
- Deja que XGrammar gestione la estructura. No intentes imponerla mediante instrucciones de formato en el prompt.
- Mide el uso de tokens con el mismo prompt, modelo, esquema y tarea. SGR puede generar más o menos tokens que una respuesta CoT de texto libre.
Consideraciones de producción
- Versiona los esquemas del mismo modo que versionas las APIs.
- Incluso con SGR, los errores de red y del servidor siguen necesitando un tratamiento robusto.
- Registra las salidas SGR sin procesar para compliance y debugging; después, registra por separado la decisión determinista de la política.
- Recalcula los precios, límites, permisos y demás reglas semánticas en el código de la aplicación antes de utilizarlos; prueba los valores límite que el esquema por sí solo no puede rechazar.
Conclusión
Describes una topología de razonamiento en Pydantic y dejas que constrained decoding imponga la forma de la salida. El resultado es:
- sintácticamente válido por construcción, lo que elimina el agent loop de reintentar y volver a parsear
- auditable a nivel de campo
- utilizable con modelos más pequeños, porque ya no tienen que acertar por sí solos con el formato
- potencialmente más barato de ejecutar, si la validación reduce los reintentos o el esquema y el modelo elegidos utilizan menos tokens de salida
La demo sgr-discount-manager es un punto de partida externo. Comprueba sus dependencias fijadas y la compatibilidad actual con vLLM antes de considerarla una referencia ejecutable.
Ideas clave
- Schema-Guided Reasoning hace explícita una topología de razonamiento prevista, en lugar de depender únicamente de instrucciones en prosa.
- Constrained decoding impide generar JSON no válido, lo que resulta más limpio que validar y reintentar después.
- Coloca los campos de análisis antes que los campos de decisión cuando la respuesta deba documentar ese recorrido de razonamiento. Usa llamadas separadas o comprobaciones de aplicación cuando el orden deba imponerse.
- Usa SGR cuando el código posterior dependa de la estructura, no cuando el producto sea prosa libre.
Referencias
Framework de SGR
- Schema-Guided Reasoning (SGR) — framework original de Rinat Abdullin
- Patrones de SGR — patrones Cascade, Routing y Cycle
xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — Yixin Dong et al., arXiv:2411.15100 (artículo técnico con benchmarks)
- xgrammar en GitHub — biblioteca rápida y flexible para generación estructurada
- Documentación de xgrammar — documentación oficial con guía de inicio rápido
- Inicio rápido de xgrammar — primeros pasos con xgrammar
- Achieving Efficient Structured Generation with XGrammar — publicación del blog de MLC sobre los internals de xgrammar
vLLM
- vLLM Structured Outputs — documentación oficial
Proyecto de demo
- sgr-discount-manager — demo ilustrativa antigua; no contiene todos los ejemplos de código de este artículo