Виды Structured Output и способы их реализации. Data Mining.. Data Mining. llm.. Data Mining. llm. Natural Language Processing.. Data Mining. llm. Natural Language Processing. Structured Output.. Data Mining. llm. Natural Language Processing. Structured Output. искусственный интеллект.. Data Mining. llm. Natural Language Processing. Structured Output. искусственный интеллект. Машинное обучение.

Structured Output это способ “заставить” модель отвечать в строго заданном формате.

Пример. Имеется пачка неструктурированных объявлений о продаже недвижимости.

Продается однокомнатная квартира площадью 35,6 кв.м. на 11-м этаже 22-этажного монолитного дома по адресу: ул. Академика Королёва, 121. Год постройки — 2018, что гарантирует современное качество и надёжность конструкции. Дом оснащён подземной парковкой.

Квартира предлагает функциональную планировку: жилая площадь — 12 кв. м, просторная кухня — 17,6 кв. м. Высота потолков 3,15 м создаёт дополнительное ощущение пространства. Окна выходят на улицу, а комнаты изолированы, что обеспечивает комфорт и уединение.

И мы хотим с помощью LLM их перевести в структурированные и положить в базу данных:

{
  "Площадь": 35.6,
  "Этаж": 11,
  "Кол-во комнат": 1,
  "Адрес": "ул. Академика Королёва, 121"
}

Чтобы добиться этого, есть три основных подхода:

  1. Повторение (Instructor)

  2. Исправление (BAML)

  3. Ограничение (Outlines)

1. Повторение (Instructor)

Самый известный представитель данного метода – библиотека Instructor.

Она работает как посредник между вашим приложением и LLM:

  • Вы описываете структуру правильного ответа (через библиотеку Pydantic).

  • Instructor отправляет запрос и получает ответ:

    • Если ответ проходит проверку на структуру, то возвращает его вам.

    • Если ответ не прошел валидацию, Instructor автоматически отправляет модели ошибку и просит исправить. И так продолжается до тех пор, пока не будет получен правильный ответ или пока не закончатся попытки.

Виды Structured Output и способы их реализации - 1

Пример использования:

import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator

# Подключаем LLM
client = instructor.from_openai(
    OpenAI(base_url="http://192.168.0.108:8000/v1", api_key="any"),
    mode=instructor.Mode.TOOLS,
    mode=instructor.Mode.MD_JSON
)

# Определяем структуру данных, которую хотим получить
class UserInfo(BaseModel):
    name: str = Field(..., description="Имя пользователя")
    age: int = Field(..., description="Возраст пользователя")
    skills: list[str] = Field(..., description="Список профессиональных навыков")

    @field_validator('age')
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

# Отправляем запрос
result = client.chat.completions.create(
    model="qwen3",
    response_model=UserInfo,
    messages=[
        {
            "role": "user", 
            "content": "Меня зовут Иван, мне 28 лет. Я эксперт в Python, Docker и Kubernetes."
        }
    ],
    max_retries = 3
)

print(result.model_dump_json(indent=3))

Здесь мы:

  • Подключаемся к LLM.

  • Описываем нужный формат ответа с помощью библиотеки Pydantic.

  • Формируем и отправляем запрос.

Обратите внимание на параметр max_retries – именно столько раз Instructor будет пытаться исправить ответ, если с первого раза он был неправильный.

Instructor поддерживает два основных режима работы:

  • TOOLS – в этом режиме вывод объявляется как функция (Function Calling) и модель вызывает ее, передавая ей параметры (поля описанные через Pydantic).

  • JSON – тут мы просим модель просто напечатать ответ в JSON и парсим его.

С таким подходом instructor может работать практически с любыми моделями, как локальными, так и по API. Даже если API не поддерживает Function Calling, Instructor всегда может попросить модель напечатать ответ в виде JSON.

Недостаток очевиден – такой подход может сожрать много токенов на попытки исправить ответ (особенно с мелкими моделями). И даже это не гарантирует результат.

instructor также унифицирует вызовы к разным API.

2. Исправление (BAML)

Данный метод пытается исправить основной недостаток предыдущего метода :)

BAML это не просто библиотека а целый фреймворк. Он состоит из своего собственного языка разметки (похожего на TS/Jinja), а также имеет свой “мягкий” парсер, который чинит сломанный JSON.

Работает он несколько сложнее, чем предыдущий метод:

1) Сначала инициируем новый проект: baml-cli init

2) BAML создаст три файла (которые вам нужно заполнить/доработать):

//baml_test/baml_src/clients.baml

client<llm> Qwen3 {
  provider "openai-generic"
  options {
    base_url "http://192.168.0.108:8000/v1"
    api_key "any"
    model "qwen3"
  }
}
//baml_test/baml_src/generators.baml

generator target {
    output_type "python/pydantic"
    output_dir "../"
    version "0.214.0"
    default_client_mode sync
}
//baml_test/baml_src/resume.baml

class Resume {
  name string
  email string
  experience string[]
  skills string[]
}

function ExtractResume(resume: string) -> Resume {
  client "Qwen3"
  prompt #"
    Extract from this content:

    {{ resume }}

    {{ ctx.output_format }}

  "#
}

Для чего они:

  • В clients.baml вы описываете как подключаться к LLM.

  • В generators.baml вы описываете как “компилировать” ваш проект.

  • В resume.baml (называться может как угодно – в данном примере мы парсим резюме поэтому и resume) мы объявляем функцию ExtractResume, в которой описываем:

    • Структуру правильного ответа

    • И функцию, которая объединяет: LLM-клиента, формат вывода и промт.

