Что это решает
Первый запрос к модели собирается за две минуты копипастом чужого curl, и до продакшена этого хватает. Дальше начинается: ответ обрывается на середине предложения, раз в час прилетает 429, счёт за месяц вдвое выше прикидки, а модель «не держит формат». Каждая из этих четырёх бед чинится конкретным полем тела запроса или конкретным полем ответа, которое обычно никто не читает. Разбор состава запроса нужен затем, чтобы дёргать нужный рычаг, а не переписывать промпт наугад в третий раз.
Как устроено
Транспорт и ключ
Снаружи это обычный HTTPS POST с JSON в теле. Обязательных заголовка два: Authorization: Bearer <ключ> и Content-Type: application/json. Ключ живёт на сервере. В браузерном бандле или в мобильном приложении его достанут за вечер, а платить будет владелец ключа. Рабочая схема одна: клиент ходит в собственный бэкенд, бэкенд — к провайдеру, и он же считает лимиты по пользователям и пишет логи.
Эндпоинтов несколько — текстовая генерация (классический chat completions и более поздний формат с единым полем входа), эмбеддинги, аудио, изображения, файлы, пакетная обработка. Форма везде одинаковая: JSON внутрь, JSON или поток событий наружу.
Тело запроса
| Поле | Чем управляет | Когда трогать |
|---|---|---|
model | какая модель считает запрос | вынести в конфиг: имена моделей живут своей жизнью, перевыкатывать код из-за них не надо |
messages (или input в новом формате) | сам диалог — массив сообщений с ролями | всегда |
tools, tool_choice | описания функций приложения, которые модель может вызвать, и принуждение к вызову | когда ответ должен опираться на данные системы, а не на память модели |
response_format / JSON-схема | форма ответа вплоть до строгой схемы | всегда, когда ответ разбирает код, а не человек |
temperature, top_p | разброс при выборе следующего токена | вниз — извлечение данных, классификация, расчёты; выше — черновики текстов |
max_tokens / max_output_tokens | потолок длины ответа | ставить всегда: это ещё и предохранитель по деньгам |
stop | строки, на которых генерация обрывается | жёсткие форматы, разделители |
stream | отдавать ответ частями | интерфейсы, где ждёт живой человек |
seed | попытка повторяемости выдачи | тесты и отладка |
user, metadata | метка запроса для логов и разбора злоупотреблений | многопользовательские продукты |
Роли и память
Сообщения различаются ролями. Системная (в новых форматах — роль разработчика) задаёт рамки: кто отвечает, в каком формате, чего не делать. Пользовательская несёт задачу. Ассистентская — прошлые ответы модели. Отдельная роль возвращает результат вызова инструмента.
Сервер диалог не хранит. Каждый запрос уезжает целиком, со всей историей, которую приложение склеило само. Часть API умеет держать состояние на своей стороне по ссылке на предыдущий ответ, но по умолчанию обрезка старых реплик, сжатие их в резюме и порядок сообщений — работа приложения.
Токены и деньги
Оплата и лимиты считаются в токенах, отдельно на вход и на выход. Из этого следует три вещи. Диалог дорожает с каждой репликой, потому что предыдущие уезжают заново. Регламент на десять страниц, вставленный в системное сообщение, оплачивается при каждом обращении, а не один раз. И постоянную часть промпта держат в начале и неизменной — так работает кэширование префикса; всё изменяемое отправляют в хвост.
Что приходит в ответ
Кроме текста в ответе есть два поля, ради которых его и читают. finish_reason говорит, почему генерация закончилась: нормально, упёрлась в лимит длины, ушла в вызов инструмента, попала под фильтр. usage показывает фактический расход токенов на вход и выход. Обрезанный JSON после finish_reason: length не распарсится, и виновата тут не модель, а отсутствие проверки в коде.
Вызов инструментов
Цикл на четыре такта. Первый: запрос уходит со списком tools — имя функции, описание, JSON-схема аргументов. Второй: модель возвращает не текст, а вызов — имя, аргументы и идентификатор вызова. Третий: приложение исполняет функцию само и добавляет в историю сообщение с результатом и тем же идентификатором. Четвёртый: запрос повторяется с расширенной историей, и модель формулирует ответ.
Модель ничего не исполняет — она формирует намерение, решение звать функцию принимает код. Аргументы приходят от модели и проверяются как любой внешний ввод: схема не мешает прислать несуществующий идентификатор заказа или отрицательное количество.
Стриминг
При stream: true ответ приходит потоком серверных событий: куски с дельтами текста и финальный маркер. Лечит «интерфейс молчит десять секунд». Плата — усложнённая обработка ошибок: соединение уже открыто, код 200 уже отдан, и падение в середине надо ловить отдельно. Счётчики токенов в потоке приходят в конце и только если попросить их явной опцией.
Ошибки и повторы
| Код | Что произошло | Реакция |
|---|---|---|
| 400 | тело запроса или схема не годятся | чинить код, повтор бессмысленен |
| 401, 403 | ключ, права, регион | чинить доступ |
| 404 | модель недоступна проекту | сверить имя модели и доступы |
| 429 | лимит запросов, лимит токенов или кончился баланс | пауза и повтор с ростом задержки |
| 5xx, таймаут | сторона провайдера | повтор с ростом задержки |
Повторять имеет смысл только 429 и 5xx: экспоненциальный бэкофф со случайной добавкой, конечное число попыток, уважение к заголовку Retry-After. У повторного POST есть риск выполнить операцию дважды — на это в API есть заголовок идемпотентности. Идентификатор запроса из заголовков ответа пишется в лог всегда: без него в поддержке разговаривать не о чем.
Полезные сценарии
- Классификация входящих обращений в строгую JSON-схему с полем уверенности и маршрутизацией спорных случаев на человека.
- Извлечение полей из писем, счетов и накладных в структуру, готовую к записи в базу.
- Вызов инструментов: поиск по каталогу, чтение остатков, черновик заказа — ответ собирается из данных системы, а не из памяти модели.
- Стриминг ответа в интерфейс чата, чтобы первые слова появлялись сразу, а не после полной генерации.
- Пакетная обработка архива: тысячи однотипных запросов уезжают файлом и считаются отложенно, дешевле поштучных вызовов.
Ограничения
- Состояния нет. Помнить диалог — задача приложения, и оно же платит за историю на каждом шаге.
- Контекстное окно конечно. Обрезка, сжатие и приоритет сообщений пишутся руками, автоматически ничего не ужимается.
- Полной повторяемости не будет даже при нулевой температуре и заданном seed. Тесты строятся на проверке свойств ответа, а не на побайтовом сравнении.
- Строгая схема гарантирует форму ответа; правдивость значений внутри неё проверяет код.
- Лимиты по запросам и токенам в минуту делают наивный цикл
forпо тысяче документов неработоспособным — нужна очередь с ограничением параллелизма. - Данные уезжают в чужой контур. Для персональных данных и коммерческой тайны нужны отдельные меры: обезличивание, договор, локальная модель.
- Привязка к провайдеру глубже, чем кажется: отличаются имена моделей, формат инструментов, поведение фильтров, подсчёт токенов. Прослойку делают тонкой, но делают.
- Задержка чужого сервиса не под контролем. Синхронный вызов модели внутри обработки веб-запроса рано или поздно упрётся в таймаут.
Как проверить результат
- Отправить минимальный запрос через curl и прочитать в ответе
usageиfinish_reason, а не только текст. - Специально выставить крошечный
max_tokensи убедиться, что код видит обрыв и не пытается разобрать неполный JSON. - Посчитать токены промпта локально до отправки и сверить с
usage— расхождение покажет, что в запрос уезжает лишнее. - Собрать golden set из 20–50 реальных примеров с эталонными ответами и прогонять его после каждой правки промпта. Сравнение «на глаз» перестаёт работать на третьей итерации.
- Проверить ретраи на заглушке, отдающей 429: число попыток конечно, задержка растёт,
Retry-Afterучитывается, лог не превращается в бесконечный цикл. - Убедиться, что идентификатор запроса и расход токенов попадают в лог по каждому вызову, включая неудачные.
Правило: всё, что должно быть предсказуемым, задаётся полями запроса и проверяется кодом; словами в промпте задаются только пожелания.
