Структурированный вывод LLM в 2026: JSON Schema в OpenAI, Anthropic и Gemini

Как получить гарантированно валидный JSON от LLM в 2026: Structured Outputs OpenAI, tool use в Claude, responseSchema в Gemini, Pydantic через Instructor и эвалы перед продакшном.

JSON Schema в LLM: OpenAI, Claude, Gemini

Обновлено: 19 июня 2026

Структурированный вывод (Structured Output) в LLM, это режим работы языковой модели, при котором ответ принудительно соответствует заранее заданной JSON Schema. Такой режим устраняет необходимость в эвристическом парсинге свободного текста. В 2026 году все три крупных вендора (OpenAI, Anthropic и Google) поддерживают этот механизм, но реализуют его по-разному. OpenAI и Gemini принимают схему напрямую через response_format и responseSchema, а Claude использует tool use с input_schema. В этой статье я разбираю различия, ограничения и практические паттерны, которые накатал на боевых проектах за последние два года.

  • OpenAI Structured Outputs (strict mode) гарантируют 100% соответствие JSON Schema на моделях gpt-4o-2024-08-06 и новее, включая gpt-5. Это контрактное обязательство, а не статистика.
  • Anthropic Claude не имеет отдельного response_format: для структурированного вывода используется механизм tool use с input_schema и tool_choice.
  • Google Gemini поддерживает responseSchema в generationConfig для моделей 2.0/2.5. Синтаксис ближе к OpenAPI 3.0, чем к чистому JSON Schema.
  • Strict-режим OpenAI требует ограниченного подмножества JSON Schema: все поля required, additionalProperties: false, не более 100 свойств, максимум 5 уровней вложенности.
  • Библиотеки instructor и outlines предоставляют единый Pydantic-интерфейс поверх вендорских различий и снимают 80% boilerplate-кода.
  • Эвалы соответствия схеме, это обязательный шаг перед продакшном: даже strict-режим может вернуть пустой объект при срабатывании safety-refusal.

Что такое структурированный вывод в LLM?

Структурированный вывод, это режим генерации, в котором LLM возвращает данные, гарантированно валидные относительно заданной JSON Schema. До августа 2024 года получить надёжный JSON от модели означало писать промт вида «верни ответ в формате JSON и ничего больше», парсить результат, ретраить при ошибке и закладывать 2–5% брака на боевом трафике. С появлением OpenAI Structured Outputs и последующих аналогов у Anthropic и Google этот класс ошибок исчез как явление. Точнее, перенёсся из рантайма в стадию проектирования схемы.

На практике структурированный вывод нужен везде, где результат модели поступает на вход коду, а не человеку: извлечение сущностей из документов, генерация аргументов для tool calls, классификация с несколькими полями, ETL-пайплайны над неструктурированным текстом, агентские системы с маршрутизацией по типу ответа. Я уже разбирал близкий механизм в материале Function Calling и Tool Use в LLM-агентах: там JSON Schema описывает аргументы инструмента, здесь же схема описывает финальный ответ.

Технически вендоры реализуют гарантию по-разному. OpenAI использует constrained decoding: на каждом шаге сэмплирования логиты токенов, нарушающих схему, обнуляются. Это и есть источник «100% гарантии», ведь модель физически не может сгенерировать невалидный JSON. Open-source аналог, библиотека Outlines от dottxt-ai, которая делает то же самое поверх vLLM, llama.cpp и Hugging Face Transformers.

Чем JSON Mode отличается от Structured Outputs

JSON Mode и Structured Outputs, это два разных уровня гарантии, и путать их дорого. JSON Mode (параметр response_format={"type": "json_object"} у OpenAI, существует с ноября 2023 года) гарантирует только то, что вывод будет синтаксически валидным JSON. Структура, имена полей, типы остаются на совести промта. Модель может вернуть {"answer": "yes"} вместо ожидаемого {"label": "positive", "confidence": 0.92}, и формально это корректный ответ.

Structured Outputs (response_format={"type": "json_schema", ...}), это надмножество JSON Mode плюс жёсткое соответствие конкретной схеме. Модель не просто вернёт JSON, она вернёт именно описанный объект с правильными типами, обязательными полями и допустимыми значениями для enum'ов. По сути JSON Mode сейчас имеет смысл использовать только при работе с моделями старше gpt-4o-2024-08-06 или там, где схему сформулировать невозможно (что почти никогда не верно).

