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.

Descripción general de SGRDescripción general de SGR

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.

Comparativa de SGRComparativa de SGR

CaracterísticaPrompt EngineeringChain of ThoughtSchema-Guided Reasoning
Estructura de salidaTexto variableProsa libreJSON/Pydantic rígido
Mecanismo de controlPersuasión semántica («Please output JSON»)Prompting heurístico («Let’s think step by step»)Constrained decoding (basado en gramática)
Flujo de razonamientoLo determina el modeloLo determina el modeloEl desarrollador describe una topología prevista
AuditabilidadBaja (requiere parsing)Baja (requiere leer la prosa)Alta (inspección a nivel de campo)
IntegraciónDifícil (parsing con regex)Difícil (formato variable)Requiere compatibilidad con esquemas y validación
Tasa de erroresAlta (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 modeloSeguir instrucciones de forma sólidaGran capacidad de razonamientoTambié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.

Patrones de SGRPatrones de SGR

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:

ProveedorCompatibilidad
OpenAIStructured Outputs (incluido Azure)
Google/GeminiCompatibilidad con JSON Schema desde noviembre de 2025 (Pydantic y Zod)
MistralCustom Structured Output
GrokStructured Outputs para varios modelos
Fireworks AIJSON Schema
CerebrasStructured Outputs
OpenRouterDepende 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:

MotorBackend
vLLMxgrammar o guidance
SGLangOutlines, XGrammar o llguidance
TensorRT-LLMGuidedDecoding
OllamaStructured 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

Imposición de esquemas con xgrammarImposición de esquemas con 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:

  1. Convierte el JSON Schema en una Context-Free Grammar.
  2. Construye un Pushdown Automaton (PDA), una máquina de estados con una pila que puede gestionar estructuras anidadas como {"a": {"b": {"c": ...}}}.
  3. Precalcula qué tokens son válidos en cada posición de la gramática. El resultado es la «adaptive token mask cache».
  4. 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:

  1. El GrammarMatcher realiza el seguimiento de la posición actual en la gramática.
  2. Consulta la máscara precalculada para los tokens context-independent.
  3. Ejecuta el PDA para comprobar los tokens context-dependent restantes.
  4. 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, false o null no 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:

  1. 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.
  2. 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é.
  3. 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.

Workflow de agenteWorkflow de agente

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.backend del 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

  1. 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.
  2. Escribe descripciones Field descriptivas. Guían la atención del modelo tanto como el nombre del campo.
  3. Restringe mediante Literal y Annotated. Usa Literal["a", "b"] para enums y Annotated[int, Ge(1), Le(10)] para los límites.
  4. 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

  1. 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.
  2. Deja que XGrammar gestione la estructura. No intentes imponerla mediante instrucciones de formato en el prompt.
  3. 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

  1. Versiona los esquemas del mismo modo que versionas las APIs.
  2. Incluso con SGR, los errores de red y del servidor siguen necesitando un tratamiento robusto.
  3. Registra las salidas SGR sin procesar para compliance y debugging; después, registra por separado la decisión determinista de la política.
  4. 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

  1. Schema-Guided Reasoning hace explícita una topología de razonamiento prevista, en lugar de depender únicamente de instrucciones en prosa.
  2. Constrained decoding impide generar JSON no válido, lo que resulta más limpio que validar y reintentar después.
  3. 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.
  4. Usa SGR cuando el código posterior dependa de la estructura, no cuando el producto sea prosa libre.

Referencias

Framework de SGR

xgrammar

vLLM

Proyecto de demo

  • sgr-discount-manager — demo ilustrativa antigua; no contiene todos los ejemplos de código de este artículo