Не промптами едиными: как история обращений помогает развивать базу знаний

—

от автора

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

В презентации всё выглядит шикарно. Бот понимает вопрос, находит нужные материалы, формулирует ответ. Остаётся немного настроить — и можно отпускать сотрудников заниматься чем-нибудь более интересным.

Но довольно быстро выясняется, что пользователи упорно не хотят действовать по алгоритму. Формулируют вопросы как придётся, опускают важные детали, ограничиваются коротким «не работает» или «помогите». Для векторного поиска такие сообщения создают проблему: в них слишком мало информации, чтобы уверенно определить смысл запроса. В результате подбор статей становится почти случайным: в выдачу попадают статьи, которые лишь условно похожи на обращение.

Бывает и хуже: нужной статьи в базе вообще нет. Тогда LLM пытается собрать ответ из того, что нашлось по смежным темам. Получается связный текст, который может иметь довольно отдалённое отношение к проблеме пользователя. Модель, в общем, старается, но исходных данных ей никто не подвёз.

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

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

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

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

«Не работает».

«Помогите, пожалуйста».

«Интернет опять пропал».

«Не проходит оплата».

По таким сообщениям человеку порой трудно понять, что именно случилось. Универсальная embedding-модель тоже не читает мысли: она преобразует текст в вектор и позволяет искать семантически близкие материалы. А когда исходный текст почти ничего не сообщает о проблеме, близость становится довольно условным ориентиром.

В результате в выдачу могут попасть статьи, которые формально связаны с обращением, но не помогают его решить. Например, статьи об оплате и настройке роутера могут оказаться рядом просто потому, что оба относятся к ситуациям, когда у клиента что-то не работает.

Но есть проблема посерьёзнее: что делать, если подходящей статьи в базе вообще нет?

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

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

Поэтому я решил не искать очередную универсальную embedding-модель, а изменить сам подход к сопоставлению обращений и статей.

Вместо универсального пространства — пространство тем

В основе нового подхода лежит набор тем, каждая из которых описывает определённый тип проблемы или пользовательское намерение. Для машинного представления тем используются slug’и. Для конкретного проекта получилось 18 тем, выделенных в ходе работы с реальными обращениями.

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

Здесь важно не само число 18. Для другого продукта тем могло бы оказаться больше или меньше. Смысл в том, чтобы описать предметную область достаточно подробно для решения практической задачи, но не превращать классификацию в бесконечный каталог частных случаев.

Для разметки используется Jev от TypeSafe AI. Модель анализирует текст и возвращает оценки соответствия разным темам. В результате обращение представляется не одним вектором в универсальном семантическом пространстве, а набором тем с соответствующими оценками. При расчёте полноты эти оценки используются как веса тем.

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

Точно так же размечаются статьи базы знаний. В результате и обращение, и статья описываются в одной системе координат: можно сравнить не только тексты в целом, но и то, насколько конкретная статья соответствует определённым темам.

Это меняет характер поиска. Вместо вопроса «Какие статьи семантически похожи на этот текст?» появляется более предметный: «Какие темы затрагивает обращение и какие статьи действительно раскрывают эти темы?»

Оценки модели при этом не следует воспринимать как вероятности в строгом статистическом смысле. Это числовые оценки тематического соответствия, которые используются в дальнейших расчётах. Их практический смысл определяется тем, как устроена разметка и какие пороги применяются в алгоритме.

Как определить, хватает ли найденных статей

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

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

Сначала система отбирает значимые темы обращения. Для каждой темы есть оценка, и в расчёт попадают только те, которые проходят заданный порог значимости. Малозначимые оценки и заранее исключённые темы, которые могут создавать шум, не учитываются.

Дальше начинается подбор статей. Алгоритм выбирает не больше установленного количества статей — по умолчанию трёх. На каждом шаге он ищет статью, которая лучше всего соответствует темам, ещё не учтённым предыдущими статьями.

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

Разберём на примере.

Предположим, пользователь пишет: «Оплатил, а интернета нет». Система выделила две значимые темы: оплату с весом 0,5 и отсутствие соединения с весом 0,4.

В базе есть две статьи. Первая объясняет, как проверить оплату, вторая посвящена диагностике подключения. Каждая хорошо раскрывает свою тему, поэтому алгоритм может выбрать обе статьи и учесть ими весь набор значимых тем. Полнота набора статей составит 100%.

Если же доступна только статья об оплате, тема отсутствия соединения останется неучтённой. Полнота составит примерно 56%: из общей суммы весов 0,9 будет учтена тема с весом 0,5.

Почему не просто посчитать, сколько тем учтено? Потому что темы могут иметь разный вес. Если обращение связано с двумя темами, оценёнными как 0,7 и 0,3, учёт первой даст 70%, а второй — 30%. Простое количество тем не отражало бы этого различия.

Формально полнота рассчитывается как сумма весов учтённых значимых тем, делённая на сумму весов всех значимых тем. Сами веса не обязаны суммироваться до единицы: при делении на общую сумму мы всё равно получаем долю.

В реализации это жадный алгоритм отбора статей (greedy cover). Он последовательно выбирает наиболее полезную статью на каждом шаге, а не перебирает все возможные комбинации статей в поиске математически оптимального набора. Для текущей задачи это практичный компромисс: число статей ограничено, а сам расчёт остаётся достаточно простым.

Порог полноты по умолчанию составляет 0,55. Если найденные статьи достаточно полно раскрывают значимые темы обращения, система не считает такое обращение признаком пробела в базе. Если полнота ниже порога, система дополнительно анализирует соответствие лучшей отдельной статьи.

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

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

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

От повторяющихся обращений к черновикам статей

Когда система научилась находить обращения, для которых полнота набора найденных статей недостаточна, возник следующий вопрос: как превратить этот сигнал в полезное изменение базы знаний?

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

Поэтому следующий этап — поиск повторяющихся обращений.

Система группирует похожие тикеты и отбирает случаи, которые встречались несколько раз. В текущей реализации минимальный размер такой группы — три обращения. Это помогает отсеять единичные нестандартные ситуации, хотя, разумеется, само по себе повторение ещё не доказывает, что для темы нужна отдельная статья.

Затем из переписки извлекаются ответы операторов. Короткие и малоинформативные ответы отфильтровываются: например, сообщения, в которых сотрудник лишь просит уточнить данные или сообщает, что разбирается в ситуации. Такие реплики могут быть уместны в диалоге, но в качестве основы для инструкции почти бесполезны.

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

Здесь есть важное ограничение: качество черновика зависит от качества исходных ответов. Если операторы давали противоречивые рекомендации или в переписке не хватало существенных деталей, одна только генерация не исправит ситуацию. Модель может структурировать материал, но не должна самостоятельно придумывать недостающие факты.

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

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

Почему публикация остаётся за человеком

На этом этапе возникает соблазн автоматизировать всё до конца: обнаружили пробел, собрали ответы, сгенерировали статью и сразу опубликовали. Технически можно построить и такой конвейер. Вопрос в том, насколько разумно доверять ему без проверки содержимого.

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

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

Это и есть human in the loop — не просто страховка от ошибок модели, а разделение задач с учётом сильных сторон каждого участника. LLM быстрее обрабатывает большие объёмы переписки и собирает материал в связную структуру. Человек лучше оценивает достоверность рекомендаций, замечает отсутствующий контекст и понимает, подходит ли текст для реальной работы поддержки.

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

Что удалось обнаружить

Самый показательный результат обнаружился при анализе обращений, связанных с превышением лимита трафика.

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

Раньше бот в таких случаях всё равно пытался ответить автоматически, используя найденные статьи по смежным темам. Получался ответ, который мог выглядеть вполне убедительно, но не объяснял, что делать именно при превышении лимита трафика. Модель не располагала нужной информацией и собирала ответ из того, что было под рукой.

Система обнаружила повторяющиеся обращения, для которых существующих статей было недостаточно, и подготовила черновик статьи на основе ответов операторов. После проверки статью добавили в базу знаний. Теперь у бота есть подходящая инструкция, на которую можно опереться при ответе на такие вопросы.

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

Ограничения и что ещё предстоит проверить

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

Во-первых, сама таксономия тем требует внимания. Если темы определены слишком широко, разные проблемы могут оказаться в одной категории. Если слишком узко — классификация станет шумной, а небольшие изменения формулировок начнут влиять на оценки. Набор тем нужно периодически проверять на новых обращениях и корректировать по мере изменения продукта.

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

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

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

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

Вместо заключения

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

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

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

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

А это, пожалуй, полезнее, чем бесконечно пытаться подобрать идеальную статью для вопроса, на который в базе пока нет ответа.

ссылка на оригинал статьи https://habr.com/ru/articles/1103448/