Перейти к содержимому
18 апр. 2026 г.·7 мин чтения

Регрессионные тесты после обновления SDK для LLM API

Регрессионные тесты после обновления SDK помогают быстро найти поломки в чатах, эмбеддингах, инструментах и стриминге до выката в прод.

Регрессионные тесты после обновления SDK для LLM API

Что обычно ломается после обновления SDK

После обновления SDK чаще ломается не сам запрос, а ожидания вокруг него. Код отправляет промпт как раньше, ответ приходит, но парсер ищет поле по старому пути, не находит нужное значение и ведет себя так, будто модель вернула пустой ответ.

Чаще всего меняется форма ответа. Новая версия SDK может иначе отдавать choices, по-другому называть служебные поля, добавлять обертку для tool_calls или менять структуру usage-метрик. Если команда работает через OpenAI-совместимый шлюз, такие сдвиги легко пропустить: endpoint тот же, а клиентская библиотека уже ждет другой JSON.

Отдельная боль - ошибки и ретраи. Старый код ловил один тип исключения, новый SDK возвращает другой. Приложение перестает повторять временные сбои или, наоборот, начинает делать лишние повторы. В проде это видно быстро: растет задержка, прыгает счетчик ошибок, а причина простая - изменилась логика разбора 429, 500 или сетевых таймаутов.

Таймауты и отмена тоже часто ведут себя иначе. Один SDK считает timeout на весь запрос, другой делит его на connect и read. Где-то отмена прерывает только локальную корутину, а где-то сразу закрывает HTTP-соединение. В чатах это заметно на длинных ответах, в эмбеддингах - на пакетной обработке, где один зависший батч тормозит всю очередь.

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

Обычно стоит проверить четыре зоны:

  • формат успешного ответа и обязательные поля;
  • типы ошибок и условия ретраев;
  • таймауты, отмену и повторное подключение;
  • расхождение между моками и реальным API.

Поломка редко выглядит как большой сбой. Гораздо чаще это один null, один новый event в стриминге или одно исключение другого класса. Именно такие мелочи потом ломают чат, инструменты и биллинг сразу в нескольких местах.

Какие сценарии проверить в первую очередь

После обновления не нужно гонять все подряд. Сначала проверьте те сценарии, которые ломаются тихо: другое имя поля, новый формат стриминга, лишняя валидация схемы инструмента, неожиданная длина эмбеддинга. Такие проблемы чаще всего и доходят до прода.

Если у вас OpenAI-совместимый endpoint, держите порядок проверок простым. Сначала докажите, что живы базовые сценарии. Редкие ветки можно оставить на потом.

  1. Начните с простого чата без инструментов. Отправьте короткий запрос и проверьте роль, текст ответа, finish_reason, usage и код ответа. Это самый дешевый тест, и он быстро показывает, цел ли базовый контракт.
  2. Затем прогоните чат с system-сообщением и длинной историей. Здесь часто всплывают ошибки сериализации массива messages, потеря контекста и смена порядка сообщений.
  3. После этого дайте запрос на эмбеддинги. Смотрите не только на статус 200, но и на размер вектора, тип данных и форму ответа. Если раньше приложение ждало 1536 чисел, а после обновления получает другой размер, проблема уже есть.
  4. Дальше проверьте вызов инструмента с валидной JSON Schema. Новые версии SDK нередко меняют имена полей, упаковку аргументов или способ чтения tool_calls.
  5. В конце включите стриминг и оборвите его раньше времени со стороны клиента. Такой тест ловит зависшие соединения, плохое закрытие потока и утечки обработчиков.

Этого набора уже хватает, чтобы быстро понять, где искать проблему: в чатах, эмбеддингах, инструментах или в транспорте.

Как собрать набор тестов шаг за шагом

Лучше собирать регрессионный набор не с нуля, а из живых запросов, которые уже работают в проде. Возьмите 10-20 реальных кейсов: короткий чат, длинный чат с историей, эмбеддинги для нескольких строк, вызов инструмента и ответ в режиме стриминга. Если вы тестируете еще и смену маршрута, не меняйте промпты, payload и base_url одновременно. Иначе потом трудно понять, что именно сломалось.

До обновления сохраните для каждого запроса эталон. Нужен не только текст ответа модели, но и весь полезный след: HTTP-статус, структура JSON, названия полей, finish_reason, usage, лимиты, время ответа и текст ошибок. Для эмбеддингов важна длина вектора. Для инструментов - форма tool_calls и аргументы. Для стриминга - порядок событий и признак завершения потока.

