【內附 coding】手把手教你變出聊天機器人,瞬間工作時間減少一半

【我們為什麼挑選這篇文章】在越來越多的粉絲頁裡都可以看到聊天機器人跟你寒暄、協助你解決一些簡單的問題,你有想過聊天機器人是怎麼做出來的嗎?這篇,不僅讓你知道如何做機器人,還手把手一步一步教,讓廣大的社群小編、行銷人、客服人員省下大把時間回復 無腦 問題,增加工作效率!(責任編輯:徐嘉偵)

聊天機器人從去年就一直不斷成為新聞寵兒,不斷地出現在許多文章裡面,你可能也會好奇這是怎麼樣的趨勢?聊天機器人是怎麼被做出來的?這篇文章就帶你瞭解它的發展以及帶你從 0 到 1、從無到有做出一個簡單的聊天機器人。

圖片取至: PyMessager

為了方便,以下「聊天機器人」可能會有各種簡稱:機器人, chatbot, bot…等)

看完這篇文章你會知道

  • 趨勢簡介:帶你了解聊天機器人為何會紅?和 App 有何不同?對於產業帶來的衝擊?
  • 發現問題:合適的使用情境為何?
  • 設計與開發:設計流程 和 如何開發。
  • Demo:做一個我自己的履歷聊天機器人。

趨勢簡介

圖片取至:This is 12K INDEX

從上面這張圖可以知道,從 2016 年開始,Artificial Intelligence / Machine Learning / Bots 不斷的發展起來,究竟是什麼原因造就這樣的現象?

為什麼 Artificial Intelligence / Machine Learning / Bots 會興起

我的心得是有幾個原因綜合起來而產生的現象:聊天就是人與人最基本的互動方式、通訊軟體普及、大家天天都會用,加上近年通訊軟體公司紛紛推出 SDK,讓開發者可以在他們的聊天平台上面開發不同的聊天機器人或應用程式,以及 AI 不斷的進步(例如:Deep Learning,AlphaGo 大戰人類)、許多工具開源(例如:TensorFlow)…等,造就聊天機器人的崛起。

「聊天」是最基本的互動 + 平台 SDK 推出 + AI/ML 進步

和 App 有什麼不同

對於使用者來說,不需要去適應五花八門的介面, 只需要用人和人最基本的互動方式:「聊天」就能夠完成事情,也省去傳統 App 下載、安裝或是更新的這些流程 ,你只需要下載聊天 App 即可,要叫車、要叫外送,跟 Bot 說一聲即可。

聊天機器人的出現「重新定義了使用者介面」。

對產業的衝擊

這邊針對三個比較相關的角色:設計師、開發者、一般業者分別來說。

  • 對設計師來說,不需要再設計複雜的介面,有人就會問說「聊天機器人會不會讓設計師失業?」我的想法是不會,這樣的發展會讓設計更聚焦在於互動設計和使用者體驗上,下圖顯示設計師更著重在於使用者和聊天機器人的互動設計上。
圖片取至: Siyi Wu 
  • 對於開發者來說,介面刻畫上會簡單一些,Bot 仰賴語意分析和人工智慧,需要電腦從語句知道要做什麼動作,以及知道要如何完成。對於開發者來說,開發更接近人的語意分析能力和人工智慧,就是需要更著重的目標。 少了語意分析,就只能讓 Bot 依照一些特定指令走,使用者還要記怎麼打指令,這樣產品的使用者體驗一定多少會打折扣,以下截圖為在 Slack 上的 Trellobot,我使用它就需要依照指令打,整體使用體驗和操作未必比圖形介面來得方便。
圖片取自:Trellobot on Slack
  • 對一般業者來說,可以有效整合多種產品,舉例:Luka.ai 就整合美食搜尋、新聞、小遊戲、Wiki、搜尋等服務到同一個 Bot,新功能或是問題修正也可以無痛更新給使用者。

發現問題

了解這波趨勢後,接下來我們可以思考兩個簡單的問題:

  1. 什麼樣的使用情境適合用聊天機器人來做?
  2. 我的產品或是問題是否適合?

以我目前的觀察,具有以下特性的產品或問題蠻適合用聊天機器人做的:

  • 須經由問答、來回溝通才能完成。
  • 問題或需求重複性高。
  • 回答或處理流程固定。
圖片取自:github

什麼樣的產品或流程具有上述特性呢?我舉幾個簡單例子:

  • 客服人員:使用者問題大致雷同,解決方式也都相對固定。
  • 問卷:問題和回答選項都固定(除了自由回答之外)。
  • 面試:面試者想問公司的問題(薪資、工作內容、工作地點和時間等)大致都相同。
  • 履歷:承上,面試的時候自我介紹、工作或是專案經驗等等也都是大同小異,接下來我將會把我自己的履歷文件轉換成聊天機器人作為範例,後面文章所講解的設計和開發都會以履歷聊天機器人為題。
