Smoke-тесты LLM-провайдера: что проверить перед релизом
Smoke-тесты LLM-провайдера помогают быстро поймать битую авторизацию, пустой стрим, кривой JSON и обрезанный контекст до запуска в прод.

Где провайдер ломается первым
Первые сбои почти всегда скучные. Провайдер редко падает красиво и очевидно. Чаще он отвечает так, будто все в порядке, и именно поэтому smoke-тесты LLM-провайдера лучше запускать до первого релизного трафика, а не после него.
Обычно все начинается с мелочей. Токен уже просрочен или выпущен не для того проекта. Запрос уходит как обычно, SDK молчит, а в ответ вы получаете 401, 403 или странный объект ошибки. Бывает и хуже: шлюз принимает запрос, но режет его по правам уже на стороне модели.
Со стримингом сбой часто маскируется под успех. API возвращает 200, открывает SSE-поток и не присылает ни одного нормального токена. Клиент висит несколько секунд, потом закрывает соединение, а в логах как будто был обычный ответ. Для чат-интерфейса это выглядит как пустая реплика, для бэкенда - как таймаут без ясной причины.
С JSON похожая история. Модель обещает объект, но добавляет перед ним пояснение, ломает кавычки или закрывает массив не той скобкой. На демо это переживают. В проде такой ответ валит парсер, ретраи и следующий шаг пайплайна.
Длинный промпт ломается тише всего. Клиент, прокси или сам провайдер могут обрезать контекст еще до отправки в модель. Ответ при этом приходит, но модель "забывает" начало инструкции, теряет вложенные данные или отвечает так, будто половины текста не было.
Именно эти четыре сбоя стоит ловить в первые пять минут. Они встречаются чаще, чем редкие аварии дата-центра, и чинятся быстрее, если тест сразу показывает точку отказа.
Что должен покрывать один smoke-пакет
Хороший smoke-пакет не измеряет качество модели. Он отвечает на более простой вопрос: провайдер вообще живой, и ваш клиент может с ним нормально работать. Для этого обычно хватает четырех коротких запросов.
Первый запрос идет без стрима. Он ловит битую авторизацию API провайдера, неверный base_url, неожиданный формат ошибки и пустой content в обычном ответе.
Второй запрос идет с stream=true. Здесь важна не красота текста, а механика. Сервер должен отдать чанки, поток не должен зависнуть, финальный кусок должен прийти, а соединение не должно рваться после первого токена.
Третий запрос просит ответ строго по JSON-схеме. Даже сильные модели иногда добавляют лишний текст, комментарий перед объектом или ломают типы полей. Такой тест быстро показывает, можно ли дальше доверять ответу по цепочке.
Четвертый запрос проверяет длину контекста. Вставьте длинный промпт с заметной меткой в начале и в конце, а затем попросите модель повторить обе метки. Если конец сохранился, а начало исчезло, контекст режется раньше, чем обещано.
Эти четыре проверки занимают минуты, но закрывают самые частые поломки перед релизом. Не стоит усложнять пакет на старте. Один сценарий на каждый риск работает лучше, чем десять похожих тестов, которые никто не смотрит.
Как собрать набор за час
Не пытайтесь сразу покрыть все режимы и все модели. Для первого прохода хватит одной модели на каждого провайдера. Берите по одному стабильному варианту и прогоняйте через него одинаковый набор запросов. Так быстрее видно, где ломается интеграция, а где проблема уже в самой модели.
Параметры запроса лучше зафиксировать сразу. Если температура, max_tokens и seed меняются от прогона к прогону, сравнение теряет смысл. Даже простой smoke-тест начинает шуметь: один и тот же промпт сегодня дал JSON, а завтра обычный текст, хотя ошибка не в провайдере, а в настройках.
Удобнее держать один шаблон запроса для всех и менять только имя модели. Если вы гоняете маршруты через единый OpenAI-совместимый endpoint, например в RU LLM, это особенно удобно: меняется base_url, а сам пакет остается тем же.
Минимальный набор выглядит просто: короткий non-stream запрос на 20-30 токенов, stream-запрос с ответом в одну фразу, запрос на JSON по строгой схеме и длинный запрос с большим контекстом и коротким итогом. Смысл в том, что все провайдеры получают один и тот же вход. Тогда различия видны сразу.
Логируйте не только факт успеха. Сохраняйте HTTP-код, тело ответа и время до первого токена. Для stream полезно писать еще первый chunk и признак завершения потока. Один статус 200 почти ничего не доказывает.
Результаты лучше складывать в простую таблицу: провайдер, модель, тест, код ответа, TTFT, длина ответа, ошибка парсинга. После такого прогона сразу понятно, кого можно пускать дальше, а кого стоит чинить до релиза.
Проверка авторизации
Авторизация ломается чаще, чем кажется. Токен может истечь, попасть не в тот заголовок или пройти через прокси так, что ошибка станет похожа на обычный ответ модели.
Для smoke-теста хватит короткого промпта, который легко узнать по ответу. Например: "Ответь одним словом: OK". С рабочим токеном модель должна вернуть нормальный результат без сюрпризов: корректный HTTP-статус, ожидаемую структуру ответа и сам текст "OK" или близкий по смыслу ответ.
Потом отправьте тот же запрос с заведомо битым токеном. Здесь тест не должен гадать. Он ждет 401 или 403 и считает любой 200 ошибкой, даже если внутри текста написано unauthorized или invalid api key.
Этого обычно достаточно:
- рабочий токен дает успешный ответ за разумное время;
- битый токен дает 401 или 403;
- тело ошибки приходит в понятном формате, а не как псевдо-успешный ответ модели;
- пустой токен и токен с лишним символом дают одинаково строгий отказ.
Проверку ротации ключей тоже полезно добавить. Если вы перевыпустили ключ, тест должен покраснеть в ту же минуту, когда старый секрет потерял силу. Иначе команда еще несколько часов будет думать, что все в порядке.
Проверка стрима
Стрим ломается чаще, чем обычный ответ. Запрос принимает stream=true, соединение открывается, но полезного текста нет. Пользователь видит, что ответ как будто идет, а читать нечего.
Для smoke-теста подойдет короткий вопрос, на который модель почти всегда отвечает сразу. Например: "Напиши одно слово: тест". Такой запрос убирает лишний шум. Если стрим ведет себя странно, дело обычно не в промпте и не в размере ответа.
Проверка стриминга LLM должна смотреть на четыре вещи. Первый чанк должен прийти быстро и не быть пустым. Среди событий должны быть чанки с текстом, а не только служебные поля. Собранный текст после удаления пробелов не должен быть пустым. И в конце поток должен завершиться с finish_reason, иначе клиенту трудно понять, ответ закончился нормально или оборвался на полуслове.
Эта проверка хорошо ловит тихие поломки после смены SDK, прокси или провайдера. Если сомневаетесь, полезно прогнать тот же запрос и через промежуточный слой, и прямым вызовом к провайдеру. Так проще понять, где именно рвется поток.
Проверка JSON-ответа
Если провайдер обещает JSON mode, лучше проверить это кодом, а не верить на слово. Частая поломка выглядит безобидно: модель возвращает почти правильный объект, но в конце дописывает пояснение, ломает экранирование или меняет тип поля. Глаз это пропустит, парсер нет.
Для smoke-теста задайте простую и жесткую форму ответа. Хватает объекта из трех полей, например status, reason, lang. Попросите вернуть только JSON без префиксов, markdown и лишнего текста. Чем схема проще, тем быстрее вы поймете, где рвется ответ.
Проверка должна идти в два шага. Сначала заберите сырое тело ответа и разберите его обычным JSON-парсером. Потом отдельно проверьте, что объект содержит ровно нужные поля, типы не съехали, после закрывающей } нет хвоста, а строки нормально переживают кириллицу, кавычки внутри текста и переносы строк.
Именно так ловятся ответы вида:
{"status":"ok","reason":"Готово","lang":"ru"}
Комментарий:
На вид все почти правильно, но продовый обработчик на этом упадет. То же бывает, когда провайдер отдает "умные" кавычки вместо обычных или дважды экранирует \n.
Хороший тест на валидный JSON ответа проходит быстро и падает громко. Если парсер не смог разобрать ответ, сохраняйте сырой текст целиком. По одному такому логу проблему обычно видно за минуту.
Проверка длины контекста
Провайдеры часто пишут одно число в документации, а держат меньше на деле. Самый простой способ поймать это - проверить, сколько текста модель реально видит целиком.
Возьмите длинный промпт и вставьте редкий маркер в самое начало и в самый конец, например BEGIN_7XQ1 и END_9KP4. Потом дайте модели узкую команду: вернуть оба маркера в точности, без пересказа и пояснений, одной строкой. Если модель видит весь вход, она повторит оба значения. Если один маркер пропал, вы уже нашли место, где контекст начал резаться.
Обычно хватает простого подхода: начать с объема, который точно меньше лимита, увеличивать вход крупными шагами, после первого сбоя уточнить предел меньшими шагами и записать реальный максимум для этой модели и этого провайдера.
Смотрите не только на сам факт сбоя, но и на его форму. Если пропадает маркер в начале, провайдер или прокси срезает старую часть промпта. Если исчезает маркер в конце, запрос мог не пройти целиком, либо клиент неверно считает токены. В проде это дает разные ошибки, поэтому разница важна.
Полезно проверять и запас. Если провайдер заявляет 128k, не останавливайтесь на 127k. Добавьте системный промпт, короткую историю диалога и ожидаемый размер ответа. На практике рабочий предел часто ниже еще на 10-20%.
В конце у вас должна остаться простая таблица: обещанный лимит, найденный лимит и размер безопасного рабочего окна. Для релиза важнее третье число.
Как это выглядит перед релизом
Типичный сценарий простой. Команда переносит чат-бота на нового провайдера вечером перед выкладкой. Базовый запрос "ответь словом ok" проходит, и всем кажется, что интеграция жива. Но такой тест почти ничего не проверяет.
Небольшой smoke-пакет занимает 5-10 минут и дает куда больше пользы. Сначала вы отправляете короткий запрос в обычном режиме и в stream. Потом просите строгий JSON с полем на кириллице. После этого даете длинную system-инструкцию с правилами бота и проверяете, видит ли модель начало и конец. В самом конце ломаете токен и смотрите, какой код ошибки приходит на самом деле.
На практике сбой часто выглядит цепочкой. Базовый запрос работает, команда переключает base_url, а утром бот в стриме молчит. Следом падает JSON-режим, потому что ответ пришел с "красивыми" кавычками. Еще через час выясняется, что длинная инструкция обрезалась, и бот перестал соблюдать первые ограничения.
Именно поэтому smoke-тесты стоит хранить рядом с кодом интеграции и запускать на каждом новом провайдере до переключения трафика.
Где сами тесты ошибаются
Многие наборы проверок подводят не потому, что идея плохая, а потому что тесты слишком добрые. Они проверяют только нормальный сценарий и считают работу законченной. В итоге релиз проходит, а первая ошибка в проде приходит от просроченного токена, пустого стрима или сломанной сериализации.
Самая частая промашка - гонять только успешный запрос. Если тест ни разу не отправил вызов с неверным API-ключом, вы не знаете, как провайдер ведет себя при отказе. Одни возвращают понятный 401, другие отдают 200 с текстом об ошибке внутри тела. Если смотреть только на статус, такой сбой легко пропустить.
Не меньше проблем дает вывод "одна модель работает, значит у провайдера все нормально". Это слишком смело. У одного и того же провайдера по-разному ведут себя hosted-модели, reasoning-модели и старые совместимые маршруты.
Еще несколько частых ошибок встречаются постоянно: тест читает только HTTP-код и не разбирает тело ответа; считает любой stream успешным, даже если пришел один пустой chunk; верит JSON "на глаз" и не проверяет обязательные поля; падает без сохранения сырого ответа и заголовков.
Последний пункт недооценивают чаще всего. Без сырого тела, request id и первых чанков стрима команда тратит лишние 30-40 минут на повтор ошибки. Иногда проблема вообще плавающая: один ответ обрезал контекст, второй вернул невалидный JSON, третий сработал нормально. Без артефактов такое почти не разобрать.
Хороший smoke-набор должен быть немного недоверчивым. Он проверяет и хороший путь, и плохой, и делает это хотя бы на двух моделях провайдера, если маршрут важен для продакшена.
Что запускать перед включением трафика
Перед тем как пускать нового провайдера в прод, прогоните один короткий пакет запросов. Он должен ответить на пять простых вопросов.
- Работает ли обычный запрос с рабочим токеном без ручных правок в коде.
- Возвращает ли битый токен явную ошибку 401 или 403.
- Отдает ли stream хотя бы один текстовый чанк и завершает ли поток с
finish_reason. - Возвращает ли модель строгий JSON, который сразу проходит через парсер.
- Видит ли модель оба маркера в длинном промпте, то есть проходит ли проверка длины контекста.
Этого набора обычно хватает, чтобы понять, можно ли доверять маршруту. Один невалидный JSON ломает интеграцию сразу, а обрезанный контекст дает тихую ошибку, которую команда замечает слишком поздно.
Что делать дальше
Если пакет уже ловит битую авторизацию, пустой стрим, кривой JSON и обрезанный контекст, не оставляйте его ручной проверкой перед релизом. Самая частая ошибка здесь очень простая: тесты есть, но их никто не гоняет регулярно.
Запускайте набор в двух режимах. Первый - в CI перед выкладкой. Второй - по расписанию, хотя бы раз в несколько часов для тех моделей и провайдеров, через которые идет живой трафик. Так вы поймаете поломку не только после своего релиза, но и после чужого изменения на стороне провайдера.
Результаты лучше хранить как историю, а не как последний статус. Полезно записывать модель, провайдера, время, код ответа, число чанков в стриме, признак валидного JSON и факт прохождения теста на длину контекста. Через пару недель такая таблица быстро покажет, кто падает редко, а кто сыпется каждую пятницу вечером.
Рабочий минимум выглядит так:
- гонять smoke-пакет в CI на каждый релиз;
- запускать те же тесты по расписанию;
- сохранять историю падений по моделям и провайдерам;
- повторять проверки после смены модели, SDK и параметров;
- не подстраивать набор тестов под одного конкретного провайдера.
Последний пункт особенно полезен, если вы переключаете маршруты через общий шлюз. В RU LLM такой пакет можно прогонять без смены SDK и клиентского кода: достаточно заменить base_url на api.rullm.com и сравнить поведение разных провайдеров и моделей в одном формате. Это удобно, когда нужно быстро проверить, остались ли предсказуемыми авторизация, стрим и формат ответа.
И еще одно правило стоит закрепить сразу: если тест упал на новом типе сбоя, не чините только текущий кейс. Добавьте этот сценарий в постоянный набор, чтобы он больше не возвращался тихо.
Часто задаваемые вопросы
Что проверять в первую очередь у нового LLM-провайдера?
Начните с четырех вещей: обычный запрос без стрима, запрос со stream=true, строгий JSON по простой схеме и длинный промпт с маркерами в начале и конце. Этот набор быстро показывает, живы ли авторизация, поток, парсер и окно контекста.
Сколько тестов нужно для первого smoke-пакета?
Обычно хватает четырех коротких запросов. Не пытайтесь сразу покрыть все режимы и все модели: один понятный сценарий на каждый риск дает больше пользы, чем большой набор, который никто не смотрит.
Зачем проверять битый токен отдельно, если рабочий уже отвечает?
Отдельный негативный тест нужен всегда. Отправьте тот же запрос с заведомо неверным токеном и ждите 401 или 403; если провайдер отвечает 200, даже с текстом про ошибку внутри, считайте это поломкой интеграции.
Как понять, что stream сломан, если API вернул 200?
Смотрите не на один статус, а на поведение потока. Первый чанк должен прийти быстро, в событиях должен быть текст, собранный ответ не должен быть пустым, а в конце нужен finish_reason; иначе клиент легко зависнет или закроет соединение как таймаут.
Какой JSON-тест лучше сделать минимальным?
Возьмите очень простую схему, например объект с полями status, reason и lang. Потом проверьте сырое тело обычным JSON-парсером и убедитесь, что после закрывающей } нет хвоста, типы не съехали, а кавычки и кириллица не ломают разбор.
Как быстро проверить реальную длину контекста?
Самый надежный способ — вставить редкие маркеры в начало и конец длинного промпта и попросить модель вернуть их дословно. Если исчезает начало, кто-то режет старую часть входа; если пропадает конец, запрос не дошел целиком или клиент неверно считает токены.
Нужно ли запускать smoke-тесты на всех моделях сразу?
Для первого прохода не нужно гнать весь парк. Возьмите по одной стабильной модели на каждого провайдера, а для важных маршрутов добавьте еще одну модель другого типа, чтобы не принять частный сбой за норму у всего провайдера.
Что логировать, кроме статуса ответа?
Сохраняйте HTTP-код, сырое тело ответа, время до первого токена, первый чанк стрима и признак нормального завершения потока. Когда тест упадет, эти данные покажут точку отказа за минуты, а не после долгих повторов.
Когда лучше запускать такой пакет?
Запускайте пакет до первого трафика, в CI перед релизом и по расписанию для живых маршрутов. Тогда вы поймаете не только свои ошибки после выкладки, но и чужие изменения на стороне провайдера.
Что делать, если smoke-тест нашел новый тип сбоя?
Не чините только текущий случай. Добавьте новый сценарий в постоянный набор, чтобы сбой не вернулся тихо после смены модели, SDK, base_url или параметров запроса.