[!NOTE] Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Schema-gestuurde Reasoning op vLLM: Structured Outputs met xgrammar en Pydantic

Het opnieuw uitvoeren van een LLM-aanroep garandeert geen geldige JSON. Het volgende voorbeeld kan op dezelfde manier mislukken, en herhaalde aanroepen leiden tot meer latency en extra kosten.

Schema-gestuurde Reasoning (SGR) zorgt ervoor dat een schema wordt nageleefd, terwijl de model elke token genereert. Je definieert de vereiste velden met Pydantic, en de inference-engine blokkeert tokens-invoer die deze structuur schendt. Het resultaat is van nature syntactisch geldig, zonder dat er opnieuw geprobeerd hoeft te worden.

Kort samengevat. SGR maakt gebruik van constrained decoding om de uitvoer van een LLM vast te leggen aan een door jou beheerd Pydantic-schema. In combinatie met het xgrammar backend van vLLM krijg je op deze manier steeds geldige JSON-objecten, met een verwaarloosbaar latency-overhead.


Wat is Schema-Guided Reasoning?

Schema-gestuurde Reasoning is een techniek die Rinat Abdullin geschreven in 2024. In plaats van model toe te staan tekst vrijelijk te voltooien (wat onconsistent of ambigu kan zijn), geeft u het een strikt sjabloon dat het volgende definiert:

Zie het maar als een cognitieve controlelijst die de model moet volgen.

SGR Overzicht

Wat het schema beheerst

Velden zoals churn_analysis, margin_math, en max_discount_percent Zorg ervoor dat de verwachte tussentijdse uitvoer duidelijk wordt weergegeven. De model moet de vereiste structuur hebben gevuld voordat het een definitieve beslissing over de korting kan nemen.

Dat geeft u:


SGR versus Chain of Thought versus prompt engineering

De drie benaderingen verschillen voornamelijk in de mate waarin ze model beperken.

SGR Vergelijking

FunctiePrompt EngineeringChain of ThoughtSchema-gestuurde Reasoning
UitvoerstructuurVariabele tekstVrije tekstvormStijve JSON/Pydantic
BeheermechanismeSemantische overtuiging (“Geef alstublieft JSON uit”)Heuristisch prompting (“Laten we stap voor stap nadenken”)Constrained decoding (op grammatica gebaseerd)
Reasoning StroomModel bepaaltModel bepaaltDe ontwikkelaar bepaalt de (schema-topologie).
AuditeerbaarheidLaag (vereist parsing)Laag (vereist lezen van de tekst)Hoog (inspectie op veldniveau)
IntegratieMoeilijk (regex-parsing)Moeilijk (variabele vorm)Triviaal (deserialisatie van native objecten)
FoutkansHoog (variatie in format)Modereren (hallucination van het formaat)
Model VereisteSterke instructievolggingSterke reasoning-functionaliteitWerkt 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 model voor “output JSON” sterker is dan zijn neiging om op een conversatieve manier te reageren. Een update van model, een verandering in de temperature‑waarde, of een ander paar-shot voorbeeld kan je parser beschadigen.

Denkketen: betere reasoning, dezelfde structuurprobleem

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 verbetert de nauwkeurigheid van reasoning, maar verslechtert daardoor de structuur. Het resultaat is onvoorspelbaar proza dat bijna onmogelijk betrouwbaar te parseren is. Meestal moet men een tweede aanroep van LLM uitvoeren om de gestructureerde gegevens te extraheren.

SGR: gestructureerde denkketen

SGR behoudt de intuïtie van CoT dat tussentijdse reasoning-verwerking de nauwkeurigheid verbetert. Het formaliseert slechts de stappen:

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

De model kan geen uitvoer genereren. max_discount_percent totdat churn_analysis, financial_analysis, en margin_math Ze worden gevuld. Het schema handhaaft de volgorde van reasoning.


SGR patronen

SGR beschikt over drie kernpatronen die samen een groter workflows vormen.

SGR Patronen

1. Cascade: sequentiële reasoning stappen

Cascade zorgt voor een reasoning volgorde. Elk veld moet worden ingevuld voordat het volgende kan worden verwerkt.

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: beoordeling van kandidaten, classificatie van documenten, analyse van conformiteit, medische diagnostiek.

