Структурированные выводы JSON Schema Вызов функций OpenAI API Gemini API

Структурированные выводы с JSON Schema: практическое руководство

Хватит извлекать JSON из прозы. Как строгий json_schema OpenAI, responseSchema Gemini и нативный output_config.format Anthropic заставляют модели возвращать данные по заданной вами схеме.

AveMujica API 8 мин чтения

В тот момент, когда вы пытаетесь передать ответ модели в код, «верни JSON» перестаёт быть достаточным. Попросите вежливо — и получите JSON, обёрнутый в извинение, JSON внутри Markdown-ограждения или JSON, который синтаксически валиден, но лишён поля, нужного вашему парсеру. Надёжное решение — перестать просить и начать ограничивать: сообщить провайдеру точную ожидаемую схему и позволить ему гарантировать — или хотя бы строго обеспечивать — соответствие ответа. Эта статья рассматривает доступные сегодня механизмы: строгий json_schema OpenAI, responseSchema Gemini и нативный output_config.format Anthropic, где строгая работа с инструментами служит аналогом для вызова функций, — правила схем каждого провайдера и причины всё равно валидировать результат.

Два режима сбоя, одна первопричина

Есть два разных способа, которыми свободнотекстовый JSON ломается, и полезно их назвать. Синтаксический сбой — это вывод, который вообще не распарсится: висячая запятая, случайное предложение, ограждение кода. Семантический сбой — это вывод, который парсится, но неверен для вашей программы: значение перечисления, которое вы никогда не определяли, отсутствующее обязательное поле, строка там, где вам нужно число. Инженерия промптов («отвечай только JSON») уменьшает первый вид и почти ничего не делает со вторым. Генерация с ограничением по схеме атакует оба, потому что схема навязывается во время декодирования, а не проверяется постфактум.

OpenAI: strict: true — это вся фича

Структурированные выводы OpenAI — это эволюция более старого JSON-режима, и разница в одном флаге. Простой JSON-режим (response_format: { "type": "json_object" }) гарантирует, что вывод — валидный JSON, но ничего о его форме. Структурированные выводы добавляют схему и переключатель strict, который привязывает модель к ней.

В Chat Completions вы передаёте response_format типа json_schema:

from openai import OpenAI
client = OpenAI(base_url="https://your-gateway.example.com/v1", api_key="AVE_GATEWAY_API_KEY")

resp = client.chat.completions.create(
    model="gpt-5.6",
    messages=[{"role": "user", "content": "Extract the invoice fields."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "invoice",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "vendor": {"type": "string"},
                    "total": {"type": "number"},
                    "currency": {"type": "string", "enum": ["USD", "EUR", "JPY"]},
                },
                "required": ["vendor", "total", "currency"],
                "additionalProperties": False,
            },
        },
    },
)

В Responses API та же схема переезжает в другое поле — text.format вместо response_format. У обычного вызова responses.create нет верхнеуровневого output_parsed; чтобы получить распарсенный объект, используйте вспомогательный метод SDK parse с типизированным форматом либо прочитайте response.output_text и провалидируйте его самостоятельно:

from pydantic import BaseModel

class Invoice(BaseModel):
    vendor: str
    total: float
    currency: str

resp = client.responses.parse(
    model="gpt-5.6",
    input="Extract the invoice fields.",
    text_format=Invoice,        # SDK builds the strict json_schema and parses the result
)
invoice = resp.output_parsed    # a validated Invoice instance

Если вы предпочитаете сырой путь, отправьте text={"format": {"type": "json_schema", "name": "invoice", "strict": True, "schema": SCHEMA}} через responses.create, затем явно распарсьте и провалидируйте resp.output_text — не ожидайте, что распарсенное поле появится само.

Отправка response_format в Responses или text.format в Chat Completions — частая и запутывающая ошибка: изменилось имя поля, а не концепция. Покрытие через шлюз зависит от адаптора и не является единообразным: этот шлюз отображает OpenAI-образный response_format на responseSchema Gemini, но сейчас не переводит его в нативное поле структурированного вывода Claude. Поэтому для Claude отправляйте нативную форму Anthropic, показанную ниже, а не предполагайте, что response_format дойдёт без изменений.

У строгого подмножества схем есть правила

strict: true поддерживает большую часть JSON Schema, но не всю, и ограничения достаточно строги, что схема, которая прекрасно валидируется в обычном валидаторе, может быть отклонена здесь:

  • Каждое свойство должно быть перечислено в required. По-настоящему опциональных полей нет. Чтобы смоделировать «может отсутствовать», сделайте поле обнуляемым объединением — "type": ["string", "null"] — вместо того чтобы опускать его из required.
  • additionalProperties: false обязательно на каждом объекте. Модель не может выдумывать лишние ключи.
  • Корень должен быть объектом. Массив верхнего уровня не разрешён; оберните его в объект со свойством типа массив.
  • Рекурсия использует {"$ref": "#"} для ссылки на корень — так вы выражаете вложенные древовидные структуры.

Когда модель отказывается по соображениям безопасности, это программно всплывает как поле refusal в сообщении, а не как контент в форме схемы, поэтому вы проверяете наличие отказа прежде чем доверять парсингу. И ответ может быть обрезан по длине: проверяйте finish_reason (значение "length" означает обрезанный JSON), потому что обрезанный строгий ответ — всё равно невалидный JSON, даже если схема была навязана.

Gemini: responseSchema плюс JSON mime-тип

Gemini ограничивает вывод через generationConfig. Вы устанавливаете responseMimeType в "application/json" и передаёте responseSchema (объект схемы в стиле OpenAPI) — или, для стандартного JSON Schema, responseJsonSchema:

from google import genai
from google.genai import types

client = genai.Client()
resp = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Classify the ticket.",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema={
            "type": "object",
            "properties": {
                "category": {"type": "string", "enum": ["billing", "bug", "feature"]},
                "priority": {"type": "integer"},
            },
            "required": ["category", "priority"],
        },
    ),
)

Gemini ограничивает генерацию схемой, поддерживает enum для классификации и позволяет закрепить порядок полей через propertyOrdering — полезно, потому что порядок может влиять на качество и согласованность. Отметьте две оговорки, о которых доки говорят прямо: поддерживается не каждая фича JSON Schema, а очень сложные схемы могут быть отклонены. Gemini также подчёркивает, что вывод — валидный JSON в форме схемы, но вы всё равно должны валидировать значения — он может произвести нечто соответствующее схеме, но семантически неверное (правдоподобную, но некорректную категорию). Механизм гарантирует форму, а не истину.

Anthropic: нативный output_config.format и строгая работа с инструментами

Anthropic теперь предлагает нативный структурированный вывод, поэтому принудительная работа с инструментами больше не является единственным путём — это совместимый запасной вариант. Две взаимодополняющие возможности покрывают задачу. JSON-вывод добавляет к запросу объект output_config с format вида {"type": "json_schema", "schema": ...}, и ответ Claude возвращается как соответствующий схеме JSON в текстовом содержимом. Строгая работа с инструментами добавляет strict: true к input_schema инструмента, гарантируя валидные по схеме аргументы. Возможности можно использовать отдельно или вместе.

import anthropic
client = anthropic.Anthropic(base_url="https://your-gateway.example.com", api_key="AVE_GATEWAY_API_KEY")

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {"vendor": {"type": "string"}, "total": {"type": "number"}},
                "required": ["vendor", "total"],
                "additionalProperties": False,
            },
        }
    },
    messages=[{"role": "user", "content": "Extract the invoice fields."}],
)
data = msg.content[0].text   # valid JSON matching your schema

Полезно знать несколько текущих ограничений: additionalProperties должен быть false, а числовые и строковые ограничения (minimum, maxLength и подобные) и рекурсивные схемы пока не поддерживаются — их отправка возвращает 400. Python SDK также предоставляет client.messages.parse(..., output_format=PydanticModel), чтобы построить схему и распарсить ответ за один шаг. Когда модель должна именно вызвать что-то, идиоматичен строгий инструмент: определите инструмент с целевой схемой в input_schema, добавьте strict: true и принудительно выберите его через tool_choice.

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=[{
        "name": "record_invoice",
        "strict": True,
        "input_schema": {
            "type": "object",
            "properties": {"vendor": {"type": "string"}, "total": {"type": "number"}},
            "required": ["vendor", "total"],
            "additionalProperties": False,
        },
    }],
    tool_choice={"type": "tool", "name": "record_invoice"},
    messages=[{"role": "user", "content": "Extract the invoice fields."}],
)
data = next(b.input for b in msg.content if b.type == "tool_use")

Принуждение инструмента делает поведение детерминированным: модель обязана вызвать record_invoice, а её input становится вашим объектом. Используйте JSON-вывод, когда нужен типизированный ответ, и строгий инструмент, когда модель должна что-то вызвать с гарантированно валидными аргументами. Принудительный инструмент без strict остаётся устаревшим запасным вариантом для старых моделей или отдельных форм циклов инструментов; на актуальных моделях предпочитайте нативные возможности. Если продукт уже маршрутизирует инструменты между провайдерами, определения переносятся — см. переносимый вызов инструментов между провайдерами.

Структурированный вывод против вызова функций: выбирайте по намерению

Эти два применения JSON Schema выглядят похоже и их легко спутать. Различие — в намерении. Используйте вызов функций, когда модель должна решить, стоит ли и как вызвать инструмент, который что-то делает: запросить базу данных, вызвать API, выполнить код. Используйте структурированный формат ответа (json_schema / text.format / responseSchema / output_config.format), когда вы просто хотите оформить ответ модели как данные для программы. Все три провайдера теперь поддерживают оба подхода: OpenAI — строгие схемы в форматах ответа и инструментах, Anthropic — нативный JSON-вывод и строгую работу с инструментами, Gemini — схемы ответа и объявления функций по отдельности. Использование вызова инструмента ради одного типизированного ответа добавляет ненужный круговой рейс.

Вы всё равно валидируете — всегда

Навязывание схемы не даёт лицензии пропускать валидацию по трём конкретным причинам. Во-первых, навязывание гарантирует форму, но никогда корректность: модель может вернуть корректно типизированное значение, которое фактически неверно, и только ваши бизнес-правила это поймают. Во-вторых, обрезание случается — ограниченный по длине или прервавшийся по тайм-ауту ответ даёт неполный JSON, который никакой режим схемы не спасёт, поэтому вы должны обрабатывать finish_reason/status и ошибку парсинга. В-третьих, поддержка схем — это подмножество, различающееся по провайдерам, так что схема, которая навязывается строго на одной вышестоящей стороне, может трактоваться более вольно на другой. Прогоняйте распарсенный объект через валидатор (Pydantic, Zod, JSON Schema) на границе и трактуйте сбой валидации как нормальную, ожидаемую ветку, а не как исключение. Это тот же принцип защиты в глубину, что стоит за ограничением любого внешнего ввода.

Шлюз помогает тем, что даёт одно место для отправки response_format и одно место для наблюдения за тем, какие запросы использовали структурированный вывод и как часто повторялись, не заставляя каждый клиент изучать имена полей трёх провайдеров. За OpenAI-совместимым API-шлюзом вы один раз пишете OpenAI-образный response_format, а шлюз направляет его на канал, обслуживающий модель. Помните, что перевод зависит от адаптора: для OpenAI поле проходит без изменений, для Gemini отображается на responseSchema, но нативный output_config.format Claude сейчас из него не создаётся, поэтому для Claude используйте форму Anthropic.

FAQ

В чём разница между JSON-режимом и структурированными выводами?

JSON-режим (response_format: {"type": "json_object"}) гарантирует, что вывод парсится как JSON, но ничего не говорит о его форме — поля, типы и перечисления не ограничены. Структурированные выводы добавляют json_schema с strict: true, что привязывает модель к вашей схеме. Предпочитайте структурированные выводы всякий раз, когда провайдер их поддерживает; оставляйте голый JSON-режим для случаев, где у вас действительно нет фиксированной формы.

Почему моя строгая схема отклоняется, хотя валидируется в других местах?

Строгое подмножество уже полного JSON Schema. Обычные причины — отсутствующее additionalProperties: false, свойство вне required или корень, который не является объектом. Сделайте каждое поле обязательным, используя обнуляемые объединения для опциональных, задайте additionalProperties: false на каждом объекте и оберните массив верхнего уровня в объект. Для Claude дополнительно удалите числовые и строковые ограничения и рекурсивные схемы: они отклоняются напрямую.

Гарантирует ли структурированный вывод, что значения корректны?

Нет. Он гарантирует, что ответ соответствует форме схемы — типам, обязательным полям, допустимым значениям перечислений, — а не что контент точен. Модель может вернуть идеально типизированный, но неверный ответ. Всегда валидируйте значения против ваших бизнес-правил после парсинга и трактуйте соответствие схеме как гарантию синтаксиса, а не корректности.

Как получить вывод с ограничением по схеме от Claude?

Для типизированного ответа отправьте output_config с format вида {"type": "json_schema", "schema": ...}. Если модель должна что-то вызвать, добавьте strict: true к принудительно выбранному инструменту. Обе возможности нативны на актуальных моделях; старый приём с принудительным инструментом без strict по-прежнему работает, но теперь это запасной вариант, а не основной путь.

Удалите одно регулярное выражение

Возьмите одно место в коде, где вы сейчас парсите свободнотекстовый ответ модели и извлекаете JSON регулярным выражением. Замените промпт на json_schema или output_config.format Claude, задайте strict: true и оберните распарсенный результат в валидатор схемы. Затем отправьте намеренно сложный ввод и подтвердите две вещи: запрос проходит без очистки JSON от ограждений, а отказ или обрезание становятся обработанной веткой, а не падением. Удаление этого регулярного выражения — конкретный признак, что ограничение выполняет работу.

Ссылки