ESPN DeportesFederación Portuguesa suspende a Cristiano Ronaldo tras abandonar la selecciónThe Jerusalem PostWWII air-raid shelter buried beneath German playground rediscovered during construction workESPNThe WNBA Finals are set! First look at how Valkyries, Dream match upPunchRonaldo will return to Portugal squad in November — OfficialComplete SportsArsenal Make 27-Year History During Leeds Comeback WinZDF heuteAktuelle Pressemitteilungen des ZDFRTP DesportoMarco Silva reconhece que Benfica não tem outra solução além de ganharFootball ItaliaItaly doctor hits back at Fabregas’ Kean comments: ‘We constantly keep clubs updated’DW EnglishTrump slams Zelenskyy, says Ukraine should elect new presidentAntara NewsPolice foils illegal trade of protected mobula ray species in BaliMyJoyOnlineHincapie injury adds to Arsenal issues after Guimaraes rescue actVariety‘Children of Blood and Bone’ Author Tomi Adeyemi Quietly Cancels New York Comic Con Panel
The Daily Newsstand · Free, Always
Saturday, October 10, 2026

Локальный ассистент для зумов, часть 10: NLI — три места, куда я его примерял, и два, откуда ушёл

Translate

Бюджет на квартал согласован и бюджет на квартал не согласован. Для эмбеддингов это почти одна фраза: слова те же, частица не в векторе весит мало. Для встречи это значит, что решили обратное.

Вот из-за таких пар в Чароите живёт NLI-модель. Ниже — где она работает третий месяц, где я её снял и почему, и во что это обошлось.

Зачем вообще полез

Не из-за ленты. Сначала были протоколы.

В июле я прогнал протокол одной и той же встречи четыре раза на боевых настройках. Получилось 39 разных утверждений. 32 из них встретились только в одном прогоне из четырёх. Ни одно не повторилось во всех. Номер задачи, которого в разговоре не было вовсе. Дата, которая была, но не попала ни в одну версию. Мне хотелось машину, которая положит четыре протокола рядом и скажет: вот тут прогоны друг другу противоречат, смотри сюда.

Взял mDeBERTa-v3-base-xnli-multilingual-nli-2mil7 — мультиязычная, обучена на двух с лишним миллионах пар. Перегнал в ONNX, вышло 1,1 ГБ. Torch тащить не стал: onnxruntime и токенизатор, больше ничего. Загрузка полторы секунды.

Склеивать перефразы она умела сразу. Срыв сроков из-за задержки выкатки доработок и срыв сроков из-за зависимых задач легли в один смысл. А противоречия ловила плохо. Собеседник 5 уточняет статус задачи против Собеседник 2 проверяет выкатку — противоречие, говорит модель. Нет, это просто про разное. Её учили на парах про одну ситуацию, и про другое от наоборот она отличает слабо.

Тогда я сделал то, что делают все: собрал свой набор. Шестнадцать пар из реальных протоколов, ручной эталон. Добавил фильтр: сначала общий предмет, потом модель. 16 из 16. Заодно сравнил с единственной живой русской NLI-моделью на Hugging Face, rubert-base-cased-nli-threeway. Без фильтра 13 из 16 против 14 у мультиязычной, с фильтром 15 против 16. Русская споткнулась там, где мне было важнее всего: репликацию делает Собеседник 1 и репликацию делает Собеседник 3 она сочла просто разными фразами, а это смена исполнителя.

Потом прогнал по архиву: 14 встреч, 51 пункт протокола, 1192 пары. Из 76 пар, которые прошли фильтр, модель назвала противоречием 30. Почти все ложные.

Разгадка лежала в длине. Средний пункт протокола — 138 знаков, 69 % длиннее 120. Пункт вида Необходимо инициировать прямой контакт с ответственным лицом для уточнения общих принципов подключения, минуя промежуточные этапы согласования — это три мысли в одном абзаце. У любых двух таких абзацев найдётся общее слово и что-нибудь, похожее на конфликт. Мои шестнадцать пар дали сто процентов, потому что я сам писал их короткими. Свой набор — не доказательство.

Противоречия в протоколах в итоге ловит не NLI. Протокол просится короткими утверждениями, до шестидесяти знаков, с названным объектом, а номера и даты сверяются со стенограммой напрямую, поиском. Это на два порядка дешевле. На том прогоне такая сверка поймала одну выдумку из десяти утверждений: релиз запланирован на 17.08, где семнадцатое августа не звучало ни цифрами, ни словами.