Structured Outputs в OpenAI: strict mode и schema

В OpenAI SDK 2026 года канонический способ запросить структурированный вывод, это вызов client.chat.completions.parse с Pydantic-моделью или явный response_format с JSON Schema. Я предпочитаю первый вариант, потому что схема и тип ответа описываются в одном месте и валидация делается автоматически. Честно говоря, это первое, что я меняю в любом старом коде, который вижу.

from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal

client = OpenAI()

class Invoice(BaseModel):
    invoice_number: str = Field(..., description="Номер счёта как напечатан в документе")
    issued_on: str = Field(..., description="Дата выпуска в формате YYYY-MM-DD")
    total_amount: float
    currency: Literal["RUB", "USD", "EUR"]
    line_items: list[dict]

resp = client.chat.completions.parse(
    model="gpt-5",
    messages=[
        {"role": "system", "content": "Извлеки поля счёта из текста."},
        {"role": "user", "content": ocr_text},
    ],
    response_format=Invoice,
)

invoice: Invoice = resp.choices[0].message.parsed
if resp.choices[0].message.refusal:
    raise RuntimeError(resp.choices[0].message.refusal)

Под капотом SDK конвертирует Pydantic-модель в JSON Schema, отправляет её с флагом strict: true и парсит ответ обратно в Pydantic-инстанс. Подробности есть в официальной документации OpenAI Structured Outputs.

Strict-mode накладывает ограничения на схему. Все свойства должны быть в required (если поле опциональное, используйте Optional[T] и явный null). На каждом объекте обязателен additionalProperties: false. Не поддерживаются minLength, maxLength, pattern, format, minimum, maximum, multipleOf. Валидацию диапазонов придётся делать после парсинга. Максимум 100 свойств в одной схеме, до 5 уровней вложенности, до 500 enum-значений суммарно.

Tool use в Anthropic Claude как способ получить структуру

Anthropic в 2026 году по-прежнему не вводит отдельный response_format. Официальная позиция в том, что tool use с input_schema покрывает все сценарии структурированного вывода. На практике это работает: вы объявляете «инструмент», единственная цель которого вернуть данные нужной формы, и форсируете его вызов через tool_choice.

import anthropic

client = anthropic.Anthropic()

extract_invoice_tool = {
    "name": "save_invoice",
    "description": "Сохраняет извлечённые поля счёта.",
    "input_schema": {
        "type": "object",
        "properties": {
            "invoice_number": {"type": "string"},
            "issued_on": {"type": "string", "description": "YYYY-MM-DD"},
            "total_amount": {"type": "number"},
            "currency": {"type": "string", "enum": ["RUB", "USD", "EUR"]},
        },
        "required": ["invoice_number", "issued_on", "total_amount", "currency"],
    },
}

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=[extract_invoice_tool],
    tool_choice={"type": "tool", "name": "save_invoice"},
    messages=[{"role": "user", "content": ocr_text}],
)

tool_use_block = next(b for b in resp.content if b.type == "tool_use")
invoice_dict = tool_use_block.input  # уже распарсенный объект

Принципиальные отличия от OpenAI: схема описывается в JSON Schema без strict-ограничений (можно использовать pattern, format, minimum и подобное, но они подсказывают модели, не валидируют ответ), и в 2026 году Claude всё ещё не делает constrained decoding на стороне сервера, а полагается на сильное обучение моделей семейства Opus/Sonnet 4.x на tool use. На моих эвалах брак по схеме у claude-sonnet-4-6 составляет порядка 0.3% на нетривиальных схемах, у claude-opus-4-8 практически нулевой, но это не контрактная гарантия. Детали есть в документации Anthropic по tool use.

responseSchema в Google Gemini

Google Gemini 2.0/2.5 принимает responseSchema в generationConfig. Синтаксис заметно отличается от обоих конкурентов. Это OpenAPI 3.0-стиль: type: "OBJECT" в верхнем регистре, properties с описаниями, required как массив. JSON Schema-расширения вроде $ref, oneOf, anyOf поддержаны частично.

from google import genai
from google.genai import types

client = genai.Client()

