Как стать автором
Обновить

Болтай, извлекай: архитектура сложных чат-ботов

Время на прочтение 13 мин
Количество просмотров 12K
Пользователи, пообщавшись с умными голосовыми ассистентами, ждут от чат-ботов интеллектуальности. Если вы разрабатываете бота для бизнеса, ожидания еще выше: заказчик хочет, чтобы юзер прошел по нужному, заранее прописанному сценарию, а юзер — чтобы робот толково и желательно человеческим языком ответил на поставленные вопросы, помог решить проблемы, а иногда просто поддержал светскую беседу.


Мы делаем англоязычные чат-боты, которые общаются с пользователями по разным каналам — Facebook Messenger, SMS, Amazon Alexa и веб. Наши боты заменяют службы поддержки, страховых агентов, и уметь просто поболтать. Каждая из этих задач требует своего подхода в разработке.

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


На первый взгляд может показаться, что проблемы, которые мы решаем, довольно тривиальны. Однако в области Natural Language Processing существует ряд сложностей, связанных как с технической реализацией, так и с человеческим фактором.
  1. Английским языком владеет миллиард человек, и каждый носитель использует его по-своему: существуют различные диалекты, индивидуальные особенности речи.
  2. Многие слова, фразы и выражения неоднозначны: характерный пример — на этой картинке.
  3. Для правильной интерпретации смысла слов необходим контекст. Однако бот, который задает клиенту уточняющие вопросы, выглядит не так круто, как тот, который может переключиться на любую тему по желанию пользователя и ответить на любой вопрос.
  4. Часто в живой речи и переписке люди либо пренебрегают правилами грамматики, либо отвечают настолько кратко, что восстановить структуру предложения практически невозможно.
  5. Иногда для того, чтобы ответить на вопрос пользователя, необходимо сверить его запрос с текстами FAQ. При этом нужно убедиться, что найденный в FAQ текст действительно является ответом, а не просто содержит несколько совпадающих с запросом слов.


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

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

Завести разговор. Dialog Manager


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

Dialog Manager позволяет создать такой сценарий: описать программно, построить из небольших кирпичиков — конкретных вопросов или действий, которые должны произойти в определенный момент. Фактически сценарий представляет собой ориентированный граф, где каждый узел — это сообщение, вопрос, действие, а ребро определяет порядок и условия перехода между этими узлами, если есть множественный выбор перехода из одного узла в другие.
Основные типы узлов
  • Узлы, ожидающие, пока до них дойдет очередь, и они отобразятся в сообщениях.
  • Узлы, ожидающие, пока пользователь проявит определенное намерение (например, напишет: «Я хочу получить страховку»).
  • Узлы, ожидающие данные от пользователя, чтобы провалидировать их и сохранить.
  • Узлы для реализации различных алгоритмических конструкций (циклов, ветвлений и т. д.).

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


«Я уже все сказал!»


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

«А кстати…»


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

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

Какие варианты?


Помимо нашего, мы рассматривали еще один AI-подход к реализации менеджера диалогов: на вход нейронной сети подается намерение пользователя и параметры, а система сама генерирует соответствующие состояния, следующий вопрос, который нужно задать. Однако на практике такой способ требует добавления rule based подхода. Возможно, такой вариант реализации подойдет для тривиальных сценариев — например, для заказа еды, где надо получить всего лишь три параметра: что пользователь хочет заказать, когда он хочет получить заказ и куда его привезти. Но в случае сложных сценариев, как в нашей предметной области, это пока недостижимо. На данный момент технологии machine learning не способны качественно провести пользователя до цели по сложному сценарию.

Dialog Manager написан на Python, Tornado framework, поскольку изначально наша AI-часть писалась как единый сервис. Был выбран язык, на котором все это можно реализовать, не тратя ресурсы на коммуникацию.

«Давай определимся». Recognition Service


Наш продукт умеет коммуницировать по разным каналам, но AI-часть является полностью клиент независимой: эта коммуникация доходит только в виде проксированного текста. Менеджер диалогов передает контекст, ответ пользователя и собранные данные в Recognition Service, который отвечает за распознавание намерения пользователя и извлечение нужных данных.
Сегодня Recognition Service состоит из двух логических частей: Recognition Manager, который управляет пайплайном распознавания, и экстракторов.

Recognition Manager