Модель осталась лежать на диске. Через два дня пригодилась.

Место первое: повторы в живой ленте

Тут нужно понять, откуда повторы. Модель, которая пишет тезисы по ходу звонка, видит только свежий кусок, около 800 знаков. Про сказанное полчаса назад она не знает. На длинной встрече люди возвращаются к теме, и модель открывает то же самое заново, другими словами. К концу часа лента наполовину из перепевов.

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

Устроено так. Последние шестнадцать тезисов держу в окне. Новый сначала сравниваю со всеми по словам, дешёвым difflib. Если пересечение слов меньше 0,3, пару вообще не трогаю: совсем разные тезисы модели не нужны. Порог 0,3 низкий нарочно. У голых фраз 2 RPS и 2 запроса в секунду общая по словам только двойка, ratio 0,33, а жадный порог отрезал бы их ещё до суда. Кто прошёл, тот идёт в модель, в обе стороны. Если обе вероятности не ниже 0,8, тезис выбрасывается, а в лог пишется строка тезис-дубль отсеян.

Почему 0,8, а не 0,75, как по умолчанию. Тут ошибки неравноценны. Повтор в ленте раздражает. Выброшенный тезис с новым фактом — это потерянный факт. Пусть лучше иногда повторит.

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

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

Место второе: ночная ревизия ядер

Про граф встреч я писал во второй части. Если коротко: после каждой встречи Чароит раскладывает её по узлам — люди, системы, темы. Темы, которые живут дольше одной встречи, я называю ядрами. У каждого ядра своя суть, хроника, люди, решения.

Беда графа в том, что одну тему называют по-разному. Пример из архитектурного документа в моём же репозитории: настройка доступа к API и получение токена — одна работа, два ядра, две хроники, и ни одна не полная. По словам совпадение ноль, difflib выдаёт 0,0. Вся надежда на смысл. Забегая вперёд: замер ниже этот пример не подтвердил, но устроена ревизия именно под такие пары.

Поэтому ревизия ядер устроена в два шага. Сначала bge-m3 собирает кандидатов: пары с косинусом от 0,55. Потом каждую пару судит NLI по сути ядер, в обе стороны. Исходов четыре:

  • дубль — обе стороны от 0,8; кандидат на слияние;

  • возможный дубль — от 0,72 до 0,8; ставится пометка, которую легко снять;

  • вложение — одна сторона от 0,85, другая ниже 0,5; узкая тема входит в широкую;

  • граница — обе от 0,45; темы соседние, связь стоит отметить.

Ядро, к которому уже привязаны три вложения, считается хабом, его ревизия не трогает. Слияние — только с бэкапом и только если включить его отдельным флагом. Без флага ревизия пишет в граф пометки.

В августе всё выглядело хорошо. 284 живых ядра, 40 186 пар. Через эмбеддинг-префильтр проходило 1806, NLI тратил 106 мс на вызов, два вызова на пару, полная ревизия занимала шесть минут. Я даже обрадовался, что не надо крутить пороги.

К октябрю ядер стало больше тысячи. Полная ревизия 27 сентября упёрлась в ночной потолок через три с половиной часа и оставила 824 ядра на следующие ночи. Обычные ночные прогоны тоже шли до потолка. А досье — сводки по теме, которые подсказки подкладывают первыми, — в восьми ночах из двенадцати не собрались вообще. Потом выяснилось, что досье мешала не ревизия: набор тем на ночь был ограничен двенадцатью и выбирался по дате правки файлов, так что слоты уходили не тем темам. Ревизия съедала ночь отдельно, и я сел смотреть, что она в итоге даёт.

Посмотрел десять случайных уверенных пар, тех, что ревизия была готова слить. Настоящих дублей среди них ноль. Спорная одна. Уверенность у ложных пар 0,93–1,00.

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

Я заранее ставил на автостатус порог повыше, 0,92, и не сливал, если у двух ядер больше половины встреч общие. Не помогло: у ложных пар было 0,93 и выше.

Дальше хуже. Пометок возможных дублей в графе набралось 239. Разобрано ноль. Пометки показывает одна служебная заметка, а её, как выяснилось, никто не открывал: среди последних 48 открытых в Obsidian файлов её нет. Строк вложения — 1028 в 462 файлах из 1047 ядер. Почти у каждого второго ядра какая-нибудь вложенность, которую никто не читал.

