[Cognitive] 使用Microsoft Cognitive QnA Maker Service,快速建立線上客服問答服務

QnA Maker已於2018/05/07正式GA,最新的操作與管理方式請參考
[Cognitive] 使用Microsoft Cognitive QnA Maker Service,快速建立線上客服問答服務 v4.0
使用原理與API的運用,依然可以參考本篇文章進行閱讀

AI在這兩年來一直都是一個很熱門的話題,自從去年微軟推出了LUIS(Language Understanding Intelligent Service, 語意辨識服務)以及Bot Framework後
越來越多的公司與第一線服務開始透過這兩個雲端功能打造自己的智慧無人客服或是問答系統
但是透過LUIS的訓練,必須花費大量的人力與時間並輸入大量的語句進行訓練,才能得到有效並準確的識別

2017年3月,微軟在Cognitive識別服務中,加入了QnA Maker的辨識服務,可以更快速的打造問答服務的訓練,並找出詢問內容中所需要的解答

QnA Maker的服務,大致上可以用這張圖來說明alt texthttps://qnamaker.ai/Documentation

簡單來說,QnA Maker完整的機制就是可以將已經整理好的FAQ內容,透過文字檔、PDF檔或是Word檔的方式匯入至QnA Maker並進行訓練,完成後再透過Microsoft Bot Framework的介接,將這樣的問答內容串接至不同的Channel上,讓多種IM或是訊息溝通平台都可以完成Q&A的服務整合,看似很像以前透過LUIS的串接與整合方式,但是QnA Maker的訓練內容與辨識卻不用像LUIS那樣的複雜。使用者只要將常用的FAQ匯入並訓練,QnA Maker就會完成後續學習的機制與任務

由於現在QnA Maker的服務還在Preview階段,所以要申請QnA Maker的服務必須先進入網站 https://qnamaker.ai/
登入後,點選[Create new service]的選單項目

在建立新的QnA Maker服務的頁面中,給予一個 [SERVIICE NAME],下方的 [FAQ URL(S)] 以及 [FAQ FILE] 都先給予空白,接著按下 [CREATE] 的按鈕,完成服務的建立

完成建立之後,畫面會直接進入 [Knowledge Base] 的設定頁,在這個頁面中,可以輸入目前公司或是服務所需要進行的問題與回答內容設定,在這裡,我先加入基本的六項客服常用的問答內容

設定完成後,點選上方的 [Save and retrain],這個動作會將剛剛設定進去的Q&A進行儲存的動作,並進行訓練,待訓練完成後,就點選右方的 [Publish] 將這個服務進行發佈

發佈完成後,點選左方的 [Test],在這個畫面中可以將剛剛輸入的訊息進行問答的測試,在這裡輸入的一些語意相同的內容,但是又不與問答設定裡的文字完全一樣的文字。可以看到回覆的訊息內容基本上準確度都還蠻高的,不用透過LUIS的處理就可以作問答的回覆

而點選上方選單中的 [Download Knowloedge Base],就可以下載目前設定好的Q&A內容檔案,這個檔案除了可以快速備份內容外,要用在建立其他相同的服務時,也可以作為初始內容進行匯入的動作

在一開始建立服務時可以提供FAQ URL(S)的欄位,指的就是可以匯入作為FAQ內容的tsv檔案網址,將檔案放在網路上,或是進行檔案上傳,就可以在服務一開始建立時就將問答內容建立完成

接著我們回到選單中的 [My services],並點選後續要使用程式碼操作的服務的項目,並點選 [View code]的連結

點選後可以看到下面的範例程式碼,在這裡有幾個參數需要記下來,以便後面開發程式所需要,分別是Subscription Key以及Knowledge Base Id

