Коротко
Просьба «верни JSON» в промпте - это не гарантия, а вежливое пожелание. Модель может обернуть ответ в объяснение, добавить ```json, поставить запятую в конце или подставить в числовое поле текст вроде <ОСТАТОК>. Парсер после этого падает в проде, а не на тестах.
Надёжность набирается уровнями, и каждый следующий дороже предыдущего в работе:
- Промпт - бесплатно, ловит половину случаев.
- JSON mode - параметр запроса, гарантирует синтаксис, но не структуру.
- Строгая схема (structured output) - гарантирует и поля, и типы.
- Валидация у себя - то, что нужно делать всегда, независимо от трёх предыдущих.
Ниже - код каждого уровня, что именно он гарантирует и где всё равно рванёт.

Structured output (структурированный вывод) - режим, в котором вы передаёте модели описание формата ответа, а сервис гарантирует, что ответ ему соответствует. JSON mode - упрощённый вариант: гарантируется только то, что ответ будет синтаксически корректным JSON, без обещаний про поля.

Уровень 1: попросить в промпте
Так делают все и на этом же обжигаются.
resp = client.chat.completions.create(
model="claude-haiku-4-5",
messages=[
{"role": "system", "content": "Верни только JSON вида {\"category\": str, \"amount\": number}. Без пояснений."},
{"role": "user", "content": "Такси до аэропорта, 1450 рублей"},
],
)
data = json.loads(resp.choices[0].message.content) # рано или поздно упадёт
Что здесь ломается на практике:
- Обёртка в markdown. Ответ приходит внутри
```json … ```, иjson.loadsспотыкается о первую же строку. - Вежливое вступление. «Конечно! Вот результат:» перед объектом.
- Плейсхолдер вместо числа. Модель не хочет считать и подставляет
"amount": "<СУММА>". Формально JSON валидный, а в базу такое не ложится. - Лишние поля. Модель решает, что вам полезно знать её уверенность, и добавляет
confidence, которого вы не просили.
Уровень имеет право на жизнь ровно в двух случаях: одноразовый скрипт и прототип, который вы смотрите глазами.
Уровень 2: JSON mode
Один параметр в запросе - и сервис гарантирует, что ответ разберётся как JSON.
resp = client.chat.completions.create(
model="claude-haiku-4-5",
messages=[...],
response_format={"type": "json_object"},
)
Что вы получаете: синтаксис. Никаких ```, никаких вступлений, ни одной висящей запятой.
Что вы не получаете: обещаний про содержимое. Поле может называться sum вместо amount, число может приехать строкой, вложенный объект может оказаться массивом. Проверено на любом достаточном объёме: из тысячи вызовов десяток приедет с другой формой.
🔴 Грабля: в JSON mode модель обязана вернуть JSON - и если она не понимает запрос, она вернёт корректный JSON с мусором внутри вместо честного «не могу». Отсутствие ошибки перестаёт быть признаком успеха.
Уровень 3: строгая схема
Здесь вы передаёте не пожелание, а описание формата - и сервис не выпускает ответ, который ему не соответствует.
SCHEMA = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["еда", "транспорт", "жильё", "прочее"]},
"amount": {"type": "number"},
"currency": {"type": "string", "enum": ["RUB", "USD"]},
},
"required": ["category", "amount", "currency"],
"additionalProperties": False,
}
resp = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Такси до аэропорта, 1450 рублей"}],
response_format={
"type": "json_schema",
"json_schema": {"name": "expense", "schema": SCHEMA, "strict": True},
},
)
Три вещи, которые делают этот уровень рабочим:
enumвместо свободной строки. Категория больше не может приехать как «Транспорт», «транспортные расходы» или «taxi» - список закрыт.additionalProperties: false. Модель не дописывает поля от себя.required. Поле не может тихо исчезнуть, когда модель не уверена.
Именно enum даёт основной выигрыш. Свободная строка в поле категории - это ваша будущая работа по нормализации: полгода спустя в базе окажется семь написаний одного и того же.
Поддержка режима зависит от модели, а не только от сервиса. Строгую схему держат современные модели OpenAI и Gemini, у остальных придётся спускаться на уровень 2 плюс валидация. Проверять это надо на своей модели до того, как схема попадёт в прод.
Уровень 4: валидация на своей стороне
Единственный уровень, который делается всегда, какой бы режим вы ни включили.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class Expense(BaseModel):
category: Literal["еда", "транспорт", "жильё", "прочее"]
amount: float = Field(gt=0)
currency: Literal["RUB", "USD"]
def parse(raw: str) -> Expense | None:
try:
return Expense.model_validate_json(raw)
except ValidationError as e:
log.warning("модель вернула нерабочий ответ: %s", e)
return None
Что это ловит из того, что не поймала схема:
- Бессмысленные, но валидные значения.
amount: 0,amount: -1450- тип верный, смысл нулевой. Отсюдаgt=0. - Ответ от модели, которая не поддерживает схему, - вы всё равно разбираетесь.
- Смену поведения после обновления модели. Вендор выкатывает новую версию, формат немного едет, и падение вы видите в логе, а не в поддержке.
Ключевой момент - что делать при провале валидации. Рабочих сценариев два: повторить запрос с добавленным текстом ошибки или отправить случай на модель посильнее. Второе дешевле, чем кажется: если строгая схема на лёгкой модели даёт 97% пригодных ответов, то оставшиеся 3% на флагмане почти не двигают счёт.
Сколько это стоит
Считаем на реальной задаче: 50 000 чеков в месяц, примерно 300 входных и 60 выходных токенов на штуку. Цены - по каталогу на 23.08.2026.
| Схема | Стоимость в месяц |
|---|---|
| Всё на Claude Sonnet 5 | $50.84 |
| Всё на Claude Haiku 4.5 | $23.58 |
| Haiku + повтор 3% случаев на Sonnet 5 | $25.11 |
Разница между «сразу флагман» и «лёгкая модель плюс подстраховка» - вдвое, при этом непригодных ответов в обеих схемах примерно поровну.