5 октября я выключил ревизию у себя одной строкой в конфиге. Автослияние не включал никогда, и теперь понятно, что правильно: оно склеило бы разные рабочие темы, и подсказка на встрече показала бы мне статус чужой задачи. Бэкап слияния живёт неделю, и в него никто не смотрит. На деле это необратимо.

Что я потерял. Среди тридцати проверенных случаев четыре-пять спорных пар, которые, может, и правда стоило свести. Они остались несведёнными. Как и раньше: сводить их всё равно было некому.

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

Место третье: гейт перед молнией

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

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

Сначала в тень: судья считает рядом и ничего не решает, только пишет в лог, что сделал бы. Четыре встречи, 117 реплик.

Модель ответила на 115. Отказала на двух. Это 1,7 % — потолок того, что гейт вообще мог сэкономить, даже идеальный. Оговорка: пустым я считал отказ самой модели. Если она отвечает и на не-вопросы, этот замер их не видит.

А NLI был не идеальный. Точность против этой метки — 55,6 %, почти монетка. Калибровка плохая: уверен он заметно чаще, чем прав. Теперь пороги:

порог

отсечено

снято пустых

потеряно вопросов

0,60

31,6 %

1 из 2

31,3 %

0,70

17,9 %

1 из 2

17,4 %

0,80

9,4 %

0

9,6 %

0,90

0

0

0

При любом пороге, который хоть что-то отсекает, он теряет настоящих вопросов в десятки раз больше, чем снимает пустых. По скорости всё было хорошо — медиана 99 мс.

Правило закрытия я записал за неделю до замера: если пустых меньше 15 %, тему закрыть. Вышло 1,7 %. Тень выключил 6 октября. Свою дообученную голову под эту задачу, которую уже прикидывал, делать не стал: она имела бы смысл, если бы пустых была треть.

Что ломалось

Самое неприятное в NLI у меня — отказ без звука.

Первый раз. Модели у Чароита — это данные, они лежат в корне данных. А код искал NLI-модель рядом с собой, в корне кода. В моём репозитории оба корня совпадают, и дефекта не видно. В установке, где данные лежат отдельно, модели по этому пути нет. А в дедупе стоит правило: нет модели на диске — дедуп выключен. Никакой ошибки, никакого предупреждения, лента просто опять повторяется. Заметил не я по ленте, а ревью, когда разбирали, кто и откуда берёт пути. Причём дважды: первый раз закрыли, через несколько дней тот же класс вылез в другом месте — путь запоминался при импорте модуля, раньше, чем программа успевала сказать, где корень. Теперь путь к модели вычисляется в момент вызова.

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

Третий — скорость. Ночные прогоны у меня идут с пониженным приоритетом, чтобы не мешать, если я сижу за ноутбуком. Под таким ограничением один вызов на ревизии стоил 1346 мс. Без ограничения — 189. На восьми потоках — 65. Узкое место не модель, а приоритет. Днём, пока идёт запись, я не отдам ревизии весь процессор: запись важнее. А бывает, что ноутбук ночью спит на батарее, и прогон стартует, когда я его разбужу.

И ещё одно, мелкое. Пробовал перегнать модель в CoreML, чтобы считать на нейроядрах. Падает. Остался на onnxruntime и процессоре.

Замер на десяти парах

Перед публикацией я сел и проверил то, что пишу. Десять пар в духе рабочих встреч, эталон мой. Три судьи: косинус bge-m3, NLI в обе стороны и локальная Qwen 3.6 35B-A3B, которая у меня пишет подсказки и протоколы. Ей дал те же пары и попросил одно слово: дубль, уточнение, противоречие или про разное.

A

B

на деле

косинус

NLI

Qwen

Бюджет на квартал согласован

Бюджет на квартал не согласован

противоречие

0,86

противоречие

противоречие

Релиз в пятницу

Релиз перенесли на следующую неделю

противоречие

0,73

противоречие

противоречие

Миграцию базы делает Иванов

Миграцию базы делает Петров

противоречие

0,845

противоречие

противоречие

Миграция базы данных

Откат миграции базы данных

про разное

0,78

противоречие

уточнение

Нагрузка на сервис — 2 RPS

Ожидаем два запроса в секунду

дубль

0,67

про разное

дубль

Тестирование задерживается, стенд не готов

Стенд не готов, поэтому тесты стоят

дубль

0,93

дубль

дубль

