Вы открываете новую модель, вставляете первый попавшийся запрос из рабочей почты, и она отвечает так, что хочется сохранить скриншот: точно, спокойно, с правильной структурой. Рука тянется к переменной окружения с названием модели. Но стоит задать себе неприятный вопрос: что было бы, если бы первым попался другой запрос? Скажем, письмо с тремя вложенными пересылками, таблицей в теле и просьбой, написанной в последней строке. Или обращение, в котором срочность спрятана в слове «опять».

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

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

Сначала разложить поток на классы запросов

Первая ошибка сравнения — считать, что у приложения одна задача. На деле под одним вызовом API обычно живёт несколько разных работ. Допустим, у вас внутренний сервис поддержки. Он извлекает из входящих писем номер заказа и сумму, помечает срочные обращения, отвечает на вопросы по базе знаний и пишет черновики ответов клиентам. Для модели это четыре разных экзамена: в первом нужна дисциплина, во втором чувствительность к оттенкам, в третьем — привязка к источнику, в четвёртом — тон.

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

Как собрать классы практически? Возьмите реальные запросы за несколько недель и сгруппируйте не по теме, а по требуемому от модели результату. Полезные оси для разбиения:

  • Форма результата. Строгий JSON, одна метка из списка, свободный текст.
  • Источник истины. Ответ определяется входным текстом, внешней базой знаний или вкусом человека.
  • Длина и шум входа. Короткая реплика и многостраничная переписка с подписями и цитатами ведут себя по-разному.
  • Редкие, но неприятные случаи. Пустой ввод, смесь языков, попытка заставить модель игнорировать инструкции, персональные данные в теле.

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

Назначить цену ошибкам до того, как увидите результаты

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

Лучше всего цену ошибки видно на задачах классификации. Возьмём метку «срочное обращение». У неё две принципиально разные ошибки. Модель может пропустить срочное обращение — отнести его к обычным. А может поднять тревогу на спокойном письме. Последствия несравнимы: пропуск означает клиента, который ждёт ответа сутки, когда у него остановилась работа; ложная тревога означает, что дежурный потратил десять минут на письмо, которое могло подождать.

Для этих двух ошибок в статистике есть два разных числа. Документация scikit-learn различает precision, recall и F-меры, а также позволяет считать метрики по отдельным классам и разными способами усреднять их. Смысл проще, чем названия:

  • Precision (точность) отвечает на вопрос: из всего, что модель назвала срочным, какая доля действительно срочная. Низкая precision — это шум и усталость дежурных.
  • Recall (полнота) отвечает на вопрос: из всех действительно срочных обращений, какую долю модель нашла. Низкий recall — это пропущенные клиенты.

Разберём условный пример с числами. В контрольном наборе 200 обращений, из которых 20 по оценке сотрудников действительно срочные. Прежняя модель отметила 30 обращений, из них 18 верно: precision 18/30 = 0,60, recall 18/20 = 0,90. Новая отметила 18, из них 15 верно: precision 15/18 ≈ 0,83, recall 15/20 = 0,75.

Кто из них лучше? Зависит от того, что вы заранее решили. Если пропущенное срочное письмо означает потерянного клиента, а десять лишних проверок в неделю — мелкая неприятность, новая модель хуже: она пропустила пять срочных против двух у старой. Если же дежурные тонут в ложных тревогах и перестали верить пометкам, вторая модель может оказаться спасением, а порог в первой модели придётся поднимать. Общая «точность» в таких данных вообще бесполезна: при десяти процентах срочных писем модель, которая ничего не помечает срочным, формально права в 90% случаев.

Поэтому для каждого класса запишите заранее две вещи: какой тип ошибки хуже и какое минимально допустимое значение соответствующей метрики. Например: «для срочных обращений recall не ниже 0,90, при этом precision не ниже 0,50». Это условие, а не надежда. Если новая модель не проходит его, обсуждение закончено — как бы красиво она ни писала.

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

Зафиксировать контрольный набор и ожидаемые свойства

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

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

Откуда брать входы? Из реальных запросов, предварительно очищенных от персональных данных и всего, что не должно попасть в сторонний сервис. Синтетические примеры годятся для проверки краевых случаев, но не для оценки частоты ошибок: модель, написавшая тест, слишком хорошо знает, как выглядят «правильные» входы.

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

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

Что проверяет машина, а что — человек

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

Для первого лагеря подходят автоматические проверки. Например, инструмент Promptfoo проверяет результаты assertions, в том числе сгруппированными условиями, а критерии задаются конфигурацией теста. Это удобно: ожидаемые свойства из набора превращаются в исполняемые условия, и каждый прогон даёт повторяемый результат без чьего-либо настроения. Важный момент: форматные проверки нужно запускать на всём наборе каждый раз, они дешёвые, и именно они первыми ловят неприятные сюрпризы — например, что модель стала оборачивать JSON пояснением на человеческом языке и парсер на вашей стороне ломается.

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

  • Сверка с эталоном. Там, где у примера есть однозначный правильный ответ (метка, число, идентификатор), сравнивайте напрямую. Это самый надёжный вариант, и по нему считаются precision и recall.
  • Оценка людьми. Там, где нужно суждение, дайте ответы двум-трём сотрудникам и скройте, какая модель что написала. Порядок показа перемешивайте. Просите не ставить оценку «от одного до десяти», а ответить на конкретные вопросы: можно ли отправить как есть, нужна ли правка, есть ли фактическая ошибка. Три бинарных ответа дают куда больше пользы, чем одна невнятная десятка.
  • Модель как судья. Другая модель может предварительно отсеять очевидно плохие ответы и сократить объём ручной работы. Но её вердикт — тоже измеряемая величина: сверьте его с оценкой людей на небольшой выборке, прежде чем ему верить. Особенно осторожно относитесь к ситуации, когда судья и кандидат — одна и та же модель или родственные: склонность хвалить собственный стиль известна заранее.

Отдельно стоит позаботиться о том, чтобы люди не знали, кто автор. Это дешёвая мера, а эффект — большой. Если оценщик видит ярлык «новая модель», он оценивает ожидание. Если видит безымянный текст — оценивает текст.

Если вы уже думали, как подойти к сравнению локальной модели с облачной, то многие шаги будут знакомы; такую задачу подробно разбирает методика сравнения локальной и облачной модели на своих задачах. Здесь же важно другое: порядок. Сначала запускаются форматные проверки. Модель, которая ломает формат в нескольких процентах случаев, не заслуживает дорогой ручной оценки смысла — по крайней мере до исправления промпта. Это экономит часы людей.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Цена и задержка: сравнивать на одинаковых входах

Теперь деньги и время. Здесь легко ошибиться двумя способами: взять цены из рекламных материалов вместо реальных вызовов или сравнить модели на разных входах. Обе ошибки делают результат бессмысленным.

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

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

Теперь о стоимости. Цена за миллион токенов — не цена задачи. Реальная метрика — цена принятого ответа:

цена принятого ответа = суммарные расходы на все вызовы (включая повторы и ответы, отброшенные проверкой) ÷ число ответов, прошедших все проверки

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

Как выглядит результат: таблица по классам, а не одно число

Теперь соберём всё вместе. Ниже — условный пример, цифры придуманы для разбора и не относятся ни к каким реальным моделям. Стоимость выражена индексом относительно прежней модели (1,00), а не в деньгах, чтобы не привязывать пример к чьему-то тарифу.

Условное сравнение прежней и новой модели по классам запросов
Класс запросовМетрика качестваПрежняяНоваяЗадержка p95, прежняя / новаяЦена принятого ответа (прежняя = 1,00)
Извлечение полей из писемДоля ответов, прошедших формат и сверку с эталоном0,860,943,1 с / 2,2 с0,70
Срочные обращенияPrecision / recall для класса «срочное»0,60 / 0,900,83 / 0,751,4 с / 1,0 с0,55
Ответы по базе знанийДоля ответов, принятых рецензентами без правок0,780,715,2 с / 6,8 с1,25
Черновики ответов клиентамДоля слепых сравнений в пользу модели0,480,526,0 с / 5,5 с0,95

Заметьте: в одной таблице четыре разные метрики, и это правильно. Нельзя усреднять precision с долей слепых предпочтений: единица измерения у каждой своя. Задержка и цена вынесены в отдельные столбцы, потому что они принимают независимые решения: качество может позволять переключение, а бюджет времени — нет.

Прочитать таблицу: когда выигрыш есть, но не везде

Посмотрим на условные строки по очереди, как это делает человек, принимающий решение.

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

Срочные обращения. Новая модель чище по precision и быстрее, но recall упал с 0,90 до 0,75. Если вы заранее записали порог «recall не ниже 0,90», разговор окончен: пять пропущенных срочных против двух — не то, что компенсируется экономией. Но решение не обязательно бинарное. Возможно, порог уверенности или инструкцию можно сдвинуть так, чтобы recall вернулся; тогда повторите замер с новым порогом на том же замороженном наборе и посмотрите, сколько precision придётся отдать. Если после этого условие выполняется, переключение класса вполне обосновано. Если нет — этот тип запросов остаётся на прежней модели.

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

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

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

Где всё это запускать

Стенд вызывает внешние API и считает результаты; для него не нужна большая вычислительная мощность, но нужна стабильная среда, в которой прогон можно повторить через месяц с теми же версиями инструментов, тем же набором и сохранённой историей результатов. Ноутбук инженера для этого не очень подходит: на нём всё время что-то обновляется, и сравнение «в прошлый вторник» нельзя воспроизвести.

Отдельный сервер решает эту задачу. На VPS MAATRIX доступны NVMe-диски, процессоры AMD EPYC и root-доступ, так что можно поставить нужную версию инструмента, хранить набор и историю прогонов рядом и запускать их по расписанию, не завися от чьего-то рабочего компьютера. Для типичного стенда, который вызывает внешние модели и пишет результаты на диск, обычно хватает небольшого тарифа вроде Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) или Uab (2 vCPU, 4 ГБ, 80 ГБ). Это ориентир, а не обещание ёмкости: реальную нагрузку покажет ваш собственный замер. Если вы планируете гонять параллельно большие наборы или собирать значительную историю, присмотритесь к Heka или выше.

Две оговорки. Во-первых, стенд на VPS сам не запускает модели на GPU: речь о проверке внешних API или небольших моделей на процессоре, и если вы собираетесь сравнивать модели, которые нужно развернуть у себя, заранее уточните, подойдут ли им ресурсы выбранного тарифа. Во-вторых, доступность конкретного внешнего API из конкретной локации — вопрос, который нужно проверять замером, а не предположением: список локаций в предложении включает UK, US, RU и FR, но это не гарантия доступа к любому стороннему сервису. Как раз поэтому замер задержки лучше делать оттуда, откуда будет работать боевое приложение, иначе вы измеряете чужую сеть.

Решение, которое можно записать одной строкой на класс

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

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