當然了,Microsoft Cognitive的服務都有提供REST API讓開發人員作使用,所以要透過程式碼進行QnA Maker的操作也是很簡單的
在QnA Maker的REST API文件中有提供了下面幾個API可以使用

  • POST Create Knowledge Base:建立一個全新的KB服務
  • DELETE Delete Knowledge Base:刪除指定的KB服務
  • GET Download Alterations:取得關鍵字變更的檔案網址
  • GET Download Knowledge Base:取得Q&A的內容檔案網址
  • POST Generate answer:傳入問題的內容,並取得回覆的答案
  • PUT Publish Knowledge Base:發佈這個服務
  • PATCH Train Knowledge Base:訓練這個服務
  • PATCH Update Alterations:更新關鍵字的置換
  • PATCH Update Knowledge Base:更新Q&A的資料(只提供刪除與新增)

以上述幾個REST API的功能來說,已經足夠讓開發者進行服務的操作與使用了,接下來就可以進行程式碼的開發,並進行REST API的操作

由於程式碼很多,建議直接參考參考資料中的Github網址,並於下載後執行操作並察看程式碼的內容

首先,我們先在程式碼中建立一個共用的Call API副程式,副程式的內容可以參考下面的程式碼 

/// <summary>
/// 呼叫API的動作
/// </summary>
/// <param name="strUrl"></param>
/// <param name="strHttpMethod"></param>
/// <param name="strPostContent"></param>
/// <param name="strSubscriptionKey"></param>
/// <param name="code"></param>
/// <returns></returns>
internal static string CallQnAMaker(string strUrl, string strHttpMethod, string strPostContent, string strSubscriptionKey, out HttpStatusCode code)
{
    strUrl = "https://westus.api.cognitive.microsoft.com/qnamaker/v2.0/knowledgebases" + strUrl;
    code = HttpStatusCode.OK;
    byte[] bs = Encoding.UTF8.GetBytes(strPostContent);

    HttpWebRequest request = HttpWebRequest.Create(strUrl) as HttpWebRequest;
    request.Method = strHttpMethod;
    request.Headers.Add("Ocp-Apim-Subscription-Key", strSubscriptionKey);
    request.ContentType = "application/json";
    request.ContentLength = bs.Length;
    request.KeepAlive = true;

    if (!string.IsNullOrEmpty(strPostContent))
    {
        Stream reqStream = request.GetRequestStream();
        reqStream.Write(bs, 0, bs.Length);
    }

    string strReturn = "";
    try
    {
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        var respStream = response.GetResponseStream();
        strReturn = new StreamReader(respStream).ReadToEnd();
        code = response.StatusCode;
    }
    catch (Exception e)
    {
        strReturn = e.Message;
        code = HttpStatusCode.BadRequest;
    }

    return strReturn;
}

在這個副程式中,可以傳入要呼叫的API的網址,呼叫的方式,送入的JSON物件字串,以及將會回傳的文字內容與HttpStatusCode物件,下面的動作,就會接著介紹幾個常用的功能了

1.建立新的KB
在建立新的KB的動作中,依據API文件中所提到的內容,必須要透過POST的方式將一些資訊送至REST API中才能進行KB的建立,所以建立新的KB的程式碼可以參考下面程式碼所示

/// <summary>
/// 呼叫API進行KB建立的動作
/// </summary>
/// <param name="objKb">要建立KB的物件</param>
/// <param name="strSubscriptionKey">存取金鑰</param>
/// <returns></returns>
public KBModel.CreateKBResultModel CreateKB(KBModel.CreateKBModel objKb, out HttpStatusCode code)
{
    KBModel.CreateKBResultModel result = null;
    string strMsg = CallQnAMaker("/create", "POST", JsonConvert.SerializeObject(objKb), this.SubscriptionKey, out code);

    if (code == HttpStatusCode.Created)
        result = JsonConvert.DeserializeObject<KBModel.CreateKBResultModel>(strMsg);

    return result;
}