schema = types.Schema(
    type=types.Type.OBJECT,
    required=["invoice_number", "issued_on", "total_amount", "currency"],
    properties={
        "invoice_number": types.Schema(type=types.Type.STRING),
        "issued_on": types.Schema(type=types.Type.STRING),
        "total_amount": types.Schema(type=types.Type.NUMBER),
        "currency": types.Schema(
            type=types.Type.STRING,
            enum=["RUB", "USD", "EUR"],
        ),
    },
)

resp = client.models.generate_content(
    model="gemini-2.5-pro",
    contents=ocr_text,
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=schema,
    ),
)

import json
invoice = json.loads(resp.text)

Альтернативный путь, это передать готовую Pydantic-модель напрямую в response_schema: SDK сконвертирует её сам. Это удобно, но Pydantic-фичи (Field-описания, валидаторы) транслируются не полностью, поэтому для сложных схем я предпочитаю явный types.Schema. Полная справка на странице Gemini API structured output.

Сравнение поддержки у трёх вендоров

Если выбирать вендора под задачу со структурированным выводом, важны не только гарантии формата, но и поддерживаемое подмножество JSON Schema, поведение при refusal и стоимость токенов схемы (она занимает место в контексте). Сводная таблица для моделей флагманской линейки на середину 2026 года.

ПараметрOpenAI (gpt-5, gpt-4o)Anthropic (Claude 4.x)Google (Gemini 2.5)
Механизмresponse_format: json_schematool use + tool_choiceresponseSchema
Constrained decodingДа (strict)НетЧастично
Гарантия валидности100% по контракту≈99.7% эмпирически≈99% эмпирически
Подмножество JSON SchemaОграниченное (strict)Почти полноеOpenAPI 3.0-стиль
Recursive schemasДа (через $ref)ДаОграниченно
RefusalsОтдельное полеЧерез stop_reasonЧерез safety_ratings
Latency overhead5–8%≈0% (это обычный tool use)3–5%

Если коротко: OpenAI выигрывает по жёсткости гарантии, Anthropic по выразительности схемы (можно использовать pattern и format как подсказки модели), Gemini удобен в мультимодальных пайплайнах, где из изображения сразу извлекается типизированный объект.

Pydantic и Instructor: единый интерфейс

На реальных проектах вендорские различия быстро становятся раздражающим источником багов. Я не люблю менять пять файлов, чтобы переключить пайплайн с GPT на Claude для A/B-теста, поэтому держу слой абстракции на библиотеке Instructor. Она предоставляет единый Pydantic-интерфейс поверх OpenAI, Anthropic, Gemini, Mistral, Cohere и Ollama.

import instructor
from anthropic import Anthropic
from pydantic import BaseModel

class Invoice(BaseModel):
    invoice_number: str
    total_amount: float
    currency: str

# Тот же код работает для OpenAI: instructor.from_openai(OpenAI())
client = instructor.from_anthropic(Anthropic())

invoice = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{"role": "user", "content": ocr_text}],
    response_model=Invoice,
    max_retries=2,
)

Instructor сам конвертирует Pydantic-модель в нужный формат, добавляет retry с feedback при ошибке валидации (модель видит, что именно сломалось, и исправляет), и нормализует refusals. Эту тему я также упоминал в гайде по созданию MCP-сервера и клиента на Python, где схемы инструментов описаны через Pydantic.

Для сценариев self-hosted-моделей (Llama 3.3, Qwen, DeepSeek-V3) разумная альтернатива, это Outlines, который делает настоящий constrained decoding поверх vLLM и Transformers. По сути, та же гарантия 100%, что и у OpenAI, но на ваших весах.

Эвалы схем перед продакшном

Даже с гарантированной валидностью JSON остаётся вопрос семантической корректности: правильное ли поле модель заполнила, верно ли извлекла дату, не перепутала ли валюту. Поэтому я никогда не выкатываю структурированный вывод в продакшн без отдельного эвал-набора. Минимальный пайплайн выглядит так.

  1. Golden dataset на 100–300 размеченных вручную примеров (10–15 рабочих часов, окупается за неделю).
  2. Schema compliance check, отдельная метрика, доля ответов, прошедших Pydantic-валидацию без retry. Должна быть 100% для strict-режима OpenAI; если меньше, у вас проблема со схемой, не с моделью.
  3. Field-level F1 по каждому полю отдельно. Усреднение скрывает систематические косяки: модель может идеально извлекать номер счёта и проваливать валюту.
  4. LLM-as-judge для свободных текстовых полей (описания, комментарии), где точное совпадение бессмысленно.