3) Затем в терминале запускаем baml-cli generate и BAML создаст в папке baml_client типизированный клиент (кучу py-файлов), который вы сможете запускать в своем коде:

import baml_client as client

raw_resume = 'Иван Петров. 10 лет. Кодил 20 лет. C#'
answer = client.b.ExtractResume(raw_resume)

Такой подход позволяет использовать даже мелкие модели. Почти любая LLM способна написать JSON. Но чем мельче модель, тем больше вероятность ошибки. А BAML аккуратно нивелирует этот недостаток, не тратя ни время ни токены.

Новый JSON он новый конечно не напишет, но вот мелкие типовые ошибки вполне исправит:

  • Забытые закрывающие скобки

  • Висячие запятые

  • Неверно экранированные символы

  • Лишние комментарии

  • Текст перед или после JSON

  • И т.д.

Из минусов: нужно учить новый синтаксис (DSL). Плюс требуется этап “компиляции” кода.

Работает с любыми API и локальными моделями.

Также есть удобный аддон для VS Code, в котором вы можете без запуска LLM тестировать ваши шаблоны.

Виды Structured Output и способы их реализации - 2

В качестве проверки можете попросить в промте написать текст перед JSON. BAML это прекрасно переварит.

3. Ограничение (Outlines)

Виды Structured Output и способы их реализации - 3

По научному этот метод называется Constrained Decoding (ограниченное декодирование) – самый “надежный” метод. А самая популярная библиотека для реализации – Outlines.

Если два предыдущих способа “просят” модель написать правильно. То Constrained Decoding ничего не просит, а заставляет модель выводить строго то, что нужно. Как это работает:

  • Вы описываете структуру правильного ответа (разными способами).

  • LLM работают итеративно. На каждом шаге, выдавая по одному токену за раз. А выбирают они эти токены из огромного словаря. И на каждом шаге LLM расставляет всем токенам вероятности появления. И чем выше вероятность, тем выше шанс, что LLM выберет этот токен. А Outlines на каждом шаге “маскирует” (обнуляет вероятности) всех токенов, которые нарушили бы схему. И модели остается выбор только из допустимых токенов.

Например, если ваша схема требует {"name": string}, то:

  • На первом шаге занулит все токены кроме открывающей фигурной скобки.

  • В последующих шагах разрешено будет написать только “name”.

  • И т.д.

А в коде это выглядит так:

from pydantic import BaseModel
from typing import Literal
from openai import OpenAI
import outlines

openai_client = OpenAI(base_url="http://192.168.0.108:8000/v1", api_key="any")
model = outlines.from_vllm(openai_client, "qwen3")

class Customer(BaseModel):
    name: str
    urgency: Literal["high", "medium", "low"]
    issue: str

customer = model(
    "Alice needs help with login issues ASAP",
    Customer)

print(customer)

Данный метод работает на уровне логитов. А значит до этих логитов надо как-то добраться. Если библиотекой инференса является transformers, то Outlines напрямую доберется до логитов и занулит их. Если Outlines работает с API, то воспользуется их возможностями. Например, для vLLM через параметр extra_body.

Outlines поддерживает множество движков инференса: OpenAI, Ollama, vLLM, LlamaCpp, Transformers. А формат вывода может описываться разными способами: Regex, JSON Schema, Context-Free Grammar.    

Плюсы: 100% гарантия соответствия схеме вывода (причем с первой попытки). Что идеально для небольших локальных моделей, так как не требует от модели быть “умной”, чтобы соблюдать формат.

Минусы: не всегда можно задействовать при работе с API (SO может просто не поддерживаться).

А теперь серьезная ложка дёгтя: есть исследования, которые показывают, что жесткое декодирование делает модель тупее :) Пример одного из последних: https://acl-bg.org/proceedings/2025/RANLP%202025/pdf/2025.ranlp-1.124.pdf

Но и есть парочка лайфхаков как обойти эту проблему. Например, вам нужно вывести строгий JSON, который начинается с открывающей фигурной скобки. Но модель может лучше ответить, если ей сначала дать немного подумать (CoT). Что тут можно сделать:

1) Вы можете дать ей подумать в самом JSON’е. Для этого вначале JSON заводите специальное поле для ризонинга, а уже дальше формируете нужный вам ответ:

{
  "reasoning": string,
  "answer": string/number
}

2) Второй способ работает в два этапа. Сначала вы просто задаете модели вопрос и она отвечает как хочет. Затем вы подсовываете первый ответ во второй запрос и просите вытащить из него ответ и задаете строгий формат вывода.

Популярные библиотеки инференса, такие как vLLM и Sglang, сами встраивают в свои движки распространенные библиотеки для Structured Output. Поэтому ничего дополнительно импортировать и осваивать не нужно, достаточно воспользоваться их документацией.

Вместо вывода

Начать нужно как минимум с Outlines (и Constrained Decoding). Возможно интеллекта вашей модели вполне хватит для решения ваших задач. Но если вы не можете залезть в мозги модели, то тогда переходите к Instructor. Если и он не справляется, то следующий кандидат – BAML. BAML несколько громоздкий для простых задач, его лучше использовать комплексно на больших проектах.


Мои курсы: Разработка LLM с нуля | Алгоритмы Машинного обучения с нуля

Автор: slivka_83

Источник

Rambler's Top100