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

Принципиальные способы автоматической поддержки пользователей:
1. Ответы на составленных парах "Вопрос-Ответ" (по контекстной близости вопроса)
2. Поиск по фрагментам или тикетам (по контекстной близости фрагмента)
3. Поиск в документации фрагмента, в котором содержится ответ (Question-Answering, BERT)
4. Дообучение (Fine-tuning) квантированных моделей собственными данными
5. Генерация ответа на основе добавленных данных (RAG)
Также возможно комбинирование способов.
Представляется, что самым простым и первым для старта является вариант на составленных парах "Вопрос-Ответ", его и рассмотрим в данной статье.
Базовая последовательность
Ответ на основе составленных пар "Вопрос-Ответ" происходит следующим образом:
1. Составляется база данных, состоящих из пар "Вопрос-Ответ".
Это может быть файл excel, таблица MySQL, таблица на движке действующего сайта - не принципиально, принцип один и тот же.
2. При получении вопроса пользователя Система ищет ближайший по смыслу вопрос из базы данных. "Ближайший по смыслу" в данном случае означает, что значение по контекстной или косинусной близости максимально.
3. Система выдает ответ из базы данных,соответствующий ближайшему по смыслу вопросу, то есть ответ с тем же номером.

Переходим к кодированию
В данном примере применяем модель cointegrated/rubert-tiny2.
За прошедшие годы появились новые, более мощные модели, но я исторически начинал с cointegrated/rubert-tiny2, и для данного примера ее вполне достаточно.
Установки
Может понадобиться установка модулей torch, transformers, openpyxl, если они еще не установлены.
Импортируем нужные библиотеки и устанавливаем модель.
Если есть GPU, то переносим модель на него.
Импорт библиотек и установка модели
from transformers import AutoTokenizer, AutoModel
import torch
import numpy as np
from numpy import dot
from numpy.linalg import norm
import pandas as pd
model = AutoModel.from_pretrained("cointegrated/rubert-tiny2").eval()
tokenizer = AutoTokenizer.from_pretrained("cointegrated/rubert-tiny2")
if torch.cuda.is_available(): model.cuda()
Исходные данные
Предположим, что пары "Вопрос-Ответ" собраны в файл excel.

Создадим датафрейм, чтобы визуально проверять при разработке и тестировании.
Создание датафрейма
df = pd.read_excel('PATHFILE')

Массив эмбеддингов (векторная база данных)
Зададим функцию определения эмбеддингов и создадим массив эмбеддингов.
Создание массива эмбеддингов
def embed_bert_cls(text, model, tokenizer):
t = tokenizer(text, padding=True, truncation=True, return_tensors='pt')
with torch.no_grad():
model_output = model(**{k: v.to(model.device) for k, v in t.items()})
embeddings = model_output.last_hidden_state[:, 0, :]
embeddings = torch.nn.functional.normalize(embeddings)
return embeddings[0].cpu().numpy()
qustions_embeddings = [embed_bert_cls(key, model, tokenizer) for key in df.questions]
В принципе, набор эмбеддингов (векторная база данных) можно сохранить отдельно и потом загружать, чтобы заново не пересчитывать при запуске файла. В таком случае может быть один отдельный файл - создание эмбеддингов, а второй отдельный файл - "рабочий". Это не принципиально, и какой конкретно вариант применять может зависеть от конкретной задачи.
Обработка вопроса и вывод ответа
Зададим функцию определения индекса вопроса из базы данных, ближайшего по контексту заданному вопросу пользователя.
Функция определения индекса
def get_index(prompt):
# Получаем эмбеддинг вопроса
prompt_emb = embed_bert_cls(prompt, model, tokenizer)
# Получаем массив значений контекстной близости
scores = [dot(prompt_emb, key)/(norm(prompt_emb)*norm(key)) for key in qustions_embeddings]
# Определяем максимальное значение
max_value = np.max(scores)
# Определяем индекс с макисмальным значением
max_index = scores.index(max_value)
return (max_value, max_index)
По коду видно, что сначала определяется эмбеддинг заданного вопроса, потом считаются показатели контекстной близости по всем вопросам из базы данных и определяется максимальное значение. Функция возвращает максимальное значение и соответствующий индекс вопроса из базы данных.
И остается лишь отправить в функцию принятый вопрос пользователя и вывести ответ с полученным индексом.
Вводим вопрос пользователя и выводим ответ из базы данных
prompt = input("Введите вопрос: ")
result = get_index(prompt)
print(df.answers[result[1]])
На данном этапе получилось внутреннее ядро, которое может работать само по себе в Google Colab или Jupyter Notebook, а также как файл с расширением py. Для реальной работы нужно организовать прием вопроса пользователя из внешнего приложения и возврат ответа, то есть модифицировать код под API.
Модифицируем для API
Запуск
Будем использовать flask.
В принципе, код будет почти тем же самим, только вопрос пользователя будет приниматься как POST, а возвращаться будет фраза ответа и значение контекстной близости.
Код файла с расширением py
# нужны torch, transformers, openpyxl, flask
# функция определения эмбеддинга
def embed_bert_cls(text, model, tokenizer):
t = tokenizer(text, padding=True, truncation=True, return_tensors='pt')
with torch.no_grad():
model_output = model(**{k: v.to(model.device) for k, v in t.items()})
embeddings = model_output.last_hidden_state[:, 0, :]
embeddings = torch.nn.functional.normalize(embeddings)
return embeddings[0].cpu().numpy()
# Функция определения индекса
def get_index(prompt):
# Получаем эмбеддинг вопроса
prompt_emb = embed_bert_cls(prompt, model, tokenizer)
# Получаем массив значений контекстной близости
scores = [dot(prompt_emb, key)/(norm(prompt_emb)*norm(key)) for key in qustions_embeddings]
# Определяем максимальное значение
max_value = np.max(scores)
# Определяем индекс с макисмальным значением
max_index = scores.index(max_value)
return (max_value, max_index)
import torch
from transformers import AutoTokenizer, AutoModel
import numpy as np
from numpy import dot
from numpy.linalg import norm
import pandas as pd
import json
from flask import Flask, request
model = AutoModel.from_pretrained("cointegrated/rubert-tiny2").eval()
tokenizer = AutoTokenizer.from_pretrained("cointegrated/rubert-tiny2")
if torch.cuda.is_available(): model.cuda()
file_data = 'PATHFILE'
df = pd.read_excel(file_data)
qustions_embeddings = [embed_bert_cls(key, model, tokenizer) for key in df.questions]
app = Flask(__name__)
@app.route('/')
def start():
return 'System is running...'
@app.route('/', methods=['POST'])
def handle_post_request():
data = request.get_json()
question = data['question']
result = get_index(question)
return json.dumps({"max_score": str(result[0]), "answer": str(df.answers[result[1]])})
if __name__ == '__main__':
app.run(debug=True)
#if __name__ == '__main__':
# app.run(host='0.0.0.0', port=5000)
Если запускать файл с командной строки на windows, то должно быть примерно так:

Это означает, что файл запустился и система отвечает на локальном хосте:
http://127.0.0.1:5000
Проверка и работа
Для проверки и работы нужно отправить POST запрос на указанный хост.
Код для проверки и работы
import requests
import json
url = 'http://127.0.0.1:5000'
response = requests.get(url)
print(response)
print(response.text)
json_data = {'question': 'Какие карты принимаете?'}
response = requests.post(url, json=json_data)
print(response)
print(json.loads(response.text)['max_score'])
print(json.loads(response.text)['answer'])
Приложенные изображения показывают, что все работает.
Jupyter

cmd python

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

Что на практике
Очевидно, что такая система может давать только "стабильные" ответы, которые не меняются некоторое время. То есть Система не сообщит клиенту текущее состояние его счета, но очень стабильно может выдавать ответы на вопросы формата FAQ и практически мгновенно.
Некоторая проблема на практике заключается в том, что пользователи могут задавать вопросы, которых нет в базе данных, даже близких по смыслу. И в таком случае Система может выдать ответ, совершенно не соответствующий вопросу пользователя. Именно для этого вместе с ответом Система возвращает показатель контекстной близости вопроса. И если показатель ниже установленного минимума, то можно добавить альтернативные варианты - например, попросить пользователя переформулировать вопрос или признаться, что пока не знает ответа на данный вопрос и пообещать, что сообщит об этом сотруднику. И действительно, такие вопросы можно и нужно собирать в отдельный список, периодически готовить на них ответы и пополнять базу данных. Важно, что набор часто встречающихся вопросов довольно быстро заканчивается и достаточно быстро Система принимает на себя основную часть "статистических" вопросов пользователей.
Другие способы организации автоматической поддержки пользователей будут рассмотрены в следующих статьях.
