До сих пор модель отвечала свободным текстом. Человеку так удобно, а программе — нет. Допустим, вы разбираете входящие заявки: нужно достать имя клиента, телефон и тему обращения и записать их в таблицу. Если попросить модель «верни имя и телефон», один раз она ответит «Имя: Иван, телефон: ...», другой раз добавит вежливое вступление, третий — переставит поля. Разбирать такие ответы регулярными выражениями — путь к постоянным поломкам.
Structured outputs решают эту проблему: вы описываете схему ответа, а модель гарантированно возвращает данные строго по ней. После урока вы сможете получать от модели готовые объекты Python с нужными полями и типами и встраивать модель в программы так же надёжно, как обычную функцию.
Что уже умеем
Вы умеете отправлять запросы через client.responses.create, задавать instructions, вести диалог через историю сообщений и previous_response_id. Во всех этих случаях результатом был текст в response.output_text. Сегодня результатом станет объект с полями.
Что вы узнаете в рамках статьи
- почему разбирать свободный текст ответа ненадёжно;
- как описать схему ответа классом Pydantic;
- как получить ответ по схеме методом
client.responses.parseи полемoutput_parsed; - как задать списки, вложенные объекты и фиксированный набор значений;
- как подсказать модели смысл полей и что делать, если данных нет.
Ключевые термины
- Structured outputs — режим, в котором модель возвращает ответ строго по заданной JSON-схеме.
- JSON — текстовый формат данных из объектов «ключ — значение» и списков.
- JSON Schema — описание структуры JSON: какие поля есть, какого они типа, какие обязательны.
- Pydantic — библиотека Python для описания структур данных классами с проверкой типов.
- Модель Pydantic — класс-наследник
BaseModel, поля которого задают схему ответа. - output_parsed — поле ответа, в котором лежит готовый объект Pydantic.
Почему свободный текст ненадёжен
Попробуем извлечь данные «по-старому»:
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-luna",
instructions="Извлеки из заявки имя и телефон. Верни JSON.",
input="Здравствуйте! Меня зовут Олег, перезвоните на +7 900 123-45-67.",
)
print(response.output_text)
Что увидим в консоли: скорее всего, что-то похожее на JSON. Но иногда он будет обёрнут в блок кода с тройными кавычками, иногда ключи окажутся name и phone, а иногда имя и телефон, иногда перед JSON появится фраза «Вот результат:». Вызов json.loads на таком ответе рано или поздно упадёт. Просьба в инструкциях — это пожелание, а не гарантия.
Схема ответа через Pydantic
Pydantic позволяет описать нужную структуру обычным классом. Пакет openai превращает класс в JSON Schema, передаёт её модели и разбирает ответ обратно в объект. Если Pydantic ещё не установлен, выполните pip install pydantic.
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class Lead(BaseModel):
name: str
phone: str
topic: str
response = client.responses.parse(
model="gpt-6-luna",
instructions="Извлеки данные клиента из заявки.",
input="Здравствуйте! Меня зовут Олег, перезвоните на +7 900 123-45-67, "
"хочу узнать про доставку.",
text_format=Lead,
)
lead = response.output_parsed
print(lead)
print(lead.name, "|", lead.phone)
Что увидим:
name='Олег' phone='+7 900 123-45-67' topic='доставка'
Олег | +7 900 123-45-67
Разберём, что изменилось. Вместо create вызывается client.responses.parse, а схема передаётся параметром text_format. Результат берётся не из output_text, а из output_parsed, и это уже объект класса Lead: к полям обращаемся через точку, редактор подсказывает их имена, а типы гарантированы. Модель физически не может вернуть лишнее поле или пропустить обязательное — схема ограничивает генерацию.
Списки, вложенность и фиксированные значения
Схемы могут быть сложнее. Поле может быть списком, другим классом Pydantic или одним значением из заранее заданного набора — для этого используют Literal из модуля typing.
from typing import Literal
from pydantic import BaseModel
class Item(BaseModel):
product: str
quantity: int
class Order(BaseModel):
customer: str
items: list[Item]
urgency: Literal["low", "normal", "high"]
response = client.responses.parse(
model="gpt-6-luna",
instructions="Разбери заказ из сообщения клиента.",
input="Это Мария. Срочно нужно 3 картриджа и 2 пачки бумаги к завтрашнему утру!",
text_format=Order,
)
order = response.output_parsed
print(order.customer, order.urgency)
for item in order.items:
print(f"- {item.product}: {item.quantity}")
Что увидим:
Мария high
- картридж: 3
- бумага: 2
Поле urgency может принять только одно из трёх значений, поэтому его безопасно использовать в условиях if: опечатки вроде "High" или "срочно" исключены. Это особенно ценно для классификации: категория обращения, тональность, приоритет. Поле quantity объявлено как int, и вы получаете число, а не строку «3 шт.».
Описания полей и отсутствующие данные
Модель видит имена полей, но не всегда понимает, что вы под ними имеете в виду. Подсказку можно дать через Field(description=...): описание попадёт в схему, и модель его прочитает. А если данных в тексте может не быть, объявите поле как допускающее None — иначе модель будет вынуждена что-то придумать, чтобы заполнить обязательное поле.
from pydantic import BaseModel, Field
class Ticket(BaseModel):
category: Literal["billing", "delivery", "technical", "other"]
summary: str = Field(description="Суть проблемы одним предложением")
order_id: str | None = Field(description="Номер заказа, если он указан, иначе null")
response = client.responses.parse(
model="gpt-6-luna",
instructions="Классифицируй обращение в поддержку.",
input="Не могу войти в личный кабинет, пишет 'неверный пароль'.",
text_format=Ticket,
)
print(response.output_parsed)
Что увидим:
category='technical' summary='Пользователь не может войти в личный кабинет из-за ошибки пароля.' order_id=None
Номера заказа в тексте нет, и модель честно вернула None вместо выдуманного значения. Помните, что схема гарантирует форму ответа, а не его правильность: модель всё ещё может ошибиться с категорией. Для важных решений проверяйте результат и тестируйте схему на разных примерах.
Частые ошибки
- Вызывать
createи читатьoutput_parsed. Разбор по схеме делает методclient.responses.parseс параметромtext_format; уcreateполеoutput_parsedне заполнится. - Передавать в
text_formatобъект вместо класса. Нужен сам классLead, а неLead()и не строка"Lead". - Делать все поля обязательными. Если данных может не быть, используйте
str | None, иначе модель начнёт их выдумывать. - Использовать
strтам, где значения ограничены. Для категорий и статусов беритеLiteral— тогда в коде не придётся сравнивать с десятком вариантов написания.
Наиболее частые вопросы
- Можно ли обойтись без Pydantic? — Можно передать JSON Schema вручную, но классы Pydantic короче, понятнее и сразу дают объект с проверкой типов.
- Занимает ли схема токены? — Да, схема передаётся модели и учитывается во входных токенах. Длинные описания полей немного увеличивают стоимость.
- Гарантирует ли схема правильность данных? — Она гарантирует структуру и типы. Содержимое генерирует модель, поэтому его по-прежнему нужно проверять.
- Можно ли использовать structured outputs в диалоге? — Да,
parseпринимает те жеinput,instructionsиprevious_response_id, что иcreate.
Итоги
Отличный результат! Теперь модель для вас не только собеседник, но и надёжный компонент программы: вы описываете схему классом Pydantic, вызываете client.responses.parse и получаете готовый объект с нужными полями, списками и ограниченными значениями. Вы знаете, как подсказать модели смысл поля и как не заставлять её выдумывать отсутствующие данные.
Этот навык — прямая подготовка к главной теме модуля. Чтобы модель могла вызывать инструменты, она должна возвращать имя функции и аргументы строго по схеме. В следующем уроке вы подключите function calling и напишете настоящий цикл агента своими руками.