Процесс обычно выглядит так:

  1. Зафиксируйте рабочие запросы на старой версии SDK.
  2. Сохраните ответы как JSON-снимки и отдельно запишите ожидаемые статусы, лимиты и формат ошибок.
  3. Обновите SDK и прогоните тот же набор кейсов без ручных правок.
  4. Сравните старый и новый результат по схеме JSON, кодам ответа и служебным полям, а не только по тексту.
  5. Соберите расхождения в отдельный отчет и разделите их на совместимые и несовместимые.

Текст ответа сам по себе мало о чем говорит. Модель может выразить ту же мысль другими словами, и это нормально. Проблемы обычно сидят в деталях: поле переименовали, null заменили на пустой массив, стриминг начал слать другой тип события, SDK перестал передавать выбор инструмента в старом формате.

Как оформить отчет

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

К критичным поломкам относятся 4xx или 5xx вместо 200, потеря обязательного поля, неверный тип данных и обрыв потока SSE. Изменения контракта - это новые имена полей, другой формат tool_calls, новая структура usage или иной тип исключения. Допустимые отличия - это другой текст при той же форме ответа.

Такой отчет удобно читать и разработчику, и тимлиду. По нему быстро видно, можно выкатывать новую версию SDK или сначала надо править клиентский код.

Проверки для чатов

Чатовые запросы чаще ломаются не на уровне HTTP, а в мелочах: поле пропало, роль поменялась, ошибка разбирается иначе. Поэтому в тестах для чатов смотрите не только на статус ответа, но и на саму форму данных.

Начните с двух запросов: один короткий, другой с историей на 4-6 сообщений. В каждом ответе сверяйте role, content и finish_reason. Проверяйте пустые строки, массивы частей и специальные символы. Если SDK внезапно вернул null там, где раньше был stop, это уже повод остановиться и разобраться.

Отдельно ловите потерю ролей во входных сообщениях. После обновления клиент может склеить system и user, отбросить первое системное сообщение или изменить порядок массива messages. Это легко не заметить: ответ приходит, но стиль, ограничения и формат уже другие. Поэтому полезно сравнивать не только ответ, но и тело запроса, которое реально ушло в API.

Параметры генерации тоже стоит проверить явно. Возьмите один и тот же промпт и прогоните его с temperature=0 и temperature=1, затем с двумя значениями max_tokens. Здесь не нужен одинаковый текст. Важно другое: модель отвечает, лимит токенов работает, а SDK не переименовал поле и не проигнорировал его.

Ошибки лучше тестировать отдельно. Для 400 проверьте, что клиент нормально показывает причину, например неверный формат messages. Для 429 убедитесь, что код и ветка ретраев работают так, как вы задумали. Для 500 посмотрите, не падает ли ваш парсер на неожиданном теле ответа.

Если вы используете OpenAI-совместимый шлюз, гоняйте те же тесты через него без правок в коде. Так легче понять, осталась ли совместимость именно в чатовых сценариях.

Проверки для эмбеддингов

Проверьте резервную модель сразу
Гоняйте один набор кейсов на основной и резервной модели через один API.

Эмбеддинги часто ломаются тихо. Запрос проходит, статус 200 на месте, а поиск по базе начинает давать странные результаты. Поэтому первый тест здесь очень простой: возьмите один и тот же текст, ту же модель и сравните длину вектора до и после обновления.

Если размерность внезапно изменилась, проблема часто не в самой модели, а в том, как SDK собрал запрос или разобрал ответ. Для векторной базы это неприятный сбой: запись не сохранится или сохранится рядом с чужими данными.

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

Еще одна частая поломка - порядок элементов в batch-запросе. Отправьте несколько разных строк в одном вызове и убедитесь, что порядок в ответе не съехал. Даже простого набора вроде "кот", "договор" и "ракета" хватает, чтобы поймать ошибку. Если индексы перепутаются, вы сохраните корректные векторы к чужим текстам.

Сравните и типы данных. После обновления массив эмбеддинга может приехать в другом числовом типе, поле index может стать строкой, а usage - исчезнуть или сменить формат. На тестовом стенде это выглядит как мелочь, но в памяти, сериализации и кеше такие отличия быстро превращаются в баг.

Хорошая практика - хранить эталонный ответ для нескольких фиксированных входов и сравнивать только стабильные поля. request_id, служебные метки и другую изменчивую часть лучше не трогать, иначе тесты начнут шуметь без пользы.