De model moet schrijven brief_candidate_summary Voor het een beoordeling kan uitvoeren, moet het eerst beoordeeld worden, en voordat het kan aanbevelen, moet het eerst beoordeeld zijn. Er is geen snellere weg.


2. Routing: een semantische switch-instructie

Routing zorgt ervoor dat de model zich committ aan één specifieke route uit een reeks opties, waarbij dit wordt gerealiseerd met Union typen.

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: intentclassificatie, keuze van hulpmiddelen, triaging van ondersteuning, en dispatch van multi-agent.

De Literal discriminator (tool_name) zorgt ervoor dat de model één specifieke branch kiest en alleen de velden invult die door die branch vereist zijn.


3. Cyclus: herhaalde reasoning met lijsten

Cycle zorgt ertoe dat de model meerdere elementen genereert, waarbij er grenzen zijn aan het aantal.

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 zijn: risicobeoordeling, probleemextractie, parallelle tool calls uitvoeringen, en meestappige planning processen.

De MinLen en MaxLen De grenzen vereisen ten minste 2 en ten hoogste 4 elementen. In combinatie met Routing is dit de manier om een batch met vaste breedte van tool calls te verzenden.


Het laten werken van SGR: constrained decoding

De bovenstaande patronen zijn slechts Pydantic-schemas. Wat ze bindend maakt, is constrained decoding (ook wel Structured Output genoemd).

Constrained decoding wijzigt de generatiestap van token. In plaats van het model-voorbeeld vrijelijk toegang te geven tot zijn woordenschat, past de engine een grammaticamasker toe dat tokens blokkeert die het schema zouden schenden. Dit vindt plaats in de inference engine, en niet in uw applicatiecode.

[!TIP] SGR heeft geen behoefte aan “reasoning models”-modellen zoals o1 of DeepSeek-R1. Het functioneert uitstekend met instructie-getunede models-modellen, en vooral goed met models-modellen die zijn geconcentreerd op basis van reasoning-modellen.

Cloudaanbieders die dit ondersteunen

De meeste moderne LLM-aanbieders bieden structured outputs via constrained decoding aan:

ProviderOndersteuning
OpenAIStructured Outputs (inclusief Azure). GPT-5 maakt gebruik van JSON Schema via llguidance
Google/GeminiJSON Schema Ondersteuning vanaf november 2025 (Pydantic en Zod)
MistralAanpasbare Structured Output
GrokStructured Outputs voor meerdere models
Fireworks AIJSON Schema
CerebrasStructured Outputs
OpenRouterHangt af van de downstream-provider en komt overeen met JSON Schema

Inference-motoren die dit ondersteunen

Voor zelf gehoste models-systemen beschikken alle belangrijkste engines over een constrained decoding backend:

MotorBackend
vLLMxgrammar of richtlijnen
SGLangOverzichten, XGrammar, of llguidance
TensorRT-LLMGeleideDecoding
OllamaStructured Outputs

Waarom dit artikel zich richt op vLLM en xgrammar

Enkele redenen:

In het volgende gedeelte wordt uitgelegd hoe xgrammar op het niveau van token daadwerkelijk een schema oplegt.


Hoe xgrammar schema’s afdraagt

Het is belangrijk om dit onderdeel nauwkeurig te begrijpen, aangezien het de manier waarop je SGR workflows deployt en afstelt, verandert.

xgrammar-toepassing

Waar het maskeren plaatsvindt

xgrammar wijzigt de uitvoerlogits na de forward pass van het model en vóór het samplen. Het verandert het model zelf niet; in plaats daarvan filtreert het welke tokens geselecteerd mogen worden.

Een standaard inference-lus 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 glipt weg tussen stap 1 en stap 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

De model berekent nog steeds zijn volledige waarschijnheidsverdeling op de GPU. Vervolgens wordt xgrammar uitgevoerd op de CPU en wordt er een bitmask toegepast op die logits voordat er wordt gesampled. Ongeldige tokens hebben hun logits ingesteld op -∞wat ervoor zorgt dat hun waarschijnlijkheid na softmax precies 0 wordt.

Twee fasen

xgrammar splitst het werk op in een compilatietijdfase en runtime, en dat is wat ervoor zorgt dat het snel werkt.

