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.

Vue d’ensemble du SGRVue d’ensemble du SGR

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.

Comparaison du SGRComparaison du SGR

FonctionnalitéPrompt engineeringChain of ThoughtSchema-Guided Reasoning
Structure de sortieTexte variableProse libreJSON/Pydantic rigide
Mécanisme de contrôlePersuasion sémantique (« Veuillez produire du JSON »)Prompt heuristique (« Réfléchissons étape par étape »)Constrained decoding (fondé sur une grammaire)
Flux de raisonnementDéterminé par le modèleDéterminé par le modèleLe 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égrationDifficile (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èleBonne capacité à suivre les instructionsForte capacité de raisonnementFonctionne 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.

Patterns de SGRPatterns de SGR

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 :

FournisseurPrise en charge
OpenAIStructured Outputs (y compris Azure)
Google/GeminiPrise en charge de JSON Schema depuis novembre 2025 (Pydantic et Zod)
MistralCustom Structured Output
GrokStructured Outputs pour plusieurs modèles
Fireworks AIJSON Schema
CerebrasStructured Outputs
OpenRouterDé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 :

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

Imposition par xgrammarImposition par xgrammar

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 :

  1. convertit le JSON Schema en grammaire hors contexte ;
  2. 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": ...}}} ;
  3. précalcule les tokens valides à chaque position de la grammaire. Le résultat est le « adaptive token mask cache » ;
  4. 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 :

  1. Le GrammarMatcher suit la position courante dans la grammaire.
  2. Il consulte le masque précalculé pour les tokens indépendants du contexte.
  3. Il exécute le PDA pour vérifier les tokens restants, dépendants du contexte.
  4. 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, false ou null ne 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 :

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

Workflow d’agentWorkflow d’agent

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

  1. 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.
  2. Rédigez des descriptions Field détaillées. Elles orientent l’attention du modèle autant que le nom du champ.
  3. Ajoutez des contraintes avec Literal et Annotated. Utilisez Literal["a", "b"] pour les enums et Annotated[int, Ge(1), Le(10)] pour les bornes.
  4. Gardez des schémas ciblés. Un schéma par phase de raisonnement, puis composez-les avec plusieurs appels.

Configuration de vLLM

  1. 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.
  2. Laissez XGrammar gérer la structure. N’essayez pas de le contourner avec des instructions de formatage dans le prompt.
  3. 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

  1. Versionnez vos schémas de la même manière que vos APIs.
  2. Même avec le SGR, les erreurs réseau et serveur doivent être gérées proprement.
  3. Journalisez les sorties SGR brutes pour la conformité et le debugging, puis journalisez séparément la décision de politique déterministe.
  4. 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

  1. Le Schema-Guided Reasoning rend explicite une topologie de raisonnement attendue au lieu de reposer uniquement sur des instructions en prose.
  2. 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.
  3. 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é.
  4. 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

xgrammar

vLLM

Projet de démonstration

  • sgr-discount-manager — ancienne démo illustrative ; elle ne contient pas tous les exemples de code de cet article