Перейти к содержимому

Разрешить сайту считать ваши посещения с помощью Яндекс Метрики? Для этого она сохранит в браузере файлы cookie. Как сайт использует cookie

VC
Кейс 17 из 25 · ИИ · базы знаний

ИИ-помощник по базе знаний компании: бот в Telegram отвечает с цитатами и знает, кому что можно показывать

Бот отвечает на вопросы по рабочим чатам, письмам, голосовым и документам и к каждому ответу прикладывает цитаты из источников. Поиск я перенёс из оперативной памяти в SQLite FTS5 — модуль полнотекстового поиска, встроенный в базу SQLite. Памяти теперь нужно в 20 раз меньше, поэтому бот переехал с ноутбука на сервер и работает даже ночью, когда ноутбук выключен.

Отрасль
Интернет-торговля, распределённая команда
Технологии
Python · SQLite FTS5 · Telegram · MCP
Сроки
≈ 8 недель работы
Итог
Память 1044 → 50 МБ, поиск 253 → 13 мс
01 · Проблема

Всё, что знала компания, хранилось в переписке

Бренд товаров для активного отдыха из США, два магазина на Shopify. Люди работают из разных мест: владелец бизнеса, операционная команда, специалист по рекламе, подрядчики. Решения нигде не записывались. Всё лежало в рабочих чатах: десятки тысяч сообщений, голосовые там, где просился документ, пересланные PDF и скриншоты. Плюс почта — больше 100 тыс. писем.

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

Владелец поставил условие прямо: «важно, чтобы в базе было как можно больше данных». Но в тех же чатах лежали личная переписка и персональные данные, которые команде показывать нельзя. Поэтому с самого начала задача звучала так: искать по всем своим данным и при этом ничего лишнего не раскрыть.

02 · Решение

Бот ищет по всем рабочим данным, и личное в индекс не попадает

Данные проходят путь в одну сторону. Их собирают, превращают в текст, очищают от секретов и кладут в индекс с правами доступа. Только после этого модель пишет ответ. Личное отсекается ещё на входе, поэтому его не вытянуть хитро заданным вопросом.

01
Сбор

Чаты выгружаются частями и склеиваются по номеру сообщения; плюс архив почты и пересланные документы

02
Всё становится текстом

Программа Whisper расшифровывает голосовые, OCR (распознавание текста на картинках) читает скриншоты. У каждого фрагмента есть ссылка на источник

03
Удаление секретов

Номера карт, коды и пароли вырезаются до записи в индекс

04
Индекс и права

Индекс хранится в SQLite FTS5, порядок результатов считает мой код. У каждого источника свой уровень доступа

05
Ответ

Модель пишет ответ с обязательными цитатами и присылает его в Telegram

Сбор: история чата не затирается

Чаты выгружаются частями и склеиваются по номеру сообщения. Причина простая: источник отдаёт только последние сообщения. Если каждый раз записывать выгрузку поверх старой, первая же неполная выгрузка сотрёт историю. Голосовые расшифровывает Whisper, скриншоты читает OCR, пересланные документы попадают в индекс как отдельные источники. В итоге всё превращается в текст, и у каждого фрагмента есть ссылка на источник.

Если сообщение отредактировали, запись обновляется, а прежняя версия уходит в историю правок. Раньше повторный номер сообщения считался дублем, и правка терялась. Из-за этого исправленный ответ сотрудницы 15 часов не попадал в базу, и по старой, обрезанной версии был сделан противоположный вывод.

Какие материалы попадают в базу

Картинки, присланные файлом, уходят в OCR. PDF без текстового слоя разбирается постранично: страницы превращаются в картинки и тоже идут в OCR. Предел — 20 страниц, и в извлечённом тексте об этом стоит пометка. Архивы, в том числе вложенные, распаковывает тот же модуль, с пределом 60 файлов и 40 МБ. Презентации тоже читаются.

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

Цифры первого прогона: 98 застрявших вложений вернулись в очередь, 45 обработаны сразу, а 76 вложений вне рабочих чатов намеренно оставлены без обработки. Из одного архива извлечено 573 572 символа, а таких архивов в очереди стояло 14. Два PDF размером 9,7 и 16,3 МБ до постраничного OCR давали ровно 0 символов. Обработчик знал только документы и фотографии, поэтому мимо него прошли 27 видеосообщений, 15 из них — в личном чате владельца. Из одного такого ролика теперь извлечён разбор товара на 2201 символ.

Статус говорит, почему файл не обработан

Три из четырёх потерянных вложений стояли со статусом «готово»: они выглядели обработанными и не попадали ни в один список проблем. Теперь статус называет причину: нет текста, PDF без OCR, видео, неподдерживаемый формат, слишком большой файл. Сводка выводит список того, что не обработано, и прогон завершается предупреждением.

Результат теперь сохраняется в базу после каждого вложения. Раньше он сохранялся один раз, в конце цикла, и любой сбой обнулял все статусы.

Метки «пусто» я перепроверил на реальных файлах

Ни одна из трёх меток не подтвердилась. 12 «сбоев скачивания» упирались в предел размера, у 6 «видео без звука» звук был, а за 20 отметками «нет текста» стояли разные причины. Отдельно нашлась такая ошибка: проверка отбрасывала весь распознанный текст, если он начинался со скобки. Таблицу на 771 символ OCR распознал без первой буквы, текст начался со скобки, и таблица попала в «нет текста».

Уровни доступа задаются в одном месте

Уровней три: владелец, команда и общий форум. Права заданы для каждого источника. Важная деталь: с базой работают двое — бот и модуль, который собирает выжимку фактов. У выжимки свой фильтр. Это отдельный код, и его правила легко могут разойтись с правами бота.

Поэтому обе части вызывают одну и ту же функцию is_private_source. За этим следит отдельный тест: если кто-то заведёт вторую копию правил приватности, автоматическая проверка кода (CI) не пройдёт.

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

Поиск переехал из памяти в SQLite

Первая версия держала весь индекс в памяти процесса и упорядочивала результаты по BM25 — классической формуле, которая оценивает, насколько документ подходит к запросу. Пока бот жил на ноутбуке, этого хватало. Я перевёл хранение на SQLite FTS5, но с одной оговоркой: FTS5 только хранит индекс (через fts5vocab). Порядок результатов по-прежнему считает мой код.

Причина конкретная. Встроенная функция bm25() в FTS5 выдаёт по документу одно общее число. Мне же нужен вклад каждого слова: по нему видно, нашлись ли все слова запроса или вес набран на одном частом слове. Поэтому порядок результатов я считаю сам.

Переход проверен так: движок переключается одной настройкой, старый и новый прогоняются на одних и тех же запросах, и выдача сверена место в место. Отличий нет. Изменились только расход памяти и время ответа.

Ответ приходит целиком

Модель, которая сначала рассуждает, иногда возвращает пустой ответ. Теперь в таком случае бот один раз повторяет запрос с лимитом 8000 токенов. Замер на рабочем запросе длиной 6466 символов показал, что одно рассуждение занимает 400–700 токенов. До этой правки на сложных вопросах бот отвечал слабее, хотя основная модель была исправна.

Что работает поверх базы

На базе знаний выросли другие инструменты:

  • Выжимка фактов. Переписка сворачивается в короткие упорядоченные факты
  • Утренние сводки в общем форуме в Telegram: по бизнесу, по рекламным кампаниям, по серверам и по трекеру
  • Около двух десятков сторожевых скриптов. Если что-то сломалось, они пишут в личные сообщения и в Discord. Они следят за работой CRM, зависшими заказами, сроками ответа на спорные платежи, важными сообщениями владельца и свежестью индекса
  • MCP-сервер поверх базы (MCP — стандартный способ подключать данные к ИИ-ассистентам). ИИ-ассистенты владельца работают с той же базой знаний, и вторую такую систему строить не пришлось
03 · Технологии

Индекс хранится в SQLite, порядок результатов считает мой код

Python

Сбор, индекс, порядок результатов и доставка написаны на одном языке

SQLite FTS5 + fts5vocab

Хранит индекс. Порядок результатов считает мой код, чтобы учитывать, все ли слова запроса нашлись

Своё ранжирование (формулы семейства BM25)

Учитывает вес каждого слова и то, насколько полно найден запрос; движок переключается одной настройкой

Языковая модель (Claude / DeepSeek)

Пишет ответ только из найденных фрагментов, цитаты обязательны

Whisper + OCR

Голосовые сообщения и скриншоты становятся полноценными источниками

Telegram Bot API

Бот в личных сообщениях и форум с отдельными ветками для сводок и оповещений

MCP-сервер

Та же база знаний доступна ИИ-ассистентам владельца

systemd + Discord

Службы, таймеры и около 20 сторожевых скриптов на сервере с оповещениями в канал

pytest

76 файлов с тестами, 1027 тестов, в том числе проверка, что права доступа не разошлись

PythonRAGSQLite FTS5fts5vocabwhisperOCRTelegram Bot APIMCPsystemdpytest
Масштаб кодовой базы

164 модуля на Python, около 40,8 тыс. строк, 378 коммитов. Главные части: движок индекса (~1300 строк), бот (~1500), удаление секретов (~560), выжимка фактов (~430), доставка в Telegram на стандартной библиотеке, MCP-сервер, сборщик чатов и около 20 сторожевых модулей.

Масштаб индекса

