Ризонинг по схеме: vLLM, XGrammar и Pydantic
Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.
Эта статья предназначена для Python-инженеров, которым нужны ответы модели, валидируемые downstream-кодом. Вы научитесь определять схему Pydantic, запрашивать structured output из vLLM и добавлять в приложение проверки смысла и политик.
Повторный вызов LLM не гарантирует получение валидного JSON. Следующий пример может завершиться той же ошибкой, а повторные вызовы увеличивают латентность и стоимость.
Schema-Guided Reasoning (SGR) применяет схему во время генерации каждого токена. Вы описываете обязательные поля с помощью Pydantic, а инференс-движок блокирует токены, которые нарушили бы эту структуру. В результате синтаксическая валидность обеспечивается самой генерацией, а не ретраями.
Коротко. SGR использует constrained decoding, чтобы удерживать ответ LLM в рамках схемы Pydantic. vLLM может использовать XGrammar для ограничения генерируемой структуры. Семантические правила проверяйте в коде приложения.
Что такое Schema-Guided Reasoning?
Schema-Guided Reasoning — это техника, которую Rinat Abdullin описал в июле 2025 года. Вместо того чтобы позволять модели свободно дополнять текст, что может приводить к несогласованным или неоднозначным результатам, вы задаёте строгий шаблон, определяющий:
- какие шаги должен представлять ответ
- предполагаемый порядок этих шагов, чтобы ревьюер мог проследить путь от данных к решению
- на чём модель должна сосредоточить внимание
Представьте это как когнитивный чек-лист, которому модель обязана следовать.
Что контролирует схема
Такие поля, как churn_analysis, margin_math и max_discount_percent, явно задают предполагаемые промежуточные результаты. Схема ограничивает форму возвращаемого ответа. Сама по себе она не устанавливает зависимость одного поля от другого и не доказывает корректность решения.
Это даёт:
- воспроизводимый ризонинг при повторных запусках
- аудируемые ответы, в которых можно проверить каждый шаг
- промежуточные поля, которые можно оценивать на тестовом датасете
- возможность использовать меньшие модели, поскольку схема предоставляет структуру, которую иначе модели пришлось бы выучить
- по словам Abdullin, прирост точности на 5–10% в наблюдавшихся им случаях «не редкость»; это наблюдение практика, а не результат бенчмарка, поэтому измеряйте эффект на собственной нагрузке
SGR vs Chain of Thought vs prompt engineering
Эти три подхода в основном различаются степенью ограничения, накладываемого на модель.
| Характеристика | Prompt Engineering | Chain of Thought | Schema-Guided Reasoning |
|---|---|---|---|
| Структура ответа | Переменный текст | Произвольная проза | Жёсткий JSON/Pydantic |
| Механизм контроля | Семантическое убеждение («Выведите JSON») | Эвристический промптинг («Давайте подумаем пошагово») | Constrained decoding (на основе грамматики) |
| Ход ризонинга | Определяет модель | Определяет модель | Разработчик описывает предполагаемую топологию |
| Аудируемость | Низкая (требуется парсинг) | Низкая (нужно читать прозу) | Высокая (проверка на уровне полей) |
| Интеграция | Сложная (парсинг regex) | Сложная (переменный формат) | Требуются поддержка схем и валидация |
| Частота ошибок | Высокая (вариативность формата) | Средняя (галлюцинации формата) | Ответы, не соответствующие схеме, блокируются; семантические ошибки сохраняются |
| Требования к модели | Сильное следование инструкциям | Сильная способность к ризонингу | Подходит и для небольших моделей |
Prompt engineering: семантическое убеждение
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!
Вы надеетесь, что понимание моделью указания «выведи JSON» перевесит её склонность к разговорному ответу. Обновление модели, изменение temperature или другой few-shot-пример могут сломать ваш парсер.
Chain of Thought: полезный трейс ризонинга, но та же проблема со структурой
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 может повысить точность задачи, если это поддерживают промпт, модель, задача и метод оценки, но результат всё равно остаётся прозой, которую сложно надёжно парсить. В итоге может понадобиться второй вызов LLM только для извлечения структурированных данных.
SGR: структурированный chain of thought
SGR позволяет сделать промежуточные поля доступными для проверки и оценки. Улучшит ли это точность задачи, зависит от модели, промпта, задачи и способа использования этих полей; сама схема лишь формализует их форму:
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
Схема описывает эти поля в указанном порядке. Одна object-схема не создаёт отдельный этап валидации между ними. Если последующие решения должны зависеть от предыдущих результатов, используйте отдельные вызовы или проверки в коде приложения.
Паттерны SGR
У SGR есть три основных паттерна, из которых можно составлять более крупные workflow.
1. Cascade: последовательные шаги ризонинга
Cascade задаёт порядок ризонинга в одном структурированном ответе. Он не обеспечивает переход состояния между полями.
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"]
Хорошо подходит для оценки кандидатов, классификации документов, анализа соответствия требованиям и медицинской диагностики.
Модель должна вернуть brief_candidate_summary, rate_skill_match и final_recommendation в указанном порядке. Если порядок является требованием политики, обеспечьте его отдельными вызовами или детерминированной логикой приложения.
2. Routing: семантический switch statement
Routing заставляет модель выбрать один путь из набора вариантов, реализованных с помощью типов 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]
Хорошо подходит для классификации намерений, выбора инструментов, триажа обращений в поддержку и диспетчеризации multi-agent-систем.
Специфичные для веток значения Literal помогают валидации различать элементы union. Но они не делают роутинг корректным. Валидируйте результат и выполняйте dispatch в коде приложения. Для явного дискриминатора Pydantic настройте и протестируйте discriminated union.
3. Cycle: повторяющийся ризонинг со списками
Cycle заставляет модель вернуть несколько элементов, ограничивая их количество.
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)]
Хорошо подходит для оценки рисков, извлечения проблем, параллельных tool calls и многошагового планирования.
Ограничения MinLen и MaxLen требуют минимум 2 и максимум 4 элемента. В сочетании с Routing это позволяет диспетчеризировать batch tool calls фиксированного размера.
Как заставить SGR работать: constrained decoding
Описанные выше паттерны — всего лишь схемы Pydantic. Связывающими их делает constrained decoding, также называемый Structured Output.
Constrained decoding изменяет этап генерации токенов. Вместо свободной выборки из словаря модели движок применяет маску грамматики, блокирующую токены, которые нарушили бы схему. Это происходит в инференс-движке, а не в коде приложения.
[!TIP] Для SGR не нужны «reasoning models» вроде o1 или DeepSeek-R1. Он хорошо работает с instruction-tuned-моделями и особенно хорошо — с моделями, дистиллированными из reasoning-моделей.
Облачные провайдеры с поддержкой
В документации следующих провайдеров на момент обновления этой статьи, 2026-06-07, заявлялась поддержка structured output. Поддержка, подмножества схем, строгость и поведение при ошибках зависят от модели и endpoint:
| Провайдер | Поддержка |
|---|---|
| OpenAI | Structured Outputs, включая Azure |
| Google/Gemini | Поддержка JSON Schema с ноября 2025 года (Pydantic и Zod) |
| Mistral | Custom Structured Output |
| Grok | Structured Outputs для нескольких моделей |
| Fireworks AI | JSON Schema |
| Cerebras | Structured Outputs |
| OpenRouter | Зависит от выбранного downstream-провайдера и маршрута |
Инференс-движки с поддержкой
Для self-hosted-моделей основные движки поддерживают бэкенд constrained decoding:
| Движок | Бэкенд |
|---|---|
| vLLM | xgrammar или guidance |
| SGLang | Outlines, XGrammar или llguidance |
| TensorRT-LLM | GuidedDecoding |
| Ollama | Structured Outputs |
Почему в статье рассматриваются vLLM и XGrammar
Причин несколько:
- vLLM — один из наиболее широко используемых open-source инференс-движков для LLM, поэтому созданные здесь решения легко перенести.
- XGrammar реализован на C++, а цитируемые бенчмарки измеряют его overhead в конкретных условиях. Измеряйте его с вашей моделью, схемой, железом и конфигурацией сервинга.
- API vLLM совместим с OpenAI, поэтому миграция с облачных провайдеров обходится недорого.
- XGrammar поддерживает сложные вложенные схемы, union и рекурсивные структуры.
Как XGrammar применяет схемы
Где применяется маскирование
XGrammar изменяет output logits после forward pass модели и перед sampling. Он не меняет саму модель, а фильтрует токены, которые можно выбрать.
Стандартный инференс-цикл выглядит так:
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 встраивается между шагами 1 и 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
Модель по-прежнему вычисляет полное распределение вероятностей на GPU. GrammarMatcher в XGrammar генерирует битовую маску на CPU, после чего serving engine переносит эту маску на устройство logits и применяет её in place перед sampling. Для logits на GPU XGrammar использует GPU kernel. Для невалидных токенов logits устанавливаются в -∞, поэтому после softmax их вероятность становится ровно 0.
Две фазы
XGrammar разделяет работу на compile-time и runtime. Такая архитектура сокращает повторные вычисления, связанные с грамматикой.
Фаза 1: компиляция грамматики, один раз для каждой схемы
# 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)
Во время компиляции XGrammar:
- Преобразует JSON Schema в Context-Free Grammar.
- Строит Pushdown Automaton (PDA) — машину состояний со стеком, способную обрабатывать вложенные структуры вроде
{"a": {"b": {"c": ...}}}. - Предварительно вычисляет, какие токены допустимы в каждой позиции грамматики. Результат называется «adaptive token mask cache».
- Классифицирует токены как «context-independent» (их можно кэшировать) или «context-dependent» (их нужно проверять в runtime с учётом состояния стека).
[!NOTE] В статье про XGrammar сообщается, что в проведённых измерениях около 99% токенов были context-independent (статья). Считайте это число специфичным для данного бенчмарка, а не универсальным соотношением.
Фаза 2: генерация маски в runtime, для каждого токена
На каждом шаге генерации:
GrammarMatcherотслеживает текущую позицию в грамматике.- Извлекает предварительно вычисленную маску для context-independent-токенов.
- Запускает PDA для проверки оставшихся context-dependent-токенов.
- Объединяет их в итоговую битовую маску, переносит её на устройство logits и применяет там.
Почему Pushdown Automaton, а не regex?
Из-за вложенности. Регулярное выражение, то есть конечная машина состояний, не может надёжно сопоставлять такие структуры:
{ "user": { "profile": { "settings": { "theme": "dark" } } } }
Сложность заключается в закрывающих фигурных скобках }}}: нужно помнить, сколько скобок было открыто. У Pushdown Automaton есть стек, который отслеживает это, поэтому он может обрабатывать вложенность произвольной глубины. По этой же причине XGrammar способен применять Union types, вложенные объекты и рекурсивные схемы, с которыми regex-подходы справляются плохо.
Конкретный пример: генерация float-поля
Когда модель генерирует "max_discount_percent":, XGrammar знает из схемы, что следующим должен идти float. Набор допустимых токенов зависит от состояния парсера и токенизатора. В начале числа маска может разрешать цифру или знак минус; после цифры — такие продолжения, как ещё одна цифра, десятичная точка или маркер экспоненты.
- Кавычка,
{,[,true,falseилиnullне могут начинать это число, поэтому маска блокирует соответствующие токены в данном состоянии. - Во время forward pass модель могла присвоить высокую вероятность токену
"fifteen". Поскольку этот токен не может продолжить числовое поле, маска удаляет его, и модель вынуждена выбрать допустимое числовое продолжение.
Что влияет на overhead
Три причины:
- Генерация и перенос маски. XGrammar создаёт маски на CPU, а serving engine переносит их на устройство logits для применения in place. Степень перекрытия операций зависит от реализации сервинга и нагрузки.
- Кэширование. Большая часть работы по проверке допустимости выполняется на этапе компиляции. В runtime в основном происходят обращения к кэшу.
- Реализация на C++. Hot path находится в C++, а не в Python; маска применяется к logits in place.
Цитируемые бенчмарки показывают низкий overhead для протестированных грамматик, токенизаторов, оборудования и нагрузок. Эти результаты не гарантируют конкретную латентность или throughput в общем случае.
Практическая реализация с vLLM
Проект sgr-discount-manager — это иллюстративное внешнее демо. В статье не фиксируется его commit и не утверждается, что приведённые ниже фрагменты запускались в этом репозитории.
Структура проекта
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
Шаг 1: определение схем
# 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.")
Шаг 2: клиент LLM, включающий 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] В текущей версии vLLM
structured_outputs: {"json": schema_dict}запрашивает JSON, соответствующий схеме. При необходимости настройте бэкенд structured output с помощью опции сервера--structured-outputs-config.backend. Это программное ограничение на инференс-сервере, а не аппаратное ограничение.
Шаг 3: оркестрация агента
# 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}")
Шаг 4: запуск vLLM с 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
Иллюстративный результат
🤖 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.
Этот результат приведён для иллюстрации. Схема ограничивает форму ответа, но перед использованием предложения приложение должно проверить корректность арифметики и политики скидок.
Чек-лист для схем, vLLM и production
Проектирование схем
- Располагайте поля в соответствии с предполагаемым потоком ризонинга. Считайте этот порядок документацией, если отдельные вызовы или проверки в приложении его не обеспечивают.
- Пишите содержательные описания
Field. Они направляют внимание модели не меньше, чем имя поля. - Используйте ограничения
LiteralиAnnotated. Для enum применяйтеLiteral["a", "b"], а для границ —Annotated[int, Ge(1), Le(10)]. - Не перегружайте схемы. Используйте одну схему на фазу ризонинга, а затем объединяйте результаты несколькими вызовами.
Конфигурация vLLM
- Используйте низкую temperature (0.1–0.3), чтобы уменьшить вариативность sampling. Для воспроизводимых тестов зафиксируйте модель и конфигурацию сервинга, включите deterministic mode сервера, если он доступен, и проверьте результат.
- Поручите XGrammar обрабатывать структуру. Не пытайтесь компенсировать это инструкциями по форматированию в промпте.
- Сравнивайте использование токенов на одном и том же промпте, с той же моделью, схемой и задачей. SGR может генерировать больше или меньше токенов, чем свободный ответ в формате CoT.
Production-аспекты
- Версионируйте схемы так же, как версионируете APIs.
- Даже при использовании SGR сетевые ошибки и ошибки сервера требуют корректной обработки.
- Логируйте исходные ответы SGR для compliance и отладки, а детерминированное решение политики — отдельно.
- Перед использованием пересчитывайте цены, лимиты, права доступа и другие семантические правила в коде приложения; тестируйте граничные значения, которые сама схема не способна отклонить.
Заключение
Вы описываете топологию ризонинга в Pydantic и позволяете constrained decoding обеспечивать форму ответа. В результате получаете:
- синтаксически валидный ответ по конструкции, что устраняет цикл retry-and-reparse
- аудит на уровне отдельных полей
- возможность использовать меньшие модели, поскольку им больше не нужно самостоятельно идеально соблюдать формат
- потенциально более дешёвый запуск, если валидация сокращает количество ретраев или выбранная схема и модель используют меньше output-токенов
Демо sgr-discount-manager — внешняя отправная точка. Перед тем как считать его запускаемым reference-примером, проверьте зафиксированные зависимости и текущую совместимость с vLLM.
Ключевые выводы
- Schema-Guided Reasoning явно задаёт предполагаемую топологию ризонинга, вместо того чтобы полагаться только на инструкции в прозе.
- Constrained decoding предотвращает невалидный JSON во время генерации, что надёжнее, чем последующая валидация с ретраями.
- Размещайте поля анализа перед полями решения, если ответ должен документировать этот путь ризонинга. Если порядок необходимо обеспечить принудительно, используйте отдельные вызовы или проверки в коде приложения.
- Используйте SGR, когда downstream-код зависит от структуры, а не когда продуктом является свободный текст.
Ссылки
SGR Framework
- Schema-Guided Reasoning (SGR) — исходный фреймворк Rinat Abdullin
- SGR Patterns — паттерны Cascade, Routing и Cycle
xgrammar
- XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models — Yixin Dong и соавторы, arXiv:2411.15100 (техническая статья с бенчмарками)
- xgrammar GitHub — быстрая и гибкая библиотека для структурированной генерации
- xgrammar Documentation — официальная документация с quick start guide
- xgrammar Quick Start — начало работы с xgrammar
- Achieving Efficient Structured Generation with XGrammar — статья MLC о внутреннем устройстве xgrammar
vLLM
- vLLM Structured Outputs — официальная документация
Demo Project
- sgr-discount-manager — устаревшее иллюстративное демо; оно не содержит всех примеров кода из этой статьи