Schema-Guided Reasoning: vLLM, XGrammar en Pydantic
Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Dit artikel is bedoeld voor Python-engineers die modeloutput nodig hebben die door downstream-code kan worden gevalideerd. Je leert hoe je een Pydantic-schema definieert, structured output opvraagt bij vLLM en application checks toevoegt voor betekenis en beleid.
Opnieuw een LLM-call uitvoeren garandeert geen geldige JSON. Het volgende sample kan op dezelfde manier falen, terwijl herhaalde calls extra latency en kosten veroorzaken.
Schema-Guided Reasoning (SGR) dwingt een schema af terwijl het model elke token genereert. Je definieert de vereiste velden met Pydantic en de inference engine blokkeert tokens die deze structuur zouden schenden. Het resultaat is syntactisch geldig by construction, in plaats van dankzij retries.
TL;DR. SGR gebruikt constrained decoding om de output van een LLM binnen een Pydantic-schema te houden. vLLM kan XGrammar gebruiken om de gegenereerde structuur te beperken. Valideer semantic rules in application code.
Wat is Schema-Guided Reasoning?
Schema-Guided Reasoning is een techniek die Rinat Abdullin in juli 2025 beschreef. In plaats van het model tekst vrij te laten aanvullen — wat inconsistent of ambigu kan zijn — geef je het een strikt template dat definieert:
- welke stappen de response moet representeren
- in welke volgorde die stappen bedoeld zijn, zodat een reviewer het pad van data naar beslissing kan inspecteren
- waar het model de aandacht op moet richten
Zie het als een cognitieve checklist die het model moet volgen.
Wat het schema beheert
Velden zoals churn_analysis, margin_math en max_discount_percent maken de bedoelde intermediate outputs expliciet. Een schema beperkt de vorm van de response. Op zichzelf zorgt het er niet voor dat het ene veld afhankelijk is van het andere en bewijst het evenmin dat de beslissing correct is.
Dat levert het volgende op:
- reproduceerbare reasoning bij herhaalde runs
- auditable outputs waarin elke stap inspecteerbaar is
- intermediate fields die je kunt beoordelen tegen een testdataset
- kleinere models die bruikbaar worden, omdat het schema de structuur levert die het model anders zou moeten leren
- Abdullin schrijft dat een accuracy-boost van 5–10% in de gevallen die hij heeft gezien “niet ongebruikelijk” is; dit is een practitioner-observatie, geen benchmarkresultaat, dus meet het op je eigen workload
SGR versus Chain of Thought versus prompt engineering
De drie benaderingen verschillen vooral in de mate waarin ze het model beperken.
| Feature | Prompt Engineering | Chain of Thought | Schema-Guided Reasoning |
|---|---|---|---|
| Output Structure | Variabele tekst | Vrije proza | Rigide JSON/Pydantic |
| Control Mechanism | Semantische overtuiging (“Please output JSON”) | Heuristic prompting (“Let’s think step by step”) | Constrained decoding (op grammar gebaseerd) |
| Reasoning Flow | Wordt door het model bepaald | Wordt door het model bepaald | Developer beschrijft een bedoelde topology |
| Auditability | Laag (parsing vereist) | Laag (proza moet worden gelezen) | Hoog (inspectie op field-niveau) |
| Integration | Moeilijk (regex parsing) | Moeilijk (variabel format) | Schema support en validation vereist |
| Error Rate | Hoog (formatvariatie) | Gemiddeld (hallucinatie van het format) | Schema-invalid output wordt geblokkeerd; semantic errors blijven bestaan |
| Model Requirement | Sterke instruction following | Sterke reasoning-capability | Werkt ook met kleinere models |
Prompt engineering: semantische overtuiging
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!
Je hoopt dat het begrip van het model van “output JSON” zwaarder weegt dan zijn neiging om conversationeel te antwoorden. Een modelupdate, een wijziging van de temperature of een ander few-shot-example kan je parser breken.
Chain of Thought: bruikbare reasoning trace, hetzelfde structurele probleem
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 kan de task accuracy verbeteren wanneer de prompt, het model, de task en de evaluatie dit ondersteunen, maar het resultaat blijft proza dat moeilijk betrouwbaar te parsen is. Mogelijk eindig je met een tweede LLM-call, uitsluitend om structured data te extraheren.
SGR: structured chain of thought
SGR kan intermediate fields beschikbaar maken voor inspectie en evaluatie. Of dit de task accuracy verbetert, hangt af van het model, de prompt, de task en de manier waarop die velden worden gebruikt; het schema formaliseert alleen hun vorm:
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
Het schema beschrijft deze velden in die volgorde. Een enkel objectschema creëert geen afzonderlijke validation step ertussen. Gebruik separate calls of application checks wanneer latere beslissingen afhankelijk moeten zijn van eerdere resultaten.
SGR-patterns
SGR heeft drie core patterns die je kunt combineren tot grotere workflows.
1. Cascade: sequentiële reasoning-stappen
Cascade representeert een reasoning-volgorde in één structured response. Het dwingt geen state transition tussen velden af.
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"]
Goede toepassingen: candidate evaluation, document classification, compliance analysis en medical diagnosis.
Het model wordt gevraagd om brief_candidate_summary, rate_skill_match en final_recommendation in die volgorde terug te geven. Als de volgorde een policy requirement is, dwing die dan af met separate calls of deterministic application logic.
2. Routing: een semantic switch statement
Routing laat het model één path uit een verzameling opties kiezen, geïmplementeerd met Union-types.
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]
Goede toepassingen: intent classification, tool selection, support triage en multi-agent dispatch.
De branch-specifieke Literal-values helpen validation om de union members van elkaar te onderscheiden. Ze maken routing niet correct. Valideer het resultaat en dispatch het in application code. Configureer en test bij een expliciete Pydantic discriminator een discriminated union.
3. Cycle: herhaalde reasoning met lists
Cycle dwingt het model meerdere items te genereren, met grenzen voor het aantal items.
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)]
Goede toepassingen: risk assessment, issue extraction, parallel tool calls en multi-step planning.
De grenzen MinLen en MaxLen dwingen minimaal 2 en maximaal 4 items af. In combinatie met Routing is dit de manier om een batch tool calls met vaste breedte te dispatchen.
SGR werkend krijgen: constrained decoding
De bovenstaande patterns zijn slechts Pydantic-schema’s. Wat ze bindend maakt, is constrained decoding (ook Structured Output genoemd).
Constrained decoding past de token-generation step aan. In plaats van het model vrij uit zijn vocabulary te laten samplen, past de engine een grammar mask toe dat tokens blokkeert die het schema zouden schenden. Dit gebeurt in de inference engine, niet in je application code.
[!TIP] SGR vereist geen “reasoning models” zoals o1 of DeepSeek-R1. Het werkt prima met instruction-tuned models en vooral goed met models die zijn distilled uit reasoning models.
Cloudproviders die dit ondersteunen
De volgende provider-documentatie vermeldde structured-output support toen dit artikel op 2026-06-07 werd bijgewerkt. Support, schema-subsets, strictness en failure behavior verschillen per model en endpoint:
| Provider | Support |
|---|---|
| OpenAI | Structured Outputs (inclusief Azure) |
| Google/Gemini | JSON Schema-support sinds november 2025 (Pydantic en Zod) |
| Mistral | Custom Structured Output |
| Grok | Structured Outputs voor meerdere models |
| Fireworks AI | JSON Schema |
| Cerebras | Structured Outputs |
| OpenRouter | Hangt af van de geselecteerde downstream provider en route |
Inference engines die dit ondersteunen
Voor self-hosted models hebben de belangrijkste engines allemaal een constrained-decoding-backend:
| Engine | Backend |
|---|---|
| vLLM | xgrammar of guidance |
| SGLang | Outlines, XGrammar of llguidance |
| TensorRT-LLM | GuidedDecoding |
| Ollama | Structured Outputs |
Waarom dit artikel zich richt op vLLM en XGrammar
Daar zijn enkele redenen voor:
- vLLM is een van de meest gebruikte open-source LLM-inference engines, waardoor wat je hier bouwt eenvoudig te porten is.
- XGrammar is geïmplementeerd in C++ en de aangehaalde benchmarks meten de overhead onder specifieke omstandigheden. Meet die met jouw model, schema, hardware en serving-configuratie.
- De API van vLLM is OpenAI-compatible, waardoor migratie vanaf cloudproviders goedkoop blijft.
- XGrammar ondersteunt complexe nested schemas, unions en recursive structures.
Hoe XGrammar schema’s afdwingt
Waar de masking plaatsvindt
XGrammar past de output logits aan na de forward pass van het model en vóór sampling. Het verandert het model zelf niet. Het filtert welke tokens kunnen worden geselecteerd.
Een standaard inference loop ziet er als volgt uit:
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 voegt zich tussen stap 1 en 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
Het model berekent nog steeds zijn volledige probability distribution op de GPU. XGrammar’s GrammarMatcher genereert het bitmask op de CPU; daarna verplaatst de serving engine dat bitmask naar het device van de logits en past het daar in place toe vóór sampling. Voor GPU-logits gebruikt XGrammar een GPU-kernel voor deze toepassing. Ongeldige tokens krijgen als logits -∞, waardoor hun probability na softmax exact 0 is.
Twee phases
XGrammar splitst het werk op in compile-time en runtime. Dit design vermindert herhaald grammar-werk.
Phase 1: grammar compilation, eenmaal per schema
# 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)
Tijdens compilation doet XGrammar het volgende:
- Zet het JSON Schema om naar een Context-Free Grammar.
- Bouwt een Pushdown Automaton (PDA), een state machine met een stack waarmee nested structures zoals
{"a": {"b": {"c": ...}}}kunnen worden verwerkt. - Berekent vooraf welke tokens op elke grammar-positie geldig zijn. Het resultaat is de “adaptive token mask cache”.
- Categoriseert tokens als “context-independent” (cacheable) of “context-dependent” (moeten tijdens runtime worden gecontroleerd tegen de stack state).
[!NOTE] Het XGrammar-paper rapporteert dat in zijn metingen ongeveer 99% van de tokens context-independent was (paper). Beschouw dit getal als benchmark-specifiek, niet als een universele ratio.
Phase 2: runtime mask generation, bij elke token
Bij elke generation step:
- De
GrammarMatcherhoudt de huidige positie in de grammar bij. - Het zoekt het vooraf berekende mask op voor context-independent tokens.
- Het voert de PDA uit om de resterende context-dependent tokens te controleren.
- Het combineert deze tot een final bitmask, verplaatst dat naar het device van de logits en past het daar toe.
Waarom pushdown automata en geen regex?
Vanwege nesting. Een regular expression (een finite state machine) kan structures zoals deze niet betrouwbaar matchen:
{ "user": { "profile": { "settings": { "theme": "dark" } } } }
Het lastige deel zijn de sluitende accolades }}}: je moet onthouden hoeveel accolades je hebt geopend. Een Pushdown Automaton heeft hiervoor een stack, zodat het arbitrary nesting depth kan verwerken. Daarom kan XGrammar ook Union-types, nested objects en recursive schemas afdwingen, terwijl regex-gebaseerde benaderingen tekortschieten.
Een concreet voorbeeld: een float-veld genereren
Wanneer het model "max_discount_percent": genereert, weet XGrammar uit het schema dat er daarna een float komt. De geldige token-set hangt af van de parser state en tokenizer. Aan het begin van het getal kan het mask een digit of minteken toelaten; na een digit kan het continuations toelaten zoals nog een digit, een decimal point of een exponent marker.
- Een quote,
{,[,true,falseofnullkan dit getal niet beginnen, dus het mask blokkeert hun tokens in die state. - De forward pass kan een hoge probability hebben toegekend aan de token voor
"fifteen". Omdat die token dit numeric field niet kan voortzetten, verwijdert het mask de token en moet het model een geldige numeric continuation kiezen.
Wat de overhead beïnvloedt
Drie redenen:
- Mask generation en transfer. XGrammar genereert masks op de CPU en de serving engine verplaatst ze naar het device van de logits om ze in place toe te passen. De mate van overlap hangt af van de serving implementation en workload.
- Caching. Het meeste validity-werk gebeurt tijdens compile-time. Runtime bestaat voornamelijk uit cache lookups.
- C++-implementation. Het hot path is C++, niet Python, en het mask wordt in place op de logits toegepast.
De aangehaalde benchmarks rapporteren lage overhead voor hun geteste grammars, tokenizers, hardware en workloads. Deze resultaten vormen geen algemene garantie voor latency of throughput.
Praktische implementatie met vLLM
Het project sgr-discount-manager is een illustratieve externe demo. Dit artikel pint geen commit en claimt niet dat de onderstaande snippets in deze repository zijn uitgevoerd.
Projectstructuur
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
Stap 1: definieer de schema’s
# 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.")
Stap 2: een LLM-client die XGrammar inschakelt
# 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] In recente vLLM-versies vraagt
structured_outputs: {"json": schema_dict}om JSON die overeenkomt met het schema. Configureer de structured-output-backend zo nodig met de--structured-outputs-config.backend-optie van de server. Dit is software enforcement in de inference server, geen hardware enforcement.
Stap 3: orkestreer de agent
# 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}")
Stap 4: voer vLLM uit met 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
Illustratieve output
🤖 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.
Deze output is illustratief. Het schema beperkt de vorm, maar de application moet controleren of de arithmetic en discount policy correct zijn voordat de offer wordt gebruikt.
Schema, vLLM en production-checklist
Schema design
- Orden velden volgens de bedoelde reasoning flow. Beschouw die volgorde als documentatie, tenzij separate calls of application checks haar afdwingen.
- Schrijf beschrijvende
Field-descriptions. Ze sturen de aandacht van het model net zo sterk als de field name. - Beperk waarden met
LiteralenAnnotated. GebruikLiteral["a", "b"]voor enums enAnnotated[int, Ge(1), Le(10)]voor bounds. - Houd schema’s focused. Gebruik één schema per reasoning phase en composeer daarna met meerdere calls.
vLLM-configuratie
- Gebruik een lage temperature (0.1–0.3) om sampling variation te beperken. Gebruik voor repeatable tests hetzelfde model en dezelfde serving-configuratie, activeer de deterministic mode van de server als die beschikbaar is en verifieer het resultaat.
- Laat XGrammar de structuur afhandelen. Probeer het niet te overrulen met formatting instructions in de prompt.
- Meet tokengebruik met dezelfde prompt, hetzelfde model, hetzelfde schema en dezelfde task. SGR kan meer of minder tokens genereren dan een free-form CoT-response.
Production considerations
- Versioneer je schema’s op dezelfde manier als je APIs.
- Ook met SGR moeten network- en server-errors graceful worden afgehandeld.
- Log raw SGR-outputs voor compliance en debugging en log de deterministic policy decision afzonderlijk.
- Bereken prices, limits, permissions en andere semantic rules opnieuw in application code voordat je ze gebruikt; test boundary values die het schema alleen niet kan afwijzen.
Conclusie
Je beschrijft een reasoning topology in Pydantic en laat constrained decoding de output shape afdwingen. Het resultaat is:
- syntactisch geldig by construction, waardoor de retry-and-reparse-loop verdwijnt
- auditable op field-niveau
- bruikbaar met kleinere models, omdat ze het format niet meer volledig zelfstandig hoeven te beheersen
- mogelijk goedkoper om uit te voeren, als validation retries vermindert of het gekozen schema en model minder output tokens gebruiken
De demo sgr-discount-manager is een extern startpunt. Controleer de pinned dependencies en de actuele vLLM-compatibility voordat je deze als runnable reference gebruikt.
Belangrijkste punten
- Schema-Guided Reasoning maakt een bedoelde reasoning topology expliciet, in plaats van uitsluitend op prose-instructies te vertrouwen.
- Constrained decoding voorkomt ongeldige JSON tijdens generation, wat cleaner is dan achteraf valideren en retrien.
- Zet analysis fields vóór decision fields wanneer de response die reasoning path moet documenteren. Gebruik separate calls of application checks wanneer de volgorde moet worden afgedwongen.
- Gebruik SGR wanneer downstream-code afhankelijk is van structuur, niet wanneer free-form prose het product is.
Referenties
SGR Framework
- Schema-Guided Reasoning (SGR) — het oorspronkelijke framework van Rinat Abdullin
- SGR Patterns — de patterns Cascade, Routing en Cycle
xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — Yixin Dong et al., arXiv:2411.15100 (technical paper met benchmarks)
- xgrammar GitHub — snelle, flexibele library voor structured generation
- xgrammar Documentation — officiële docs met quick-start guide
- xgrammar Quick Start — aan de slag met xgrammar
- Achieving Efficient Structured Generation with XGrammar — MLC-blogpost over de internals van xgrammar
vLLM
- vLLM Structured Outputs — officiële documentatie
Demo Project
- sgr-discount-manager — oudere illustratieve demo; deze bevat niet elk codevoorbeeld uit dit artikel