15 864 фрагмента в поисковом индексе. На сервер индекс переносится файлом на 32,7 МБ, и при приёме сервер сверяет контрольную сумму. Почта — два ящика: 94 800 и 8022 письма.

04 · Результат

Выдача та же, а памяти и времени нужно в разы меньше

Пик памяти при поиске
1044 МБ 50 МБ

в 20 раз меньше, выдача та же

Время ответа поиска
253 мс 13 мс

выдача сверена строка в строку, отличий нет

Приватных фрагментов на уровне «команда»
1327 0

замер на рабочем индексе до и после правки прав доступа

Весь сбор данных переехал с ноутбука на сервер

Памяти стало нужно в 20 раз меньше, и бота удалось перенести с ноутбука на сервер, где всего 961 МБ оперативной памяти. Прежняя беда «ноутбук уснул — бот ночью молчит» ушла: теперь бот не зависит от того, включён ли ноутбук.

Следом переехала почта: теперь ноутбук забирает с сервера готовый разобранный архив. Раньше он сам скачивал по IMAP 34 ГБ сырых писем ради 459 МБ разобранных. С конца июля сервер собирает тот же архив сам: он весит 450 МБ и в целом полнее. А диск ноутбука был заполнен почти целиком: свободными оставались 12 гигабайт из 460.

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

Историю страхует отдельная проверка: файл заменяется, только если новый не короче локального. На первом же рабочем прогоне она не дала заменить архив второго ящика: локально в нём было 8022 письма, на сервере 7985. Письма, которых на сервере не было, остались на месте.

Заодно с ноутбука убрана сборка индекса на 103 МБ. Он пересобирался 96 раз в сутки, и никто его не читал: время последнего чтения обоих его файлов совпадало со временем записи.

Сторожевые скрипты работают на сервере

Около двух десятков сторожевых скриптов запускаются на сервере по расписанию systemd. Их прежние фоновые задачи на ноутбуке (launchd) остановлены и помечены как отключённые, а команда для отката записана в журнал.

Один случай оттуда стоит рассказать. Сторож, который следит за свежестью данных из мессенджера, шесть часов писал «обновлялся 0 минут назад», хотя сборщик не работал. Скрипт неправильно читал время изменения файла и получал мусор, а мусор принимался за ноль минут. Теперь способы чтения пробуются по очереди, и скрипт проверяет, получилось ли вообще число.

Приватность проверена на рабочем индексе

Права доступа можно долго обсуждать на словах. Поэтому я измерил на рабочем индексе, сколько личных фрагментов на самом деле видит уровень «команда». Ответ был неприятный: 1327.

После правки прав этому уровню доступно 4560 фрагментов (было 6139), и личных среди них 0. Отдельно я проверил журнал обращений: к этим фрагментам никто не успел получить доступ.

Что изменилось в работе

Вопрос «что мы по этому решили» теперь задают боту. Он отвечает с цитатами из конкретных сообщений, в том числе голосовых. Утренние сводки приходят сами, о сбоях сообщают сторожевые скрипты. ИИ-ассистенты владельца через MCP работают с той же базой, и второй системы с копией данных не понадобилось.

05 · Применимость

Где ещё работает тот же подход

За кейсом стоит типовая задача: «знания компании разбросаны по переписке, и часть из них показывать нельзя». Она встречается почти везде, где команда работает дольше года:

  • → Поддержка и продажи — история переписки с клиентами становится базой ответов, и персональные данные при этом остаются закрытыми
  • → Агентства и студии — созвоны, брифы и голосовые от клиента находятся поиском с цитатой, и гадать «кажется, договаривались так» не приходится
  • → Производство и сервис — заявки, фото с объектов, переписка прорабов; ответ бота с цитатой избавляет от обзвона
  • → Новые сотрудники — уровень доступа «команда» вводит их в курс дела и не открывает личную переписку руководства
  • → Компании, которым нельзя хранить данные в облаке — всё, кроме составления ответа, работает на своём компьютере: индекс лежит в файле SQLite
Что переиспользуется на следующих проектах
  • Сбор чатов со склейкой по номеру сообщения: история не затирается, если источник выгрузился не полностью
  • Удаление секретов до попадания в индекс и одно место, где заданы правила приватности, с тестом, который не даёт им разойтись
  • Схема «FTS5 хранит индекс, порядок результатов считает свой код»: памяти нужно меньше, а выдача остаётся прежней
  • Переключение поискового движка одной настройкой и сверка выдачи строка в строку до и после перехода
  • Сторожевые скрипты и утренние сводки поверх базы: нужные сведения приходят сами
Похожая задача?

Если знания компании лежат в чатах и почте и открывать их всем страшно, их можно собрать в базу с уровнями доступа

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