Локальный ассистент для зумов, часть 4: вторая модель отвечала в пустоту
После нажатия «Стоп» в локальном ассистенте для zoom-встреч вторая модель конвейера иногда продолжала генерировать ответ ещё несколько секунд — в закрытый канал, без получателя. Причина: сигнал отмены обрывал только первую модель, вторая проверяла его лишь после того, как дописывала свой кусок текста целиком.
23:47: в логе два потока вместо одного
Ассистент разбирал созвон в реальном времени: одна локальная модель вела саммари по ходу разговора, вторая параллельно готовила черновик ответа на последний вопрос собеседника — чтобы менеджер мог быстро скопировать формулировку в чат. Обе модели крутились на одном сервере, каждая в своём воркере, через локальный рантайм без выхода во внешние API.
В третьей части этой серии ассистент научился разделять реплики по говорящим и не путать вопрос клиента с репликой менеджера. Проблему с кнопкой «Стоп» тогда отложили — она не мешала демонстрации, только реальным звонкам с частыми перебивами.
В 23:47 тестировщик задал вопрос, дождался начала генерации черновика и через секунду нажал «Стоп» — обычный сценарий, когда собеседник меняет тему быстрее, чем модель успевает ответить. Интерфейс погасил спиннер сразу. Но в логе процесса ещё пять строк подряд писала вторая модель: она дописывала фразу до точки, как будто отмены не было вовсе.
Дело не в скорости генерации. Дело в том, что текст, который дописала вторая модель, никуда не попадал — канал, в который она писала токены, слушателя уже не имел. Интерфейс отписался от него в момент клика по «Стоп». Модель тратила видеопамять и секунды процессора на ответ, который стирался, не долетев до экрана.
почему «стоп» гасит только одну модель из двух
Кнопка «Стоп» вызывала один общий context.CancelFunc на весь запрос. Первая модель, та, что вела саммари, читала токены в цикле с проверкой ctx.Done() и останавливалась на следующей итерации — в пределах одного токена. Вторая модель, черновик ответа, была написана раньше и проще: она просто вычитывала канал до конца стрима, без проверки контекста внутри цикла.
Разница в семь строк кода превращалась в разное поведение при отмене. Первая модель слушала сигнал. Вторая — только факт закрытия входного канала, а закрывал его сам стрим по завершении генерации, то есть уже после того, как модель всё сказала.
что показал профайлер
Трасса GPU за время инцидента подтвердила догадку: после клика «Стоп» нагрузка не падала до нуля, а держалась на уровне одной активной модели ещё несколько секунд. На горячей линии, где за смену проходит по несколько десятков звонков и в каждом бывает по два-три прерывания, эта разница накапливается — сервер держит модели дольше расчётного времени, а следующий черновик стартует не мгновенно, а с паузой, которую пользователь чувствует как подвисание ассистента посреди разговора.
во что это обходится, если не чинить
Пока в очереди один звонок, лишние секунды генерации никто не замечает. Проблема всплывает на количестве: если за встречу собеседник пять раз меняет тему, а менеджер пять раз жмёт «Стоп», сервер пять раз досчитывает ненужный ответ до конца. Видеокарта занята чужой работой ровно в момент, когда должна была освободиться под следующий вопрос.
Второй эффект хуже первого. Осиротевший поток не просто тратит ресурс — он иногда успевает дойти до действия. Если по логике пайплайна черновик ответа должен завершаться записью в лог диалога или вызовом внешнего сервиса, эта запись всё равно уходит — уже после того, как пользователь решил, что вопрос снят с повестки. Разбираться с таким «призрачным» действием постфактум дороже, чем потерянные секунды GPU: это ошибка не в скорости, а в данных.
три попытки остановить вторую модель — что сработало
Первым делом попробовали грубо: убить процесс модели в момент клика по «Стоп». Генерация обрывалась мгновенно, но вместе с ней падал весь воркер — следующий вопрос ждал холодного старта модели, а это заметно дольше, чем пять лишних секунд, которые пытались убрать.
Второй вариант — тот самый общий context.CancelFunc без изменений в цикле чтения — и есть причина исходного бага: работает для одной модели, не работает для второй, потому что вторая его не спрашивает.
Третий вариант закрыл вопрос: в цикл чтения токенов второй модели добавили select с ctx.Done() наравне с чтением из канала, а сам канал-приёмник стали закрывать явно при отмене, а не по завершении стрима. Обе модели теперь останавливаются на границе токена, а не на границе фразы.
| подход к отмене | что происходит при клике «стоп» | итог |
|---|---|---|
| kill процесса модели | генерация обрывается мгновенно, воркер падает целиком | следующий ответ стартует с холодного старта — задержка хуже исходной |
| один context.CancelFunc на весь запрос | первая модель останавливается, вторая дописывает фразу до конца | исходный баг — вторая модель «говорит в пустоту» |
| ctx.Done() внутри цикла чтения плюс явное закрытие канала-приёмника | обе модели останавливаются на границе токена | рабочее решение, ушло в прод |
если ассистент дальше пишет в 1С — цена гонки выше
У части клиентов ассистент не останавливается на тексте в чате. По итогам звонка он создаёт черновик коммерческого предложения в 1С или проверяет остаток по позиции, которую упомянул собеседник. Здесь та же гонка процессов стоит дороже: осиротевший поток, который не узнал об отмене, успевает отправить вызов в 1С уже после того, как пользователь закрыл сессию и решил, что вопрос снят.
Результат — черновик документа, который никто не создавал руками и никто не проверил. На следующий день менеджер находит в базе лишнее коммерческое предложение или задвоенную заявку и тратит время, чтобы понять, откуда она взялась. Для интеграции чат-бота с учётной системой это типовая проблема идемпотентности: обработчик на стороне 1С должен отличать повторный или поздний вызов от нового и не создавать по нему документ. Такая логика — это отдельная доработка 1С, а не настройка стандартного функционала.
Если чат-бота встраивают в 1С на этапе самого проекта — например, когда идёт внедрение 1С и ассистент задуман как часть интерфейса для менеджеров, правильнее сразу закладывать обработку отмены и идемпотентные вызовы в архитектуру, а не чинить их после первой жалобы. То же касается момента после обновления 1С: меняется API методов, под которые был написан обработчик, и окно гонки может увеличиться незаметно для команды, которая тестирует только счастливый путь.
что проверить, если заказываете похожего ассистента
Разработчику полезно задать три вопроса, а не верить на слово демо-звонку без прерываний.
- ✓как реализована отмена — один общий контекст на весь пайплайн или у каждой модели свой обработчик остановки;
- ✓что происходит с действием, которое модель успела начать до отмены, но не закончила — оно откатывается, доводится до конца или зависает;
- ✓если ассистент пишет во внешнюю систему вроде 1С или CRM — защищён ли обработчик от повторного или позднего вызова.
Тестировать нужно вживую, а не по документации: попросить нажать «Стоп» в момент, когда модель на середине фразы, и смотреть в лог, а не на экран — экран покажет, что всё остановилось, лог покажет, что происходит на самом деле.
Такие проверки и доработку логики отмены в интеграциях с 1С мы ведём по ставке сопровождения — от 3800 руб/час, с почасовой отчётностью по задаче, а не фиксом «на глаз».
❓ Частые вопросы
Почему после нажатия «Стоп» иногда всё равно приходит ответ ассистента?
Обычно причина в том, что отмена реализована на уровне общего запроса, а не каждой модели в конвейере отдельно. Одна модель проверяет сигнал отмены в цикле генерации и останавливается сразу, другая — только по факту завершения стрима, то есть уже после того, как дописала весь ответ.
Можно ли исправить такую гонку процессов без переписывания всего ассистента?
Да, чаще всего достаточно добавить проверку контекста отмены в цикл чтения токенов каждой модели и явно закрывать канал-приёмник при клике «Стоп», а не дожидаться естественного завершения стрима. Это точечная правка в коде обработчика, а не изменение архитектуры.
Как эта ошибка связана с интеграцией чат-бота с 1С?
Если ассистент по итогам диалога создаёт документ или проверяет данные в 1С, осиротевший поток после отмены может всё равно отправить вызов — уже когда пользователь закрыл сессию. Решение — идемпотентный обработчик на стороне 1С, который отличает поздний повторный вызов от нового и не создаёт дубль.
Сколько стоит проверить и доработать логику отмены в уже работающем ассистенте?
Такая работа идёт по ставке сопровождения и доработки 1С — от 3800 руб/час, с почасовой отчётностью по конкретной задаче. Точную оценку по времени даём после того, как смотрим код обработчика отмены и логи реальных звонков с прерываниями.
Обязательно ли использовать две модели в пайплайне, или одна работает надёжнее?
Две модели решают разные задачи параллельно — саммари и черновик ответа — и это ускоряет работу ассистента, но требует, чтобы каждая независимо слушала сигнал отмены. Одна модель проще в этом смысле, но не даёт того же отклика в реальном времени во время звонка.
Или позвоните: +7 906 045-28-27 — в рабочее время с 9:00 до 19:00
Остались вопросы? Нужна помощь?
Менеджеры компании с радостью ответят на ваши вопросы, произведут расчет стоимости услуг и подготовят индивидуальное коммерческое предложение.
Бесплатная консультация