Проверки для инструментов

После обновления SDK вызовы инструментов ломаются особенно тихо. Чат отвечает, токены считаются, а функция уже не вызывается или получает аргументы в другом виде. Поэтому проверяйте не только сам факт вызова, но и точную форму запроса и ответа.

Сначала сравните схему инструмента в старой и новой версии. Частая проблема выглядит банально: SDK меняет имя поля, убирает required, по-другому упаковывает parameters или переводит описание функции во внутренний формат. Если тест смотрит только на финальный ответ модели, вы этого не увидите.

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

Часто ломаются вложенные объекты. Например, модель вызывает create_order, а в аргументах есть customer, delivery и массив items. Старый SDK отправлял объект как есть, новый вдруг превращает часть полей в строки или выбрасывает пустой массив. На простом демо это легко пропустить, а в проде заказ уже нельзя разобрать без лишних костылей.

Полезно сохранять сырое тело запроса и ответа. Тогда сразу видно, где ошибка: в вашем коде, в SDK или в адаптации формата на стороне клиента.

Еще один обязательный тест - второй шаг после ответа инструмента. Много проблем всплывает именно здесь: вы передали tool result, а SDK поменял роль сообщения, потерял tool_call_id или положил результат не в тот массив. В итоге модель не продолжает диалог, а снова просит вызвать тот же инструмент.

Нормальный сценарий выглядит просто: модель вызывает функцию, тест подставляет реальный результат инструмента, затем делает второй запрос и проверяет, что модель использует полученные данные, а не запускает цикл заново.

Проверки для стриминга

Стриминг ломается тише других функций. Ответ вроде приходит, но клиент теряет первые чанки, путает порядок токенов или не получает событие завершения. После обновления полезно прогонять один и тот же запрос в обычном режиме и в стриминге, а потом сравнивать результат целиком.

Для такого теста лучше взять короткий и стабильный промпт с предсказуемым ответом. Так проще заметить, что SDK начал склеивать чанки в другом порядке или обрезать хвост.

Что фиксировать в тесте

В тесте стриминга полезно сохранять:

  • время до первого токена;
  • порядок чанков по мере получения;
  • наличие финального события и finish_reason;
  • корректное закрытие соединения после отмены.

Задержку до первого токена измеряйте отдельно от общего времени ответа. После обновления SDK иногда появляется лишний буфер, и пользователь видит паузу в 1-2 секунды, хотя сама генерация идет нормально. Без стримингового теста такой сбой легко пропустить.

Отмену тоже проверяйте явно. Запустите длинный ответ, прервите поток на стороне клиента и убедитесь, что соединение закрывается без зависших сокетов, повторных чанков и ошибок в логах. Если отмена работает плохо, под нагрузкой это быстро превращается в лишние расходы и странные таймауты.

В конце соберите полный ответ из всех чанков и сравните его с ответом того же запроса без стриминга. Текст у недетерминированных моделей не обязан совпадать символ в символ, но структура, смысл и признак завершения должны быть одинаковыми.

Где ошибаются сами тесты

Тестируйте с логами в РФ
Запускайте запросы через маршрут, где логи и бэкапы хранятся на серверах в РФ.

Часто падает не SDK и не API, а сами тесты. Это особенно заметно там, где команда ждет от модели один и тот же текст посимвольно. После обновления меняется порядок полей, формат служебных данных или мелкие детали генерации, и тест красный, хотя для пользователя все работает нормально.

Для чатов лучше проверять то, что действительно важно: статус запроса, структуру сообщения, роль, наличие текста, корректный JSON у вызова инструмента и ожидаемый смысл ответа. Если модель должна вернуть короткое резюме, не сравнивайте каждую запятую. Сравните обязательные фразы, формат и ограничение по длине.

Еще одна типичная ошибка - старые фикстуры. Команда один раз сохранила удачный ответ и потом годами сверяет все только с ним. Но после смены провайдера, версии модели или клиента ответ может немного отличаться даже при том же промпте. Эталонные примеры лучше собирать из свежего обезличенного трафика и обновлять по понятным правилам.

Плохой набор тестов почти всегда слишком добрый. Он проверяет только успешный запрос и пропускает ошибки. А потом в проде выясняется, что пустой массив messages ломает обработчик, некорректная tool schema проходит локально, эмбеддинги для пустой строки ведут себя иначе, а стриминг обрывается на середине.