Fase 1: compilatie van de grammatica, één keer 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 de compilatie, xgrammar:

  1. Zet de JSON Schema om in een contextvrije grammatica.
  2. Maak een pushdownautomaat (PDA) aan, die een state machine is met een stapel zodat deze genestte structuren kan verwerken. {"a": {"b": {"c": ...}}}.
  3. Er wordt van tevoren berekend welke tokens geldig zijn op elke grammaticale positie. Het resultaat vormt de “adaptieve token maskercache”.
  4. tokens worden geclassificeerd als “contextonafhankelijk” (cachébaar) of “contextafhankelijk” (moeten op runtime worden gecontroleerd tegen de toestand van de stack).

[!NOTE] Ongeveer 99% van tokens blijkt contextonafhankelijk te zijn en wordt uiteindelijk opgeslagen in de cache.Artikel over XGrammar). De meeste validatiecontroles bij runtime zijn slechts cacheopvragen, waardoor xgrammar zo snel is.

Fase 2: generatie van de runtime-maskers, bij elke token

Bij elke generatiestap:

  1. De GrammarMatcher Het houdt de huidige positie in de grammatica bij.
  2. Het zoekt het van tevoren berekende masker op voor contextonafhankelijke tokens.
  3. Het voert de PDA uit om de overgebleven contextafhankelijke tokens te controleren.
  4. Het combineert deze resultaten tot een eindmasker en past dit toe op de logits.

Waarom pushdownautomaten en niet regex?

Vanwege het nesten. Een reguliere expressie (een eindige state machine) kan structuren als deze niet betrouwbaar matchen:

{ "user": { "profile": { "settings": { "theme": "dark" } } } }

Het moeilijke deel zijn de sluitende haakjes. }}}: Je moet onthouden hoeveel instellingen je hebt geopend. Een pushdownautomaat beschikt over een stapel die dit bijhoudt, zodat hij een willekeurige diepte van nesting kan verwerken. Dit is ook de reden waarom xgrammar Unietypen, genest objecten en recursieve schema’s kan doorvoeren, terwijl regex-gebaseerde aanpakken hier niet toe in staat zijn.

Een concreet voorbeeld: het genereren van een float-veld

Wanneer de model wordt gegenereerd "max_discount_percent":, xgrammar weet uit het schema dat een float Hier volgt de masker:

De forward pass heeft mogelijk een hoge waarschijnlijkheid toegewezen aan het woord. "fifteen"Na de maskering door xgrammar heeft dat token een waarschijnlijkheid van 0. model moet cijfers uitvoeren.

Waarom “bijna-nul overhead”

Drie redenen:

  1. Parallelle uitvoering. De berekening van het masker op de CPU overlapt met de volgende forward pass op de GPU. Terwijl de GPU logits voor token N+1 berekent, wordt het masker voor token N gegenereerd door de CPU.
  2. Caching. Het grootste deel van de validatiewerkzaamheden vindt tijdens de compilatie plaats. Runtime bestaat voornamelijk uit cacheopvragen.
  3. C++-implementatie. De belastingspath is in C++, en niet in Python, waarbij het masker rechtstreeks op de logits wordt toegepast.

Bij benchmarks levert xgrammar een verwaarloosbaar overhead op, en gestructureerde generatie kan af en toe sneller zijn dan onbeperkte generatie, omdat het beperkte woordenschat de sampling-kosten verlaagt.


Praktische implementatie met vLLM

De referentie is de sgr – discount-manager een project, een kleine demo die SGR gebruikt voor dynamische prijsbepaling.

Agent Workflow

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):
    """
    Strict reasoning topology for dynamic pricing.
    Fields are ordered to enforce the 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. The Decision Constraint
    max_discount_percent: float = Field(...,
        description="Max allowed discount %. NEVER exceed margin.")

    # 4. Final Output
    offer_code: str = Field(..., description="Generated code (e.g. SAVE20).")
    customer_message: str = Field(..., description="The final polite offer text.")

Stap 2: een LLM-client die xgrammar activeert

# sgr/utils/llm_client.py
from openai import OpenAI
from pydantic import BaseModel
from typing import TypeVar
import json

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"):
        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's guided_json with xgrammar backend to enforce
        strict schema constraints at the token generation level.
        """
        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's guided_json with the xgrammar backend
        completion = self.client.chat.completions.create(
            model=self.model,
            messages=enhanced_messages,
            temperature=0.1,  # Low temp for deterministic reasoning
            extra_body={
                "guided_json": schema_dict,  # Pydantic schema as dict
                "guided_decoding_backend": "xgrammar",  # Hardware-enforced
            },
        )

        raw_response = completion.choices[0].message.content
        return schema_class.model_validate_json(raw_response)

