- BrainTools - https://www.braintools.ru -
Structured Output это способ “заставить” модель отвечать в строго заданном формате.
Пример. Имеется пачка неструктурированных объявлений о продаже недвижимости.
Продается однокомнатная квартира площадью 35,6 кв.м. на 11-м этаже 22-этажного монолитного дома по адресу: ул. Академика Королёва, 121. Год постройки — 2018, что гарантирует современное качество и надёжность конструкции. Дом оснащён подземной парковкой.
Квартира предлагает функциональную планировку: жилая площадь — 12 кв. м, просторная кухня — 17,6 кв. м. Высота потолков 3,15 м создаёт дополнительное ощущение пространства. Окна выходят на улицу, а комнаты изолированы, что обеспечивает комфорт и уединение.
И мы хотим с помощью LLM их перевести в структурированные и положить в базу данных:
{
"Площадь": 35.6,
"Этаж": 11,
"Кол-во комнат": 1,
"Адрес": "ул. Академика Королёва, 121"
}
Чтобы добиться этого, есть три основных подхода:
Самый известный представитель данного метода – библиотека Instructor [4].
Она работает как посредник между вашим приложением и LLM:
Вы описываете структуру правильного ответа (через библиотеку Pydantic).
Instructor отправляет запрос и получает ответ:
Если ответ проходит проверку на структуру, то возвращает его вам.
Если ответ не прошел валидацию, Instructor автоматически отправляет модели ошибку [5] и просит исправить. И так продолжается до тех пор, пока не будет получен правильный ответ или пока не закончатся попытки.

Пример использования:
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.
Формируем и отправляем запрос.
Обратите внимание [6] на параметр max_retries – именно столько раз Instructor будет пытаться исправить ответ, если с первого раза он был неправильный.
Instructor поддерживает два основных режима работы:
TOOLS – в этом режиме вывод объявляется как функция (Function Calling) и модель вызывает ее, передавая ей параметры (поля описанные через Pydantic).
JSON – тут мы просим модель просто напечатать ответ в JSON и парсим его.
С таким подходом instructor может работать практически с любыми моделями, как локальными, так и по API. Даже если API не поддерживает Function Calling, Instructor всегда может попросить модель напечатать ответ в виде JSON.
Недостаток очевиден – такой подход может сожрать много токенов на попытки исправить ответ (особенно с мелкими моделями). И даже это не гарантирует результат.
instructor также унифицирует вызовы к разным API.
Данный метод пытается исправить основной недостаток предыдущего метода :)
BAML [7] это не просто библиотека а целый фреймворк. Он состоит из своего собственного языка разметки (похожего на 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 тестировать ваши шаблоны.

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

По научному этот метод называется Constrained Decoding (ограниченное декодирование) – самый “надежный” метод. А самая популярная библиотека для реализации – Outlines [8].
Если два предыдущих способа “просят” модель написать правильно. То 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 [9]
Но и есть парочка лайфхаков как обойти эту проблему. Например, вам нужно вывести строгий JSON, который начинается с открывающей фигурной скобки. Но модель может лучше ответить, если ей сначала дать немного подумать (CoT). Что тут можно сделать:
1) Вы можете дать ей подумать в самом JSON’е. Для этого вначале JSON заводите специальное поле для ризонинга, а уже дальше формируете нужный вам ответ:
{
"reasoning": string,
"answer": string/number
}
2) Второй способ работает в два этапа. Сначала вы просто задаете модели вопрос и она отвечает как хочет. Затем вы подсовываете первый ответ во второй запрос и просите вытащить из него ответ и задаете строгий формат вывода.
Популярные библиотеки инференса, такие как vLLM и Sglang, сами встраивают в свои движки распространенные библиотеки для Structured Output. Поэтому ничего дополнительно импортировать и осваивать не нужно, достаточно воспользоваться их документацией.
Начать нужно как минимум с Outlines (и Constrained Decoding). Возможно интеллекта [10] вашей модели вполне хватит для решения ваших задач. Но если вы не можете залезть в мозги модели, то тогда переходите к Instructor. Если и он не справляется, то следующий кандидат – BAML. BAML несколько громоздкий для простых задач, его лучше использовать комплексно на больших проектах.
Мои курсы: Разработка LLM с нуля [11] | Алгоритмы Машинного обучения с нуля [12]
Автор: slivka_83
Источник [13]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/23428
URLs in this post:
[1] Повторение (Instructor): #1
[2] Исправление (BAML): #2
[3] Ограничение (Outlines): #3
[4] Instructor: https://github.com/567-labs/instructor/tree/main
[5] ошибку: http://www.braintools.ru/article/4192
[6] внимание: http://www.braintools.ru/article/7595
[7] BAML: https://docs.boundaryml.com/home
[8] Outlines: https://github.com/dottxt-ai/outlines
[9] https://acl-bg.org/proceedings/2025/RANLP%202025/pdf/2025.ranlp-1.124.pdf: https://acl-bg.org/proceedings/2025/RANLP%202025/pdf/2025.ranlp-1.124.pdf
[10] интеллекта: http://www.braintools.ru/article/7605
[11] Разработка LLM с нуля: https://stepik.org/a/231306/pay?promo=5e79340ae02bce0d
[12] Алгоритмы Машинного обучения с нуля: https://stepik.org/a/68260/pay?promo=b997c468b105096d
[13] Источник: https://habr.com/ru/articles/978534/?utm_source=habrahabr&utm_medium=rss&utm_campaign=978534
Нажмите здесь для печати.