Structured Outputs в 2026: JSON-контракты для production LLM

Structured outputs в 2026: OpenAI Responses API, Claude structured JSON и strict tools, Gemini responseSchema, schema limits, retries и app-side validation.

Structured outputs в production нужны не потому, что “JSON красивее текста”, а потому что приложению нужен контракт, а не свободный prose. Если модель возвращает неструктурированный текст, вы получаете хрупкий парсинг, бесконечные edge cases и слабую совместимость между шагами пайплайна. Если модель возвращает output по схеме, у вас появляется надёжный интерфейс между LLM и кодом.

В 2026 важно различать не один, а три разных режима:

  • final JSON output: модель должна отдать конечный JSON-объект;
  • tool / function arguments: модель должна заполнить параметры действия по схеме;
  • app-side validation: даже валидный JSON нужно прогонять через Pydantic/Zod и бизнес-правила.
Structured output для LLM похож на typed API response. Без него вы просите модель “ответь как-нибудь понятно”. С ним вы говорите: “верни объект с такими полями, и только с ними”. Для backend-кода это разница между “надеемся, что распарсится” и “работаем по контракту”.
Не сводите structured outputs только к JSON mode. Валидный JSON сам по себе не решает production-задачу. Вам нужен либо schema-constrained final output, либо строго валидированные tool arguments. И почти всегда нужна вторая линия защиты на стороне приложения.

Короткая версия

В 2026 production stack выглядит так:

  1. OpenAI: Responses API + JSON Schema / strict tool calling.
  2. Anthropic: либо direct structured JSON через output_config.format, либо strict tool use.
  3. Gemini: responseMimeType + responseSchema.
  4. Pydantic / Zod: обязательная app-side validation поверх provider guarantees.

Что выбрать

СценарийЛучший контракт
extraction / classificationfinal JSON по schema
agent action / API calltool / function schema
long pipeline между шагамиfinal JSON + app-side validation
side effects и внешние действияstrict tool use + business checks
Без техники
Модель возвращает текст вроде «Категория: billing, priority: high, next step: refund review». Дальше начинаются regex, split(':') и хрупкие if-ы.
С техникой
Модель возвращает объект `{category, priority, next_step}` по схеме. Код работает с типизированным контрактом, а не с текстом, который надо угадывать.
ПромптSchema-first extraction
Извлеки из тикета: «С меня дважды списали 12 990 тг, аккаунт premium, операция была вчера вечером». Верни object с полями `issue_type`, `amount`, `currency`, `account_tier`, `needs_human_review`.
Ответ модели

{ "issue_type": "duplicate_charge", "amount": 12990, "currency": "KZT", "account_tier": "premium", "needs_human_review": true }

1. Structured outputs в 2026: не один механизм, а несколько контрактов

Старые статьи часто описывали эволюцию как:

prompt -> JSON mode -> strict mode

Но production-реальность в 2026 лучше описывать так:

КонтрактЧто гарантируетДля чего подходит
JSON mode“вернётся JSON”простые внутренние сценарии
JSON Schema outputформа результата фиксированаextraction, classification, routing
Tool / function schemaвалидные аргументы действияagents, workflows, side effects
App-side validationбизнес-корректностьвсё, что идёт в БД, CRM, биллинг, actions

Главная мысль: валидный JSON ещё не значит корректные данные. Structured outputs уменьшают хаос формата, но не заменяют прикладную валидацию.

2. OpenAI: Responses API и strict schemas

У OpenAI в current docs structured outputs завязаны на Responses API и JSON Schema. Это уже не старый фокус на chat.completions как основной путь.

Практически есть два production-паттерна:

  • schema-constrained final output через structured output format;
  • strict function calling через tools с strict: true.

Что важно по official docs:

  • structured outputs поддерживают только subset of JSON Schema;
  • все поля обычно проектируются как required;
  • для optional-полей docs рекомендуют union с null;
  • function calling можно сделать strict, чтобы модель выдавала только валидные arguments;
  • response может завершиться refusal, и это нужно обрабатывать как отдельный path, а не как “сломанный JSON”.

То есть у OpenAI production-выбор обычно такой:

  • если нужен конечный typed result для вашего приложения, используйте structured final output;
  • если модель должна вызвать действие, используйте strict function calling;
  • если side effect дорогой или рискованный, final decision всё равно должен проходить app-side checks.
Не пытайтесь через function calling решать все задачи подряд. Если вам не нужно реальное действие, а нужен просто объект-результат, final structured JSON обычно проще, дешевле в оркестрации и понятнее для downstream-кода.

3. Anthropic: direct structured JSON и strict tool use

У Claude в 2026 уже есть отдельный structured output path, а не только старый паттерн “делайте fake tool и считайте, что это structured output”.

Anthropic docs разводят два режима:

  • structured JSON output через output_config.format;
  • tool use для action-oriented workflows.

Это важное улучшение относительно старых материалов. Теперь у Claude не обязательно моделировать extraction-задачу как tool call, если вам нужен просто конечный JSON.

При этом tool use всё ещё остаётся правильным выбором, когда:

  • модель должна вызвать реальный инструмент;
  • вы строите agent loop;
  • нужно строго ограничить допустимые аргументы действия.

Anthropic отдельно пишет про strict tool schemas и про то, что grammar compilation кэшируется примерно на 24 часа. Это значит, что новые схемы могут давать более дорогой и медленный first request, но затем становятся нормальным production path.

4. Gemini: responseSchema и typed generation

У Gemini API structured outputs строятся через:

  • responseMimeType;
  • responseSchema.

Это делает Gemini очень удобным для:

  • extraction;
  • classification;
  • typed summaries;
  • pipelines, где следующий шаг ожидает object, а не prose.

Официальные docs отдельно предупреждают о practical limits:

  • сложность schema влияет на качество и latency;
  • не все schema features одинаково доступны во всех языках/SDK;
  • на Gemini 3 structured outputs можно комбинировать с tools, но это отдельный workflow и не стоит смешивать его с простым final JSON без причины.

5. Final JSON vs Tool Arguments

Это центральное production-решение, которое часто принимают неправильно.

Когда нужен final JSON

Подходит, если модель должна вернуть:

  • category / score / label;
  • extraction object;
  • triage decision;
  • typed summary;
  • объект для следующего шага пайплайна.

Признак: ничего не вызывается наружу, модель просто возвращает результат.

Когда нужен tool / function schema

Подходит, если модель должна:

  • вызвать CRM/API action;
  • инициировать refund / escalation / approval;
  • выбрать инструмент;
  • передать параметры следующему внешнему шагу.

Признак: результат модели вызывает side effect.

Без техники
{ "title": "Плохо", "content": "Любую extraction-задачу оформляют как tool call, хотя никакого инструмента в системе нет." }
С техникой
{ "title": "Лучше", "content": "Extraction и routing идут как final JSON. Реальные действия и API-инвокации идут как strict tools/functions." }

6. App-side validation всё ещё обязательна

Даже если провайдер гарантирует schema-valid output, остаются три класса ошибок:

  • semantic errors: JSON валиден, но смысл неверный;
  • business rule errors: формат ок, но значение недопустимо;
  • policy errors: модель выбрала опасное или запрещённое действие.

Поэтому production stack почти всегда такой:

  1. provider-level schema enforcement;
  2. Pydantic / Zod validation;
  3. business validation;
  4. optional retry / fallback;
  5. audit logging.

Примеры бизнес-проверок:

  • amount > 0;
  • email принадлежит разрешённому домену;
  • priority=critical требует human review;
  • refund=true запрещён без transaction lookup.
Если модель вернула объект строго по схеме, это ещё не разрешение выполнять действие. Для billing, legal, healthcare, security и agentic workflows schema-valid output должен быть только входом в policy layer, а не финальным решением.

7. Где structured outputs действительно окупаются

Лучшие production-кейсы:

  • extraction из email, тикетов, логов, договоров;
  • routing и triage;
  • moderation labels;
  • scoring;
  • handoff между шагами workflow;
  • tool arguments для agents.

Слабые кейсы:

  • длинные креативные тексты;
  • brainstorming;
  • свободные аналитические ответы;
  • essay-like outputs, где важнее richness, чем format contract.

8. Ограничения схем и latency

У всех провайдеров есть общая закономерность: чем сложнее schema, тем выше operational cost.

Что обычно ухудшает надёжность:

  • глубокая вложенность;
  • огромные enum lists;
  • schema, которая пытается закодировать всю бизнес-логику;
  • смешение final output и tool orchestration в одном запросе.

Что работает лучше:

  • маленькие и чёткие объекты;
  • явные enums;
  • один контракт на одну задачу;
  • разбиение сложного pipeline на несколько typed steps.
Что чаще всего ломает structured output в production
Слишком сложная schema30%
Нет app-side validation26%
Смешение final JSON и tool actions20%
Retry без диагностики14%
Хрупкий prompt вместо schema-first design10%

OpenAI: final JSON через Responses API

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.4-mini",
    input=[
        {
            "role": "developer",
            "content": "Извлекай только факты. Не выдумывай отсутствующие поля.",
        },
        {
            "role": "user",
            "content": (
                "Тикет: С меня дважды списали 12990 тг. "
                "Аккаунт premium, нужна проверка возврата."
            ),
        },
    ],
    text={
        "format": {
            "type": "json_schema",
            "name": "ticket_triage",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "issue_type": {"type": "string"},
                    "amount": {"type": "number"},
                    "currency": {"type": "string"},
                    "account_tier": {"type": "string"},
                    "needs_human_review": {"type": "boolean"},
                },
                "required": [
                    "issue_type",
                    "amount",
                    "currency",
                    "account_tier",
                    "needs_human_review",
                ],
                "additionalProperties": False,
            },
        }
    },
)

print(response.output_text)

OpenAI: strict function calling для действий

response = client.responses.create(
    model="gpt-5.4-mini",
    input="Пользователь просит вернуть оплату за дубль-списание.",
    tools=[
        {
            "type": "function",
            "name": "create_refund_review",
            "description": "Создаёт запрос на ручную проверку возврата",
            "parameters": {
                "type": "object",
                "properties": {
                    "transaction_id": {"type": "string"},
                    "reason": {"type": "string"},
                    "priority": {"type": "string", "enum": ["normal", "high"]},
                },
                "required": ["transaction_id", "reason", "priority"],
                "additionalProperties": False,
            },
            "strict": True,
        }
    ],
)

Anthropic: direct structured JSON

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=600,
    messages=[
        {
            "role": "user",
            "content": "Классифицируй тикет: «Не пришёл чек после оплаты подписки»",
        }
    ],
    output_config={
        "format": {
            "type": "json_schema",
            "name": "support_label",
            "schema": {
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "enum": ["billing", "technical", "account", "other"],
                    },
                    "priority": {
                        "type": "string",
                        "enum": ["low", "medium", "high"],
                    },
                    "needs_human_review": {"type": "boolean"},
                },
                "required": ["category", "priority", "needs_human_review"],
                "additionalProperties": False,
            },
        }
    },
)

Anthropic: strict tool use для side effects

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=600,
    tools=[
        {
            "name": "open_support_case",
            "description": "Открывает кейс в support system",
            "input_schema": {
                "type": "object",
                "properties": {
                    "queue": {"type": "string"},
                    "summary": {"type": "string"},
                    "priority": {"type": "string", "enum": ["normal", "high"]},
                },
                "required": ["queue", "summary", "priority"],
                "additionalProperties": False,
            },
        }
    ],
    tool_choice={"type": "tool", "name": "open_support_case"},
    messages=[
        {
            "role": "user",
            "content": "Создай support case по проблеме с двойным списанием.",
        }
    ],
)

Gemini: responseSchema

from google import genai
from google.genai import types

client = genai.Client()

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Извлеки данные из тикета о неуспешной оплате.",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema={
            "type": "object",
            "properties": {
                "category": {"type": "string"},
                "payment_failed": {"type": "boolean"},
                "needs_escalation": {"type": "boolean"},
            },
            "required": ["category", "payment_failed", "needs_escalation"],
        },
    ),
)

print(response.text)

App-side validation с Pydantic

from pydantic import BaseModel, Field


class TicketTriage(BaseModel):
    issue_type: str
    amount: float = Field(gt=0)
    currency: str
    account_tier: str
    needs_human_review: bool


triage = TicketTriage.model_validate_json(response.output_text)

Практический шаблон

ПромптStructured output audit
У нас support workflow. Модель сначала классифицирует тикет, потом иногда вызывает action в CRM. Где нужен final JSON, а где tool schema?
Ответ модели

Классификация и triage должны возвращаться как final JSON. CRM action должен идти отдельным шагом через strict tool/function schema после app-side validation и policy checks.

Проверьте себя

Проверьте себя

1. Когда обычно лучше использовать final structured JSON, а не tool call?

2. Что structured outputs не решают автоматически?

3. Какой production-антипаттерн встречается чаще всего?