Локальный ассистент для зумов, часть 4: что происходит после кнопки Стоп — и почему вторая модель говорила в пустоту
В первой части ассистент записывал созвон и складывал его в граф, во второй граф учился не врать неделями, в третьей я трижды ошибся с вопросом, кто это говорит. Эта часть про двадцать пять минут после кнопки Стоп: на экране ничего не происходит, на диске происходит всё. Там на этой неделе нашлась дыра, которая жила с июля. Облачная ревизия находила ошибки локальной модели, аккуратно их описывала, и никто её не слушал.
Одна встреча, четыре файла, двадцать пять минут
Пятница, рабочий созвон на два часа. Запись остановлена в 17:33. По логам:
17:33 стоп; живой черновик стенограммы закрыт
17:36 финальная стенограмма: диаризация по каналам, распознавание заново, имена
17:38 заметка встречи в графе, узлы людей, минутки, разбор локальной модели,
семь фактов ушли в память
17:58 облачная ревизия готова: 20 минут, 57 правок графа перенесены,
в минутки дописано 7 поручений
Локальная модель записала в минутки три решения и список поручений. Ревизия, прочитав стенограмму целиком, два решения из трёх отвергла: одно было шуткой про бюджет (один токен на всех, следующий запрос в следующем году), второе — задачей соседней команды. У третьего верное решение и не тот исполнитель. Три поручения сняла: человеку, которого на встрече не было, со сроком до 25-го, который никто не называл; без срока и исполнителю, который со следующей недели в отпуске; и одно с числом, прочитанным наоборот — план принят за разницу. Плюс имя: дорожку ведущего распознавание назвало чужим именем по обращению, ревизия по тем же обращениям нашла настоящее.
Всё это лежало в файле ревизии с цитатами и таймкодами. Дальше:
в минутки ревизия умела только дописывать: семь восстановленных поручений добавились, три ложных остались и уехали во вкладку Задачи;
в заметке встречи шутка осталась с пометкой 📌 решение, опровержение лежало ниже, в разделе для человека;
семь фактов ушли в память в 17:38, за двадцать минут до того, как ревизия признала два из них несуществующими. Поиск по памяти потом спокойно отдавал единый токен для всех команд как принятое решение.
Ревизия работала с конца июля, каждый день, по каждой встрече. Её выводы никуда не доходили: второй проход был устроен как ещё один читатель, без права отменять.
Что происходит после стопа
Конвейер в пяти шагах, про первые два подробно было в прошлых частях.
Пересборка. Живой черновик собран из трёхсекундных кусков по ходу встречи. После стопа запись диаризуется заново по каналам, эхо микрофона режется по перекрытию с системным звуком, имена дорожек угадываются по обращениям. Старая версия уходит в скрытую папку: человек мог править стенограмму руками. Минутки либо пересобираются, либо перештамповываются — вместо номеров собеседников подставляются имена.
Граф. Локальная модель (35B, MoE, через Ollama) разбирает стенограмму в JSON: люди, системы, решения, поручения. Из этого — заметка встречи, узлы людей и систем, строка в оглавление, факты в память. Тут же пишется разбор: что это было и что делать дальше.
Ревизия. Если включён облачный слой, отдельный процесс отдаёт стенограмму, минутки, разбор и контекст графа большой модели через CLI по подписке. Модель пишет ревизию: где локальная ошиблась, что пропустила. В режиме правки она правит клон графа, и правки переносятся под замком с проверкой каждого файла, это было во второй части.
Доставка. Ревизия кладётся рядом со стенограммой, в архив и в документацию, минутки перечитываются во вкладку Задачи.
Ночь. Досье, ревизия ядер, утренний бриф.
Три модели: распознавание, локальная, облачная. И вопрос, который я полтора месяца не задавал: кто отвечает, если шаг три не согласен с шагом два?
Зачем второй проход
Почему не сделать локальную модель умнее? Она ошибается по одной причине: пишет минутки по ходу встречи, каждые две с половиной минуты видит хвост стенограммы и должна выдать документ. Шутка про бюджет выглядит как решение, потому что смех через десять секунд модель ещё не видела. Человек, которого трижды упомянули, выглядит как участник. Слово текучка в разговоре про бюджет читается как текучесть кадров, и в рисках встречи появляется остановка разработки из-за увольнений.
Большая модель читает стенограмму один раз и целиком. Она видит, что на вопрос к Игорю отвечает дорожка с другим именем. Видит, что рекомендации в разборе пришли с другой встречи через контекст графа. Один проход большой модели после встречи дешевле, чем заставлять маленькую думать как большую на ходу.
Но тогда второй проход должен уметь снимать, а не только дописывать. То есть менять файлы, которые уже прочитали вкладка Задачи, граф и память. Этим я и занимался всю неделю.
Снимать, а не дописывать
Ревизия говорит с кодом не прозой, а строгими разделами в конце файла. Проза остаётся человеку.
## Восстановленные поручения
- [ ] **Имя** — что сделать — срок, если назван
## Снятые поручения
- **Имя** — что сделать — причина: …
## Исправления имён
- **Метка** → **Имя** — основание: …
Восстановленные были и раньше. Снятые и Исправления имён — новые. Модель просят писать имя и дело так же, как в минутках, чтобы код нашёл строку.
Самое скучное место — как код находит строку минуток по пересказу модели. Первая версия считала совпадением вложение в любую сторону: подготовить отчёт вкладывается в подготовить отчёт по бюджету, значит, это оно. Внешний ревьюер за пятнадцать минут показал, что так один снятый пункт снимает все похожие строки: отчёт по бюджету и отчёт по срокам уезжают из задач вместе. Съеденное поручение человек не увидит никогда, лишний дубль увидит и снимет одним кликом. Цена ошибок несимметричная, отсюда правила:
один пункт ревизии снимает ровно одну строку;
если подходят несколько — точное совпадение перевешивает, а без точного не снимается ничего, в лог уходит строка: подходит к двум пунктам, не гадаем;
вложение засчитывается от двух общих значимых слов: одно слово позвонить не снимает позвонить в банк;
пункт, который человек отметил сделанным, не снимается никогда;
снятая строка не удаляется, а переезжает в раздел Снято ревизией зачёркнутой, с причиной. Из Задач она исчезает, из минуток нет.
Правило не гадать в проекте уже было, для узлов графа: имя похоже на два существующих узла — узел не создаётся, кандидаты уходят человеку. Оно оказалось общим: где выбор идёт по нечёткому совпадению, а ошибка — тихая потеря, лучше ничего не делать и сказать об этом.
Второй круг ревью принёс историю про владельца. В настройках он записан двумя словами, минутки пишут его полным именем, а для сравнения код сводил владельца к первому слову только с одной стороны. Ключи расходились на фамилию, снятие для владельца не срабатывало никогда, и ни один тест этого не ловил: все тесты были с однословным владельцем. Две внешние головы нашли это независимо, одна поставила Critical. Чинится одной функцией канона на обе стороны. Тестовые данные должны включать неудобный случай из конфига, а не удобный из головы.
С решениями проще. Облако помечает ошибочное решение в заметке на месте: 📌 становится ⛔, в конце строки причина. Всё, что читает заметку — досье, поиск, память, — знает, что ⛔ не решение.
Память: догнать ревизию
У памяти ассистента (отдельный сервис с векторной базой и Postgres, общий с моим чат-ботом) нет ключа факта, только ключ встречи. Нельзя забыть одно решение, можно забыть встречу. Поэтому после переноса правок код сравнивает список живых решений в заметке до и после. Изменился — встреча забывается целиком и присылается заново без ⛔. Не изменился — ничего не трогаем.
На бумаге две строки. В коде это вышел самый дорогой кусок недели: четыре круга ревью, и в каждом что-то находилось здесь. Обрыв между забыть и прислать заново оставлял память встречи пустой навсегда — появился файл-долг рядом с отметкой отправки. Долг жил не у того, кто отправляет, и повтор обработки его не гасил. Долг гасился по числу чужих ключей в отметке — при мёртвом сервисе, без единого удачного запроса. Это уже был Critical. Долг чужой встречи гасил чужой процесс посреди своей отправки. Пришлось добавить замок отправителя, возраст долга и очередь. К четвёртому кругу код был правильным и уродливым.
Аудит по зонам (о нём ниже) добил: из семнадцати подтверждённых дефектов пять сидели в синхронизации с памятью, и три вердикта из десяти независимо назвали один и тот же — долг встречи из другого графа гасится без оплаты. Механизм, который держит вторую память в согласии с первой, оказался главным местом, где они расходятся.
Тут я остановился и посчитал, что вторая память умеет такого, чего нет в графе. Заметка встречи с решениями — в графе есть. Семантический поиск — граф и так ищется гибридом. Уникального две вещи: личные факты, которых в графе встреч нет по замыслу, и память диалогов. Остальное — копия, за которую я плачу долгами, замками и кругами голов.
Вторую память я выключаю. Не сегодня и не удалением: сначала личное, несколько сотен фактов, переезжает в граф обычными markdown-файлами; подсказки получают свой поиск по графу без сервиса; диктовка и дневник пишутся в папку. Потом дамп, остановка служб и через неделю удаление. Старая память работает, пока новая не отвечает на те же вопросы. Две памяти без общего ключа — как два писателя в одну папку без замка из второй части: та же потеря данных, только медленнее.
Имена: чья это дорожка
Распознавание называет дорожку по обращениям и ошибается, ревизия это видит. Раньше она писала об этом абзац, и всё: заголовки реплик, участники минуток, копии в архиве оставались с чужим именем.
Теперь по разделу Исправления имён код перештамповывает заголовки реплик и строку участников в стенограмме и минутках. Версия до правки уходит в ту же скрытую папку. Хеш машинного текста обновляется, чтобы следующая пересборка не приняла правку за ручную. Четыре ограничения, каждое из находки ревьюера:
Поручения не переименовываются. Игорь — согласовать план могло относиться к настоящему Игорю из речи, не к дорожке с этим именем. Такие поручения ревизия снимает и возвращает сама, теми же разделами.
Обмен меток одним проходом. Последовательная замена А на Б, потом Б на А превращала двух участников в одного. Critical от внешней головы.
Слияние дорожек запрещено. Два голоса под одним именем без отката хуже, чем чужое имя на одной дорожке.
Метка микрофона владельца не трогается. Канал — факт железа, модель тут ни при чём.
И решение, с которым я спорил сам с собой. Перештамповка идёт только когда облаку разрешено править граф и перенос сверен. В режиме только чтения стенограмма не переименовывается, иначе получится расколотая правда: в стенограмме верное имя, в узле человека чужое, и никто их не сведёт. Обе головы назвали это риском, я согласился. Но восстановленное поручение с верным именем в этом режиме получало бы пометку, что исполнитель не участник. Поэтому карта имён из ревизии всё равно строится, файлы не трогает и отдаётся мосту поручений как список участников.
Повтор и честный статус
За три дня девятнадцать ревизий, четыре без результата: таймаут в тридцать минут на длинной стенограмме, два ненулевых кода через восемь и восемнадцать минут, один незапуск — в этот момент обновлялся CLI. Во всех четырёх случаях повторить можно было только всю обработку с пересборкой, а статус встречи показывал готовность.
Теперь у ревизии четыре исхода: доставлено, ответ не похож на ревизию, процесс не запустился или упал, таймаут. Повторяется один класс — упавший процесс, один раз, через десять минут и не под живой встречей. Таймаут повторять бессмысленно: тот же контекст упрётся в тот же потолок. Ответ с кодом ноль, но не похожий на ревизию, — ответ модели, второй заход даст тот же.
Этап ревизии лежит в статусе встречи отдельным полем: идёт, повтор, готово, сбой. Готовность встречи он не сдвигает. Поле переживает запись готовности и протухает через три часа, если воркер убили в паузе. Ещё одну дыру нашёл ревьюер: кнопка Повторить обработку в приложении появляется через тридцать минут тишины, а легитимная ревизия идёт до тридцати минут плюс замок. Кнопка появлялась как раз тогда, когда первый воркер ещё жив, и второй запускал платный разбор заново. Теперь перед вторым заходом проверяется, не появилась ли ревизия свежее стенограммы.
Аудит по зонам за семьдесят девять центов
Про внешние головы было во второй части: каждое изменение перед мержем читают DeepSeek и GLM, пока круг приносит Critical — гоним следующий. На этой неделе добавился другой режим: аудит кода по зонам.
Пять зон конвейера после встречи: пересборка и имена, граф, облачная ревизия, мост минуток, память и статусы. На зону — чистый клон репозитория, инструменты только на чтение, список файлов в порядке чтения и строка бюджета: двенадцать минут и не больше сорока ходов, дальше вердикт по прочитанному. Без бюджета широкая зона не читается: в прошлый раз одна голова дала три пустые попытки, вторая ответила с третьей. Обе головы получают одни и те же пять зон, десять процессов стартуют вместе.
10 зон (5 DeepSeek 4.1 flash + 5 GLM 5.3 flash), все параллельно
стена 10 мин 13 с (самая долгая зона — 10 мин)
на зону 6–10 мин, 17–39 ходов инструментов
токенов на зону 120–155 K входных, 1–2,8 M кэшированных, 33–61 K выходных
цена $0,38 + $0,40 = $0,79 за десять вердиктов
DeepSeek — 12 Important и 16 Minor, GLM — 1 Critical, 9 Important и 23 Minor. Critical — про фильтр эха, который режет речь владельца при одновременном разговоре: известный компромисс, померенный в третьей части, отклонил. Из двадцати одного Important по коду подтвердились все, после слияния дублей — семнадцать дефектов. Три вердикта из десяти сошлись на одном и том же дефекте, и это лучший сигнал, который может дать такой аудит.
Головы не голосуют. Каждую находку я проверял по коду сам, и это заняло больше времени, чем весь аудит. Восемь центов за зону — цена вердикта, не правды.
Одна поломка была в самой обвязке — скрипте вне репозитория, который поднимает клоны и собирает вердикты. Его гейт на исчерпанный лимит искал в ответе подстроки вроде 402 и rate_limit_error. Два вердикта из десяти он объявил остановкой по лимиту, потому что аудируемый код содержит те же строки, а вердикт их цитировал. Восстановил из сырого ответа, гейт теперь смотрит только на короткие ответы. Любой фильтр по подстроке однажды встретит текст, который про этот фильтр рассказывает.
Что изменилось за неделю
Ревизия умеет снимать: ложные поручения уходят из задач в раздел Снято ревизией, ошибочные решения помечаются на месте, память встречи переотправляется без них.
Имена дорожек перештамповываются по строгому разделу, только под сверенной правкой графа и без гаданий.
Ревизия повторяется после сбоя запуска и показывает свой этап в статусе.
Разбор перестал тащить рекомендации соседних встреч: контекст графа теперь только для имён и терминов.
Вторая память признана дублем и идёт на выключение.
Аудит по зонам: десять минут, меньше доллара, семнадцать дефектов.
Что дальше
Семнадцать дефектов — в пяти карточках, каждая своим PR со своим кругом голов. Выключение второй памяти по этапам. И слепок голоса владельца, который я обещаю третью часть подряд: единственный способ отвечать на вопрос, где я, не по каналу, а по голосу.
Код и релизы: github.com/charoiteai/Charoite_audio, Apache 2.0. Девлог по-английски, с замерами и отменёнными решениями: charoiteai.github.io/Charoite_audio. Первая часть — про устройство и запись, вторая — про граф встреч как память, третья — про диаризацию и три ложных диагноза.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.