JSON и токены ChatGPT: как настроить ответ через API

В этой статье

Если приложение отправляет запрос к ChatGPT и потом разбирает ответ, строки вроде «верни JSON» недостаточно. Модель может вернуть корректный JSON, но с неожиданными полями, или потратить весь доступный объём на длинный ответ. Для программной интеграции нужно отдельно настроить формат, ограничить генерацию и проверить результат после HTTP-запроса.

Сначала разберём слово «токен». API-ключ — это секрет авторизации, его передают в заголовке Authorization. Токены в usage — единицы текста, которые API посчитал во входном запросе и выходном ответе. А max_completion_tokens — верхняя граница для генерируемых токенов. Ни один из них не является самим JSON и не заменяет проверку его структуры.

Для нового кода используйте json_schema

В Chat Completions предпочтительный вариант — Structured Outputs: в response_format передаётся схема, а strict: true просит модель точно ей соответствовать. Поддерживаемое подмножество JSON Schema зависит от модели и текущей версии API, поэтому перед заменой модели стоит свериться с описанием Chat Completions API.

Ниже — минимальный запрос. Он просит вернуть профиль с обязательными полями name и skills. В реальном проекте секрет берётся из переменной окружения, а не вставляется в файл с кодом.

curl --fail-with-body https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d @- <<'JSON' > response.json
{
  "model": "gpt-4o-mini",
  "messages": [
    {
      "role": "system",
      "content": "Ты возвращаешь данные профиля разработчика. Не добавляй пояснений вне JSON."
    },
    {
      "role": "user",
      "content": "Составь профиль для разработчика Node.js, который работает с REST API."
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "profile",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "skills": {
            "type": "array",
            "items": { "type": "string" }
          }
        },
        "required": ["name", "skills"],
        "additionalProperties": false
      }
    }
  },
  "max_completion_tokens": 200
}
JSON

Модель в примере — только рабочий идентификатор для иллюстрации. Если выбранная модель не поддерживает Structured Outputs или конкретное ограничение схемы, запрос нужно адаптировать под её актуальную документацию. Например, не каждая сложная конструкция JSON Schema одинаково доступна во всех моделях.

Ответ Chat Completions содержит оболочку API, а JSON профиля лежит внутри строкового поля choices[0].message.content:

{
  "name": "Разработчик Node.js",
  "skills": ["Node.js", "REST API", "HTTP", "JSON"]
}

То есть приложение сначала читает ответ API, затем парсит содержимое message.content. Не стоит считать, что наличие response_format освобождает от обработки ошибок: сеть может оборваться, API может вернуть ошибку, а бизнес-правила вроде «навыков должно быть не больше десяти» схема сама по себе не проверяет.

Проверка ответа в командной строке

Для быстрой проверки нужно проверить оба уровня: HTTP-ответ и JSON внутри content. Флаг --fail-with-body завершит curl с ошибкой при HTTP 4xx/5xx, а если response.json уже сохранён, jq выполнит минимальную структурную проверку:

jq -e '
  (.choices | type == "array" and length > 0) and
  (.choices[0].message.content | fromjson |
    (.name | type == "string") and
    (.skills | type == "array" and all(.[]; type == "string")))
' response.json

Нулевой код завершения означает, что обязательные поля имеют ожидаемые типы. Отдельно полезно вывести расход токенов:

jq '.usage | {prompt_tokens, completion_tokens, total_tokens}' response.json

В usage API сообщает статистику входа, выхода и общего расхода. Длину строки JSON нельзя использовать как замену этому полю: символы и токены — разные вещи, а в некоторых моделях в completion_tokens входят также reasoning tokens.

То же самое в JavaScript

