Как добиться стабильного JSON-ответа от AI API: структурированный вывод, Schema и обработка ошибок (2026)
JSON mode, JSON Schema и проверка на стороне клиента: практическая инструкция со snippets для Python и Node.js, обработкой SSE, повторными запросами и защитой от невалидных ответов.
Как добиться стабильного JSON-ответа от AI API: структурированный вывод, Schema и обработка ошибок (2026)
Короткий ответ: одной фразы «верни JSON» в промпте недостаточно. Надёжная интеграция сочетает три уровня: параметр структурированного вывода, чёткое описание полей в промпте и строгую проверку ответа на стороне клиента. Тогда лишний комментарий модели или изменение маршрута не сломает ваш код.
Почему «похожий на JSON» ответ не подходит программе
Модель может добавить Markdown-ограждение, вступительную фразу, пропустить кавычку или вернуть число строкой. Для человека такой ответ понятен, но JSON.parse() его не примет. В потоковом режиме проблема ещё заметнее: до события завершения вы получили только часть объекта.
Поэтому нужно отдельно проверять синтаксис JSON и соответствие бизнес-схеме. Валидный JSON всё ещё может содержать неправильные поля.
Сначала проверьте возможности провайдера
У OpenAI-совместимых сервисов набор параметров различается:
- JSON mode обычно гарантирует валидный JSON, но не наличие всех полей.
- JSON Schema / Structured Outputs дополнительно ограничивает структуру объекта.
- При отсутствии этих режимов остаётся промпт и проверка в приложении.
Не считайте параметр поддерживаемым только потому, что endpoint называется OpenAI-compatible. Ориентируйтесь на документацию и реальный ответ. Ошибка 400 invalid parameter означает, что несовместимый параметр нужно убрать, а не повторять тот же запрос.
Проектируйте небольшую и однозначную схему
Допустим, нужно классифицировать обращение в поддержку:
{
"category": "billing",
"priority": "high",
"confidence": 0.92,
"needs_human": false
}
Практические правила:
- Используйте стабильные имена полей на английском, а текст переводите на уровне интерфейса.
- Ограничивайте варианты через перечисление (
enum), чтобы не получать синонимы. - Явно различайте числа, строки, массивы и Boolean.
- Не добавляйте в Schema поля, которые не нужны бизнес-логике.
- Если нужен строгий контракт, запретите дополнительные поля.
Пример системной инструкции:
Ты классификатор обращений. Верни только один JSON-объект без Markdown и пояснений.
category: только billing, technical или account;
priority: только low, normal или high;
confidence: число от 0 до 1;
needs_human: Boolean.
Промпт снижает вероятность ошибки, но не заменяет валидатор.
Python: запрос и проверка полей
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ["AI_BASE_URL"],
)
response = client.chat.completions.create(
model=os.environ["AI_MODEL"],
temperature=0,
messages=[
{"role": "system", "content": (
"Верни только JSON. category: billing, technical или account; "
"priority: low, normal или high; confidence: число 0..1; "
"needs_human: Boolean."
)},
{"role": "user", "content": "Пользователь не может пополнить баланс: платёж отклонён."},
],
# Оставляйте параметр только если провайдер его поддерживает
response_format={"type": "json_object"},
)
raw = response.choices[0].message.content or ""
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise RuntimeError(f"Ответ не является корректным JSON: {raw[:200]}") from exc
if (
not isinstance(data, dict)
or data.get("category") not in {"billing", "technical", "account"}
or data.get("priority") not in {"low", "normal", "high"}
or not isinstance(data.get("confidence"), (int, float))
or not 0 <= data["confidence"] <= 1
or not isinstance(data.get("needs_human"), bool)
):
raise RuntimeError(f"Поля ответа не соответствуют контракту: {data}")
В рабочем проекте лучше использовать Pydantic или JSON Schema. Регулярные выражения не подходят для проверки вложенных объектов.
Node.js: парсинг без опасной «чистки» строки
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AI_API_KEY,
baseURL: process.env.AI_BASE_URL,
});
const completion = await client.chat.completions.create({
model: process.env.AI_MODEL,
temperature: 0,
messages: [
{ role: "system", content: "Верни только JSON без Markdown и пояснений." },
{ role: "user", content: "Классифицируй отзыв: платёж не проходит при пополнении." },
],
response_format: { type: "json_object" },
});
const raw = completion.choices?.[0]?.message?.content ?? "";
let result;
try {
result = JSON.parse(raw);
} catch {
throw new Error(`Ответ не является JSON: ${raw.slice(0, 200)}`);
}
if (typeof result.category !== "string" || typeof result.needs_human !== "boolean") {
throw new Error("В ответе отсутствует обязательное поле или неверный тип");
}
Не полагайтесь только на replace("```json", ""): это не исправляет обрезанный ответ и неправильные типы. Если нужно поддержать старую модель, можно аккуратно удалить кодовое ограждение, но после этого всё равно обязательны JSON.parse() и проверка Schema.
Как повторять запрос после ошибки
Повторяйте только исправимые ошибки:
- Записывайте request ID, модель, HTTP-код и первые 200 символов ответа с удалением чувствительных данных. API Key в логах быть не должно.
- Для тайм-аута и 5xx используйте экспоненциальную задержку, например 1, 2 и 4 секунды, с ограниченным числом попыток.
- При HTTP 200 и невалидном JSON можно один раз отправить модели краткое сообщение об ошибке и попросить исправить формат.
- При 400 из-за неподдерживаемого параметра удалите
response_formatили Schema и повторите совместимый вариант. - После исчерпания попыток передайте задачу в ручную обработку. Не записывайте обрезанный объект в базу.
Потоковый ответ разбирайте только после завершения
В каждом событии SSE может находиться лишь часть JSON. Сначала соберите весь текст и дождитесь события завершения, затем выполните JSON.parse(). Если пользователю нужно показывать текст сразу, отделите поток предпросмотра от итогового структурированного результата. О сбоях SSE подробно рассказано в руководстве по прерыванию потокового вывода AI API.
Чек-лист перед публикацией
- Проверена поддержка
response_formatили Schema у провайдера - Протестированы обычные, пограничные и пустые входные данные
- Проверены обязательные поля, перечисления и типы
- Раздельно обработаны timeout, 429, 5xx и невалидный JSON
- В логах есть request ID, но нет ключей и чувствительного содержимого
- Неполный ответ никогда не записывается в рабочую базу
- После смены модели или маршрута запущены регрессионные тесты
Итог: параметр структурированного вывода задаёт направление, Schema решает, принимать ли результат, а повтор и запасной сценарий сохраняют работоспособность сервиса. Только сочетание всех трёх уровней даёт поддерживаемую интеграцию JSON API.
Полезные материалы:
- Вызов OpenAI-совместимого API через curl, Python и Node.js
- Что делать при ошибке вызова AI API
- Как отлаживать прерывание потокового вывода SSE
Дата обновления: 7 сентября 2026 года