Recognition Manager отвечает за все базовые этапы распознавания смысла речи: токенизацию, лемматизацию и т. д. Также он определяет порядок экстракторов (объектов, распознающих сущности и признаки в текстах), по которым будет пропущено сообщение, и решает, когда стоит остановить распознавание и вернуть готовый результат. Это позволяет запускать только необходимые экстракторы в наиболее ожидаемом порядке.

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

В зависимости от контекста порядок запуска экстракторов может меняться. Такой подход позволяет нам существенно снизить нагрузку на весь сервис.

Экстракторы


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

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

Использование наших же микросервисов с Machine Learning внутри (экстракторы отправляют сообщение на этот сервис, иногда дополняют его имеющейся у них информацией и возвращают результат).

  • Использование POS tagging, Syntactic parsing, Semantic parsing (например, определение намерения пользователя по глаголу)
  • Использование полнотекстового поиска (может применяться, чтобы найти марку и модель машины в сообщениях)
  • Использование регулярных выражений и паттернов ответов
  • Использование сторонних API (таких как Google Maps API, SmartyStreets, и т. д.)
  • Дословный поиск предложений (если человек ответил коротко “yep”, то пропускать это через ML-алгоритмы для поиска намерения нет смысла).
  • Мы также используем в экстракторах уже готовые решения по обработке естественного языка.

Какие варианты?


Мы рассматривали библиотеки NLTK, Stanford CoreNLP и SpaCy. NLTK первой выпадает в выдаче Google, когда начинаешь ресерч по NLP. Она очень крута для прототипирования решений, обладает обширной функциональностью и довольно проста. Но ее производительность оставляет желать лучшего.

У Stanford CoreNLP есть серьезный минус: она тянет за собой виртуальную машину Java с очень большими модулями, встроенными библиотеками, и потребляет много ресурсов. Кроме того, вывод из этой библиотеки сложно кастомизировать.

В итоге мы остановились на SpaCy, потому что она располагает достаточной для нас функциональностью и обладает оптимальным соотношением легковесности и быстродействия. Библиотека SpaCy работает в десятки раз быстрее, чем NLTK, и предлагает намного более качественные словари. При этом она гораздо легче, чем Stanford CoreNLP.

На данный момент мы используем spaCy для токенизации, векторизации сообщения (с помощью встроенной обученной нейросети), первичного распознавание параметров из текста. Поскольку библиотека покрывает только 5% наших потребностей в области распознавания, нам пришлось дописать множество функций.

«Раньше-то как было…»


Recognition Service не всегда был двухсоставной структурой. Первая версия была самой тривиальной: мы по очереди задействовали разные экстракторы и пытались понять, есть ли в тексте те или иные параметры, намерения. AI там даже не пахло — это был полностью rule based подход. Сложность заключалась в том, что одно и то же намерение можно выразить массой способов, каждый из которых нужно описать в правилах. При этом необходимо учитывать контекст, поскольку одна и та же фраза пользователя в зависимости от поставленного вопроса может требовать различных действий. Например, из диалога: «Ты женат?» – «Уже два года» можно понять, что юзер женат (boolean значение). А из диалога «Сколько ты ездишь на этой машине?» – «Уже два года» нужно извлечь значение «2 года».

Мы с самого начала понимали, что поддержка rule based решения потребует больших усилий, и с ростом числа поддерживаемых намерений количество правил будет увеличиваться гораздо быстрее, чем в случае с системой на базе ML. Однако точки зрения бизнеса. нам нужно было запустить MVP, a rule based подход позволял сделать это быстро. Поэтому мы использовали его, а параллельно работали над ML-моделью распознавания интентов. Как только она появилась и начала давать удовлетворительные результаты, от rule-based подхода потихоньку начали отходить.

Для большинства случаев экстракции информации мы использовали ChatScript. Эта технология предоставляет собственный декларативный язык, позволяющий писать шаблоны для экстракции данных из естественного языка. Благодаря WordNet под капотом это решение очень мощное (например, можно указать в шаблоне распознавания «цвет», и WordNet распознает любое сужающее понятие, такое как «красный»). Аналогов мы на тот момент не видели. Но написан ChatScript очень криво и глючно, с его использованием практически невозможно реализовать сложную логику.

В итоге недостатки перевесили, и мы отказались от ChatScript в пользу NLP-библиотек в Python.
В первой версии Recognition Service мы уперлись в потолок по гибкости. Внедрение каждой новой фичи сильно замедляло всю систему в целом.

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

«Чего ты хочешь?». Intent Classifier