圖片為我的部分紙本履歷

設計聊天機器人

在設計流程上,其實跟一般傳統 App 或者網頁雷同,都是要經歷以下的流程,其中又以前三個最為重要,下面分別來描述:

  • 需求分析:履歷聊天機器人最大的需求就是可以先代替我本人回答人資或是面試官最基本的問題,回答完如果對方有興趣聯絡我,可以提供我的聯絡資訊。
  • Functional Map: 這個階段要把所有功能都定義出來,在我過往去面試、或是面試人的經驗當中,很多問題是不斷的被問和問人,所以那些常見基本問題問答就可以列成我 Bot 的功能之一,再加上一些其他自我介紹,讓有興趣的人可以更了解我,最後還有提供聯絡我或是工作邀約等功能。
圖片為 Functional Map
  • Conversational Flow (v.s. UI Flow): 在一般 App 設計流程中,這個步驟是 UI Flow,就是要設計整個 App 的使用流程,一開始要顯示哪個畫面、做什麼動作會發生什麼或是到其他的畫面…… 等,而聊天機器人則是要定義 Conversational Flow,定義使用者要怎麼使用 Bot,要如何和 Bot 互動,換句話說,就是要設計處理流程、對話劇本。
圖片為 Conversational Flow

設計準則 Design Guideline

這邊我提供幾個簡單又重要的設計準則給大家參考,可以讓你的聊天機器人不至於陷入很差的使用者體驗:

起手式

當使用者一開始接觸到你的聊天機器人的時候,就要讓使用者了解到你的聊天機器人有提供哪些服務或者功能,以及要如何開始使用這些功能,以免使用者面對空白的聊天界面因為沒有足夠的提示而不知所措。

有所回應

千萬不要已讀不回,你要想辦法讓機器人不論在哪種情況下,都會回應使用者,不要讓使用者覺得機器人壞掉了。

 

互動 + 心理學

我們可以把聊天機器人的對答設計的比較人性化、不會死板板的機器對答,這就可以多參考互動設計和心理學。

平台最佳化

每個聊天平台都會提供不同的互動、介面和元件,你可以依照這些東西來設計適合你資訊呈現的方式或者互動,例如:快速選單、常駐功能表、列表清單等等。

圖片取自:Slack API 

開發聊天機器人

完成定義功能以及設計好流程之後,我們就可以準備來寫程式做出來,這個聊天機器人是做在 Facebook Messager 上,詳細的起始設定可以參考我之前的文章,這邊不再多做說明:

用 Python 開發 Facebook Bot

首先先從整個程式架構開始說起,一個聊天機器人概括架構如下圖所示,以下會分別搭配程式各部分一起解釋:

聊天平台

目的為串接不同的聊天平台介面,像是:Facebook Messager, LINE, Slack, Telegram…… 等等,處理訊息的傳遞和接收,這一層獨立出來讓我們可以很快的把相同功能在不用更動程式主邏輯的情況下做在不同的聊天平台上,直接抽換聊天平台這一層即可。

訊息接收端程式碼如下:

@app.route(config.web_hook_url, methods=["POST"])
def receive_message():
    message_entries = json.loads(request.data.decode("utf-8"))
    print("message_entries:", message_entries)

    for entry in message_entries["entry"]:
        for message in entry["messaging"]:
            sender = message["sender"]["id"]
            # print("sender:", sender)
            if chat_thread.get(sender, None) is None:
                bot = Bot(sender)

                # detect language
                user_req = requests.get("https://graph.facebook.com/v2.6/{user_id}?access_token={token}"
                                        .format(user_id=sender, token=config.access_token))
                if user_req.status_code == 200:
                    bot.locale = user_req.json()["locale"]
                chat_thread[sender] = bot
            else:
                bot = chat_thread[sender]

            if message.get("message"):
                print("\tmessage:", message["message"])
                bot.receive_message(message)
            elif message.get("postback"):
                print("\tpostback:", message["postback"])
                bot.receive_postback(message)

    return Response(status=200)

註:如果你對上述程式碼完全不知道是怎麼一回事,可以參考 我前一篇文章。

接收端會先判斷目前對話的使用者是否為新使用者,如果是新使用者,則建立一個 Bot 實體來處理他的請求,並且判斷該使用者的語系為何。 接收來的訊息可能為純文字 message 或者為按鈕的事件 postback,Bot 就依照接收到不同的資料來做處理。

而訊息傳送端我已經將幾個常用的方法封裝成 API,可以直接 import 使用。

聊天機器人本體

目的就是定義整個 Bot 的使用流程、Domain Knowledge、做為每個元件的控制中心。 在上面設計流程的 Conversational Flow,就是在 Bot 本體裡面實作,這邊我們要把履歷問答處理機制做進來。

