В локальном ассистенте для встреч Charoite AI NLI-проверку оставили ровно в одном месте: дедупликация тезисов в живой ленте. Там двустороннее следование работает: пары «срыв сроков из-за задержки выкатки доработок» и «срыв сроков из-за зависимых задач» легли в один смысл. Из ночной ревизии ядер графа встреч NLI убрали, гейт перед кнопкой быстрого ответа закрыли. Причина у обоих решений одна: на реальных протоколах модель уверенно называла противоречием то, что противоречием не было, а цена такой ошибки оказалась выше пользы.
Технически все скромно. Модель mDeBERTa-v3-base-xnli-multilingual-nli-2mil7 переведена в ONNX, занимает 1,1 ГБ на диске и запускается через onnxruntime с токенизатором, без torch. Загрузка около 1,5 секунды. Двусторонняя проверка пары означает два вызова модели вместо одного.
Ниже разбор кейса: что именно NLI решает, где он ломается на живых данных и какие грабли стоит обойти при внедрении.
Что такое NLI и зачем он нужен в ассистенте для встреч
NLI (Natural Language Inference) определяет логическое отношение между двумя текстами: следует ли один из другого, противоречат ли они или связи между ними нет. Модель получает пару и выдает один из трех классов: entailment, contradiction, neutral. Для ассистента встреч это переводится в рабочий вопрос: эту мысль уже озвучили или она новая?
Задача возникает сразу, как только тезисы начинают собираться в живую ленту. Реплики приходят потоком, формулировки плавают, один и тот же факт повторяется другими словами. Нужен дешевый способ решать, добавлять тезис в ленту или считать его повтором уже сказанного.
Почему эмбеддингов недостаточно для дедупликации
Первый очевидный инструмент это эмбеддинги: считаем косинусную близость и склеиваем похожие фразы. На паре «Бюджет на квартал согласован» и «бюджет на квартал не согласован» схема ломается. Слова те же, частица «не» в векторе весит мало, для модели это почти одна фраза. Для встречи это значит, что решили обратное решение.
Это не только наблюдение из кейса. Исследования универсальных текстовых эмбеддингов фиксируют «слепоту к отрицанию»: модели часто интерпретируют пары текстов с отрицанием как семантически похожие, хотя отрицание полностью меняет смысл предложения. Отдельная работа по оценке семантической близости предложений объясняет механику: эмбеддинги неадекватно отражают влияние слов-отрицаний, и векторы обоих предложений оказываются очень близко друг к другу в пространстве представлений.
Эмбеддинги хорошо дают похожесть: быстро отсеивают нерелевантное, находят тематически близкие куски. Различить дубль, уточнение и отрицание на близости векторов не получается. NLI закрывает именно этот зазор, потому что отвечает на вопрос «это то же самое или уже наоборот».
Как работает двустороннее следование на практике
Одного направления мало, поэтому проверка идет в обе стороны:
- дубль: A следует из B и B следует из A;
- уточнение: следование проходит только в одну сторону, более узкая формулировка вкладывается в широкую;
- ни одно следование не проходит: темы разные либо утверждения конфликтуют, и здесь решает уже класс contradiction самой модели.
Как это выглядело на данных. Перефразы склеивались сразу: «срыв сроков из-за задержки выкатки доработок» и «срыв сроков из-за зависимых задач» легли в один смысл. С противоречиями вышло хуже: пару «Собеседник 5 уточняет статус задачи» и «Собеседник 2 проверяет выкатку» модель назвала противоречием, хотя это просто про разное.
Цена схемы понятная: два прогона модели на каждую пару. При десятках миллисекунд на вызов это дешевле, чем держать в ленте пять копий одного тезиса.
Как NLI-модель встроили в локальный ассистент: ONNX и onnxruntime
Для локального ассистента выбор упирается в размер модели и ее зависимости. Взяли mDeBERTa-v3-base-xnli-multilingual-nli-2mil7: мультиязычная, обучена на двух с лишним миллионах пар. По карточке модели она была дообучена на датасете XNLI и на датасете multilingual-NLI-26lang-2mil7. Для русскоязычных встреч это практично: модель не заточена под один английский и нормально переносит формулировки на других языках.
Важная оговорка о качестве обучающих данных: датасет multilingual-nli-26lang-2mil7 создавался с помощью машинного перевода, а это снижает качество данных для такой сложной задачи, как NLI. То есть мультиязычность здесь достигнута ценой шума в разметке, и часть ошибок модели может идти именно оттуда.
Модель перегнали в ONNX, на диске она занимает 1,1 ГБ. От torch отказались: onnxruntime и токенизатор закрывают весь инференс. Загрузка занимает около полутора секунд.
Отдельно стоит знать, что существует версия этой модели с ONNX-весами, совместимая с Transformers.js. Это удобно, если инференс нужен в браузере или в JS-окружении, но это не официальная карточка модели на Hugging Face, а сторонняя сборка, поэтому совместимость и качество стоит проверять самостоятельно.
Почему ONNX без torch - это практично
torch тянет набор зависимостей и раздувает дистрибутив, а для инференса NLI-модели он не нужен. Сборка получается проще, меньше поводов ломаться при обновлении окружения, а запуск ассистента не ждет, пока поднимется тяжелый фреймворк. Для локального продукта, который ставят на машину пользователя, лишний гигабайт зависимостей это лишний повод для проблем с установкой.
Полторы секунды на загрузку для ассистента встреч приемлемы: NLI не нужен в первую секунду разговора, а после загрузки проверка идет десятками миллисекунд.
Сколько ресурсов занимает NLI в локальном ассистенте
Цифры кейса: 1,1 ГБ на диске и около 1,5 секунды на загрузку. Порядок величин такой, что NLI можно вызывать на каждой новой реплике, а не пакетно раз в минуту. Значения получены на конкретной конфигурации: на слабом железе и под нагрузкой они будут выше, поэтому свои замеры стоит сделать до того, как строить на них архитектуру.
Ночные прогоны в этой картине отдельный риск: пониженный приоритет процесса замедляет инференс, и ревизия тянется дольше, чем ожидается по замерам на нормальном приоритете.
Где NLI реально работает: дедупликация тезисов в живой ленте
Основное место, где модель пригодилась, это живая лента. Новый тезис проверяется против уже собранных, двустороннее следование дает правило, по которому повтор отсекается до попадания в ленту, а не после того, как он там продублировался в третий раз. Работает это на коротких репликах, то есть ровно на том формате, на котором NLI-модели и обучают.
Чтобы проверить модель на своих данных, автор кейса собрал свой набор: шестнадцать пар из реальных протоколов с ручным эталоном. С фильтром по общему предмету результат вышел 16 из 16. Тут же и главная оговорка, которую автор делает сам: шестнадцать пар дали сто процентов потому, что он писал их короткими. Свой набор не доказательство.
Фильтр по общему предмету: как повысить точность
Фильтр работает до модели: сначала проверяется, есть ли у двух фраз общий предмет (общие сущности, ключевые слова), и только потом запускается NLI. Пары без общего предмета до модели не доходят, а значит, не порождают ложных срабатываний. Цифры на своем наборе: без фильтра 14 из 16 у мультиязычной модели, с фильтром 16 из 16.
Общий вывод для похожих проектов: вокруг модели стоит обвязка, и она меняет результат сильнее, чем сама модель. Наглядный пример того же эффекта на другой задаче разобран в материале про дизайн harness и разброс точности замороженной 4B-модели от 60% до 82%.
Сравнение с русской NLI-моделью rubert-base-cased-nli-threeway
Автор кейса сравнил mDeBERTa с rubert-base-cased-nli-threeway, которую в разборе называет единственной живой русской NLI-моделью на Hugging Face. Это утверждение стоит воспринимать как оценку автора на момент написания, а не как проверенный факт: каталог русскоязычных NLI-моделей на Hugging Face шире, и перед выбором стоит посмотреть актуальный список самому.
Ожидание было простым: модель, заточенная под русский язык, должна выиграть. На наборе автора она не выиграла. Без фильтра 13 из 16 против 14 у мультиязычной, с фильтром 15 против 16.
Показательный провал: пару «репликацию делает Собеседник 1» и «репликацию делает Собеседник 3» русская модель сочла просто разными фразами. Это смена исполнителя, то есть как раз то различие, ради которого NLI и ставили в ассистент.
Оговорка по масштабу обязательна: шестнадцать пар это маленький набор, и вывод «мультиязычная лучше русской» держится на нем, а не на сотнях примеров. Для своего проекта тест стоит повторить на своих протоколах и своей лексике.
Где NLI не работает: ночная ревизия ядер графа встреч
Второе место примерки это ночная ревизия ядер графа встреч. Ядра, устойчивые смысловые узлы, собираются из протоколов и повторяются от встречи к встрече, а ревизия должна находить между ними дубли и конфликты. NLI здесь отключили.
Данных хватило, чтобы решение не выглядело вкусовым. На десяти случайных уверенных парах, которые модель считала дублями, настоящих дублей не оказалось ни одного. Ложные срабатывания шли с уверенностью 0,93-1,00, и дело не в пороге: сравнивались шаблонные автостатусы вместо сути ядер, а две одинаковые по форме служебные фразы модель честно считает одним и тем же.
Отдельный прогон по архиву: 14 встреч, 51 пункт протокола, 1192 пары. Через фильтр прошли 76 пар, из них модель назвала противоречием 30. Почти все эти срабатывания ложные.
Причина видна в статистике текста: средний пункт протокола это 138 знаков, 69% пунктов длиннее 120 знаков. Такой пункт почти всегда содержит несколько мыслей, поэтому у любых двух пунктов находится общее слово и что-нибудь похожее на конфликт.
Почему длинные пункты протокола ломают NLI
Пример из кейса: «Необходимо инициировать прямой контакт с ответственным лицом для уточнения общих принципов подключения, минуя промежуточные этапы согласования». Здесь три мысли в одном абзаце: контакт, цель контакта и обход этапов согласования. Модель обучена на коротких парах и не разбирает такую конструкцию на составляющие.
Практический вывод: протокол стоит писать короткими утверждениями до 60 знаков, где назван объект действия. Тогда и NLI, и прямой поиск работают предсказуемее, а расхождение между двумя пунктами видно без ручного разбора абзаца.
Ложный ноль в ревизии: как не обмануться
Отключение NLI в ревизии легко спутать с чистым результатом: дублей не нашлось, значит, их нет. Это ложный ноль. Ноль здесь означает, что инструмент не справился с длинными абзацами, а не что граф в порядке. Пока ревизия не переписана под короткие утверждения, проверять пары придется вручную или сверкой по стенограмме.
Гейт перед кнопкой быстрого ответа: почему его закрыли
Третье место примерки это гейт перед кнопкой быстрого ответа. Идея простая: NLI решает, есть ли в последней реплике вопрос, на который стоит отвечать, и если вопроса нет, кнопка не показывается. Правило срабатывания задали заранее, до прогона: если пустых реплик окажется меньше 15%, гейт не нужен. Пустых оказалось 1,7%.
Дальше арифметика вышла против гейта. При любом работающем пороге он терял настоящих вопросов в десятки раз больше, чем отсекал пустых реплик. Спрятать настоящий вопрос дороже, чем показать кнопку там, где отвечать нечего, поэтому гейт закрыли. Решение принято на данных, а не на интуиции, и это как раз тот случай, когда обвязка важнее модели: пороги уверенности и цена ошибки определяют, будет ли функция полезной. Похожие истории из продакшена собраны в разборе 1352 кейсов внедрения ИИ в России.
Цифры 1,7% и 15% относятся к конкретному набору реплик и конкретному продукту. Переносить их на другой ассистент без пересчета не стоит, а вот сам прием переносится хорошо: задать порог заранее и посчитать, сколько настоящих срабатываний он убьет.
Типовые ошибки при внедрении NLI в локальный ассистент
Четыре ошибки из разбора, каждая стоила времени и не все были видны сразу.
Молчаливый отказ: как не потерять модель
Если файла модели нет на диске, NLI-проверка может просто не выполняться: код не падает, логи чистые, лента собирается как обычно. Разработчик и пользователь уверены, что дедупликация работает. Лечится явной проверкой наличия файла при старте и отдельной записью в лог, что проверка отключена. Полезно, если ассистент показывает статус проверок на видном месте, а не молчит.
Пониженный приоритет ночных прогонов: как не убить производительность
Ночные прогоны обычно запускают с пониженным приоритетом, чтобы не мешать дневной работе. NLI-задачи в таком режиме считаются медленнее, и ревизия растягивается. Смотреть надо на приоритет процесса, а не на модель: тем же кодом на нормальном приоритете прогон идет быстрее. Конкретных цифр падения в разборе нет, поэтому порог, после которого это становится проблемой, придется замерять на своей машине.
Неудачный перевод в CoreML: стоит ли пробовать
Идея перевести NLI-модель в CoreML, чтобы ускорить инференс на Mac, не сработала. Ускорять при этом особо нечего: ONNX с onnxruntime уже дает десятки миллисекунд на вызов. Если у вас похожий стек, начинать стоит с профилирования, а не с конвертации.
Общие ограничения такого перехода известны: артефакты, ориентированные на Apple (Core ML, MLX), не всегда напрямую переносятся в производственные тулчейны вроде TensorRT и ONNX, и наоборот. Это добавляет этапы конвертации и валидации, а на выходе можно получить те же миллисекунды плюс новую точку отказа. Отдельный риск на Apple Silicon в 2025 году это отслеживаемые в PyTorch проблемы с scaled_dot_product_attention (SDPA), которые могут вызывать сбои на macOS, и регрессии памяти в MPS. Прямого бенчмарка, который сравнивал бы скорость CoreML и ONNX Runtime для трансформеров, в разборе нет, поэтому вывод «CoreML не быстрее» стоит держать как результат конкретной попытки, а не как общее правило.
Что использовать вместо NLI для контроля качества протоколов
Противоречия в протоколах в итоге ловит не NLI.
Короткие утверждения вместо длинных абзацев
Протокол просится короткими утверждениями до 60 знаков с названным объектом. Причина простая: короткое утверждение проверяемо. Его можно сверить со стенограммой, сравнить с другим утверждением и увидеть место расхождения. Абзац из трех мыслей не проверяется ничем: любая проверка либо пропустит конфликт, либо найдет его там, где его нет.
Поиск по стенограмме: дешёвая альтернатива для номеров и дат
Номера, даты, имена и суммы сверяются со стенограммой напрямую поиском. Такой проход дешевле NLI и ловит то, что модель пропускает: выдуманные детали. Пример из разбора: утверждение «релиз запланирован на 17.08» при том, что семнадцатого числа в разговоре не было. Сверка поиском поймала одну выдумку из десяти утверждений.
Оценка «на два порядка дешевле» это порядок величины из кейса, а не измеренное сравнение стоимости поиска и NLI: прямых бенчмарков такого сравнения в разборе нет. Что подтверждается независимо, так это сама идея сверки с источником. Детекция галлюцинаций на основе стенограммы в целом превосходит прямую обработку аудио. При этом даже при точном и релевантном извлеченном контенте RAG-модели могут генерировать выводы, противоречащие извлеченной информации, поэтому сверка нужна не только для проверки модели, но и для проверки самого пайплайна.
NLI остается полезен там, где важен вопрос «это то же самое или уже наоборот». Для проверки фактов и цифр лучше подходит прямое сопоставление со стенограммой.
Вывод: когда NLI оправдан, а когда нет
Сложились три четкие зоны.
- Оправдан: дедупликация тезисов в живой ленте. Короткие фразы, поток реплик, нужна скорость, вопрос ставится как «то же самое или наоборот». Двустороннее следование дает понятное правило, а фильтр по общему предмету снимает часть ложных срабатываний.
- Не оправдан: ночная ревизия ядер графа встреч. Длинные пункты протокола, шаблонные автостатусы и уверенность 0,93-1,00 на ложных парах. Ошибку не видно без ручной проверки, она выглядит как чистая работа.
- Не оправдан: гейт перед кнопкой быстрого ответа. Потеря настоящих вопросов в десятки раз превышает пользу от отсечения пустых реплик.
Разделение проходит по природе задачи. Эмбеддинги хорошо дают похожесть, но путают дубли с отрицанием и уточнением. NLI различает логическое отношение, зато требует коротких и однозначных формулировок, а на длинных абзацах и служебных шаблонах выдает уверенный мусор.
Начинать стоит с малого: собрать собственный набор пар из реальных протоколов с ручным эталоном, прогнать модель с фильтром по общему предмету и смотреть не на процент попаданий, а на состав ошибок. Свой набор при этом не доказательство: если пары короткие и написаны автором теста, 16 из 16 не значат почти ничего.