Чтобы бот мог адекватно общаться — выдавать нужную информацию по запросу и фиксировать данные юзера – необходимо определить намерение (интент) пользователя на основе отправленного им текста. Список интентов, по которому мы можем взаимодействовать с пользователями, ограничен бизнес-задачами клиента: это может быть намерение узнать условия страховки, заполнить данные о себе, получить ответ на часто задаваемый вопрос и так далее.

К классификации интентов существует множество подходов, основанных на нейронных сетях, в частности на рекуррентных LSTM/GRU. Они отлично зарекомендовали себя в последних исследованиях, однако у них есть общий недостаток: для корректной работы необходима очень большая выборка. На небольшом объеме данных такие нейросети либо сложно обучить, либо они выдают неудовлетворительные результаты. То же касается фреймворка Fast Text от Facebook (мы рассматривали его, поскольку это state-of-the-art решение для обработки коротких и средних фраз).

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

Поэтому мы искали решение, которое будет работать на небольшой выборке. Хорошее качество обработки данных продемонстрировал классификатор Random Forest, обученный на данных, которые были преобразованы в вектора нашей bag-of-words модели. При помощи кросс-валидации мы подобрали оптимальные параметры. Среди преимуществ нашей модели — быстродействие и размер, а также относительная легкость развертывания и дообучения.

В процессе работы над Intent Classifier стало понятно, что для некоторых задач его использование неоптимально. Предположим, пользователь хочет поменять имя, указанное в страховке, или номер автомобиля. Чтобы классификатор правильно определял это намерение, пришлось бы вручную добавлять в датасет все шаблонные фразы, которые используются в таком случае. Мы нашли другой выход: сделать маленький экстрактор для Recognition Service, который определяет намерение по ключевым словам и NLP-методам, а Intent Classifier использовать для нешаблонных фраз, в которых метод с ключевыми словами не работает.

«Про это всегда спрашивают». FAQ


У многих наших клиентов есть разделы с FAQ. Чтобы пользователь мог получить такие ответы прямо из чат-бота, необходимо было предоставить решение, которое бы а) распознавало запрос FAQ; б) находило бы наиболее релевантный ответ в нашей базе и выдавало его.

Есть ряд моделей, обученных на датасете SQUAD от Стэнфорда. Они хорошо работают в случае, когда текст ответа из FAQ содержит в себе слова из вопроса пользователя. Допустим, в FAQ написано: «Фродо сказал, что отнесет Кольцо в Мордор, хоть и не знает дороги туда». Если пользователь спросит: «Куда Фродо отнесет Кольцо?», система ответит: «В Мордор».

У нас сценарий, как правило, был другим. Например, на два похожих запроса — «Can I pay?» и «Can I pay online?» бот должен реагировать по-разному: в первом случае предлагать человеку форму платежа, во втором отвечать — да, заплатить онлайн можно, вот адрес страницы.

Еще один класс решений для оценки схожести документов ориентирован на длинные ответы — как минимум несколько предложений, среди которых содержится информация, интересующая пользователя. К сожалению, в случаях с короткими вопросами и ответами («Как мне оплатить онлайн?» — «Вы можете оплатить при помощи PayPal») они работают очень нестабильно.

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

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

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

«А поговорить?». Small Talk


Иногда пользователь пишет что-то абсолютно нерелевантное, например: «Погода сегодня хорошая». Это не входит в список интересующих нас интентов, но мы все равно хотим ответить осмысленно, продемонстрировав интеллектуальность нашей системы.

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

Обучение


Сейчас у нас нет цели создать бота, который бы обучался на каждом сообщении, принятом от клиентов: во-первых, как показывает опыт, это путь к смерти бота (вспомним, как IBM Watson пришлось стирать базу, потому что он стал ставить диагнозы с матом, а Twitter-бот Microsoft успел стать расистом всего за сутки). Во-вторых, мы стремимся как можно качественнее закрывать задачи страховых компаний; самообучающийся бот — не наша бизнес-задача. Мы написали ряд инструментов для лингвистов и QA-команды, с помощью которых они могут вручную дообучать ботов, исследуя диалоги и переписки с пользователями ходе постмодерации.

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

Планы


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

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

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

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



Статью писали вместе с Сергеем Кондратюком и Михаилом Казаковым. Пишите в комментарии свои вопросы, подготовим по ним более практические материалы.
Теги:
Хабы:
+8
Комментарии 6
Комментарии Комментарии 6

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн