Я поставила на этот сайт ИИ-бота — того самого, что отвечает в правом нижнем углу. Через несколько часов он начал уверенно называть посетителям неправильные цены. Причина оказалась не в модели, не в промпте и не в базе знаний, а в одной строке кода. Рассказываю целиком, потому что ошибка типовая, а ищут её обычно не там.
Бот отвечает по базе: мои услуги, цены, условия. Механика простая — вопрос попадает в поиск, поиск достаёт подходящие куски базы, модель отвечает строго по ним. Проверяю живыми вопросами:
— Сколько стоит ускорить сайт?
Ускорение сайта — от 5 000 ₽. Для начала можно провести
бесплатную диагностику.
В базе написано 3 500. Пять тысяч — цена другой услуги, стоящей в тексте по соседству. Дальше хуже:
— Сколько стоит ИИ-бот?
Содержание ИИ-бота стоит от 6 500 ₽ за пакет на 100 миллионов
токенов в год.
6 500 — это стоимость пакета токенов у поставщика нейросети, то есть расходы клиента после запуска, а вовсе не цена работы. Бот взял число из соседнего раздела и подал его как цену услуги.
Самое неприятное здесь не сама ошибка, а её тон. Бот не сомневался. Он не сказал «уточните» и не предложил связаться — он назвал сумму, как будто прочитал её в прайсе.
Первое, что приходит в голову: поиск слабый, надо взять модель поумнее. У меня стоял простой поиск по словам, и логика казалась очевидной — заменить его на нейросетевые эмбеддинги, которые ищут по смыслу, и всё наладится.
Я почти так и сделала. Остановило одно: прежде чем менять, стоит померить. Иначе потом не отличишь «стало лучше» от «стало иначе».
Сделала простой стенд. Список вопросов, по каждому известно, какой кусок базы правильный. Прогон считает, попал ли нужный кусок на первое место и в тройку. Отдельно — вопросы не по теме, на которых бот обязан молчать.
Ключевая деталь, без которой замер бессмысленен: часть вопросов отложена. На них ничего не настраивается, они смотрятся один раз, в конце. Это защита от подгонки — о ней ниже, она в этой истории сыграла главную роль.
Первый же прогон показал, где теряется ответ. Вот что находил поиск на вопрос про ускорение сайта:
0.259 Стартовые цены — в разделе услуг. Точную назову, когда...
0.199 Ежедневная сводка по рекламным каналам — сколько заявок...
0.186 Речь не о цене разработки — она отдельная, от 5 000 ₽...
0.129 Исправлю ошибку на сайте — от 1 000 ₽. Белый экран...
Нужного куска — того, где написано «ускорение сайта, от 3 500 ₽» — в четвёрке нет вообще. Зато есть три куска, объединённых одним: в них часто встречается слово «цена». Модель получила их и добросовестно взяла ближайшее число.
Вывод, который экономит часы: если бот отвечает не то, сначала смотрите, что доехало до модели. Промпт тут ни при чём — модель отвечала ровно по тому, что ей дали.
В поиске по словам есть стоп-слова — те, что встречаются почти везде и темы не несут: предлоги, местоимения, вопросительные слова. Их выбрасывают, чтобы они не тянули поиск на себя.
Рядом стоит лемматизация — приведение слова к начальной форме, чтобы «ускорить» и «ускорение» считались одним словом.
Код выглядел так:
# сначала отсев
if slovo in STOP or len(slovo) < 3:
continue
# и только потом начальная форма
slovo = morf.parse(slovo)[0].normal_form
Отсев идёт до приведения к начальной форме. В списке стоп-слов лежит «стоить». В запросе человек пишет «стоит». Совпадения нет — слово проходит дальше, уже потом превращается в «стоить», и участвует в поиске полноправно.
Результат: любой вопрос «сколько стоит X» тянул поиск на любые куски, где часто говорится про деньги, вместо куска про саму услугу X. Чем чаще в тексте слово «цена», тем выше он всплывал.
Починка — переставить порядок и отсеять ещё раз после лемматизации:
if slovo in STOP or len(slovo) < 3:
continue
if MORF is not None:
slovo = MORF.parse(slovo)[0].normal_form
# отсев повторно: «стоит» и «стоимость» отсекаются
# только после приведения к начальной форме
if slovo in STOP:
continue
Плюс в список стоп-слов добавились вопросительные и ценовые слова: «сколько», «стоить», «стоимость», «цена», «какой». Довод простой: тему вопроса несёт название услуги, а не они.
После правки тот же вопрос:
0.280 Ускорение сайта, сайт медленно грузится, долго открывается...
0.086 На сайте, который и так стоял на быстрой схеме, прибавка...
Нужный кусок первый, с отрывом втрое.
Дальше я всё-таки собрала смысловой поиск — компактную нейросетевую модель для русского языка, работающую локально. И прогнала оба способа на одних и тех же вопросах.
| Способ поиска | Верный кусок первым | Всего верных решений |
|---|---|---|
| Поиск по словам (после починки) | 17 из 22 | 39 из 45 |
| Поиск по смыслу, компактная модель | 8 из 22 | 31 из 45 |
«Умный» поиск проиграл простому, причём заметно. И проиграл он там, где должен был выигрывать: на переформулировках, ради которых его и берут.
Причина видна в цифрах близости. У смысловой модели вопросы по теме дали 0,42–0,70, а вопросы не по теме — 0,30–0,65. Диапазоны перекрываются целиком: порога, который пропускает нужное и не пропускает лишнее, просто не существует. У поиска по словам зазор был широкий: по делу 0,17–0,35, мимо темы 0,07–0,16.
Это не приговор эмбеддингам. Это довод против того, чтобы ставить их по умолчанию: на небольшой базе и коротких вопросах простой способ может оказаться точнее, а узнать это можно только замером.
По ходу работы нашёлся вариант настройки, который на рабочем наборе давал 40 верных из 45. Отличный результат, хотелось брать.
Проверка на отложенных вопросах дала 5 из 11 — хуже, чем без всякой настройки.
То есть параметры подобрались под конкретные вопросы, а не под задачу. Без отложенного набора этот вариант уехал бы в работу как улучшение, и обнаружилось бы это на живых посетителях, месяцем позже, в виде «бот что-то странно отвечает».
Правило, которое стоит завести до первой настройки: вопросы делятся на два набора сразу. На одном подбираем, второй не трогаем вовсе до финальной проверки.
Порог отсечки — цифра, ниже которой бот говорит «в моих материалах нет ответа», — принадлежит не боту, а паре «способ поиска + база». У прежнего способа он был 0,32, у нового 0,15. Число само по себе не значит ничего; переносить его между системами нельзя.
Отсюда практическое следствие: после заметной правки базы знаний порог стоит перемерить. Не потому, что он «сбился», а потому, что изменилась база, а он к ней привязан.
Похожий случай, где код был написан правильно, а вёл себя неверно: промокод, который увеличил сумму заказа. И ещё один, про доверие к настройке по внешним признакам: сжатие, работавшее только для HTML.
Чаще всего дело не в модели, а в поиске: до модели доезжает не тот кусок базы. Модель добросовестно отвечает по тому, что ей дали. Проверяется это просмотром найденных кусков до генерации ответа — если нужного куска среди них нет, чинить надо поиск, а не промпт.
Стоп-слова — это слова, которые встречаются почти в любом тексте и потому не несут темы: предлоги, местоимения, вопросительные слова. Их убирают из поиска. Важен порядок: если отсев идёт до приведения слова к начальной форме, то «стоит» не совпадёт со «стоить» из списка и останется в запросе. Тогда вопросы вида «сколько стоит X» начинают притягивать любые куски, где часто говорится про деньги.
Нет. На небольшой базе поиск по словам с морфологией может оказаться точнее нейросетевых эмбеддингов. В нашем замере на 45 вопросах лексический поиск дал 39 верных решений из 45, а смысловой на компактной модели — 31. Выяснить это можно только замером на своих вопросах: общего ответа не существует.
Разделить вопросы на два набора: на одном подбирать параметры, второй держать отложенным и не смотреть при настройке. Если результат хорош на первом наборе и разваливается на втором — это подгонка. У нас именно так отсеялся вариант, который на подборе давал 40 верных из 45, а на отложенных вопросах — 5 из 11.
Бот отвечает клиентам не то, что нужно? Посмотрю, что доезжает до модели, и скажу, чинить поиск, базу или промпт. Проверить, как это работает, можно прямо здесь — виджет в правом нижнем углу отвечает по моим же материалам.
Спросить ИИ-бота здесь