Что такое structured output / constrained decoding и зачем это нужно?
Краткий тезис
Structured output (структурированный вывод) — это техника, заставляющая LLM возвращать данные строго по заданной схеме (например, JSON, XML). decoding|Constrained decoding (ограниченная декодировка) — метод, при котором генерация токенов ограничивается грамматикой или регулярным выражением, чтобы гарантировать соблюдение схемы. В production без этого невозможно надёжно встроить LLM в бизнес-процессы: ручной парсинг сырого текста ненадёжен, а валидация схемы на стороне клиента требует сложной логики повторных попыток.
1. Термины и контекст
- Structured output — вывод LLM, который соответствует заранее заданной структуре (JSON Schema, Pydantic модель, XML DTD).
- Constrained decoding — процесс генерации, при котором на каждом шаге разрешены только те токены, которые не нарушают синтаксис целевой структуры. Реализуется через logit masking или грамматики.
- Function calling (API-метод) — возможность, предоставляемая OpenAI, Anthropic и другими провайдерами, когда модель сама выбирает, какую функцию вызвать, и возвращает аргументы в JSON.
- JSON mode (режим JSON) — опция API, гарантирующая, что ответ будет валидным JSON (но без гарантии соблюдения конкретной схемы).
- Outlines, Guidance, LMQL — библиотеки с открытым исходным кодом для constrained decoding на самодельных моделях (Llama, Mistral).
Проблема: LLM по умолчанию генерирует свободный текст. Даже при инструкции «верни JSON» модель может добавить лишние пояснения, комментарии, неправильно экранировать кавычки. В production (сотни запросов в секунду) любой сбой синтаксиса — потерянный ответ или дорогой повторный запрос.
2. Зачем это нужно: ключевые мотивации
| Мотивация | Описание |
|---|---|
| Надёжность | 100%-ная гарантия, что ответ можно прогрузить json.loads(). Без constrained decoding % ошибок может достигать 5-20% даже у GPT-4. |
| Простота интеграции | Не нужно писать парсеры, fallback-логику, повторные промпты «исправь JSON». Снижается кодовая сложность. |
| Безопасность схемы | Можно жёстко ограничить поля: например, action только из списка ["refund", "cancel", "info"]. |
| Скорость разработки | Схема JSON автоматически превращается в pydantic-модель, сразу валидируется. |
| Возможность композиции | Несколько вызовов LLM с разными схемами объединяются в конвейер (Multi‑agent workflows). |
3. Подходы к structured output
3.1. API-уровень (managed services)
- OpenAI Function Calling — передаём tools (или functions), модель сама решает, вызывать функцию и с какими параметрами. Ответ содержит
tool_callsс JSON. Гарантирует структуру, но не всегда валидность значения (например, число может быть строкой). - OpenAI JSON Mode — параметр response_format={"type": "json_object"}. Модель возвращает только JSON (без текста). Нельзя задать конкретную схему — только общая гарантия JSON.
- Anthropic Tool Use — аналог function calling. Через tools и
tool_useблок. - Google Gemini —
response_mime_type="application/json"с опциональнойresponse_schema.
Плюсы: простота, не требует дополнительных библиотек.
Минусы: привязанность к облачному API, нет полного контроля над генерацией (например, нельзя запретить пустые строки).
3.2. Self-hosted constrained decoding
Используется для локальных моделей (Llama, Mistral, Qwen). Основные инструменты:
| Инструмент | Принцип работы |
|---|---|
| Outlines | Задаётся JSON Schema → строится контекстно-свободная грамматика (PCFG) → на каждом шаге маскируются токены, не соответствующие грамматике. |
| Guidance (Microsoft) | Шаблонизация: смешивает «ручной» текст и «дырки», которые заполняет LLM. Поддерживает {{gen "field" type="json" schema=...}}. |
| LMQL | Язык запросов с аннотациями: [JSON_OUTPUT] с указанием схемы. Транслируется в constrained decoding. |
Плюсы: полный контроль, работа офлайн, совместимость с любыми моделями (через Hugging Face Transformers).
Минусы: требует GPU, настройка токенизатора, сложнее деплой.
4. Как работает constrained decoding изнутри
Иллюстрация на примере Outlines:
- Пользователь определяет JSON Schema (например,
{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}}). - Outlines преобразует схему в конечный автомат (FSM) — грамматику, описывающую все возможные строки JSON, удовлетворяющие схеме.
- Во время генерации каждый новый токен проверяется: допустим ли он в текущем состоянии FSM. Если нет — его вероятность зануляется (logit masking).
- После генерации одного токена FSM переходит в новое состояние. Процесс повторяется до завершения.
Псевдокод (упрощённо):
from outlines import generate, models
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
model = models.transformers("meta-llama/Llama-2-7b-hf")
generator = generate.json(model, Person)
result = generator("Extract person info: John is 30 years old")
print(result) # Person(name='John', age=30)
5. Сравнение способов получения structured output
| Критерий | Function Calling (OpenAI) | JSON Mode (OpenAI) | Outlines (self-hosted) |
|---|---|---|---|
| Гарантия схемы | Да (только для выбранной функции) | Нет (только валидный JSON) | Да (полная schema) |
| Произвольная структура | Да (через tools) | Нет (объект, не массив) | Да (любая JSON Schema) |
| Зависимость от модели | GPT-4, GPT-3.5 | GPT-4 Turbo+ | Любая модель HF |
| Затраты | Оплата токенов API | То же | Бесплатно (своё GPU) |
| Простота интеграции | Несколько строк кода | Один параметр | Требует настройки |
6. Примеры кода
6.1. OpenAI function calling
import openai
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "What’s the weather in Berlin?"}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
}],
tool_choice="auto"
)
tool_call = response.choices[0].message.tool_calls[0]
weather_data = json.loads(tool_call.function.arguments)
6.2. Outlines (self-hosted)
from transformers import AutoModelForCausalLM, AutoTokenizer
from outlines import generate, models
# Загрузка модели
hf_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-Instruct-v0.2")
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.2")
model = models.Transformers(hf_model, tokenizer)
# Схема
schema = {
"type": "object",
"properties": {
"summary": {"type": "string"},
"sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]}
},
"required": ["summary", "sentiment"]
}
generator = generate.json(model, schema)
result = generator("Analyze: I loved the movie!")
print(result) # {'summary': 'User expresses strong positive emotion', 'sentiment': 'positive'}
7. Практические советы для production
- Всегда используйте валидацию на стороне клиента даже при constrained decoding. Ошибки всё ещё возможны (например, модель выходит за лимит токенов).
- Для облачных моделей предпочитайте function calling (или tool use) — это стандарт индустрии.
- Для self-hosted используйте Outlines — он наиболее зрелый и быстрый (на базе
xgrammarилиllama.cpp). - Если нужно несколько полей с разными условиями — объединяйте в одну JSON Schema, не делайте несколько вызовов.
- Fallback: если structured output не удалось (ошибка схемы), повторяйте запрос с промптом «верни только JSON без пояснений».
- Мониторинг: логируйте % ответов, которые не прошли валидацию (dеградация может сигнализировать о проблемах с моделью).
8. Ограничения и риски
- Увеличение latency: constrained decoding добавляет шаг проверки токенов (на GPU – незначительно, на CPU – заметно).
- Не все модели поддерживают: для function calling нужна модель, обученная на tool use; для самодельных – требуется совместимость токенизатора с грамматикой.
- Потеря креативности: если схема слишком жёсткая, модель может «застрять» или выдавать неинформативные значения.
- Сложность вложенных схем: глубокие JSON (массив объектов с вложенными объектами) могут быть медленнее и требуют тщательной грамматики.
Пет-проект для закрепления
Задача: Разработать сервис извлечения контактных данных из email-писем в формате JSON.
Инструменты: Python, OpenAI API (или Outlines для локальной модели), FastAPI, Pydantic.
Шаги:
- Определить Pydantic-модель
ContactInfo(name, email, phone, company). - Написать промпт: «Извлеки контактные данные из письма. Верни только JSON согласно схеме».
- Реализовать endpoint
/extract, который принимает текст письма, вызывает LLM с function calling (или Outlines), парсит ответ и возвращает JSON. - Добавить валидацию: если модель вернула невалидный JSON, сделать повторный запрос с уточнением.
- Написать unit-тесты с разными форматами писем.
Ожидаемый результат: Стабильно работающий API, который извлекает контакты из реальных писем с точностью >90%, а все ответы проходят pydantic.validation.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 76 | Prompt engineering для structured output |
| 77 | Оценка качества structured output (метрики) |
| 78 | Влияние constrained decoding на latency |
| 79 | Кеширование ответов со структурированным выводом |
| 80 | Пакетная обработка запросов с JSON-схемами |
| 81 | Оптимизация затрат при использовании function calling |
Навигация
- Предыдущий: 74
- Следующий: 76
- Индекс: 00. Индекс разборов