Если модель должна вернуть финальный объект заданной формы, используйте Structured Outputs через text.format. Если до ответа нужно получить живые данные, вызвать ваш API или изменить состояние системы, используйте Function Calling. Объединяйте их только тогда, когда результат инструмента нужно затем превратить в типизированный финальный ответ.
Главная граница проходит не по JSON: обе функции могут работать со схемой. Она проходит по владельцу следующего шага. Structured Outputs завершает ответ модели. Function Calling создаёт запрос, который ещё должно проверить и выполнить ваше приложение.
| Ситуация | Правильный контракт | Кто делает следующий шаг | Доказательство успеха |
|---|---|---|---|
| Классифицировать уже переданный текст и вернуть UI-карточку | text.format | Модель формирует финальный объект | Schema разобрана, значения прошли бизнес-проверку |
| Получить актуальный статус заказа | Function Calling | Backend вызывает систему заказов | Есть tool result из источника данных |
| Получить статус и вернуть карточку фиксированной формы | Function Calling + text.format | Backend выполняет 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. Нужно вернуть карточку для интерфейса, а не искать заказ во внешней системе. Здесь функция ничего не добавляет.
pythonimport 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, а затем возвращает результат модели.
javascriptimport 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:
javascriptconst 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»
Используйте эту лестницу приёмки:
- Transport: запрос завершился, нет
incompleteили API error. - Shape: JSON разобран, schema соблюдена, refusal обработан.
- Meaning: значения согласуются с вводом, правилами и source of truth.
- 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 — следует проектировать вокруг этой границы, а не вместо неё.