/// <summary>
/// 點選建立KB的動作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnCreate_Click(object sender, EventArgs e)
{
    // 取出資料
    KBModel.CreateKBModel objKb = new KBModel.CreateKBModel()
    {
        name = "[KB的名稱]",
        urls = new List<string>() { "[這裡可以直接給予已經設定完的Q&A的tsv的檔案網址,直接匯入]" },
        qnaPairs = new List<KBModel.QnAList>(),
    };

    for (int i = 0; i < gvQnA.Rows.Count; i++)
    {
        // 在這裡加入question以及answer的清單資料,當然一開始建立時也可以不給予任何的內容
        objKb.qnaPairs.Add(
            new KBModel.QnAList()
            {
                answer = "[這是回答]",
                question = "[這是問題]",
            }
        );
    }

    // 送出新增的動作
    HttpStatusCode code = HttpStatusCode.OK;
    KBModel.CreateKBResultModel result =  base.iQnAMaker.CreateKB(objKb, out code);
}

在建立KB的程式碼中,一共分成兩個部份,CreateKB的副程式主要是將傳入要建立KB的模型物件透過呼叫API的副程式並進行送出請求的動作
而btnCreate_Click的動作,則是組成要送出新增KB物件的模型內容,並在初始內容中,決定是否要給予初使的問答訊息,或是前面步驟下載的tsv檔案網址

2.下載KB檔案
在[Download Knowledge Base] 的API中,可以取得這個KB服務上所設定的Q&A資料,QnA Maker的服務會將檔案放置在Blob中並回傳檔案連結網址供下載,要呼叫這個API的話可以參考下面的程式碼進行使用

/// <summary>
/// 下取得KB的問答資料tsv檔的Blob路徑
/// </summary>
/// <param name="strKbId"></param>
/// <param name="code"></param>
public string DownloadKB(string strKbId, out HttpStatusCode code)
{
    return Utility.CallQnAMaker("/" + strKbId, "GET", "", this.SubscriptionKey, out code).Replace("\"", "");
}

/// <summary>
/// 下載並放入Kb的問答資料
/// </summary>
private string DownloadData()
{
    HttpStatusCode code = HttpStatusCode.OK;
    string strKbId = base.KBList[cbxKbId.SelectedIndex].kbId;
    string strTsvFileUrl = iQnAMaker.DownloadKB(strKbId, out code);

    // 下載tsv的內容
    strFilePath = Application.StartupPath + $"\\Download\\{strKbId}.tsv";

        
    using (var client = new WebClient())
    {
        // 儲存新檔案
        client.DownloadFile(strTsvFileUrl, strFilePath);
    }

    return strFilePath;
}

下載檔案的程式碼就顯得很簡單,透過GET的方式,傳入相對應的網址後,在DownloadKB的副程式中,直接回傳該tsv檔案存放的網址並進行下載另存新檔

下載tsv的檔案,在執行Update之後並不會即時反映最新的資料,有可能是排程更新檔案,也有可能是排程Purge CDN,大概需要等5~10分鐘才會更新下載的檔案內容

3.更新KB的內容
在 [Update Knowledge Base] 這個API中有幾點需要特別注意的,就是這個API所PATCH的內容並沒有真正的修改這件事,而是只有新增(add)以及刪除(delete),所以在使用的操作上就要更小心了,相關的程式碼可以參考下面的內容

/// <summary>
/// 更新KB的資料
/// </summary>
/// <param name="strKbId"></param>
/// <param name="value"></param>
/// <param name="code"></param>
/// <returns></returns>
public string UpdateKB(string strKbId, KBModel.UpdateKBModel value, out HttpStatusCode code)
{
    return Utility.CallQnAMaker("/" + strKbId, "PATCH", JsonConvert.SerializeObject(value), this.SubscriptionKey, out code);
}

/// <summary>
/// 送出更新的動作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnUpdate_Click(object sender, EventArgs e)
{
    KBModel.UpdateKBModel objUpdate = new KBModel.UpdateKBModel()
    {
        add = new  KBModel.UpdateKBModel.Add()
        {
            qnaPairs = new List<KBModel.QnAList>(),
            urls = new List<string>()
        },
        delete = new KBModel.UpdateKBModel.Delete()
        {
            qnaPairs = new List<KBModel.QnAList>()
        },
    };

    for (int i = 0; i < objQnA.Count; i++)
    {
        if (objQnA[i].source == "delete")
        {
            objUpdate.delete.qnaPairs.Add(new KBModel.QnAList()
            {
                answer = "[要刪除的回答]",
                question = "[要刪除的問題]",
            }
            );
        }
        else if (objQnA[i].source == "add")
        {
            objUpdate.add.qnaPairs.Add(new KBModel.QnAList()
            {
                answer = "[要新增的回答]",
                question = "[要新增的問題]",
            }
            );
        }
    }

    HttpStatusCode code = HttpStatusCode.OK;
    string strMsg = UpdateKB(base.KBList[cbxKbId.SelectedIndex].kbId, objUpdate, out code);
}

從上面的程式碼可以看到,在送出更新的JSON資料中,必須將刪除與新增兩個不同的資料分成兩個List來作傳送,操作上難度比較高,但是對於無法登入網頁介面進行內容編輯的人來說,能開發這樣的功能總比要進入介面設定好很多了

4.取得回覆的答案
在 [Generate answer] 這個API中,可以透過傳入的問題取得相對的答案,並回傳比對後分數的高低,使用的方式可以參考下面的內容

/// <summary>
/// 詢問問題的動作
/// </summary>
/// <param name="value"></param>
/// <param name="code"></param>
/// <returns></returns>
public KBModel.GenerateAnswerResultModel GenerateAnswer(string strKbId, KBModel.GenerateAnswerModel value, out HttpStatusCode code)
{
    KBModel.GenerateAnswerResultModel result = null;
    string strUrl = $"/{strKbId}/generateAnswer";
    string strResult = Utility.CallQnAMaker(strUrl, "POST", JsonConvert.SerializeObject(value), this.SubscriptionKey, out code);

    if (code == HttpStatusCode.OK)
        result = JsonConvert.DeserializeObject<KBModel.GenerateAnswerResultModel>(strResult);

    return result;
}

/// <summary>
/// 開始進行問答的動作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnQuery_Click(object sender, EventArgs e)
{
    KBModel.GenerateAnswerModel objQuery = new KBModel.GenerateAnswerModel()
    {
        question = "[要詢問的問題]",
        top = int.Parse([列出最接近的回覆的總筆數]),
    };

    HttpStatusCode code = HttpStatusCode.OK;
    string strKbId = base.KBList[cbxKbId.SelectedIndex].kbId;
    KBModel.GenerateAnswerResultModel result = GenerateAnswer(strKbId, objQuery, out code);
}

這邊需要注意的,就是當透過API進行問題的詢問時,必須傳入一個top的參數,告訴QnA Maker希望能夠回傳幾筆最接近的資料,API會回傳比對出有類似結果的內容並告知比對後的分數高低
而下圖是實際執行的結果畫面從執行結果的畫面可以看到,回傳的score欄位說明了Q&A比對後的分數,可以讓前端呈現內容的資料作比較

QnA Maker這個辨識服務,透過問答的方式比對問題的內容並回傳有可能的答案,這樣快速方便的機制,除了省去需要大量訓練語意辨識的資料外,對於希望能快速建立無人客服或是線上機器人回覆的機制,大大加快了開發的效益並縮短開發時間。若是能夠再透過Microsoft Bot Framework進行多種IM的整合,對於公司與產品整體服務來說會有更高的價值

目前QnA Maker還在Preview階段,且尚未針對非英語系進行完整測試,所以比對出來的結果不一定完全準確,想要使用在正式環境或是對外服務的人需特別的格外注意這件事

 參考內容
QnAMaker - V2.0
API V2.0 Reference
QnA Maker API

GitHub範例程式下載
https://github.com/madukapai/maduka-QnAMaker