
26 ноября в Москве прошла третья в своей истории Conversations – конференция по разговорному искусственному интеллекту для разработчиков и бизнеса, на которой был представлен новый доклад компании «Аэроклуб ИТ». В прошлый раз речь шла об одном из наших исследовательских проектов, теперь же рассказ был сосредоточен вокруг инструмента, который мы применяем для чат-ботов. Сперва я планировал просто написать статью по мотивам доклада, но получился целый tutorial, так что под катом вас ждёт довольно подробное описание некоторых возможностей Dialogflow, и даже попадутся неочевидные «хаки».
Вообще, чат-боты и навыки для голосовых ассистентов не являются основной специализацией компании «Аэроклуб ИТ», в которой мы занимаемся разработкой программных решений в сфере B2B-travel.
Однако в 2018 году мы проводили исследования применимости навыков Алисы в нашем нелёгком деле, и в какой-то момент стали использовать Dialogflow.
Когда я был на первой Conversations в качестве слушателя, меня, как участника тех исследований, в большей степени интересовал доклад про Dialogflow. Он был отличный, но, к сожалению, достаточно поверхностный.
Спустя год и несколько практических кейсов его применения, пришло время поделиться нашим опытом. Итак, будем двигаться от простого к интересному.
Если вы уже знакомы с этим сервисом и не хотите читать всё подряд, для вас есть
Оглавление
Что такое Dialogflow?
Это облачный сервис распознавания естественного языка от Google, который поддерживает различные языки, в том числе русский. У него есть бесплатные лимиты использования, а для работы с API можно воспользоваться библиотеками для разных языков, потому его достаточно легко интегрировать в свои проекты. Также в Dialogflow (сокращённо — «DF») «из коробки» есть интеграции с различными мессенджерами, так что для простых сценариев написание своего кода может даже не понадобиться.
Агент
Перво-наперво нужно создать агента, который будет выполнять основную работу по общению с пользователем. Как сказано в документации, для понимания сути «агента» DF можно провести параллель с сотрудником call-центра, который обрабатывает запросы клиента (пользователя).
Вам нужно придумать его название, выбрать основной язык общения (позже можно будет выбрать дополнительные), и часовой пояс, в котором он будет работать:

JSON-ключ
Если планируете обращение к сервису через API, то вам понадобится JSON-ключ, необходимый для авторизации. Для этого в настройках агента (шестерёнка возле названия) нужно кликнуть по идентификатору Project ID. В новом окне найти в меню слева пункт «IAM & Admin», а в нём — «Service Accounts». Там надо нажать «Create service account», в разделе «Service account details» ввести любое значение, нажать «Create» (не «Done»!), и в «Grant this service account access to project» указать Role «Dialogflow API Client». И вот теперь нажать «Done»:

Если всё-таки не удалось указать роль при создании аккаунта, тогда нужно в меню слева найти
Iam — Add — выбрать созданный аккаунт — указать Role «Dialogflow API Client» — Save:

В «Service Accounts» возле созданного аккаунта надо найти меню «Actions», а в нём — пункт «Manage keys». Далее — Add key — Create new key — JSON — Create, и вам будет предложено скачать файл с ключом:

Как именно применять этот ключ, вы сможете найти в документации к библиотеке, которую используете в своём проекте.
Намерения (Intents)
Чтобы агент начал обрабатывать запросы пользователя, нужно добавить в него Intents (намерения, цели). Можно сказать, что они должны соответствовать намерениям пользователя, который «общается» с чат-ботом. Например, купить что-то, получить какую-то информацию, и так далее.
Как правило, после создания агента в нём уже присутствуют сразу две цели: одна — для реакции на приветствие и начала диалога (Default Welcome Intent), и другая — специальная, на тот случай, если не удалось ничего распознать (Default Fallback Intent).
В любом из намерений можно настроить «признаки», по которым будет происходить переход именно в него. Самое простое — это добавить тренировочные фразы (Training phrases), на основе которых Dialogflow определяет то или иное намерение пользователя.
Также можно указывать события (Events), используя стандартные, или придумав что-то своё. Тогда переход в Intent можно будет форсировать, передав в запросе к DF нужное название:

Теперь агент «научится» отвечать на приветствие! При разработке навыков для голосовых ассистентов часто рекомендуют, чтобы в одной и той же ситуации они не выдавали подряд одинаковые ответы. Используя Dialogflow, можно не беспокоиться об этом, потому что сервис случайным образом выбирает одну из фраз, указанных в разделе Responses. Проверить это можно в «тестовой консоли» которая находится справа:


Это может понадобиться, когда у нескольких целей одинаковые тренировочные фразы, но отличаются контексты. Пример такой ситуации приведён почти в самом конце, где рассказывается про вопросы на уточнение.
Даже с этими минимальными настройками уже можно сделать какую-нибудь «болталку»: чат-бота, который может поприветствовать пользователя, ответить на простые вопросы, и переспросить, если ему «не понятно».
Сущности (Entities)
Одной из полезных возможностей Dialogflow с уверенностью можно назвать распознавание сущностей. Он хорошо справляется с большинством общих объектов: датами, городами, даже музыкальными группами, и прочим. Каждую тренировочную фразу намерения можно разметить и таким образом указать, какой объект в ней искать и в какой параметр записывать распознанное значение:

В сервисе есть множество системных сущностей, которые могут использоваться в большинстве сценариев. Здесь распознаются диапазоны дат (sys.date-period), «цельные» даты (sys.date), полные названия городов (sys.geo-city), и группы (music-artist):

В этом примере видно, что «Питер» правильно распознан без каких-то дополнительных настроек. Если же Dialogflow сам не справляется с распознаванием этих или других общих объектов, то некоторые системные сущности можно дополнить самостоятельно. Это делается в разделе Entities — System, где отображён список используемых системных объектов. На те из них, которые доступны для дополнения, можно «кликнуть», и в левой колонке указать полное название объекта, а справа — его синонимы:

Для работы с объектами из какой-нибудь специфической предметной области можно создать собственные сущности в разделе Entities — Custom, а при разметке фраз указывать соответствующее название.
Теперь, используя распознавание объектов, можно реализовывать простые сценарии диалога, в которых чат-бот или навык ассистента «понимает», о чём идет речь, и как-то использует информацию, полученную от Dialogflow. Например осуществляет поиск билетов в определённые города на нужные даты.
Параметры
В ответе
На этом сфера применения параметров не ограничивается. Как вы могли заметить в примерах выше, информацию из параметров текущего намерения можно встраивать в ответ. Для этого необходимо прямо в тексте написать нужное название через знак «доллара»:

Это можно использовать, например, для уточнения у пользователя, все ли названные им параметры распознаны верно.
Кстати, если при добавлении блока ответов выбрать «Custom Payload», то, вместе с обычным текстовым ответом, сервис будет передавать и доп-информацию в виде JSON. В неё тоже можно встраивать значения параметров.
Посмотреть итоговое содержимое Payload можно в окне «Diagnostic info» под тестовой консолью, справа. Там, вообще, много всякой информации, в том числе полезной:

В событии
Если вы указываете название события для перехода в то или иное намерение, то вместе с ним можно передавать и параметры. Возьмём, например, намерение, в котором указано событие PersonalGreeting, и один-единственный параметр «name», используемый в ответе:

Если в запросе к Dialogflow задать значение этого параметра («Степан» в данном случае), оно будет использовано в ответе. Запрос к API из библиотеки для .net и ответ выглядят примерно так:

Как видно, указанное значение теперь содержится в ответе, и получилось приветствие по имени!
Уточнение
Теперь возьмём более сложный пример: чат-бот ориентируется на значения параметров, которые приходят из Dialogflow, и выполняет на их основе какие-то действия. Что делать, если сервису не удалось что-то распознать, или пользователь не назвал какую-то важную информацию (например, адрес доставки)? К счастью, Dialogflow умеет задавать уточняющие вопросы для обязательных параметров. Если слева от нужного параметра поставить галку в колонке «Required», то справа появится колонка «Prompts»:

Если нажать на неё — откроется окно для добавления уточняющих фраз:

Теперь, пока информация для обязательного параметра не будет распознана, сервис будет передавать в ответ одну из фраз, указанных в этом окне. Если в намерении несколько обязательных параметров, то сервис будет задавать уточняющие фразы для каждого из них, по мере заполнения предыдущего, в соответствии с их порядком в намерении.
В ответе на запрос к Dialgoflow присутствует поле «AllRequiredParamsPresent». На него можно ориентироваться в своём коде чтобы понять, что все необходимые параметры заполнены, и тогда выполнять какое-нибудь действие.
Action
Кстати, насчёт действий. Внимательные читатели наверняка заметили текстовое поле над блоком с параметрами. Ему можно задать любое значение, даже на русском языке. Оно приходит в ответе от сервиса в поле «Action». Как сказано в документации, это просто текстовое поле, которое помогает выполнять какую-либо логику в вашем сервисе. То есть, через него можно указать, например, какое действие должно быть выполнено в вашем коде, который обрабатывает ответ от Dialogflow.
С помощью описанных выше возможностей ваш чат-бот сможет вести более «продвинутые» диалоги: спрашивать у пользователя недостающие данные, переспрашивать параметры заказа, и выполнять указанные действия, если все параметры заполнены.
Контексты (Contexts)
«Ветки» диалога
И вот мы подошли к одной из интересных возможностей сервиса. При помощи Dialogflow можно построить такой сценарий, в котором разговор пойдёт по той или иной «ветке», в зависимости от выбора пользователя: например, доставка или самовывоз. От этого зависит, что агент спросит дальше: адрес доставки или подходящую точку вывоза.
Самый простой способ сделать «ветку» — это создать связанный Intent с помощью кнопки «Add follow-up intent», которая появляется при наведении на то или иное намерение. После этого под «родительским» элементом будут отображаться «дочерние», для которых также можно создать связанные намерения:

Их взаимосвязь основана на указанных контекстах: у родительского появляется «выходящий» контекст (нижнее поле), а у дочернего — «входящий» (верхнее поле):

Например, пользователь выбрал доставку, тогда у текущего диалога будет контекст «Delivery-followup», поскольку такое название написано в поле «Output context». После этого пользователь может спросить про стоимость. Для ответа на подобный вопрос заведено намерение «Delivery — cost» с соответствующими тренировочными фразами и ответами, где поясняется стоимость доставки:

Если же пользователь выберет самовывоз и спросит про стоимость, то разговор попадёт в намерение «Pickup — cost», в котором тренировочные фразы точно такие же, но ответы немного другие:

Как вы наверняка уже поняли, диалог пойдёт по этой ветке, потому что после выбора самовывоза будет активен контекст «Pickup-followup». За этим легко проследить с помощью консоли тестирования:

Условиями попадания в одно из этих намерений являются входной контекст и тренировочные фразы. Если пользователь скажет то же самое, но диалог не будет ни в одном из этих контекстов, то он попадёт в то намерение, которое предназначено для любых непонятных ситуаций (по умолчанию — Default Fallback Intent). Кстати, подобную цель можно создавать специально для отдельных контекстов.
Обратите внимание, что «выходящий» контекст намерения присваивается диалогу только тогда, когда заполнены все необходимые параметры и разговор переходит в следующее намерение.
Параметры в контексте
Контексты удобны ещё и тем, что через них можно передавать параметры между намерениями. Как вы уже знаете, их значения можно встраивать в ответ в том же Intent'е, но если всё правильно сделать, то это можно сделать и в «дочерней» цели.
Например, для выполнения заказа диалог с пользователем проходит через несколько этапов, на каждом из которых распознаются различные объекты. В самом конце необходимо переспросить, все ли параметры заказа верные.
В намерении «Order», с которого начинается заказ, в параметр «any» записывается название товара, и задаётся выходящий контекст «Order-followup». Для того, чтобы «достать» этот параметр дальше по цепочке, текст ответа следует дополнить конструкцией «#Order-followup.any». При этом «Order-followup», конечно же должен быть указан в качестве «входящего»:

В результате этого примера получается вот такой ответ:

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

Вопрос на уточнение

На картинке выше показан пример уточнения телефона для доставки, и этому соответствует контекст «delivery_dialog_params_phone-number». Можно создать специальное намерение, для которого указать его в качестве «входящего», и сделать аналогичное для самовывоза, но с другим названием контекста, конечно же.
Теперь пользователь получит правильный ответ:

Это происходит за счёт того, что название одного из контекстов диалога и пользовательская фраза совпадают с теми же параметрами в этих намерениях.
Однако, если в вашем сценарии уже предусмотрен «общий» Intent без входного контекста, отвечающий на подобные вопросы, то диалог может перейти именно в него. Чтобы этого избежать можно понизить его приоритет, нажав на цветную точку возле названия и выбрав нужный уровень.
Срок жизни
В настройках намерений вы могли заметить число возле названия «выходящего» контекста — это, как вы уже могли догадаться, его «срок жизни». Оно указывает кол-во высказываний пользователя, на протяжении которого контекст будет «держаться». Например, «2» означает, что он будет существовать на протяжении 2х фраз, где первая — та, в которой он появился. То есть на третьей он уже пропадёт.
Для того, чтобы контекст поддерживался как можно дольше, можно задать ему какое-нибудь большое число (например, 90 000, почему нет). Но в этом случае, в соответствии с документацией, такой контекст будет «жить» 20 минут. Однако, если разговор попадёт в намерение, где он указан в качестве «выходящего», то отсчёт его срока начинается заново. Таким образом, контекст может поддерживаться «вечно».
Для того, чтоб DF мог учитывать контекст, ему нужно передавать один и тот же идентификатор сессии на протяжении разговора. Он должен быть, конечно же, уникальным для каждого диалога.
С помощью этого функционала можно построить достаточно сложные сценарии диалога, которые смогут покрыть множество задач. Как я уже писал ранее, общение между пользователем и ботом, особенно голосовым ассистентом, должно быть естественным. Данный сервис поможет в этом, по крайней мере для разговора на не очень сложные темы.
История изменений
При разработке диалога, как и любого другого проекта, легко сделать изменения, которые, возможно, понадобится «откатить». Обычно в этом помогает история изменений, или, на худой конец, система контроля версий. Первого, к сожалению в Dialogflow нет, но настройки агента можно экспортировать в json-файлы и «положить» их в SVN.
В настройках агента есть вкладка «Export and Import», в которой можно выгрузить агента в виде zip-архива. В нём содержатся файлы, которые можно отправить в github, например, как я и поступил с агентом, на примере которого писал этот материал. Благодаря этому легко отследить внесённые изменения, и, при необходимости, восстановить какую-либо версию: эти файлы можно снова собрать в архив и воссоздать агента, или дополнить его.
Выводы
Используя Dialogflow при разработке чат-ботов и навыков ассистентов, вы получаете широкий спектр возможностей: ведение диалога, получение нужной информации, гибкое поведение в зависимости от контекста, а также переиспользование одного и того же сценария диалога в чат-ботах на различных платформах, без необходимости его дублирования или портирования.
Однако, не обойдётся без минусов: ваш проект будет очень сильно зависеть от Dialogflow. Если вы пользуетесь бесплатной версией, то при превышении её лимитов (более 180 текстовых запросов в минуту) сервис, а значит — и ваш чат бот, не будут корректно отвечать на запросы пользователя.
С другой стороны, если через ваш проект проходит столько трафика (3 запроса в секунду!), значит, скорее всего, он пользуется популярностью. Если так, то его наверняка можно монетизировать. Например, в Яндекс-диалогах не так давно запустили рекламу, а ещё можно выиграть премию Алисы.
За счёт монетизации, в таком случае, можно воспользоваться платной версией сервиса. Понимаю, совет не самый лучший, но и количество запросов к нашим чат-ботам пока не выходило из предоставленных лимитов, а это значит, что их хватает для небольшого проекта.
Заключение
Здесь описаны далеко не все приёмы работы с сервисом, а лишь те, которые мы использовали в своей практике. Помимо сказанного, в нём есть fulfillment, мега-агенты, генерация голоса, и ещё много всего. Если вы используете Dialogflow, расскажите об этом, пожалуйста, в комментариях, а лучше — в своей статье.
Когда мы только начинали работу с ним, нам очень помог другой tutorial, за что спасибо его автору.
Спасибо, что дочитали до конца! Не прощаюсь, встретимся на хабре!
P.S.
UPD 15.04.2021: дополнил процесс получения JSON-ключа