#!/usr/bin/python3
# -*- encoding: utf-8 -*-
import configparser
import time
from enum import Enum

from nlp_parser import parse_sentence, Intent
from message import Messager, QuickReply, GenericElement, ActionButton, ButtonType
import config
import api

__author__ = "Engine Bai"


class Bot(object):
    def __init__(self, sender, locale="zh_TW"):
        self.sender = sender
        self.locale = locale
        self._config = configparser.ConfigParser()
        self._config.read("res/strings")
        self._reset_context()
        self._context_intent = Intent.HELP.name
        self._messager = Messager(config.access_token)

    def receive_message(self, message_payload):
        current_intent = parse_sentence(message_payload["message"])
        if current_intent == self._context_intent:  # don't handle duplicate intent
            return
        else:
            self._context_intent = current_intent
            self._handle_intent(message_payload)

    def receive_postback(self, message_payload):
        current_intent = message_payload["postback"]["payload"]
        if current_intent == self._context_intent:
            return
        else:
            self._context_intent = current_intent
            self._handle_intent(message_payload)

    def _handle_intent(self, message_payload):
        print("Handle intent", self.sender, self._context_intent, message_payload)

        if self._context_intent == Intent.HELP.name or self._context_intent == Intent.CONFUSED.name:
            self.send_help()
        elif self._context_intent == Intent.PROJECTS.name:
            projects = api.data["projects"]
            project_list = []
            for project_id in projects.keys():
                project = projects[project_id]
                project_list.append(GenericElement(project["title"], project["description"],
                                                   config.api_root + project["image_url"], [
                                                       ActionButton(ButtonType.POSTBACK,
                                                                    self._get_string("button_more"),
                                                                    # Payload 用 Intent 本身作為開頭
                                                                    payload=Intent.PROJECTS.name + project_id)
                                                   ]))
            self._messager.send_generic(self.sender, project_list)
        elif self._context_intent == Intent.ARTICLES.name:
            self.send_link_list("articles")
        elif self._context_intent == Intent.ADVANTAGES.name or self._context_intent == Intent.PERSONALITY.name:
            contents = api.data[self._context_intent.lower()]
            for content in contents:
                self.send_contents(content)
            self.next()
	elif
	    # 略...
	    pass
        else:
            if self._context_intent is None:
                self._context_intent = Intent.CONFUSED.name
                self.send_help()
            # 直接從 Payload 來的
            elif self._context_intent.startswith(Intent.PROJECTS.name):
                project = api.data[Intent.PROJECTS.name.lower()][self._context_intent.replace(Intent.PROJECTS.name, "")]
                for detail in project["detail"]:
                    self.send_contents(detail)
                self.next()

    def send_help(self, restart=False):
        """
        傳送幫助資訊
        :param restart: 
        :return: 
        """
        self._messager.send_quick_replies(self.sender,
                                          self._get_string("title_help_restart")
                                          if restart else self._get_string("title_help"), [
                                              QuickReply(self._get_string("button_works_primary"),
                                                         Intent.WORKS_PRIMARY.name),
                                              QuickReply(self._get_string("button_works_secondary"),
                                                         Intent.WORKS_SECONDARY.name),
                                              QuickReply(self._get_string("button_advantages"), Intent.ADVANTAGES.name),
                                              QuickReply(self._get_string("button_personality"),
                                                         Intent.PERSONALITY.name),
                                              QuickReply(self._get_string("button_contact_me"), Intent.CONTACT_ME.name)
                                          ])

    def short_break(self, sleep=1):
        """
        每一個句子之間的停頓
        :param sleep: 停頓秒數,預設為 1 秒
        :return: 
        """
        self._messager.typing(self.sender)
        time.sleep(sleep)

    def next(self):
        """
        在每一個段落之後,可以短暫停留然後繼續問說下一步
        :return: 
        """
        self.short_break(2)
        self.send_help(True)

    def send_link_list(self, data_payload):
        """
        傳送列表資料
        :param data_payload: 
        :return: 
        """
        data = api.data[data_payload]
        items_list = []
        for item_id in data.keys():
            item = data[item_id]
            links = item["link"]
            action_buttons = []
            for i in range(len(links)):
                action_buttons.append(ActionButton(ButtonType.WEB_URL, self._get_string("button_link") +
                                                   (str(i + 1) if len(links) > 1 else ""), links[i]))
            items_list.append(GenericElement(item["title"], item["description"],
                                             config.api_root + item["image_url"], action_buttons))
        self._messager.send_generic(self.sender, items_list)
        self.next()

    def send_contents(self, detail):
        """
        傳送一般語句和圖片或連結
        :param detail: 
        :return: 
        """
        self.short_break()
        if "message" in detail:
            self._messager.send_text(self.sender, detail["message"])
        elif "image_url" in detail and "link" not in detail:
            self._messager.send_image(self.sender, config.api_root + detail["image_url"])
        elif "title" in detail and "link" in detail:
            self._messager.send_generic(self.sender, [
                GenericElement(detail["title"], detail["subtitle"], config.api_root + detail["image_url"], [
                    ActionButton(ButtonType.WEB_URL, detail["button"], detail["link"])
                ])])

    def _reset_context(self):
        self._current_task = None
        self._current_data = None
        self._context_intent = Intent.HELP.name

    def _get_string(self, key):
        """
        從文字檔取得句子
        :param key: 
        :return: 
        """
        return self._config[self.locale][key]