Доступ к API выдадут после согласования с безопасностью

Безопасность должна согласовать, прежде чем дадут доступ

дубль

0,91

дубль

дубль

Бюджет согласован

Бюджет согласован до конца года

уточнение

0,87

уточнение

уточнение

Отчёт готовит аналитик

Отчёт готовит аналитик, срок — среда

уточнение

0,88

уточнение

уточнение

Настройка доступа к API

Получение токена

про разное

0,50

противоречие

уточнение

Эмбеддинги своё подтвердили. Порог 0,85 склеил бы пять пар: два настоящих дубля, но вместе с ними оба уточнения и бюджет с частицей не. Исполнителя Иванова на Петрова они пропустили бы на волосок, 0,845.

NLI — 7 из 10. Все три противоречия и оба уточнения на месте. Промахи поучительные. 2 RPS против двух запросов в секунду он дублем не считает: в одну сторону 0,60, в другую 0,03. В таком виде мой фильтр в ленте отсёк бы пару ещё по словам, там совпадение ноль. А дойди она до модели, дубля всё равно не вышло бы. Перефразы с аббревиатурами в ленте остаются повторами. Откат миграции и получение токена он зовёт противоречием — это та самая июльская болезнь, про другое против наоборот. И пара про API и токен, которую я привёл выше как одну работу: косинус 0,50, ниже порога кандидатов, ревизия её бы даже не увидела. NLI зовёт её противоречием, Qwen уточнением. Пример из моего же документа оказался спорным для всех трёх судей.

Qwen — 8 из 10. И тут я был неправ. Я был уверен, что большая модель судит пару секундами. Замер: 0,3 с на пару, максимум 0,47. NLI — 40 мс на вызов, два вызова на пару. Разница в четыре раза, а не в десятки.

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

Почему всё-таки NLI

Можно спросить: если из трёх мест одно работает, одно выключено, а одно закрыто, зачем держать модель на гигабайт.

Потому что то, что у меня реально болит в графе и в ленте, — это не похожесть. Похожесть эмбеддинги дают хорошо и дёшево. Болит другое: это то же самое или уже наоборот. Новый факт перекрыл старый или уточнил. Ответ модели опирается на найденный кусок стенограммы или придуман рядом с ним. Это всё вопросы про следование и противоречие. Можно спрашивать большую модель. На десяти парах она судила не хуже, а по скорости отстала в разы, не на порядки: 0,3 с против 0,08. Держит меня другое: модель у меня одна, и на звонке она пишет подсказки и ответы ⚡. Каждая пара на суд — это очередь перед подсказкой. NLI считает на процессоре рядом, в своей очереди, и подсказкам не мешает. Этот довод я пока не мерил: замер шёл без звонка. Ночью он слабее, и для ревизии ядер я его ещё буду пересматривать.

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

Дальше в очереди три места, все про одну боль.

Повторы автоподсказок. Тот же механизм, что в ленте тезисов, модель уже загружена. Должно быть дёшево.

Устаревание в памяти. Сейчас на вопрос что последнее по такой-то теме поиск по графу иногда отдаёт старое. Хочу помечать факт устаревшим, если более свежий ему противоречит. Но только после того, как у каждого факта будет дата встречи, а не дата правки файла. И с префильтром: у людей в графе медиана девять пунктов, но бывает и 464, и без отбора один такой узел судился бы минутами.

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

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

Что в итоге

NLI в Чароите — это гигабайт на диске, onnxruntime без torch, сорок миллисекунд на вызов. Включается сам, если модель лежит в папке моделей. Если не лежит, дедуп ленты и ревизия ядер выключены, остальное работает.

Каждый день работает дедуп тезисов в живой ленте. Ревизия ядер у меня выключена до переделки суда, в продукте пока включена. Гейт перед ⚡ закрыт по правилу 15 %.

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

Код и релизы: github.com/charoiteai/Charoite_audio, Apache 2.0. Девлог по-английски: charoiteai.github.io/Charoite_audio. Первая часть — про устройство и запись, вторая — про граф встреч как память, третья — про диаризацию и три ложных диагноза, четвёртая — про ревизию после кнопки Стоп, пятая — про облако и что уходит наружу, шестая — про полчаса тишины, которых не было в протоколе, седьмая — про семь пакетов в плане и один в коде, восьмая — про модель, ради которой я поменял диаризацию за один вечер, девятая — про неделю на Nemotron.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.