Два письма пришли в одну минуту. Первое — реклама SEO-услуг, которая по ошибке попала в очередь поддержки. Второе — клиент пишет, что с карты дважды списали оплату и он больше не будет ждать, но классификатор уверенно отправил его в «рекламу и рассылки». Формально это одна и та же ошибка: неверный класс. Практически это две разные цены. Лишнее письмо в поддержке стоит тридцати секунд внимания оператора. Потерянная жалоба стоит ожидания, повторного обращения, а иногда и клиента.
Поэтому вопрос «что выгоднее: небольшой классификатор на CPU или платный API» нельзя решать, сравнивая только цену за тысячу запросов. Сначала нужно понять, за что вы платите, когда ошибаетесь. Только потом считать деньги.
Сначала классы и цена ошибки, потом деньги
Допустим, входящие обращения делятся на пять потоков: «оплата и возвраты», «технический сбой», «вопрос по тарифу», «жалоба и срочная эскалация», «реклама и мусор». Для каждого потока полезно записать не только название, но и то, что происходит, если письмо ушло не туда. Короткая таблица делает это видимым.
| Фактический класс | Куда ушло по ошибке | Последствие | Тяжесть |
|---|---|---|---|
| Реклама и мусор | Поддержка | Оператор тратит секунды на закрытие | 1 |
| Жалоба и эскалация | Реклама и мусор | Письмо лежит без ответа, клиент злится | 9 |
| Оплата и возвраты | Вопрос по тарифу | Задержка на круг пересылки | 4 |
| Технический сбой | Оплата и возвраты | Лишняя пересылка внутри команды | 3 |
Из таблицы видно главное: ошибки несимметричны. Класс «жалоба и эскалация» редкий, но важный. Если он составляет три-пять процентов потока, общая доля верных ответов почти ничего не скажет о качестве именно там, где оно нужнее всего. Модель, которая вообще не умеет находить жалобы и всегда отвечает «реклама или тариф», всё равно покажет высокую общую точность просто потому, что остальные девяносто с лишним процентов обращений угадать проще.
Эти оценки тяжести вы потом используете дважды: при выборе метрики и при расчёте стоимости ошибок. Поэтому лучше договориться о них заранее с теми, кто разбирает очередь, а не подбирать под удобный результат.
Выборка, которую нельзя трогать при обучении
Дальше нужны размеченные примеры. Здесь возникает самая частая ловушка: на тех же письмах и настраивают локальную модель, и сравнивают её с API. Модель, которая видела ответы, выглядит лучше, чем будет на новых письмах. Поэтому набор делят минимум на две части.
- Обучающая часть нужна локальному классификатору, а для API — для подбора формулировки инструкции и примеров в запросе. Здесь можно экспериментировать сколько угодно.
- Контрольный набор откладывается и не используется ни для обучения, ни для подгонки промптов. Его запускают один раз на каждого кандидата, и оба варианта проверяются на одних и тех же письмах в одном и том же виде.
Контрольный набор должен повторять реальный поток, а не быть «красиво сбалансированным». Если жалоб в жизни четыре процента, в контрольном наборе их должно быть примерно столько же, но достаточно в абсолютных числах. Чтобы по редкому классу можно было хоть что-то сказать, нужно несколько десятков примеров. Если вы берёте тысячу писем, а жалоб в ней сорок, то один пропущенный случай уже меняет долю на два с половиной процентных пункта. Это нормально, но об этом стоит помнить, читая цифры.
Ещё две мелочи, которые портят сравнение. Во-первых, дубликаты и почти одинаковые письма-шаблоны: если одно и то же письмо попало и в обучение, и в контроль, получится утечка. Во-вторых, разметка. Если два человека размечают спорные письма по-разному, никакая модель не покажет «правильного» результата; спорные письма лучше разметить вдвоём и записать, как именно решили.
Что на самом деле говорят precision и recall
Для редкого важного класса нужны две метрики, и они отвечают на разные вопросы. В документации scikit-learn precision, recall и F-меры описаны отдельно, а оценку можно получать по каждому классу и по-разному усреднять. Для нашей задачи это означает следующее.
- Recall по классу «жалоба» — какую долю настоящих жалоб модель нашла. Это ответ на вопрос «сколько срочных писем мы потеряли».
- Precision по классу «жалоба» — какая доля писем, помеченных как жалобы, действительно ими является. Это ответ на вопрос «сколько лишней работы мы создали операторам».
Условный пример для контрольного набора из 1000 писем, где настоящих жалоб 40. Допустим, некая модель пометила как жалобы 50 писем, и среди них оказалось 34 настоящих. Тогда recall равен 34 из 40, то есть 85%, а precision — 34 из 50, то есть 68%. Шесть настоящих жалоб потеряно, шестнадцать лишних писем попало в срочную очередь. Это иллюстрация арифметики, а не результат чьего-то замера.
Теперь видно, почему цена ошибки из первой таблицы важна. Потеря жалобы оценена в 9, лишнее срочное письмо — в 2 или 3. Значит, для этого класса нужно сдвигать порог в сторону recall, пусть даже за счёт precision. Если классификатор возвращает вероятность или оценку уверенности, порог можно настраивать на обучающей части, а затем один раз проверить на контрольной. Если же API отдаёт только метку без уверенности, возможностей меньше: вы можете попросить модель вернуть и уровень уверенности, но к этому числу стоит относиться как к ещё одному признаку, который надо проверять на тех же примерах, а не как к вероятности.
Усреднение тоже нужно выбрать осознанно. Среднее по всем классам с учётом их размера скроет проблему редкого класса; среднее, где все классы равноправны, покажет её сразу. Для разбора обращений полезны обе цифры, но решение принимается по строке нужного класса.
Ручной разбор ошибок: ради него всё и затевалось
Таблица метрик говорит, сколько ошибок, но не говорит почему. Чтобы это понять, выгрузите все пропущенные жалобы и все ложные срабатывания обоих кандидатов и прочитайте их глазами. Через двадцать-тридцать писем обычно складываются закономерности: жалобы без слова «жалоба», вежливые, но срочные письма, сарказм, письма с несколькими вопросами сразу, длинные цепочки пересылок, в которых суть в самом низу.
Эти закономерности определяют дальнейший выбор лучше любого прайса. Если локальная модель путает вежливую срочность и нейтральный вопрос, возможно, ей не хватает размеченных примеров, и это решается разметкой. Если API ошибается на узкоспециальных терминах вашей области, это решается примерами в запросе или другим маршрутом. А если ошибаются оба на одних и тех же письмах, скорее всего, дело в самой разметке: класс описан нечётко.
Для автоматизации такого сравнения можно использовать инструменты оценки. В Promptfoo результаты проверяются assertions, которые можно объединять в группы условий, а среди проверок предусмотрены стоимость и задержка; пороги задаются в конфигурации теста. Удобно, что одним прогоном можно получить и верность метки, и время, и расходы по тем же письмам. Но сам набор писем и критерии «хорошо или плохо» всё равно придётся придумать вам.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Калькуляция: API против собственного сервера
Теперь можно считать деньги. Ниже — одна прозрачная калькуляция. Все числа условные: подставьте свои объёмы, свои тарифы и свои зарплаты.
Допущения. Поток — 30 000 писем в месяц. Средний запрос к API — 400 токенов на вход (письмо плюс инструкция) и 10 на выход. Цену обозначим как переменные: Pвх — цена за миллион входных токенов, Pвых — за миллион выходных. Человек стоит Cч рублей в час, разбор одного спорного письма занимает 1 минуту.
Вариант А: платный API.
- Токены в месяц: вход 30 000 × 400 = 12 млн, выход 30 000 × 10 = 0,3 млн.
- Плата за вызовы: 12 ×
Pвх+ 0,3 ×Pвых. - Инженерная работа: обёртка с повторами и таймаутами, контроль лимитов, учёт расходов. Допустим, 2 рабочих дня на запуск и 2 часа в месяц на сопровождение.
- Ошибки: допустим, API ошибается на 8% писем и ещё столько же времени идёт на ручную проверку выборочных случаев. Стоимость — число ошибочных писем × средняя цена ошибки.
Вариант Б: локальный классификатор на CPU.
- Сервер: небольшой VPS с несколькими vCPU и достаточным объёмом памяти под модель и очередь; фиксированная цена в месяц, не зависящая от числа писем в пределах загрузки.
- Разметка: допустим, 1500 примеров по 20 секунд на пример, то есть около 8,5 часов человека на старте и 2 часа в месяц на дополнение новыми случаями.
- Разработка: подготовка признаков или эмбеддингов, обучение, сервис с очередью, тесты. Допустим, 5 рабочих дней на запуск и 4 часа в месяц на сопровождение.
- Переобучение при дрейфе: появляются новые продукты, изменились формулировки, и модель теряет качество. Нужна периодическая проверка на свежей выборке.
Формула для сравнения по месяцам выглядит так:
Итого_API = 12*Pвх + 0,3*Pвых + труд_сопровождения + потери_на_ошибках_API + (запуск / срок_окупаемости)
Итого_CPU = сервер + труд_сопровождения + разметка_в_месяц + потери_на_ошибках_CPU + (запуск / срок_окупаемости)Важно, что в обоих вариантах есть слагаемое «потери на ошибках», и именно оно часто перевешивает всё остальное. Допустим, в контрольном наборе жалоба, потерянная в очереди, обходится вам в среднем в 15 минут работы руководителя и один повторный контакт. Если API или локальная модель пропускают на несколько жалоб в месяц больше, эта разница легко превысит стоимость серверного месяца. Но верно и обратное: если оба кандидата показывают на жалобах близкий recall, то решать будут расходы на вызовы, труд и сервер, а для небольшого потока и это может оказаться копейками.
Что подсказывает эта арифметика, не называя победителя заранее:
- Расходы API растут вместе с объёмом и длиной писем. Расходы CPU-варианта растут ступенями: пока сервер справляется с нагрузкой, цена не меняется, затем приходится брать ресурсов больше.
- У API нет затрат на обучение, но есть зависимость от внешнего сервиса: лимиты, изменение условий, доступность из вашей сети. Эти риски нужно проверить для вашего случая, а не считать данностью.
- У локальной модели нет платы за вызов, но есть стоимость человеческого труда на разметку и поддержку. Для малых объёмов она часто важнее, чем цена сервера.
- Если письма содержат персональные данные клиентов, вопрос о том, можно ли отправлять их за пределы вашей инфраструктуры, решается до расчётов, а не после них. Это вопрос вашей политики и требований, а не техники.
Задержку тоже стоит измерить на тех же письмах. Если письма разбираются пачками в фоне, то 300 миллисекунд против 3 секунд ничего не изменят. Если же классификация стоит между пользователем и ответом в чате, то разница уже заметна. Измерьте медиану и хвост, то есть самые медленные запросы, а не только среднее время.
Где нужен сервер и сколько ему нужно
Небольшой текстовый классификатор — например, линейная модель поверх признаков или компактная модель эмбеддингов — обычно не требует GPU, и именно поэтому сравнение «CPU против API» вообще имеет смысл. Но сколько ресурсов нужно именно вам, зависит от модели, длины писем и нагрузки, и это нужно измерять, а не угадывать по тарифной сетке.
Практичный путь такой: взять VPS самого скромного подходящего размера, поднять сервис, прогнать контрольный набор и реальный поток писем, посмотреть загрузку процессора, память и очередь. У MAATRIX линейка начинается с тарифа Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и идёт через Uab (2 vCPU, 4 ГБ) и Heka (4 vCPU, 8 ГБ) до более крупных конфигураций, а root-доступ позволяет поставить любой стек классификатора. Эти ресурсы — исходные параметры конфигурации, а не обещание пропускной способности: сколько писем в секунду выдержит ваш сервис, покажет только ваш замер. Для самой оценки нагрузки полезно держать в уме, как считается стоимость обучения и эксплуатации модели в целом, и в этом поможет материал про расчёт стоимости обучения модели на своём железе.
Отдельно: локальный классификатор — это проект, который вы собираете сами, а не готовая возможность какого-либо шлюза или панели. Вам нужно описать, как запускается сервис, где лежат модель и версии данных, как обновляется модель и как откатываться, если новая версия хуже старой.
Гибрид: пусть простое решается дёшево
Часто лучший ответ — не выбор одного, а маршрутизация. Идея в том, что большая часть писем простая: чёткие вопросы по оплате, типовые сбои, явная реклама. С ними справится небольшая локальная модель. Сомнительные письма — те, где уверенность ниже порога, — уходят на другой маршрут: к более сильной модели через API или к человеку.
Покажем это на тех же условных 30 000 письмах. Пусть локальная модель уверенно разбирает 85% потока, а 15% считает спорными. Тогда в API уходит 4 500 писем в месяц вместо 30 000, и счёт за токены сокращается пропорционально: вместо 12 млн входных токенов остаётся 1,8 млн. А сервер для лёгкой модели всё равно нужен, поэтому экономия на вызовах должна перекрывать его цену и труд на поддержку двух маршрутов. Для небольшого потока это может не сойтись, и тогда проще оставить один вариант.
Тут критичны два условия, которые надо проверять на контрольном наборе, а не принимать на веру:
- Порог уверенности должен отсекать именно трудные случаи. Если локальная модель уверенно ошибается на жалобах, то отправка «только сомнительных» в API ничего не спасёт: жалобы окажутся среди «уверенных». Поэтому для редкого важного класса смотрите долю пропусков среди писем, прошедших порог.
- Доля спорных писем нельзя назначать, её надо измерять. Если окажется, что к «сомнительным» относятся 40%, а не 15%, экономия на вызовах заметно уменьшится. В калькуляции стоит прогнать несколько значений: 10%, 25%, 40%.
Для особо важного класса можно сделать страховку: любое письмо, в котором локальная модель видит хоть небольшой признак жалобы, идёт по маршруту повышенной проверки. Да, precision упадёт и операторов придётся нагрузить, но цена пропуска у этого класса выше цены лишней проверки, и именно это мы записали в самом начале.
Гибрид добавляет и усложнение. Появляются две модели, которые нужно оценивать, два набора ошибок для разбора и логика переключения, которую тоже нужно тестировать. Внешний API при этом требует таймаутов, ограничения повторов и запасного поведения на случай недоступности: например, письмо в этом случае можно отправлять в очередь ручного разбора. Если вызов внешнего сервиса и запись в вашу базу происходят отдельно, нужно заранее продумать, что произойдёт при сбое посередине. Просто сохранять результат в одной транзакции со своей базой недостаточно: внешний вызов не входит в неё автоматически.
Граница, на которой решение меняется
Вместо победителя полезнее получить несколько условий-переключателей. Они вытекают из калькуляции выше, и вы можете подставить в них собственные числа.
- Объём мал, а ошибки дёшевы. Если писем немного, а пропуск не ведёт к потерям, платный API экономит вам недели разметки и разработки. Сервер и модель окупаться просто не успеют.
- Объём растёт, а типы писем стабильны. Когда вызовы становятся основной строкой расходов, а классы не меняются каждый месяц, локальная модель или гибрид начинают выигрывать по деньгам. Точка перехода определяется вашей калькуляцией: когда месячный счёт за вызовы превысит сумму сервера, труда на сопровождение и амортизации запуска.
- Редкий важный класс не добирает recall. Если на контрольном наборе ни один вариант не находит достаточно жалоб, дело не в выборе между CPU и API, а в разметке, описании класса и пороге. Менять технологию бессмысленно, пока не разобраны ошибки вручную.
- Данные нельзя отправлять наружу. Тогда внешний API отпадает независимо от цены, и сравнение превращается в выбор между вариантами локальной модели.
- Поток и формулировки быстро меняются. Если появляются новые продукты и темы каждый месяц, локальная модель потребует постоянного дообучения, и труд на неё может перевесить экономию на вызовах.
Когда вы прогоните одинаковый контрольный набор через обоих кандидатов, посчитаете precision и recall по самому дорогому классу, прочитаете ошибки и подставите свои цифры в формулы, ответ на вопрос «CPU или API» обычно перестаёт быть вопросом вкуса. Он превращается в простое сравнение двух чисел — и в понимание, какое из них поменяется первым, если изменится ваш поток писем.