Вызов модели — это сетевой вызов чужого сервиса

Работа с 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, однажды сохранит в базу обрезанный текст и не заметит этого.

Что измерять с первого дня

У вызова модели есть три величины, которые нужно снимать сразу, иначе потом их взять неоткуда: полная задержка от отправки до последнего байта, время до первого байта ответа и расход токенов по каждому направлению. Первая величина определяет ваши таймауты, вторая — ощущение скорости у пользователя, третья — счёт. Все три сильно зависят от длины контекста, поэтому усреднять их по всем вызовам подряд бессмысленно: разбивайте по типу запроса.

Инсайт. Статус 200 не означает, что ответ пригоден. Ответ пригоден, когда finish_reason равен stop и содержимое прошло вашу валидацию — это два разных условия, и проверять надо оба.
Частая ошибка. Замерять расход по длине строки в символах. Один и тот же текст на кириллице и латинице даёт разное число токенов, а системная часть промпта в вашу строку вообще не входит.
Про-совет. Логируйте id ответа и поле model рядом с каждым результатом. Когда через месяц придёт жалоба на качество, это единственный способ понять, какая версия её породила.

Шпаргалка

  • Вызов модели — нестабильная сетевая зависимость, а не вызов функции.
  • Расход берите из usage, а не из длины строки.
  • finish_reason проверяйте всегда: length и content_filter приходят со статусом 200.
  • Фактическая версия модели живёт в ответе, а не в вашем конфиге.
1. Что означает finish_reason со значением length?
2. Откуда брать данные о расходе токенов?
3. Почему поле model в ответе стоит логировать?

🔒 Ответьте на вопрос верно, чтобы перейти к следующему уроку.

Вызов модели — это сетевой вызов чужого сервиса — Работа с LLM API: продвинутое — Skilvy