上面程式可以看到 Bot 建立的時候就指定一個傳送者 sender 和目前意圖  _context_intent ,意圖會隨著你接收到的語句而改變,Bot 會處理一般訊息和按鈕事件,然後在做出對應的事件 _handle_intent。

NLP (Natural Language Processing) 自然語言處理

這邊包括處理使用者輸入後的語意分析,讓電腦可以知道使用者要做什麼動作。

我直接選用 FB 後來收購的 Wit.ai 來做語言解析,這邊 Wit.ai 是使用關鍵字辨識的方式去做語意分析,這樣關鍵字辨識的方式,你一開始提供的越多種講法、新增越多同義字,它就可以越精準辨識,然後使用機器學習再去擴充。在我們完成語料庫之後,就可以用 Wit.ai SDK 把這些東西串到我們的 bot 裡面去做簡單的語意分析。 整合方式蠻簡單的,可以見下列程式碼:

class Intent(Enum):
    HELP = "help"
    WORKS_PRIMARY = "works_primary"
    WORKS_SECONDARY = "works_secondary"
    PROJECTS = "projects"
    OPEN_SOURCES = "open_sources"
    JOBS = "jobs"
    ARTICLES = "articles"
    SPEECH = "speech"
    REPORT = "report"
    ADVANTAGES = "advantages"
    PERSONALITY = "personality"
    CONTACT_ME = "contact_me"
    CONFUSED = "confused"


def parse_sentence(message):
    if "quick_reply" in message:
        print("quick_reply", message["quick_reply"])
        return message["quick_reply"]["payload"]
    elif "text" in message:
        resp = wit_client.message(message["text"])

        print("wit", resp["entities"])
        print("wit", resp)

        intents = resp["entities"]["intent"]
        intent_values = set()

        # add intent values with high confidence
        for intent in intents:
            if float(intent["confidence"]) > 0.9:
                print("intent value", intent["value"])
                intent_values.add(intent["value"])

        print(intent_values)
        if {"嗨"} <= intent_values:
            return Intent.HELP.name
        elif {"報導"} <= intent_values:
            return Intent.REPORT.name
        elif {"專案"} <= intent_values:
            return Intent.WORKS_PRIMARY.name
        else:
            return Intent.HELP.name

上面包括定義我們的意圖 Intent 以及串接 wit.ai SDK,每一個語意辨識結果都會有一個「信心指數」表示說目前將輸入的語句辨識出某一個意圖的可信程度有多少,我們這邊把超過 0.9 的意圖值全部加到一個集合內,然後用關鍵字組合去判斷最終的意圖為何。

回應語句

Bot 針對不同情境需要產生不同回答,當某個情境發生時,就觸發某個回答,這邊我們用一個語句庫來實作,語句用 key-value 形式儲存,我們 bot 有需要支援不同語系,這樣儲存方式也適合擴充到多語系。

[zh_TW]
    title_help = 想從哪方面開始聊?
    title_help_restart = 還想知道哪些方面?
    button_works_primary = 專案和工作經歷
    button_works_secondary = 其他經歷
    button_advantages = 專長優勢
    ... 略
[en]
    ... 略

 Demo

Bot 提交審核中,先附上幾個截圖:

備註:內文的程式碼範例可能因為 Facebook 更動 SDK 或 API 而使得行為或結果有所不同。

─ ─

(本文經作者 白昌永(大白)授權轉載,並同意 TechOrange 編寫導讀與修訂標題,原文標題為 〈聊天機器人入門:從 0 到 1〉;首圖來源:PEXELS, CC Licensed。)

延伸閱讀

即將席捲世界的 AI,聊天機器人 Chatbot 將回答你所有問題!
給行銷人的最完整 AI 行銷工具包!自動剪影片、寫文案都不是夢
讓小編人生更美麗的 10 個 Google Chrome 外掛,我每個都載了!

 


我們正在找夥伴!

2019 年我們的團隊正在大舉擴張,需要你的加入跟我們一起找出台灣創新原動力! 我們正在徵 《採訪社群編輯》、《助理編輯》,詳細職缺與應徵辦法 請點我

點關鍵字看更多相關文章: