6 августа 2024 года в API OpenAI появилась функция Structured Outputs: вывод модели теперь надёжно соответствует JSON-схемам, которые задаёт разработчик. Функция связана с новой моделью gpt-4o-2024-08-06 — в оценках сложных JSON-схем со Structured Outputs она показала 100% точности.
Structured Outputs доступна в двух формах: через вызов функций и через формат ответа. Ниже — как устроен механизм, чем он отличается от прежнего JSON modeрежим вывода, при котором модель гарантированно возвращает валидный JSON, но без привязки к конкретной схеме и какие ограничения накладывает.
Что именно изменилось
Structured Outputs включается двумя способами. В форме вызова функцийспособ задания схемы, при котором модель заполняет заранее описанный инструмент, а её вывод соответствует этому описанию достаточно установить strict: true внутри определения функции — тогда вывод модели будет соответствовать переданному определению инструмента. В форме формата ответаспособ задания схемы, при котором сама JSON-схема ответа передаётся в запросе напрямую можно указать JSON-схему напрямую. Выбор между формами определяется тем, описываете ли вы инструмент, который должна вызвать модель, или просто задаёте структуру её ответа.
Схема задаётся на разных уровнях в зависимости от способа. Через response_format она лежит на верхнем уровне запроса: указывается тип json_schema, вложенный объект json_schema и флаг strict со значением true. Через text.format схема уходит внутрь объекта text, во вложенный format, где задаются тип json_schema, strict и сама схема в поле schema. Различие только в уровне вложенности.
JSON mode — более базовая версия. Он гарантирует, что вывод является валидным JSON, но не сопоставляет его с конкретной схемой. Включается он в Chat Completions через response_format со значением { "type": "json_object" }, а в Responses API — через text.format с тем же значением. При вызове функций JSON mode всегда включён. Важное отличие в поведении: при JSON mode нужно всегда явно просить модель генерировать JSON в сообщении — иначе модель может выдать бесконечный поток пробелов, а API выбросит ошибку, если строка «JSON» не встречается в контексте.
Structured Outputs доступны в новейших больших языковых моделях, начиная с GPT-4o. Более старые модели вроде gpt-4-turbo и ранее могут использовать вместо этого JSON mode. Через вызов функций Structured Outputs работают со всеми моделями, поддерживающими инструменты, включая gpt-4-0613 и gpt-3.5-turbo-0613 и более поздние.
Как это работает: constrained decoding
Механизм основан на технике constrained decodingограничение выбора токенов при сэмплировании: модель может генерировать только те токены, которые допустимы согласно заданной схеме. По умолчанию при сэмплировании модель не ограничена и может выбрать любой токен из словаря — именно эта свобода позволяет ей ошибаться, например поставить фигурную скобку там, где это нарушит JSON. Чтобы принудительно получать валидный вывод, модель ограничивают только допустимыми по схеме токенами.
Сложность в том, что допустимые токены меняются по ходу генерации. Поэтому применяется динамическое constrained decoding: допустимые токены определяются заново после каждого сгенерированного токена, а не один раз в начале ответа.
Сначала JSON Schema преобразуется в контекстно-свободную грамматикунабор правил, задающих допустимый язык; в отличие от конечных автоматов выражает более широкий класс языков, включая рекурсивные структуры. Компоненты грамматики предобрабатываются для эффективного доступа во время сэмплирования. Во время генерации после каждого токена движок вывода по уже созданным токенам и правилам грамматики вычисляет список допустимых следующих токенов. Этот список маскирует следующий шаг сэмплирования, что снижает вероятность недопустимых токенов до нуля. Предобработка схемы объясняет задержку при первом запросе с новой схемой; кэшированная структура данных позволяет дальше проверять с минимальными накладными расходами.
Чем это отличается от FSM и регулярных выражений
Альтернативные подходы к constrained decoding часто используют конечные автоматымодель вычислений с конечным числом состояний, которая не может выражать рекурсивные типы (FSM) или регулярные выражения, обычно реализуемые через FSM. Они работают похоже — динамически обновляют допустимые токены после каждого сгенерированного токена, — но есть ключевое различие: контекстно-свободные грамматики выражают более широкий класс языков, чем FSM.
Для очень простых схем это неважно, но существенно для сложных схем с вложенными или рекурсивными структурами данных. FSM в общем случае не могут выражать рекурсивные типы, поэтому подходы на FSM могут испытывать трудности с сопоставлением скобок в глубоко вложенном JSON. Рекурсивные схемы поддерживаются в Structured Outputs, но не могут быть выражены с помощью FSM.
Что можно описать в схеме
Structured Outputs поддерживает подмножество JSON Schema. Среди типов — Stringстрока текста, Numberчисло, в том числе дробное, Booleanлогическое значение «истина» или «ложь», Integerцелое число, Objectнабор именованных полей, Arrayупорядоченный список значений, Enumзаранее заданный перечень допустимых значений и anyOfконструкция, разрешающая значение одного из нескольких описанных вариантов. Для строк поддерживаются ограничения pattern и format с форматами date-time, time, date, duration, email, hostname, ipv4, ipv6, uuid; для чисел — ограничения кратности, верхней и нижней границы, в том числе строгие; для массивов — ограничения минимального и максимального числа элементов. Поддерживаются definitions для подссхем, на которые ссылаются по всей схеме.
Есть жёсткие требования к структуре. Корневой объект схемы обязан быть объектом и не может использовать конструкцию, разрешающую один из нескольких вариантов. Все поля или параметры функции должны быть помечены как required. В объектах всегда нужно задавать additionalProperties: false, чтобы разрешить генерацию только заданных ключей. Опциональность эмулируется через объединение типа с null.
Порядок ключей в выводе совпадает с порядком ключей в схеме: выходные данные формируются в том же порядке.
Обработка отказов и краевых случаев
Отказы и краевые случаи обрабатываются проверкой полей ответа. Если модель отказалась отвечать по соображениям безопасности, в ответе будет непустое поле refusal — булев признак отказа, который проверяется на истинность; при срабатывании выводится содержимое отказа. При непустом refusal в Chat Completions поле .content содержит объяснение отказа, если оно было сгенерировано.
Если достигнут лимит токенов и ответ неполный, choices[0].finish_reason будет равно "length", и это нужно обрабатывать как ошибку: ответ обрезан до завершения JSON. В Responses API те же краевые случаи проверяются через status == "incomplete" и incomplete_details.reason == "max_output_tokens", а тип элемента message.content различает "refusal" и "output_text".
Ошибки вида finish_reason, refusal, content_filter и другие обрабатываются через общий except Exception.
Нативная поддержка в SDK
Python и Node SDK поддерживают Structured Outputs нативно: достаточно передать Pydantic- или Zod-объект в качестве схемы для инструментов или формата ответа. SDK сам преобразует тип данных в поддерживаемый JSON Schema, десериализует JSON-ответ в типизированную структуру и разбирает отказы.
В Python для этого используется метод client.chat.completions.parse: он принимает response_format с типом модели ответа и автоматически конвертирует pydantic-модель в JSON schema, отправляет её в API и парсит содержимое ответа обратно в заданную модель. Если модель отказалась отвечать, доступно поле refusal, иначе — parsed. В Node SDK аналогично используется openai.chat.completions.parse с response_format: zodResponseFormat(...). Нативная поддержка Structured Outputs доступна и для response_format.
Ограничения и открытые вопросы
Structured Outputs поддерживает только подмножество JSON Schema, что ограничивает набор допустимых конструкций. Для объектов действуют лимиты: до 5000 свойств суммарно и до 10 уровней вложенности. По enum: до 1000 значений суммарно по всем enum-свойствам.
Для дообученных моделей ограничения на строки и числа — такие как проверка по регулярному выражению, предопределённые форматы, кратность, верхние и нижние границы значений, а также ограничения на количество элементов массива — пока не поддерживаются.
Авторы оставляют без ответа ряд открытых вопросов. Structured Outputs не предотвращает все виды ошибок модели — например, ошибки внутри значений JSON-объекта. Функция несовместима с параллельными вызовами функций: при генерации параллельного вызова он может не соответствовать переданным схемам, поэтому для отключения параллельных вызовов следует установить parallel_tool_calls: false. JSON-схемы, передаваемые в Structured Outputs, не подходят для Zero Data Retentionрежима, при котором данные запроса не сохраняются на сторонах, участвующих в обработке. Кроме того, модель может не следовать схеме, если отказывается от небезопасного запроса или если генерация достигает max_tokens либо иного условия остановки.