Есть еще один слепой угол - метрики. Если тесты не измеряют latency и размер ответа, команда пропустит неприятную деградацию. После обновления ответ может приходить на 800 мс позже или занимать вдвое больше токенов. Формально совместимость сохранилась, а стоимость и скорость уже изменились.

Быстрый чек-лист перед релизом

Перед выкладкой полезно разделять быстрый прогон и полный набор. Smoke-тесты отвечают на вопрос, жив ли основной путь для чатов, эмбеддингов, инструментов и стриминга. Полный прогон нужен отдельно, потому что один удачный запуск часто скрывает мелкие, но опасные расхождения.

Перед релизом достаточно пройтись по короткому списку:

  • запустить smoke-тесты и полный набор раздельно и сохранить оба отчета;
  • прогнать старый и новый SDK на одних и тех же кейсах с одинаковыми промптами и параметрами;
  • проверить хотя бы две модели: основную и резервную;
  • открыть логи вручную и убедиться, что в них есть request_id и текст ошибки.

Сравнивайте не только статус ответа. Смотрите на форму данных: пришел ли message.content, сохранились ли вызовы инструментов, совпал ли размер вектора эмбеддингов, не оборвался ли поток SSE после первого чанка. Именно здесь новые версии SDK чаще всего и ломаются.

Если новый клиент работает только на одной модели, а на резервной дает другой формат ошибки или теряет request_id, релиз лучше остановить. Час на проверку обычно дешевле, чем ночной разбор в проде.

Простой сценарий миграции

Сравните провайдеров на одном API
Один и тот же сценарий удобно проверить на разных моделях и маршрутах.

Команда обновила SDK в пятницу вечером и решила, что риск невелик: обычный чатовый запрос проходит, ответ приходит, значит все живо. В понедельник оказалось, что вызовы инструментов перестали собираться в ожидаемую структуру, стриминг начал отдавать куски JSON, которые клиент не мог склеить, а эмбеддинги вернулись в другом формате и сломали кеш по старому хэшу.

Такой сбой редко выглядит как один большой пожар. Обычно это несколько мелких поломок в разных местах. Поэтому набор проверок лучше строить не вокруг одного сценария без ошибок, а вокруг одного и того же запроса в четырех режимах: чат, эмбеддинги, инструменты и SSE.

Хватает простого набора:

  • один чат без инструментов, где вы сравниваете статус, структуру ответа и роль сообщения;
  • один чат с tool_call, где тест ловит имя инструмента, аргументы и finish_reason;
  • один запрос на эмбеддинги, где проверяется длина массива, тип чисел и стабильность сериализации для кеша;
  • один стриминговый запрос, где клиент собирает ответ по чанкам и валидирует финальную структуру.

Если у вас OpenAI-совместимый маршрут, этот сценарий удобно гонять на том же коде с новым SDK и прежним endpoint. Тогда быстро видно, сломался ли клиентский слой после обновления или проблема сидит в обработке конкретного типа ответа.

Признак здоровой миграции выглядит просто: чат отвечает, tool_calls приходят в прежнем контракте, стриминг не ломает структуру, эмбеддинги дают тот же размер вектора и тот же способ упаковки массива. Если один набор тестов падает сразу в нескольких местах, начинайте с сериализации SDK, обработчика SSE и маппинга полей ответа.

Что делать дальше

Набор проверок не должен жить в заметке или в голове одного инженера. Поставьте его в CI и запускайте перед каждым релизом SDK, сменой провайдера и любым обновлением сетевого слоя. Такой прогон обычно занимает несколько минут, а потом экономит полдня на поиске причины, почему чат перестал возвращать tool_calls или эмбеддинги внезапно сменили размер.

После этого зафиксируйте эталонные кейсы рядом с кодом. Для чатов обычно хватает 5-10 запросов с ожидаемой структурой ответа: роль, content, finish_reason, usage и вызовы инструментов. Для эмбеддингов достаточно короткого набора строк, ожидаемой длины вектора, типа данных и допустимого порога сходства, если вы обновляете модель. Когда команда хранит такие кейсы в репозитории, она меняет их через review, а не по памяти.

Полезно держать три обязательных прогона: на текущем SDK и текущем провайдере, на новой версии SDK и на том же SDK с другим base_url. Последний вариант особенно полезен, если вы проверяете OpenAI-совместимый маршрут через RU LLM. В таком тесте меняется только адрес API, а SDK, код и промпты остаются теми же. Это помогает быстро отделить проблемы клиентской библиотеки от проблем конкретного маршрута или провайдера.

