Перейти к основному содержанию

Structured Outputs или Function Calling в OpenAI: что выбрать

9 мин чтенияAPI Гайды

Structured Outputs задаёт форму финального ответа, а Function Calling передаёт приложению запрос на действие. Выбор определяется не наличием JSON Schema, а тем, должен ли backend прочитать или изменить внешнее состояние.

Схема выбора: финальный JSON через Structured Outputs или запрос инструмента, который выполняет приложение

Если модель должна вернуть финальный объект заданной формы, используйте Structured Outputs через text.format. Если до ответа нужно получить живые данные, вызвать ваш API или изменить состояние системы, используйте Function Calling. Объединяйте их только тогда, когда результат инструмента нужно затем превратить в типизированный финальный ответ.

Главная граница проходит не по JSON: обе функции могут работать со схемой. Она проходит по владельцу следующего шага. Structured Outputs завершает ответ модели. Function Calling создаёт запрос, который ещё должно проверить и выполнить ваше приложение.

СитуацияПравильный контрактКто делает следующий шагДоказательство успеха
Классифицировать уже переданный текст и вернуть UI-карточкуtext.formatМодель формирует финальный объектSchema разобрана, значения прошли бизнес-проверку
Получить актуальный статус заказаFunction CallingBackend вызывает систему заказовЕсть tool result из источника данных
Получить статус и вернуть карточку фиксированной формыFunction Calling + text.formatBackend выполняет tool, модель форматирует результатПодтверждены источник, schema и смысл
Просто ответить пользователю текстомОбычный текстМодельТекст решает задачу; JSON не нужен

Stop rule простой: если никакого внешнего чтения или действия нет, не создавайте фиктивную функцию только ради JSON. Если внешний вызов есть, не выдавайте schema-conforming аргументы за доказательство того, что вызов состоялся.

Две похожие схемы отвечают на разные вопросы

Официальное руководство OpenAI по Structured Outputs описывает два способа применить строгую схему:

  • к финальному ответу через text.format;
  • к аргументам function tool через strict: true.

Поэтому формулировка «Structured Outputs против Function Calling» слегка обманчива. Строгая схема может быть частью Function Calling. Но интерфейсы всё равно не взаимозаменяемы:

  • text.format отвечает на вопрос: какой объект вернёт модель пользователю или следующему этапу pipeline?
  • Function Calling отвечает на вопрос: какую работу модель просит выполнить приложение и с какими аргументами?

Сама модель не запускает вашу функцию. В официальном lifecycle Function Calling приложение получает function_call, проверяет имя и аргументы, исполняет код, а затем отправляет function_call_output с тем же call_id. После этого модель может дать финальный ответ или запросить следующий инструмент.

Эта разница особенно важна для write actions. Объект вроде {"order_id":"A-17","action":"cancel"} может идеально соответствовать схеме и всё равно быть неавторизованным, устаревшим или повторным запросом.

Финальный типизированный ответ: один вызов без tool loop

Предположим, текст обращения уже находится в prompt. Нужно вернуть карточку для интерфейса, а не искать заказ во внешней системе. Здесь функция ничего не добавляет.

python
import json from openai import OpenAI client = OpenAI() card_schema = { "type": "object", "properties": { "category": { "type": "string", "enum": ["delivery", "billing", "account", "other"], }, "needs_human": {"type": "boolean"}, "summary": {"type": "string"}, "missing_information": { "type": "array", "items": {"type": "string"}, }, }, "required": [ "category", "needs_human", "summary", "missing_information", ], "additionalProperties": False, } response = client.responses.create( model="YOUR_CURRENT_TEXT_MODEL", input=[ { "role": "developer", "content": ( "Классифицируй обращение. Не придумывай данные заказа. " "Если информации не хватает, перечисли недостающие поля." ), }, { "role": "user", "content": "Посылка не пришла, но номера заказа в сообщении нет.", }, ], text={ "format": { "type": "json_schema", "name": "support_card", "strict": True, "schema": card_schema, } }, ) refusal = next( ( part.refusal for item in response.output if item.type == "message" for part in item.content if part.type == "refusal" ), None, ) if refusal: raise RuntimeError(f"Модель отказалась: {refusal}") card = json.loads(response.output_text) assert card["category"] == "delivery" assert "order_id" in card["missing_information"]

Здесь schema гарантирует наличие четырёх полей и допустимый category. Она не доказывает, что классификация верна. Поэтому пример отдельно запрещает выдумывать заказ и проверяет ожидаемый бизнес-результат. Для production нужны eval cases с двусмысленными обращениями, пустым вводом и текстом, который вообще не относится к поддержке.

Подставьте модель, которая сейчас доступна вашему project и поддерживает нужный путь Structured Outputs. Имя модели и schema support — изменяемые параметры; перед деплоем проверьте их по актуальному model guide, тогда как архитектурная граница между финальным объектом и внешним исполнением остаётся той же.

Function Calling: приложение обязано завершить работу

Теперь пользователь передал номер заказа и просит актуальный статус. Модель не владеет этой информацией. Правильный ответ требует чтения system of record.

Ниже JavaScript-пример показывает не только tool definition, но и участок, который часто пропускают: backend действительно вызывает getOrderStatus, а затем возвращает результат модели.

javascript
import OpenAI from "openai"; const openai = new OpenAI(); const tools = [ { type: "function", name: "get_order_status", description: "Получить текущий статус заказа из системы заказов", strict: true, parameters: { type: "object", properties: { order_id: { type: "string", description: "Идентификатор заказа, явно переданный пользователем", }, }, required: ["order_id"], additionalProperties: false, }, }, ]; async function getOrderStatus({ order_id }) { if (!/^A-\d+$/.test(order_id)) { return { ok: false, error: "INVALID_ORDER_ID" }; } // Здесь находится настоящий вызов вашей базы или order API. return { ok: true, order_id, status: "in_transit", checked_at: new Date().toISOString(), }; } const first = await openai.responses.create({ model: "YOUR_CURRENT_TEXT_MODEL", input: "Проверь актуальный статус заказа A-17.", tools, tool_choice: "auto", }); const toolOutputs = []; for (const item of first.output) { if (item.type !== "function_call") continue; if (item.name !== "get_order_status") { throw new Error(`Запрещённый инструмент: ${item.name}`); } const args = JSON.parse(item.arguments); const result = await getOrderStatus(args); // Код выполняет приложение. toolOutputs.push({ type: "function_call_output", call_id: item.call_id, output: JSON.stringify(result), }); } if (toolOutputs.length === 0) { throw new Error("Модель не запросила обязательное живое чтение"); } const final = await openai.responses.create({ model: "YOUR_CURRENT_TEXT_MODEL", previous_response_id: first.id, input: toolOutputs, tools, }); console.log(final.output_text);

В stateful flow previous_response_id связывает продолжение с предыдущим ответом. Если приложение работает stateless, оно должно вернуть необходимые output items, включая relevant reasoning items для соответствующих reasoning models. Не отбрасывайте контекст tool call, а call_id не заменяйте собственным случайным идентификатором.

Для опасных действий одного allowlist имени недостаточно. Перед оплатой, отменой, отправкой письма или изменением аккаунта backend повторно проверяет identity, authorization, свежесть данных, лимиты и idempotency key. После вызова он читает system of record и подтверждает фактический результат.

Гибрид нужен после инструмента, а не вместо него

Допустим, frontend принимает только объект:

json
{ "order_id": "A-17", "display_status": "В пути", "can_contact_support": false, "source_checked_at": "2026-07-28T09:00:00Z" }

Первый Responses call должен запросить статус, приложение — получить его, а второй call — превратить подтверждённый tool result в финальную schema. Для этого во втором запросе можно добавить text.format и запретить новый tool call:

javascript
const orderCardFormat = { type: "json_schema", name: "order_card", strict: true, schema: { type: "object", properties: { order_id: { type: "string" }, display_status: { type: "string" }, can_contact_support: { type: "boolean" }, source_checked_at: { type: "string" }, }, required: [ "order_id", "display_status", "can_contact_support", "source_checked_at", ], additionalProperties: false, }, }; const typedFinal = await openai.responses.create({ model: "YOUR_CURRENT_TEXT_MODEL", previous_response_id: first.id, input: toolOutputs, tools, tool_choice: "none", text: { format: orderCardFormat }, });

Такой hybrid оправдан, если типизированный объект действительно нужен downstream-системе. Если пользователю достаточно строки «заказ в пути», второй schema contract лишь добавит complexity. Если данные уже находились в исходном prompt, не нужен и первый tool call.

Что именно гарантирует strict: true

Для function tools strict mode OpenAI требует:

  • additionalProperties: false для каждого object;
  • все properties перечислены в required;
  • бизнес-опциональное значение представлено nullable type, например ["string", "null"].

Это гарантирует форму аргументов, но не следующие свойства:

Не гарантируетсяКто проверяет
Выбран правильный toolПолитика приложения, allowlist, evals
order_id существует и принадлежит пользователюBackend и system of record
Действие разрешеноAuthentication/authorization layer
Повторный call безопасенIdempotency и deduplication
Запись реально измениласьПовторное чтение или подтверждение API
Значения финальной schema правдивыSemantic validator и source comparison

tool_choice тоже не переносит эту ответственность модели. Режим required может заставить модель вызвать один или несколько tools, а forced tool — выбрать конкретное имя, но ни один режим не выдаёт разрешение на side effect.

Отказ, плохой ввод и ошибка инструмента — разные ветки

Structured Outputs могут вернуть программно различимый refusal вместо обычного parsed object. Это не «сломанный JSON»: приложение должно обработать отказ отдельно.

Есть и другой случай: ввод безопасен, но несовместим с задачей. Если schema требует order_status: string, а пользователь не дал номер заказа, модель может заполнить поле правдоподобным значением, чтобы соблюсти форму. Предусмотрите missing_information, nullable value или явный status вроде cannot_determine.

Tool error также нельзя скрывать за красивым финальным объектом. Возвращайте модели ограниченный машинно-читаемый результат, например:

json
{"ok": false, "error": "ORDER_SERVICE_TIMEOUT", "retryable": true}

Затем решайте в приложении, разрешён ли retry и сколько попыток допустимо. Для write tools автоматический повтор без idempotency обычно хуже явной ошибки.

Четыре проверки вместо одного «200 OK»

Используйте эту лестницу приёмки:

  1. Transport: запрос завершился, нет incomplete или API error.
  2. Shape: JSON разобран, schema соблюдена, refusal обработан.
  3. Meaning: значения согласуются с вводом, правилами и source of truth.
  4. Effect: внешний вызов выполнен один раз, результат подтверждён в целевой системе.

Structured Outputs в первую очередь закрывают второй уровень. Function Calling организует переход к внешней работе, но не закрывает третий и четвёртый автоматически.

Минимальный regression set должен содержать happy path, отсутствующий идентификатор, неверный идентификатор, refusal, timeout инструмента, запрещённый tool, повторный call_id/одинаковое действие и schema-valid, но семантически неверный объект.

JSON mode — запасной формат, а не третий маршрут

JSON mode гарантирует синтаксически валидный JSON, но не соответствие конкретной schema. OpenAI рекомендует Structured Outputs, когда они поддерживаются.

Оставляйте JSON mode только как ограниченный migration/fallback для совместимости. Он не заменяет semantic validation и тем более не превращает ответ модели в выполненный tool call.

Перед внедрением

  • Для финального объекта начните с text.format.
  • Для live data или actions определите function tool и application-owned handler.
  • В strict schema закройте required и additionalProperties.
  • Добавьте отдельные ветки refusal, incompatible input и tool error.
  • Для write action проверьте authorization и idempotency до исполнения.
  • Подтвердите результат в system of record после исполнения.
  • Добавляйте hybrid только при реальном downstream schema contract.

Если у вас ещё нет рабочего project key и первого Responses call, начните с руководства по настройке OpenAI API. А если вы переносите уже существующий tool loop с Chat Completions, используйте пошаговую миграцию Function Calling в Responses API.

Правильный выбор виден по границе ответственности: модель либо завершает типизированный ответ, либо просит приложение выполнить работу. Всё остальное — schema, strict, retries и формат UI — следует проектировать вокруг этой границы, а не вместо неё.

#OpenAI API#Structured Outputs#Function Calling#Responses API#JSON Schema
Поделиться: