Rozumowanie sterowane schematem: vLLM, XGrammar i Pydantic
Tłumaczenie automatyczne Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.
Ten artykuł jest przeznaczony dla inżynierów Pythona, którzy potrzebują danych wyjściowych modelu możliwych do zwalidowania przez kod downstream. Dowiesz się, jak zdefiniować schemat Pydantic, zażądać ustrukturyzowanych danych wyjściowych z vLLM oraz dodać w aplikacji kontrole znaczenia i zgodności z zasadami.
Ponowienie wywołania LLM nie gwarantuje poprawnego JSON. Kolejna próbka może zakończyć się niepowodzeniem w ten sam sposób, a wielokrotne wywołania zwiększają opóźnienie i koszt.
Schema-Guided Reasoning (SGR) wymusza zgodność ze schematem, gdy model generuje kolejne tokeny. Definiujesz wymagane pola za pomocą Pydantic, a silnik inferencji blokuje tokeny, które naruszałyby tę strukturę. Rezultat jest poprawny składniowo z samej konstrukcji, a nie dzięki ponawianiu prób.
TL;DR. SGR wykorzystuje constrained decoding, aby utrzymać dane wyjściowe LLM w granicach schematu Pydantic. vLLM może używać XGrammar do ograniczania generowanej struktury. Reguły semantyczne waliduj w kodzie aplikacji.
Czym jest Schema-Guided Reasoning?
Schema-Guided Reasoning to technika, którą Rinat Abdullin opisał w lipcu 2025 roku. Zamiast pozwalać modelowi na swobodne uzupełnianie tekstu — co może prowadzić do niespójności lub niejednoznaczności — przekazujesz mu ścisły szablon definiujący:
- jakie kroki powinna reprezentować odpowiedź
- zamierzoną kolejność tych kroków, aby recenzent mógł prześledzić drogę od danych do decyzji
- obszary, na których model powinien skupić uwagę
Można to traktować jak poznawczą checklistę, której model musi przestrzegać.
Co kontroluje schemat
Pola takie jak churn_analysis, margin_math i max_discount_percent jasno określają zamierzone dane pośrednie. Schemat ogranicza kształt zwracanych danych. Sam w sobie nie sprawia jednak, że jedno pole zależy od drugiego, ani nie dowodzi poprawności decyzji.
Daje to:
- powtarzalne rozumowanie w kolejnych uruchomieniach
- audytowalne dane wyjściowe, w których można przeanalizować każdy krok
- pola pośrednie, które można oceniać na podstawie zbioru testowego
- możliwość praktycznego wykorzystania mniejszych modeli, ponieważ schemat dostarcza struktury, której model w przeciwnym razie musiałby się nauczyć
- Abdullin pisze, że w obserwowanych przez niego przypadkach wzrost dokładności o 5–10% „nie jest czymś niezwykłym”; jest to obserwacja praktyka, a nie wynik benchmarku, dlatego zmierz ten efekt na własnym workloadzie
SGR a Chain of Thought i prompt engineering
Te trzy podejścia różnią się głównie stopniem, w jakim ograniczają model.
| Cecha | Prompt engineering | Chain of Thought | Schema-Guided Reasoning |
|---|---|---|---|
| Struktura danych wyjściowych | Zmienny tekst | Swobodna proza | Sztywny JSON/Pydantic |
| Mechanizm kontroli | Perswazja semantyczna („Wygeneruj JSON”) | Heurystyczne promptowanie („Pomyślmy krok po kroku”) | Constrained decoding (oparte na gramatyce) |
| Przepływ rozumowania | Określa go model | Określa go model | Deweloper opisuje zamierzoną topologię |
| Audytowalność | Niska (wymaga parsowania) | Niska (wymaga czytania prozy) | Wysoka (inspekcja na poziomie pól) |
| Integracja | Trudna (parsowanie regexami) | Trudna (zmienny format) | Wymaga obsługi schematów i walidacji |
| Wskaźnik błędów | Wysoki (zmienność formatu) | Umiarkowany (halucynowanie formatu) | Dane wyjściowe niezgodne ze schematem są blokowane; błędy semantyczne pozostają |
| Wymagania wobec modelu | Silne podążanie za instrukcjami | Silne zdolności rozumowania | Działa również z mniejszymi modelami |
Prompt engineering: perswazja semantyczna
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!
Masz nadzieję, że rozumienie przez model polecenia „wygeneruj JSON” przeważy nad jego skłonnością do prowadzenia rozmowy. Aktualizacja modelu, zmiana temperatury lub inny przykład few-shot mogą zepsuć parser.
Chain of Thought: użyteczny ślad rozumowania, ten sam problem ze strukturą
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 może poprawić dokładność zadania, gdy wspierają go prompt, model, zadanie i ewaluacja, ale wynik nadal pozostaje prozą, którą trudno niezawodnie parsować. Możesz skończyć na wykonywaniu drugiego wywołania LLM tylko po to, aby wyodrębnić ustrukturyzowane dane.
SGR: ustrukturyzowany Chain of Thought
SGR może udostępnić pola pośrednie do inspekcji i ewaluacji. To, czy poprawi dokładność zadania, zależy od modelu, promptu, zadania oraz sposobu wykorzystania tych pól; sam schemat jedynie formalizuje ich kształt:
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
Schemat opisuje te pola w tej kolejności. Pojedynczy schemat obiektu nie tworzy osobnego etapu walidacji między nimi. Gdy późniejsze decyzje muszą zależeć od wcześniejszych wyników, użyj osobnych wywołań lub kontroli w kodzie aplikacji.
Wzorce SGR
SGR ma trzy podstawowe wzorce, które można łączyć w większe workflow.
1. Cascade: sekwencyjne kroki rozumowania
Cascade reprezentuje kolejność rozumowania w jednej ustrukturyzowanej odpowiedzi. Nie wymusza przejścia stanu między polami.
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"]
Dobre zastosowania: ocena kandydatów, klasyfikacja dokumentów, analiza zgodności i diagnoza medyczna.
Model ma zwrócić brief_candidate_summary, rate_skill_match i final_recommendation w tej kolejności. Jeśli kolejność jest wymaganiem polityki, wymuś ją za pomocą osobnych wywołań lub deterministycznej logiki aplikacji.
2. Routing: semantyczna instrukcja switch
Routing sprawia, że model wybiera jedną ścieżkę ze zbioru opcji, zaimplementowaną za pomocą typów 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]
Dobre zastosowania: klasyfikacja intencji, wybór narzędzia, triage zgłoszeń oraz przekazywanie zadań między agentami.
Wartości Literal specyficzne dla gałęzi pomagają walidacji odróżniać elementy unii. Nie sprawiają jednak, że routing jest poprawny. Waliduj wynik i wykonuj dispatch w kodzie aplikacji. Jeśli potrzebujesz jawnego dyskryminatora Pydantic, skonfiguruj i przetestuj discriminated union.
3. Cycle: powtarzalne rozumowanie z listami
Cycle wymusza wygenerowanie wielu elementów, z ograniczeniem ich liczby.
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)]
Dobre zastosowania: ocena ryzyka, ekstrakcja problemów, równoległe wywołania narzędzi i planowanie wieloetapowe.
Ograniczenia MinLen i MaxLen wymuszają co najmniej 2 i najwyżej 4 elementy. W połączeniu z Routing jest to sposób na wysłanie partii wywołań narzędzi o stałej szerokości.
Jak sprawić, aby SGR działał: constrained decoding
Powyższe wzorce to po prostu schematy Pydantic. Tym, co sprawia, że są wiążące, jest constrained decoding (nazywany również Structured Output).
Constrained decoding modyfikuje etap generowania tokenów. Zamiast pozwalać modelowi na swobodne próbkowanie ze słownika, silnik nakłada maskę gramatyczną, która blokuje tokeny naruszające schemat. Dzieje się to w silniku inferencji, a nie w kodzie aplikacji.
[!TIP] SGR nie wymaga „modeli rozumujących”, takich jak o1 czy DeepSeek-R1. Działa dobrze z modelami dostrojonymi do wykonywania instrukcji, a szczególnie dobrze z modelami destylowanymi z modeli rozumujących.
Dostawcy chmurowi, którzy to obsługują
Poniższa dokumentacja dostawców informowała o obsłudze structured output w chwili aktualizacji tego artykułu, 2026-06-07. Obsługa, podzbiór schematu, restrykcyjność i sposób obsługi błędów różnią się w zależności od modelu i endpointu:
| Dostawca | Obsługa |
|---|---|
| OpenAI | Structured Outputs (w tym Azure) |
| Google/Gemini | JSON Schema od listopada 2025 (Pydantic i Zod) |
| Mistral | Custom Structured Output |
| Grok | Structured Outputs dla wielu modeli |
| Fireworks AI | JSON Schema |
| Cerebras | Structured Outputs |
| OpenRouter | Zależy od wybranego dostawcy downstream i trasy |
Silniki inferencji, które to obsługują
W przypadku modeli self-hosted wszystkie główne silniki mają backend constrained decoding:
| Silnik | Backend |
|---|---|
| vLLM | xgrammar lub guidance |
| SGLang | Outlines, XGrammar lub llguidance |
| TensorRT-LLM | GuidedDecoding |
| Ollama | Structured Outputs |
Dlaczego ten artykuł koncentruje się na vLLM i XGrammar
Powodów jest kilka:
- vLLM jest jednym z najczęściej wdrażanych open-source’owych silników inferencji LLM, więc to, co tutaj zbudujesz, łatwo przeniesiesz.
- XGrammar jest zaimplementowany w C++, a cytowane benchmarki mierzą jego narzut w określonych warunkach. Zmierz go z własnym modelem, schematem, sprzętem i konfiguracją serwowania.
- API vLLM jest zgodne z API OpenAI, co ogranicza koszt migracji od dostawców chmurowych.
- XGrammar obsługuje złożone schematy zagnieżdżone, unie i struktury rekurencyjne.
Jak XGrammar wymusza zgodność ze schematami
Gdzie odbywa się maskowanie
XGrammar modyfikuje logity danych wyjściowych po przejściu w przód modelu i przed próbkowaniem. Nie zmienia samego modelu. Filtruje tokeny, które mogą zostać wybrane.
Standardowa pętla inferencji wygląda tak:
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 włącza się między krokami 1 i 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
Model nadal oblicza pełny rozkład prawdopodobieństwa na GPU. Metoda GrammarMatcher w XGrammar generuje maskę bitową na CPU, a następnie silnik serwujący przenosi tę maskę na urządzenie, na którym znajdują się logity, i stosuje ją tam in place przed próbkowaniem. W przypadku logitów na GPU XGrammar używa do tego kernela GPU. Niepoprawnym tokenom ustawiane są logity na -∞, co sprawia, że po softmaxie ich prawdopodobieństwo wynosi dokładnie 0.
Dwie fazy
XGrammar dzieli pracę na etap kompilacji i etap runtime. Taka konstrukcja ogranicza wielokrotne wykonywanie pracy związanej z gramatyką.
Faza 1: kompilacja gramatyki, raz na schemat
# 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)
Podczas kompilacji XGrammar:
- Konwertuje JSON Schema na gramatykę bezkontekstową.
- Buduje automat ze stosem (PDA), czyli maszynę stanów ze stosem, dzięki któremu może obsługiwać zagnieżdżone struktury, takie jak
{"a": {"b": {"c": ...}}}. - Wstępnie oblicza, które tokeny są poprawne w każdej pozycji gramatyki. Wynikiem jest „pamięć podręczna adaptacyjnej maski tokenów”.
- Kategoryzuje tokeny jako „niezależne od kontekstu” (możliwe do buforowania) lub „zależne od kontekstu” (wymagające sprawdzenia w runtime względem stanu stosu).
[!NOTE] W pomiarach opisanych w artykule XGrammar około 99% tokenów było niezależnych od kontekstu (paper). Traktuj tę liczbę jako specyficzną dla benchmarku, a nie uniwersalny współczynnik.
Faza 2: generowanie maski w runtime, dla każdego tokena
Na każdym kroku generowania:
GrammarMatcherśledzi bieżącą pozycję w gramatyce.- Wyszukuje wstępnie obliczoną maskę dla tokenów niezależnych od kontekstu.
- Uruchamia PDA, aby sprawdzić pozostałe tokeny zależne od kontekstu.
- Łączy je w końcową maskę bitową, przenosi ją na urządzenie logitów i stosuje tam.
Dlaczego automaty ze stosem, a nie regex?
Powodem jest zagnieżdżanie. Wyrażenie regularne (maszyna stanów skończonych) nie potrafi niezawodnie dopasować struktur takich jak:
{ "user": { "profile": { "settings": { "theme": "dark" } } } }
Trudność stanowią zamykające klamry }}}: trzeba pamiętać, ile klamer otwarto. Automat ze stosem ma stos, który to śledzi, dzięki czemu może obsługiwać dowolną głębokość zagnieżdżenia. Z tego samego powodu XGrammar może wymuszać typy Union, zagnieżdżone obiekty i schematy rekurencyjne, w których podejścia oparte na regexach sobie nie radzą.
Konkretny przykład: generowanie pola typu float
Gdy model generuje "max_discount_percent":, XGrammar wie ze schematu, że następny element ma być typu float. Zbiór poprawnych tokenów zależy od stanu parsera i tokenizera. Na początku liczby maska może dopuścić cyfrę lub znak minus; po cyfrze może dopuścić kontynuacje, takie jak kolejna cyfra, kropka dziesiętna lub znacznik wykładnika.
- Cudzysłów,
{,[,true,falselubnullnie może rozpoczynać tej liczby, dlatego maska blokuje odpowiadające im tokeny w tym stanie. - Przejście w przód mogło przypisać wysokie prawdopodobieństwo tokenowi reprezentującemu
"fifteen". Ponieważ ten token nie może kontynuować tego pola numerycznego, maska go usuwa, a model musi wybrać poprawną kontynuację liczby.
Co wpływa na narzut
Są trzy przyczyny:
- Generowanie i transfer maski. XGrammar generuje maski na CPU, a silnik serwujący przenosi je na urządzenie logitów, aby zastosować je in place. Zakres nakładania się tych operacji zależy od implementacji serwowania i workloadu.
- Buforowanie. Większość pracy związanej z poprawnością jest wykonywana podczas kompilacji. Runtime to głównie wyszukiwanie w cache.
- Implementacja w C++. Hot path działa w C++, a nie w Pythonie, a maska jest stosowana in place do logitów.
Cytowane benchmarki pokazują niski narzut dla przetestowanych gramatyk, tokenizerów, sprzętu i workloadów. Wyniki te nie stanowią ogólnej gwarancji opóźnienia ani przepustowości.
Praktyczna implementacja z vLLM
Projekt sgr-discount-manager jest poglądowym zewnętrznym demo. Ten artykuł nie przypina jego commita ani nie twierdzi, że poniższe fragmenty uruchomiono w tym repozytorium.
Struktura projektu
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
Krok 1: definiowanie schematów
# 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.")
Krok 2: klient LLM, który włącza 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] W bieżącej wersji vLLM
structured_outputs: {"json": schema_dict}żąda JSON zgodnego ze schematem. W razie potrzeby skonfiguruj backend structured output za pomocą opcji serwera--structured-outputs-config.backend. Jest to wymuszanie programowe w serwerze inferencji, a nie wymuszanie sprzętowe.
Krok 3: orkiestracja agenta
# 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}")
Krok 4: uruchomienie vLLM z 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
Przykładowe dane wyjściowe
🤖 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.
Te dane wyjściowe są poglądowe. Schemat ogranicza kształt, ale przed użyciem oferty aplikacja musi sprawdzić poprawność arytmetyki i polityki rabatowej.
Schemat, vLLM i checklista produkcyjna
Projektowanie schematu
- Uporządkuj pola zgodnie z zamierzoną ścieżką rozumowania. Traktuj tę kolejność jako dokumentację, chyba że osobne wywołania lub kontrole aplikacji ją wymuszają.
- Pisz opisowe
Field. Kierują uwagą modelu w podobnym stopniu jak nazwa pola. - Ograniczaj wartości za pomocą
LiteraliAnnotated. UżywajLiteral["a", "b"]dla enumów iAnnotated[int, Ge(1), Le(10)]do określania zakresów. - Utrzymuj schematy wąskie i skoncentrowane. Jeden schemat na fazę rozumowania, a następnie łącz je za pomocą wielu wywołań.
Konfiguracja vLLM
- Używaj niskiej temperatury (0.1–0.3), aby ograniczyć zmienność próbkowania. W przypadku powtarzalnych testów przypnij model i konfigurację serwowania, użyj trybu deterministycznego serwera, jeśli jest dostępny, i zweryfikuj wynik.
- Pozwól XGrammar obsługiwać strukturę. Nie próbuj wymuszać jej dodatkowymi instrukcjami formatowania w prompcie.
- Mierz zużycie tokenów przy użyciu tego samego promptu, modelu, schematu i zadania. SGR może wygenerować więcej lub mniej tokenów niż swobodna odpowiedź CoT.
Kwestie produkcyjne
- Wersjonuj schematy tak samo jak API.
- Nawet przy SGR błędy sieci i serwera nadal wymagają poprawnej obsługi.
- Loguj surowe dane wyjściowe SGR na potrzeby zgodności i debugowania, a następnie osobno loguj deterministyczną decyzję dotyczącą polityki.
- Przed użyciem ponownie obliczaj ceny, limity, uprawnienia i inne reguły semantyczne w kodzie aplikacji; testuj wartości brzegowe, których sam schemat nie może odrzucić.
Podsumowanie
Opisujesz topologię rozumowania w Pydantic i pozwalasz constrained decoding wymusić kształt danych wyjściowych. Rezultat:
- jest poprawny składniowo z samej konstrukcji, co eliminuje pętlę ponawiania i ponownego parsowania
- jest audytowalny na poziomie pól
- może działać z mniejszymi modelami, ponieważ nie muszą one samodzielnie perfekcyjnie odwzorowywać formatu
- może być tańszy w uruchomieniu, jeśli walidacja ograniczy liczbę ponowień albo wybrany schemat i model wykorzystają mniej tokenów wyjściowych
Demo sgr-discount-manager jest zewnętrznym punktem wyjścia. Przed potraktowaniem go jako działającego przykładu sprawdź przypięte zależności i bieżącą zgodność z vLLM.
Najważniejsze wnioski
- Schema-Guided Reasoning jasno określa zamierzoną topologię rozumowania, zamiast polegać wyłącznie na instrukcjach zapisanych prozą.
- Constrained decoding zapobiega generowaniu niepoprawnego JSON, co jest czystsze niż walidowanie i ponawianie próby po fakcie.
- Umieszczaj pola analizy przed polami decyzji, gdy odpowiedź powinna dokumentować tę ścieżkę rozumowania. Jeśli kolejność musi być wymuszona, użyj osobnych wywołań lub kontroli w kodzie aplikacji.
- Używaj SGR, gdy kod downstream zależy od struktury, a nie wtedy, gdy produktem jest swobodna proza.
Referencje
SGR Framework
- Schema-Guided Reasoning (SGR) — oryginalny framework autorstwa Rinat Abdullina
- SGR Patterns — wzorce Cascade, Routing i Cycle
xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — Yixin Dong i in., arXiv:2411.15100 (artykuł techniczny z benchmarkami)
- xgrammar GitHub — szybka i elastyczna biblioteka do generowania ustrukturyzowanych danych
- xgrammar Documentation — oficjalna dokumentacja z przewodnikiem Quick Start
- xgrammar Quick Start — wprowadzenie do xgrammar
- Achieving Efficient Structured Generation with XGrammar — wpis na blogu MLC o wewnętrznym działaniu xgrammar
vLLM
- vLLM Structured Outputs — oficjalna dokumentacja
Demo Project
- sgr-discount-manager — starsze poglądowe demo; nie zawiera wszystkich przykładów kodu z tego artykułu