RAG-бот по каталогу: полгода в продакшене
С 23 апреля 2026 года на сайте противопожарного оборудования работает вторая версия нашего чат-бота. Первая версия была до неё, и с апреля в работе только вторая. С июля на том же ядре работает бот на сайте лаборатории аутоиммунной диагностики. Ниже рассказываем, как он устроен, где мы ошибались и что видно по диалогам. Пишем для владельцев бизнеса, а не для программистов: термины объясняем по ходу.
Задача
Каталог противопожарного оборудования большой. На момент одного из замеров в базе бота было 2071 товар, 503 категории и 477 статей. Клиент приходит с вопросом вида «какой огнетушитель нужен на склад 10×10 метров» или «есть ли ГПСС-2000 в наличии». Менеджер отвечает на такое за минуту, если он свободен. Бот должен отвечать так же быстро, по фактам из каталога, и вовремя передавать разговор человеку.
Главное требование было одно: бот не имеет права придумывать. Неверная цена или несуществующая модель в B2B стоят дороже, чем честное «уточню у менеджера».
Как устроено
Схема называется RAG (retrieval-augmented generation): сначала бот находит в базе подходящие документы, потом языковая модель составляет ответ только по ним.
Индекс. Каждая карточка товара хранится в двух видах. Короткая выжимка на 200–500 символов превращается в числовой вектор (эмбеддинг), по которому ищутся похожие по смыслу тексты. Полная карточка в поиске не участвует и передаётся модели целиком, когда товар найден. Так поиск остаётся точным, а у модели есть все характеристики.
Обновление. Индекс обновляется сам. Синхронизация сравнивает отпечаток (хэш) полного описания и переиндексирует только изменившиеся карточки. Цены и наличие обновляются отдельным процессом пачками по 300 товаров. Наличие мы сознательно не кладём в текст для поиска: оно меняется часто, и каждый раз пересчитывать векторы незачем. Источник наличия — товарный фид сайта, и порядок проверки там важен:
if (nalRaw)
availability = nalRaw; // "Снято с производства", "Под заказ", "В наличии"
else if (ost > 0) availability = "В наличии";
else availability = "Уточняйте наличие";
Текстовый статус главнее складского остатка. Если поменять порядок, бот начнёт предлагать снятые с производства модели, у которых на складе ещё что-то лежит.
Поиск. Вопрос клиента редко бывает самодостаточным. «А он уличный?» без истории ничего не значит. Поэтому перед поиском отдельная модель переписывает реплику с учётом диалога и страницы, на которой открыт чат. Если в одном сообщении несколько товаров, вопрос делится на подзапросы, и каждый ищется отдельно. Поиск гибридный: по смыслу через векторы и по словам, чтобы находились артикулы вроде ОП-4 или ОУ-2. Затем реранк, то есть повторная сортировка кандидатов, отбирает лучшие.
Защита от выдумок. В инструкции модели сказано, что любая цена и название модели должны дословно присутствовать в найденном контексте. На сайте лаборатории мы довели это правило до кода: после генерации отдельная проверка сверяет каждую цену и срок с той карточкой, на которую ведёт ссылка, а не с любой карточкой в контексте. Перефразированное название анализа вырезается, даже если цена верная. Ответ становится менее гладким, зато выдуманное название до клиента не доходит.
Заявки. Когда клиент называет количество и хочет купить, бот предлагает оформить заявку кнопками, собирает форму и отправляет письмо менеджерам. Если нужен человек или пришёл файл, диалог переходит к оператору.
Что пошло не так
Почти все исправления последних месяцев выросли из конкретных диалогов. Вот те, что повлияли на устройство бота.
Бот принимал подсказки страницы за вопрос. В мае клиент на странице огнетушителя ОУ-2 написал «10×10 метров». Бот переписал это как «ОУ-2 для 10×10», хотя человек спрашивал, сколько огнетушителей нужно на такую площадь. Теперь контекст страницы подмешивается только тогда, когда в вопросе есть местоимение вроде «он» или «этот». Числовые ответы с единицами измерения бот всегда связывает с историей диалога.
Служебная пометка попала в память. В том же мае внутренняя отметка о статусе заявки оказалась в истории диалога, и на следующем ходу бот сообщил клиенту, что всё «уже отправлено». После этого в память пишется только реплика клиента. Тогда же убрали из инструкции шаблон «Заявка №N отправлена»: модель иногда выдавала клиенту букву N дословно. Номер теперь подставляет код после реальной записи в базу.
«Первый вариант». В августе бот предложил две модификации генератора пены, клиент ответил «первый вариант». Модель переписала запрос правильно, но наши же проверки отклонили результат, потому что формально он нарушал их правила. Добавили отдельное распознавание порядкового выбора.
Два письма на одну заявку. 1 сентября клиент нажал «Отправить» дважды с разницей в 1,7 секунды, и менеджеры получили два одинаковых письма. Проверка статуса не спасала, потому что статус хранился в атрибутах диалога, а виджет сайта их перезаписывал. Источником истины сделали строку заявки в базе: первый клик атомарно «захватывает» её до отправки письма, второй находит заявку уже занятой и молча завершается. Если почта не ушла, заявка возвращается в очередь. Потерять заявку хуже, чем отправить письмо дважды.
Бот молчал на собственную кнопку. 2 сентября клиент отправил текст и файл одновременно. Файл перевёл диалог на оператора, а параллельно бот успел показать кнопки заявки. Клик по кнопке фильтр отбросил: в диалоге с оператором бот должен молчать. Теперь клик по элементу, который нарисовал сам бот, обслуживается при любом статусе диалога.
Сбои сервиса эмбеддингов. Провайдер векторов периодически отвечал отказом сразу по всем ключам в одну и ту же секунду, хотя наш трафик — около 10 запросов в день. Ротация ключей не помогала. Помогли паузы между попытками и второй маршрут к той же модели, чьи векторы совпадают с исходными, так что переиндексация не понадобилась. Если отказали все маршруты, бот ищет по словам.
Ответы, которые выглядели удачными. В сентябре мы проверили 400 записей журнала за месяц с небольшим. Провал ответа раньше определялся по фразам-маркерам, и такой способ находил только 63% реальных провалов. Из 100 ответов, которые система считала успешными, независимая проверка забраковала 24. Мы перевели реранк на отдельную модель оценки, которая видит историю диалога, и оставили прежний вариант как запасной. На 100 реальных запросах точность первого результата выросла с 85% до 93%. Каждый ответ бота теперь получает автоматическую оценку качества.
Мониторинг пропустил сбой. Проверка здоровья смотрела только открытые диалоги. Оператор закрыл диалог через 7 минут после сбоя, и проверка его не увидела. Теперь она смотрит все диалоги по времени последней активности.
Что видно по диалогам
Бот на сайте лаборатории работает на базе примерно 1260 документов, из них 405 карточек анализов с ценами. Обычно это 28–45 диалогов в неделю, пиковые недели дали 99 и 115.
Самое полезное для бизнеса здесь не количество диалогов. Мы отмечаем каждый ответ, в котором бот сам признал, что нужного материала на сайте нет. При первом разборе таких оказалось 66 из 381 содержательного вопроса, и они делятся на три группы:
- на сайте нет нужного текста: «натощак?», температурный режим, бесплатно ли по ОМС;
- клиент написал с опечаткой или сокращением, и поиск не понял;
- такой услуги нет вовсе.
Каждая группа решается по-своему. Первая — задача для контент-менеджера: список вопросов уходит ему еженедельно, и это готовый план статей, написанный словами клиентов. Вторая чинится на стороне поиска. Третья показывает спрос, о котором стоит знать.
Тот же разбор ловит ложные отказы бота. Однажды бот сообщил, что у лаборатории нет Telegram-бота, хотя он есть. Такие ответы мы находим в отчёте и исправляем.
Был и показательный случай. Клиент описал жалобу обычными словами, поиск ничего не нашёл, и модель придумала ограничение: «лаборатория не проводит диагностику кожных патологий». Подходящие карточки в базе при этом были. Мы добавили два механизма. Описание жалобы до поиска переводится в названия заболеваний, потому что в базе пишут именно ими. Отдельная проверка вырезает фразы вида «лаборатория не делает X», если такое ограничение не задокументировано.
Что ставить у себя
Если вы думаете о таком боте для своего каталога, вот что мы бы сделали с первого дня.
Журнал каждого ответа. Нужны исходная реплика клиента дословно, переписанный запрос, найденные документы и оценка ответа. Без журнала вы не узнаете, что каждый четвёртый «успешный» ответ на деле пустой.
Проверка кодом, а не только инструкцией. Правило «не выдумывай цены» в промпте модель иногда нарушает. Сверка цены с карточкой после генерации работает каждый раз.
Источник истины в базе. Всё, что приводит к внешнему действию — письму, заявке, звонку, — должно фиксироваться в базе до этого действия. Состояние в интерфейсе чата ненадёжно.
Запасной путь у каждого внешнего сервиса. Сервисы эмбеддингов и моделей падают без предупреждения. У бота должен быть второй маршрут и честный режим деградации, при котором он не утверждает, что товара нет.
Отчёт о пробелах для контента. Это самая дешёвая часть системы и самая полезная для сайта. Вопросы, на которые бот не нашёл ответа, — это готовое ТЗ на статьи и карточки.
Живой человек рядом. Бот должен знать, есть ли сейчас операторы, и уметь передать диалог. Если операторов нет, он берёт контакт и отправляет переписку менеджеру.