Конкретные фреймворки и стек я разбираю отдельно в материале оценка LLM в 2026: RAGAS, DeepEval, Promptfoo. Для structured output больше всего подходит Promptfoo с кастомными assertions и DeepEval с готовыми метриками точности по полям.

Подводные камни и ограничения

Несколько граблей, на которые я наступал лично и регулярно вижу у коллег.

Слишком сложные схемы ломают качество

Модель видит JSON Schema как часть системного промта. Схема на 80 полей с глубокой вложенностью съедает 2–3 тысячи токенов контекста и снижает качество извлечения по сравнению с двумя последовательными вызовами на простых схемах. Если у вас больше 40 полей, почти всегда лучше разбить на этапы.

Опциональные поля и null

В strict-режиме OpenAI все поля обязательны. «Опциональное поле» эмулируется через Optional[str] в Pydantic, что транслируется в {"type": ["string", "null"]}. Модель тогда честно вернёт null, но забывает об этом примерно в 1% случаев, так что добавляйте дефолты в Pydantic.

Enum'ы не масштабируются на тысячи значений

JSON Schema формально позволяет любое число enum-значений, но на практике с более чем сотней элементов модель начинает «срезать углы» и выдавать первое подходящее. Для классификаторов с сотнями классов работает лучше двухступенчатый подход: сначала свободный текст, потом отдельный embedding-классификатор.

Refusals можно проспать

Самый тонкий баг: модель срабатывает на safety-фильтре, возвращает refusal (OpenAI) или пустой tool_use (Anthropic), а ваш код продолжает работать с пустым объектом. Контракт API выполнен, ошибки нет, но в БД попадает мусор. Обязательно делайте assertion на наличие обязательных полей с непустыми значениями после парсинга.

Разные tokenizer-стоимости

JSON Schema, отправленная в каждом запросе, оплачивается как входные токены каждый раз. На длинных схемах это заметные деньги. У OpenAI и Anthropic это закрывается prompt caching: схема попадает в кэшируемый префикс и стоит в 10 раз дешевле на повторных вызовах. Включайте кэширование сразу.

Часто задаваемые вопросы

Поддерживает ли Claude structured outputs нативно?

Отдельного параметра response_format, как у OpenAI, у Claude нет. Anthropic рекомендует использовать механизм tool use: объявить инструмент с нужной input_schema и форсировать его вызов через tool_choice: {"type": "tool", "name": ...}. Эмпирически валидность схемы на Claude 4.x составляет 99.5–99.9%, но это не контрактная гарантия.

Чем JSON Mode отличается от Structured Outputs в OpenAI?

JSON Mode (type: "json_object") гарантирует только синтаксически валидный JSON, структура и поля целиком на промте. Structured Outputs (type: "json_schema") дополнительно гарантирует, что объект соответствует переданной JSON Schema: правильные типы, обязательные поля, допустимые значения enum. JSON Mode имеет смысл только для legacy-моделей.

Какие ограничения у strict-режима в OpenAI?

Все свойства должны быть в required, на каждом объекте обязательно additionalProperties: false, максимум 100 свойств, 5 уровней вложенности, 500 enum-значений в сумме. Не поддерживаются minLength, maxLength, pattern, format, minimum, maximum. Эти валидации придётся делать после парсинга в коде.

Как использовать Pydantic с OpenAI Structured Outputs?

Используйте метод client.chat.completions.parse и передайте Pydantic-класс в параметре response_format. SDK сконвертирует модель в JSON Schema, отправит запрос со strict-режимом и автоматически распарсит ответ в типизированный инстанс через resp.choices[0].message.parsed. Не забудьте проверить message.refusal на случай срабатывания safety-фильтров.

Какой вендор лучше для структурированного вывода в 2026?

Для жёсткой контрактной гарантии валидности это OpenAI (strict mode). Для выразительных схем со сложными ограничениями и валидаторами как подсказками модели это Anthropic Claude. Для мультимодальных задач, где из изображения сразу извлекается типизированный объект, это Google Gemini. На реальных пайплайнах разумно держать абстракцию через Instructor и переключать вендора под задачу.

Daichi Watanabe
Об авторе Daichi Watanabe

LLM integration specialist with a strong opinion about function calling and an even stronger one about evaluations.