Вызов модели — это сетевой вызов чужого сервиса
Работа с LLM API: продвинутое · Урок 1 / 21
Модель находится за проводом
Со стороны кода обращение к модели выглядит как одна строка библиотеки. На деле это HTTP-запрос в чужой дата-центр: TLS-рукопожатие, балансировщик, очередь на стороне провайдера, счётчик квоты по вашему ключу и заметная вероятность, что соединение оборвётся на середине ответа. Всё, что вы уже умеете делать с внешними зависимостями — таймауты, повторы, деградация, кэш — переносится сюда без изменений.
Отличий два, и оба неприятные. Вызов длится не миллисекунды, а секунды или десятки секунд, поэтому обычный клиентский таймаут в 3 секунды бесполезен. И вызов стоит денег, поэтому неудачный повтор — это не только потерянное время, но и списанные средства.
Из чего состоит запрос
POST /v1/chat/completions
Authorization: Bearer [[ключ]]
Content-Type: application/json
{
"model": "имя-модели",
"messages": [список сообщений с ролями],
"temperature": 0.2,
"max_tokens": 512,
"stream": false
}
Тело запроса — это данные, а не команда. Модель не выполняет ваш JSON, она получает его как контекст и продолжает текст. Отсюда следует всё дальнейшее: нет гарантированного соответствия запроса и ответа, есть вероятностное соответствие, которое вы повышаете параметрами и схемами.
Из чего состоит ответ
{
"id": "идентификатор вызова",
"model": "фактически отработавшая версия",
"choices": [
{ "message": {"role": "assistant", "content": "текст"},
"finish_reason": "stop" }
],
"usage": {"prompt_tokens": 812, "completion_tokens": 143, "total_tokens": 955}
}
Поле usage — единственный достоверный источник данных о деньгах: считайте расход по нему, а не по своим оценкам длины текста. Поле model показывает, какая версия отработала на самом деле, и оно расходится с запрошенным именем чаще, чем принято думать — псевдонимы вроде latest переезжают на новую версию без вашего участия.
finish_reason важнее содержимого
- stop — модель закончила сама. Ответ можно считать полным.
- length — упёрлись в max_tokens. Текст обрезан на полуслове, JSON невалиден. Это не ошибка HTTP, статус будет 200.
- tool_calls — модель не отвечает текстом, а просит вызвать функцию. Ваш content при этом пустой.
- content_filter — сработала фильтрация. Ответа нет, ретрай того же запроса не поможет.
Код, который читает choices[0].message.content и не смотрит на finish_reason, однажды сохранит в базу обрезанный текст и не заметит этого.
Что измерять с первого дня
У вызова модели есть три величины, которые нужно снимать сразу, иначе потом их взять неоткуда: полная задержка от отправки до последнего байта, время до первого байта ответа и расход токенов по каждому направлению. Первая величина определяет ваши таймауты, вторая — ощущение скорости у пользователя, третья — счёт. Все три сильно зависят от длины контекста, поэтому усреднять их по всем вызовам подряд бессмысленно: разбивайте по типу запроса.
Шпаргалка
- Вызов модели — нестабильная сетевая зависимость, а не вызов функции.
- Расход берите из usage, а не из длины строки.
- finish_reason проверяйте всегда: length и content_filter приходят со статусом 200.
- Фактическая версия модели живёт в ответе, а не в вашем конфиге.