中文翻译暂不可用,显示俄语原文。

Что такое 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 Geminiresponse_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:

  1. Пользователь определяет JSON Schema (например, {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}}).
  2. Outlines преобразует схему в конечный автомат (FSM) — грамматику, описывающую все возможные строки JSON, удовлетворяющие схеме.
  3. Во время генерации каждый новый токен проверяется: допустим ли он в текущем состоянии FSM. Если нет — его вероятность зануляется (logit masking).
  4. После генерации одного токена 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.5GPT-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.

Шаги:

  1. Определить Pydantic-модель ContactInfo(name, email, phone, company).
  2. Написать промпт: «Извлеки контактные данные из письма. Верни только JSON согласно схеме».
  3. Реализовать endpoint /extract, который принимает текст письма, вызывает LLM с function calling (или Outlines), парсит ответ и возвращает JSON.
  4. Добавить валидацию: если модель вернула невалидный JSON, сделать повторный запрос с уточнением.
  5. Написать unit-тесты с разными форматами писем.

Ожидаемый результат: Стабильно работающий API, который извлекает контакты из реальных писем с точностью >90%, а все ответы проходят pydantic.validation.


Связь с другими вопросами

ВопросТема
76Prompt engineering для structured output
77Оценка качества structured output (метрики)
78Влияние constrained decoding на latency
79Кеширование ответов со структурированным выводом
80Пакетная обработка запросов с JSON-схемами
81Оптимизация затрат при использовании function calling

Навигация