Если вашей команде нужен единый OpenAI-совместимый endpoint в РФ, такой прогон лучше сделать обычной частью процесса, а не разовой миграцией. Для маршрута через RU LLM это естественный сценарий: вы проверяете тот же набор запросов теми же SDK и тем же кодом, а затем смотрите, где именно расходится контракт.

Тогда обновление SDK перестает быть лотереей. У команды есть живой набор проверок на реальные поломки, и каждый следующий релиз проходит заметно спокойнее.

Часто задаваемые вопросы

Что чаще всего ломается после обновления SDK?

Чаще всего ломается не сам вызов, а разбор ответа. SDK может поменять форму choices, tool_calls, usage или тип исключения, и ваш код начнет видеть null там, где раньше было значение.

Из-за этого сбой выглядит тихо: статус 200 есть, модель ответила, а чат, инструменты или биллинг уже работают не так.

Какие сценарии стоит проверить в первую очередь?

Начните с самого простого: короткий чат без инструментов, затем чат с историей, потом эмбеддинги, вызов инструмента и стриминг с досрочной отменой. Такой порядок быстро показывает, где съехал контракт.

Не гоняйте сразу весь набор. Сначала проверьте сценарии, где ошибка проходит тихо и доходит до прода без явного падения.

Сколько реальных кейсов нужно для регрессионного набора?

Обычно хватает 10–20 реальных запросов из прода. Возьмите короткий чат, длинный чат с историей, несколько эмбеддингов, один tool_call и один стриминговый ответ.

Этого достаточно, чтобы поймать смену полей, размеров, типов данных и поведения на транспорте без лишнего шума.

Что нужно сохранить как эталон до обновления SDK?

Сохраняйте не только текст ответа, а весь полезный след: HTTP-статус, JSON целиком, имена полей, finish_reason, usage, время ответа и текст ошибки. Для эмбеддингов нужен размер вектора, для инструментов — форма tool_calls, для стриминга — порядок событий и признак завершения.

Если у вас есть только текст модели, вы пропустите большую часть поломок после обновления.

Как проверить, что чатовый контракт не съехал?

Смотрите на role, content, finish_reason и на то, какое тело запроса реально ушло в API. После обновления клиент может поменять порядок messages, склеить роли или проигнорировать параметр вроде max_tokens.

Полезно прогнать один и тот же промпт с разными значениями temperature и max_tokens. Тогда сразу видно, передает ли SDK параметры без искажений.

Что смотреть в эмбеддингах кроме статуса 200?

Для эмбеддингов первым делом сравните длину вектора до и после обновления. Если размерность изменилась, поиск и запись в векторную базу могут сломаться даже при статусе 200.

Потом проверьте batch-запрос из нескольких строк и убедитесь, что порядок элементов в ответе остался прежним. Иначе вы привяжете правильные векторы к чужим текстам.

Как ловить ошибки в вызовах инструментов?

Тест должен сверять имя инструмента, JSON Schema, аргументы и второй шаг после вызова. Часто SDK меняет упаковку parameters, формат аргументов или теряет tool_call_id при передаче результата обратно модели.

Если проверить только финальный текст ответа, вы пропустите поломку до того, как она дойдет до бизнес-логики.

Как правильно тестировать стриминг после обновления?

Сравните обычный ответ и стриминг на одном и том же запросе. Смотрите на время до первого токена, порядок чанков, finish_reason и на то, закрывает ли клиент соединение после отмены.

Отдельно проверьте досрочный обрыв потока со стороны клиента. Здесь часто всплывают зависшие сокеты, повторные чанки и утечки обработчиков.

Почему одних моков мало после обновления SDK?

Моки часто возвращают идеальный старый JSON и прячут проблему. Тесты проходят, а реальный сервер уже шлет другой формат ответа или другой тип ошибки.

После апдейта держите моки, но добавьте несколько живых запросов к настоящему API. Иначе вы проверяете только свои ожидания, а не реальный контракт.

Что сделать перед релизом новой версии SDK?

Перед релизом прогоните старый и новый SDK на одних и тех же кейсах с одинаковыми промптами и параметрами. Потом сравните не только статус, но и JSON-схему, ошибки, request_id, tool_calls, размер эмбеддингов и поведение SSE.

После этого положите набор в CI и гоняйте его при каждом обновлении SDK, смене провайдера и смене base_url. Так вы ловите поломки до выкладки, а не ночью в проде.