🔴 Оговорка про длину. Строгая схема не спасает от обрыва: если ответ упёрся в max_tokens, вы получите оборванный JSON. Смотрите finish_reason - при length ответ нужно не парсить, а перезапрашивать. Через шлюз запросы без явного max_tokens режутся на 4096 токенов, и на длинных выгрузках это заметно.
С чего начать
- Опишите формат схемой, а не фразой в промпте. Даже если пока используете JSON mode - схема пригодится как источник правды для валидатора.
- Закройте все перечислимые поля через
enum. Это самая дешёвая правка с самым большим эффектом. - Поставьте валидацию на своей стороне и логируйте каждый провал с текстом ответа.
- Прогоните сотню реальных примеров на лёгкой модели и посмотрите долю брака. Обычно она такая, что флагман не нужен.
- Проверьте
finish_reasonперед разбором ответа.
Все 44 модели с ценами и параметрами - на витрине. Про то, как раздавать задачи разным моделям, - отдельный разбор, а про остальные способы не переплачивать - десять приёмов с расчётами.
FAQ
Что такое structured output простыми словами?
Это режим, в котором вы передаёте модели описание нужного формата ответа - какие поля, каких типов, какие обязательны, - а сервис не выпускает ответ, который этому описанию не соответствует. В отличие от просьбы в промпте, это гарантия, а не пожелание.
Чем JSON mode отличается от строгой схемы?
JSON mode гарантирует только синтаксис: ответ точно разберётся как JSON. Названия полей, их типы и состав он не контролирует - поле может приехать под другим именем или числом-строкой. Строгая схема контролирует и структуру тоже.
Почему модель возвращает JSON внутри markdown-блока?
Потому что её обучали отвечать человеку, а человеку код показывают в блоке. Просьба «без пояснений» уменьшает частоту, но не убирает её. Убирает - response_format в запросе, а не формулировка промпта.
Что делать, если модель подставляет плейсхолдер вместо числа?
Это происходит, когда модель не хочет считать. Лечится двумя вещами сразу: типом number в схеме, который не пропустит строку, и валидацией с ограничением вроде «больше нуля» на своей стороне. Полагаться только на формулировку промпта здесь нельзя.
Все ли модели поддерживают строгую схему?
Нет. Современные модели OpenAI и Gemini держат её штатно, у части остальных доступен только JSON mode. Проверять поддержку нужно на конкретной модели до выката, а код писать так, чтобы он работал и без неё - за счёт валидации у себя.
Нужна ли валидация, если схема строгая?
Да. Схема проверяет типы, но не смысл: сумма -1450 пройдёт как валидное число. Плюс валидация - ваша страховка на случай смены версии модели и единственный способ узнать о поломке из лога, а не из обращения пользователя.