Назад к блогу

Structured Outputs в API OpenAI: как модель гарантированно попадает в JSON-схему

Structured Outputs в API OpenAI: как модель гарантированно попадает в JSON-схему

OpenAI добавила в API структурированные выводы, которые гарантируют соответствие ответа модели заданной JSON-схеме — вплоть до 100% точности на сложных схемах. Разбираем, как работает этот механизм на основе constrained decoding, чем он отличается от прежнего JSON mode и почему контекстно-свободные грамматики справляются с рекурсивными структурами лучше конечных автоматов.

6 августа 2024 года в API OpenAI появилась функция Structured Outputs: вывод модели теперь надёжно соответствует JSON-схемам, которые задаёт разработчик. Функция связана с новой моделью gpt-4o-2024-08-06 — в оценках сложных JSON-схем со Structured Outputs она показала 100% точности.

Structured Outputs доступна в двух формах: через вызов функций и через формат ответа. Ниже — как устроен механизм, чем он отличается от прежнего JSON mode и какие ограничения накладывает.

Что именно изменилось

Structured Outputs включается двумя способами. В форме вызова функций достаточно установить strict: true внутри определения функции — тогда вывод модели будет соответствовать переданному определению инструмента. В форме формата ответа можно указать 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 либо иного условия остановки.

Источники

Похожее