Raisonnement guidé par schéma : vLLM, XGrammar et Pydantic
Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.
Cet article s’adresse aux ingénieurs Python qui ont besoin de sorties de modèle validables par le code en aval. Vous apprendrez à définir un schéma Pydantic, à demander une sortie structurée à vLLM et à ajouter des contrôles applicatifs pour le sens et les règles métier.
Réessayer un appel à un LLM ne garantit pas l’obtention d’un JSON valide. L’échantillon suivant peut échouer de la même manière, et les appels répétés ajoutent de la latence et des coûts.
Le Schema-Guided Reasoning (SGR) impose un schéma pendant que le modèle génère chaque token. Vous définissez les champs requis avec Pydantic, et le moteur d’inférence bloque les tokens qui violeraient cette structure. Le résultat est syntaxiquement valide par construction, plutôt qu’à la suite de nouvelles tentatives.
En bref. Le SGR utilise le constrained decoding pour maintenir la sortie d’un LLM dans les limites d’un schéma Pydantic. vLLM peut utiliser XGrammar pour contraindre la structure générée. Validez les règles sémantiques dans le code applicatif.
Qu’est-ce que le Schema-Guided Reasoning ?
Le Schema-Guided Reasoning est une technique que Rinat Abdullin a décrite en juillet 2025. Au lieu de laisser le modèle compléter librement le texte — ce qui peut produire des réponses incohérentes ou ambiguës — vous lui fournissez un modèle strict qui définit :
- les étapes que la réponse doit représenter
- l’ordre prévu de ces étapes, afin qu’un reviewer puisse examiner le cheminement des données jusqu’à la décision
- les éléments auxquels le modèle doit accorder son attention
Considérez-le comme une checklist cognitive que le modèle doit suivre.
Ce que contrôle le schéma
Des champs tels que churn_analysis, margin_math et max_discount_percent rendent explicites les sorties intermédiaires attendues. Un schéma contraint la forme de la réponse. À lui seul, il ne rend pas un champ dépendant d’un autre et ne prouve pas que la décision est correcte.
Cela vous apporte :
- un raisonnement reproductible lors de plusieurs exécutions
- des sorties auditables dont chaque étape peut être inspectée
- des champs intermédiaires que vous pouvez évaluer par rapport à un dataset de test
- des modèles plus petits qui deviennent exploitables, puisque le schéma fournit la structure qu’ils devraient sinon apprendre
- Abdullin écrit qu’un gain de précision de 5 à 10 % n’est « pas inhabituel » dans les cas qu’il a observés ; il s’agit d’une observation de praticien, et non d’un résultat de benchmark : mesurez donc ce gain sur votre propre workload
SGR vs Chain of Thought vs prompt engineering
Ces trois approches diffèrent principalement par le degré de contrainte imposé au modèle.
| Fonctionnalité | Prompt engineering | Chain of Thought | Schema-Guided Reasoning |
|---|---|---|---|
| Structure de sortie | Texte variable | Prose libre | JSON/Pydantic rigide |
| Mécanisme de contrôle | Persuasion sémantique (« Veuillez produire du JSON ») | Prompt heuristique (« Réfléchissons étape par étape ») | Constrained decoding (fondé sur une grammaire) |
| Flux de raisonnement | Déterminé par le modèle | Déterminé par le modèle | Le développeur décrit une topologie attendue |
| Auditabilité | Faible (parsing requis) | Faible (lecture de la prose requise) | Élevée (inspection au niveau des champs) |
| Intégration | Difficile (parsing avec des regex) | Difficile (format variable) | Nécessite la prise en charge des schémas et la validation |
| Taux d’erreur | Élevé (variabilité du format) | Modéré (hallucination du format) | Les sorties invalides selon le schéma sont bloquées ; les erreurs sémantiques subsistent |
| Exigences concernant le modèle | Bonne capacité à suivre les instructions | Forte capacité de raisonnement | Fonctionne aussi avec des modèles plus petits |
Prompt engineering : persuasion sémantique
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!
Vous espérez que la compréhension qu’a le modèle de « produire du JSON » l’emporte sur sa tendance à converser. Une mise à jour du modèle, un changement de température ou un exemple few-shot différent peut casser votre parser.
Chain of Thought : une trace de raisonnement utile, mais le même problème de structure
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.
Le CoT peut améliorer la précision d’une tâche lorsque le prompt, le modèle, la tâche et l’évaluation s’y prêtent, mais le résultat reste une prose difficile à parser de manière fiable. Vous pourriez finir par effectuer un second appel à un LLM uniquement pour extraire des données structurées.
SGR : une chain of thought structurée
Le SGR peut rendre les champs intermédiaires disponibles pour inspection et évaluation. Son effet sur la précision de la tâche dépend du modèle, du prompt, de la tâche et de la manière dont ces champs sont utilisés ; le schéma ne fait que formaliser leur forme :
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
Le schéma décrit ces champs dans cet ordre. Un schéma d’objet unique ne crée pas d’étape de validation distincte entre eux. Utilisez des appels séparés ou des contrôles applicatifs lorsque les décisions ultérieures doivent dépendre des résultats précédents.
Patterns de SGR
Le SGR repose sur trois patterns fondamentaux qui peuvent être combinés pour former des workflows plus complexes.
1. Cascade : étapes de raisonnement séquentielles
Cascade représente un ordre de raisonnement dans une réponse structurée unique. Elle n’impose pas de transition d’état entre les champs.
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"]
Cas d’usage adaptés : évaluation de candidats, classification de documents, analyse de conformité, diagnostic médical.
Il est demandé au modèle de renvoyer brief_candidate_summary, rate_skill_match et final_recommendation dans cet ordre. Si cet ordre constitue une exigence de politique, imposez-le avec des appels séparés ou une logique applicative déterministe.
2. Routing : un switch statement sémantique
Routing oblige le modèle à choisir une voie parmi plusieurs options, au moyen de types 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]
Cas d’usage adaptés : classification d’intention, sélection d’outils, triage du support, dispatch multi-agent.
Les valeurs Literal propres à chaque branche aident la validation à distinguer les membres de l’union. Elles ne rendent pas le routing correct. Validez le résultat et effectuez le dispatch dans le code applicatif. Pour utiliser un discriminateur Pydantic explicite, configurez et testez une union discriminée.
3. Cycle : raisonnement répété avec des listes
Cycle force le modèle à produire plusieurs éléments, dans une plage de cardinalité donnée.
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)]
Cas d’usage adaptés : évaluation des risques, extraction de problèmes, appels d’outils parallèles, planification en plusieurs étapes.
Les bornes MinLen et MaxLen imposent au moins 2 et au plus 4 éléments. Combiné à Routing, ce pattern permet de dispatcher un batch de tool calls de largeur fixe.
Faire fonctionner le SGR : constrained decoding
Les patterns précédents ne sont que des schémas Pydantic. Ce qui les rend contraignants, c’est le constrained decoding, également appelé Structured Output.
Le constrained decoding modifie l’étape de génération des tokens. Au lieu de laisser le modèle échantillonner librement dans son vocabulaire, le moteur applique un masque de grammaire qui bloque les tokens susceptibles de violer le schéma. Cette opération a lieu dans le moteur d’inférence, et non dans votre code applicatif.
[!TIP] Le SGR ne nécessite pas de « modèles de raisonnement » comme o1 ou DeepSeek-R1. Il fonctionne très bien avec des modèles instruction-tuned, et particulièrement bien avec des modèles distillés à partir de modèles de raisonnement.
Fournisseurs cloud compatibles
La documentation des fournisseurs suivants annonçait la prise en charge des structured outputs lors de la mise à jour de cet article, le 2026-06-07. La prise en charge, les sous-ensembles de schémas, le niveau de strictness et le comportement en cas d’échec varient selon le modèle et l’endpoint :
| Fournisseur | Prise en charge |
|---|---|
| OpenAI | Structured Outputs (y compris Azure) |
| Google/Gemini | Prise en charge de JSON Schema depuis novembre 2025 (Pydantic et Zod) |
| Mistral | Custom Structured Output |
| Grok | Structured Outputs pour plusieurs modèles |
| Fireworks AI | JSON Schema |
| Cerebras | Structured Outputs |
| OpenRouter | Dépend du fournisseur downstream et de la route sélectionnés |
Moteurs d’inférence compatibles
Pour les modèles self-hosted, les principaux moteurs disposent tous d’un backend de constrained decoding :
| Moteur | Backend |
|---|---|
| vLLM | xgrammar ou guidance |
| SGLang | Outlines, XGrammar ou llguidance |
| TensorRT-LLM | GuidedDecoding |
| Ollama | Structured Outputs |
Pourquoi cet article se concentre sur vLLM et XGrammar
Quelques raisons :
- vLLM est l’un des moteurs d’inférence LLM open source les plus largement déployés ; ce que vous construisez ici se porte donc facilement ailleurs.
- XGrammar est implémenté en C++, et les benchmarks cités mesurent sa surcharge dans des conditions précises. Mesurez-la avec votre modèle, votre schéma, votre matériel et votre configuration de serving.
- L’API de vLLM est compatible avec OpenAI, ce qui réduit le coût d’une migration depuis des fournisseurs cloud.
- XGrammar gère les schémas imbriqués complexes, les unions et les structures récursives.
Comment XGrammar impose les schémas
Où le masquage a-t-il lieu ?
XGrammar modifie les logits de sortie après le forward pass du modèle et avant l’échantillonnage. Il ne modifie pas le modèle lui-même : il filtre les tokens qui peuvent être sélectionnés.
Une boucle d’inférence standard ressemble à ceci :
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 s’intercale entre les étapes 1 et 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
Le modèle calcule toujours l’intégralité de sa distribution de probabilités sur le GPU. Le GrammarMatcher de XGrammar génère le bitmask sur le CPU, puis le serving engine transfère ce bitmask vers le device des logits et l’applique in place avant l’échantillonnage. Pour des logits sur GPU, XGrammar utilise un GPU kernel pour cette application. Les tokens invalides voient leurs logits définis à -∞, ce qui rend leur probabilité exactement égale à 0 après le softmax.
Deux phases
XGrammar sépare le travail entre la compilation et le runtime. Cette conception réduit le travail répété sur la grammaire.
Phase 1 : compilation de la grammaire, une fois par schéma
# 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)
Lors de la compilation, XGrammar :
- convertit le JSON Schema en grammaire hors contexte ;
- construit un Pushdown Automaton (PDA), c’est-à-dire une machine à états dotée d’une pile, capable de gérer des structures imbriquées telles que
{"a": {"b": {"c": ...}}}; - précalcule les tokens valides à chaque position de la grammaire. Le résultat est le « adaptive token mask cache » ;
- classe les tokens comme « indépendants du contexte » (mis en cache) ou « dépendants du contexte » (à vérifier au runtime selon l’état de la pile).
[!NOTE] L’article consacré à XGrammar indique qu’environ 99 % des tokens étaient indépendants du contexte dans ses mesures (article). Considérez ce chiffre comme propre au benchmark, et non comme un ratio universel.
Phase 2 : génération du masque au runtime, à chaque token
À chaque étape de génération :
- Le
GrammarMatchersuit la position courante dans la grammaire. - Il consulte le masque précalculé pour les tokens indépendants du contexte.
- Il exécute le PDA pour vérifier les tokens restants, dépendants du contexte.
- Il les combine en un bitmask final, le transfère vers le device des logits et l’y applique.
Pourquoi utiliser des automates à pile plutôt que des regex ?
À cause de l’imbrication. Une expression régulière — une machine à états finis — ne peut pas reconnaître de manière fiable des structures telles que :
{ "user": { "profile": { "settings": { "theme": "dark" } } } }
La difficulté réside dans les accolades fermantes }}} : il faut mémoriser le nombre d’accolades ouvrantes. Un Pushdown Automaton possède une pile qui suit ce nombre et peut donc gérer une profondeur d’imbrication arbitraire. C’est également pourquoi XGrammar peut imposer des types Union, des objets imbriqués et des schémas récursifs, là où les approches fondées sur des regex atteignent leurs limites.
Exemple concret : générer un champ float
Lorsque le modèle génère "max_discount_percent":, XGrammar sait, grâce au schéma, qu’un float doit suivre. L’ensemble des tokens valides dépend de l’état du parser et du tokenizer. Au début du nombre, le masque peut autoriser un chiffre ou un signe moins ; après un chiffre, il peut autoriser des continuations telles qu’un autre chiffre, un point décimal ou un marqueur d’exposant.
- Une quote,
{,[,true,falseounullne peut pas commencer ce nombre ; le masque bloque donc leurs tokens dans cet état. - Le forward pass peut avoir attribué une probabilité élevée au token correspondant à
"fifteen". Comme ce token ne peut pas poursuivre ce champ numérique, le masque le supprime et le modèle doit choisir une continuation numérique valide.
Ce qui influence la surcharge
Trois raisons principales :
- Génération et transfert du masque. XGrammar génère les masques sur le CPU, puis le serving engine les transfère vers le device des logits pour les appliquer in place. Le degré de chevauchement dépend de l’implémentation du serving et du workload.
- Mise en cache. La majeure partie du travail de validation est effectuée à la compilation. Le runtime consiste principalement en des recherches dans le cache.
- Implémentation en C++. Le hot path est en C++, et non en Python ; le masque est appliqué in place aux logits.
Les benchmarks cités rapportent une faible surcharge pour les grammaires, tokenizers, matériels et workloads testés. Ces résultats ne constituent pas une garantie générale de latence ou de throughput.
Implémentation pratique avec vLLM
Le projet sgr-discount-manager est une démo externe à vocation illustrative. Cet article ne verrouille pas son commit et ne prétend pas que les snippets ci-dessous ont été exécutés dans ce repository.
Structure du projet
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
Étape 1 : définir les schémas
# 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.")
Étape 2 : un client LLM qui active 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] Dans les versions actuelles de vLLM,
structured_outputs: {"json": schema_dict}demande un JSON conforme au schéma. Configurez le backend de structured output avec l’option--structured-outputs-config.backenddu serveur lorsque cela est nécessaire. Il s’agit d’une enforcement logicielle dans le serveur d’inférence, et non d’une enforcement matérielle.
Étape 3 : orchestrer l’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}")
Étape 4 : exécuter vLLM avec 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
Sortie illustrative
🤖 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.
Cette sortie est illustrative. Le schéma contraint la forme, mais l’application doit vérifier que les calculs et la politique de remise sont corrects avant d’utiliser l’offre.
Checklist de production pour les schémas, vLLM et le serving
Conception du schéma
- Ordonnez les champs selon le flux de raisonnement attendu. Considérez cet ordre comme de la documentation, sauf si des appels séparés ou des contrôles applicatifs l’imposent.
- Rédigez des descriptions
Fielddétaillées. Elles orientent l’attention du modèle autant que le nom du champ. - Ajoutez des contraintes avec
LiteraletAnnotated. UtilisezLiteral["a", "b"]pour les enums etAnnotated[int, Ge(1), Le(10)]pour les bornes. - Gardez des schémas ciblés. Un schéma par phase de raisonnement, puis composez-les avec plusieurs appels.
Configuration de vLLM
- Utilisez une température basse (0,1–0,3) pour réduire la variation de l’échantillonnage. Pour des tests répétables, fixez le modèle et la configuration de serving, utilisez le mode déterministe du serveur s’il en fournit un, puis vérifiez le résultat.
- Laissez XGrammar gérer la structure. N’essayez pas de le contourner avec des instructions de formatage dans le prompt.
- Mesurez l’utilisation des tokens avec le même prompt, le même modèle, le même schéma et la même tâche. Le SGR peut générer plus ou moins de tokens qu’une réponse CoT en prose libre.
Considérations de production
- Versionnez vos schémas de la même manière que vos APIs.
- Même avec le SGR, les erreurs réseau et serveur doivent être gérées proprement.
- Journalisez les sorties SGR brutes pour la conformité et le debugging, puis journalisez séparément la décision de politique déterministe.
- Recalculez les prix, limites, permissions et autres règles sémantiques dans le code applicatif avant utilisation ; testez les valeurs limites que le schéma seul ne peut pas rejeter.
Conclusion
Vous décrivez une topologie de raisonnement en Pydantic et laissez le constrained decoding imposer la forme de la sortie. Le résultat est :
- syntaxiquement valide par construction, ce qui supprime la boucle retry-and-reparse
- auditable au niveau des champs
- exploitable avec des modèles plus petits, puisqu’ils n’ont plus à réussir seuls la mise en forme
- potentiellement moins coûteux à exécuter, si la validation réduit les retries ou si le schéma et le modèle choisis utilisent moins de tokens de sortie
La démo sgr-discount-manager constitue un point de départ externe. Vérifiez ses dépendances verrouillées et sa compatibilité actuelle avec vLLM avant de la considérer comme une référence exécutable.
Points clés
- Le Schema-Guided Reasoning rend explicite une topologie de raisonnement attendue au lieu de reposer uniquement sur des instructions en prose.
- Le constrained decoding empêche la génération de JSON invalide, ce qui est plus propre que de valider puis de réessayer après coup.
- Placez les champs d’analyse avant les champs de décision lorsque la réponse doit documenter ce cheminement. Utilisez des appels séparés ou des contrôles applicatifs lorsque l’ordre doit être imposé.
- Utilisez le SGR lorsque le code en aval dépend de la structure, et non lorsque le produit attendu est une prose libre.
Références
Framework SGR
- Schema-Guided Reasoning (SGR) — framework original de Rinat Abdullin
- Patterns SGR — patterns Cascade, Routing et Cycle
xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — Yixin Dong et al., arXiv:2411.15100 (article technique avec benchmarks)
- xgrammar GitHub — bibliothèque de génération structurée rapide et flexible
- xgrammar Documentation — documentation officielle avec guide de démarrage rapide
- xgrammar Quick Start — prise en main de xgrammar
- Achieving Efficient Structured Generation with XGrammar — article du blog MLC sur les composants internes de xgrammar
vLLM
- vLLM Structured Outputs — documentation officielle
Projet de démonstration
- sgr-discount-manager — ancienne démo illustrative ; elle ne contient pas tous les exemples de code de cet article