[!NOTE] > guided_json Accepteert een JSON Schema-dict. Met guided_decoding_backend: "xgrammar"De LLM kan alleen tokens genereren die voldoen aan de geldende JSON-specificaties van uw schema.

Stap 3: orkestreren van de agent

# sgr/agent.py
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 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: SGR Logic Execution (Uses PricingLogic) ---
        print("🧠 Calculating Offer (Schema Enforced)...")
        offer = llm.run_sgr(history, PricingLogic)

        # Audit log — the SGR benefit: explicit reasoning traces
        print(f"   [Audit] Math: {offer.margin_math}")
        print(f"   [Audit] Max Allowed: {offer.max_discount_percent}%")

        return offer.customer_message

    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 (default in recent versions)
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-7B-Instruct \
    --port 8000

# Run the agent
uv run python -m sgr.agent

Voorbeelduitvoer

🤖 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%
🧠 Calculating Offer (Schema Enforced)...
   [Audit] Math: Cart $200 * 0.20 Margin = $40
   [Audit] Max Allowed: 15.0%

💬 Final Reply: We value your loyalty! Here's a special 15% discount
   with code SAVE15. This reflects our appreciation for your continued
   business with us.

Het auditlog laat het daadwerkelijke werk van de model zien: het berekende de marge (40 dollar voor een winkelwagen ter waarde van 200 dollar bij een marge van 20%) en beperkte de korting zodat het aanbod binnen de winstvereisten bleef.


Beste praktijken

Schemaontwerp

  1. Sorteer de velden op basis van de reasoning-stroom. Analysevelden komen voor decisionvelden.
  2. Schrijf beschrijvende tekst. Field Beschrijvingen. Zij sturen de attention van de model evenzeer aan als de veldnaam dat doet.
  3. Beperk met Literal en Annotated. Gebruik Literal["a", "b"] voor enumeraties en Annotated[int, Ge(1), Le(10)] voor de grenzen.
  4. Houd de schema’s gefocust. Eén schema per reasoning fase, en combineer ze vervolgens met meerdere oproepen.

vLLM configuratie

  1. Gebruik een lage temperatuur (0,1–0,3) voor deterministische reasoning-verwerking.
  2. Laat xgrammar de structuur beheren; probeer deze niet te beïnvloeden met opmaak instructies in prompt.
  3. Houd de gebruiksintensiteit van token in de gaten. SGR maakt doorgaans minder tokens-instellingen nodig dan CoT, omdat er geen uitgebreide beschrijvingen aanwezig zijn.

Overwegingen voor productieomgevingen

  1. Versieer je schema’s op dezelfde manier als je APIs versieert.
  2. Zelfs met SGR moeten netwerk- en serverfouten nog steeds op een soepele manier worden afgehandeld.
  3. Logeer de ruwe uitvoer van SGR voor compliancedoeleinden en bij het oplossen van problemen.
  4. Test op uiterste gevallen zodat het schema ook aan de randvoorwaarden voldoet.

Conclusie

SGR is wat ervoor zorgt dat iets niet langer alleen in een demo omgeving functioneert, maar ook daadwerkelijk in de productieomgeving werkt. Je definieert de reasoning topologie in Pydantic, laat xgrammar deze op het juiste moment controleren via decode, en het resultaat is:

De sgr – kortingbeheerder Demo Wire verbindt elk codevoorbeeld uit deze post met een echte vLLM-server. Cloneer deze server en begin de schema’s aan te passen aan uw eigen workflow.


Belangrijkste conclusies

  1. Schema-gestuurde Reasoning maakt de reasoning-topologie expliciet, in plaats van te verwachten dat de model de beschreven instructies opvolgt.
  2. Constrained decoding voorkomt ongeldige JSON-waarden al tijdens het genereren, wat efficiënter is dan deze later nog te valideren en opnieuw te proberen.
  3. Plaats analysevelden vóór beslissingsvelden wanneer het schema vereist dat reasoning eerst wordt uitgevoerd voordat er output wordt gegenereerd.
  4. Gebruik SGR wanneer downstream-code afhankelijk is van een specifieke structuur, en niet wanneer het eindresultaat bestaat uit vrije tekst.

Referenties

SGR Framework

xgrammar

vLLM

Demo-project