В серверном Node.js-коде проверка выглядит так. Пакет openai и переменную OPENAI_API_KEY настраивают по официальному quickstart для JavaScript. Ключ не должен попадать в браузер, логи или репозиторий.

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const completion = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [
    { role: "system", content: "Верни профиль только в JSON." },
    { role: "user", content: "Профиль backend-разработчика для резюме" }
  ],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "profile",
      strict: true,
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          skills: { type: "array", items: { type: "string" } }
        },
        required: ["name", "skills"],
        additionalProperties: false
      }
    }
  },
  max_completion_tokens: 200
});

if (!completion.choices?.[0]?.message?.content) {
  throw new Error("API не вернул содержимое сообщения");
}

let profile;
try {
  profile = JSON.parse(completion.choices[0].message.content);
} catch (error) {
  throw new Error("content не является JSON", { cause: error });
}

if (
  typeof profile.name !== "string" ||
  !Array.isArray(profile.skills) ||
  !profile.skills.every((skill) => typeof skill === "string")
) {
  throw new Error("Профиль не прошёл проверку типов");
}

console.log(profile);
console.log(completion.usage);

Эта проверка отвечает на вопрос «можно ли безопасно разобрать ответ и передать его следующему компоненту». Она не отвечает на вопрос «правильны ли факты внутри полей». Например, строка name может быть синтаксически корректной, но не соответствовать данным пользователя. Смысловые ограничения проверяются отдельно — обычным кодом приложения или дополнительным валидатором.

Когда нужен json_object

response_format.type = "json_object" — более старый JSON mode. Он просит API вернуть валидный JSON, но не описывает набор полей и их типы. Инструкцию о JSON всё равно нужно явно оставить в system или user-сообщении. Иначе можно получить ошибку запроса или не тот формат поведения, который ожидался.

Если приложению достаточно произвольного объекта, json_object остаётся практичным вариантом. Если downstream-код ожидает конкретный контракт, лучше задать json_schema и strict: true, а затем всё равно провалидировать обязательные поля на своей стороне. Схема на границе API уменьшает количество защитного кода, но не отменяет обработку ошибок.

Для новых интеграций не смешивайте синтаксис Chat Completions с Responses API: это соседний современный интерфейс OpenAI со своими полями запроса и ответа. Выберите один интерфейс по документации и используйте его формат последовательно. В этой статье примеры относятся именно к /v1/chat/completions.

max_completion_tokens и типичная путаница

Старый параметр max_tokens в актуальном коде лучше не использовать: в документации он помечен устаревшим и несовместим с o-series. Для Chat Completions задавайте max_completion_tokens. Это верхняя граница генерируемых токенов, включая видимый ответ и reasoning tokens, а не общий лимит контекста всего запроса.

Если JSON иногда обрывается, сначала проверьте эту границу и ошибку API. Увеличьте лимит настолько, чтобы помещалась ваша схема и нормальный ответ, но не превращайте его в бесконечный запас. После этого проверьте finish_reason и валидируйте содержимое до записи в базу или передачи в очередь.

И ещё раз о названии «токен ChatGPT». API-ключ не покупает и не выдаёт токены текста: это секрет, которым приложение авторизует запрос. Сколько текстовых токенов ушло на запрос, видно в usage; сколько разрешено сгенерировать — в max_completion_tokens. Такое разделение быстро находит ошибку в конфигурации.

Если вам нужен именно пользовательский доступ к ChatGPT, а не API-ключ и не пополнение баланса OpenAI API, можно посмотреть варианты доступа к цифровым AI-сервисам в каталоге Amber Market. Условия нужно сверить в карточке перед заказом; для подходящих заказов доступна оплата через СБП. Каталог не заменяет настройку API и не выдаёт ключ для вашего серверного запроса.

Практический маршрут поэтому короткий: задайте поддерживаемую схему через json_schema, ограничьте генерацию max_completion_tokens, проверьте HTTP и JSON.parse, затем провалидируйте поля и сохраните usage. После такой границы JSON становится контрактом между моделью и приложением, а не удачным совпадением текста с ожиданиями разработчика.

Источники

Есть следующая задача?Ещё по теме «Работа с API» →