使用 Dify 串接 LINE Bot

一開始寫這篇文章的時候,我有點困擾。
到底這一篇我到底該命名 『使用 Dify 串接 LINE Bot』 ,還是 『使用 LINE Bot 串接 Dify』??

但我想,反正你應該知道我的意思,本文就是討論如何把兩者串在一起。
如果不熟悉 Dify ,本篇文章的前面還有兩邊文章你可以簡單的介紹了一下,所有連結位置在底下:
https://studyhost.blogspot.com/search?q=dify

總的來說(我20年前就這麼說話了,最近有人說AI很喜歡說『總的來說』,但顯然是AI學我的),我們可以在 Dify 裡面設計一個類似底下這樣的 workflow,他可以是一個簡單的對談聊天機器人,也可以是串接到後端HR請假系統,可以協助企業進行請假的 AI 助理:
圖片

顯而易見,使用 Dify 的好處很多。而在這邊它對我們來說,意義上是一個所視即所得的 Chat Bot(AI助理)設計平台,你可以在 Dify 中先把對談流程設定好,並且進行測試。

設計初步完善之後,我們可以透過Dify API來進行串接,例如串接到 Teams Bot或是LINE Bot。API的部分我們之前在這一篇介紹過。

由於Dify內建處理了對談前後文與記憶的部分,並且可以直接串接OpenAI的API(或是其它LLM亦可,同時也便於抽換),這讓我們開發 AI 助理輕省不少。

這也是我覺得 Dify 作為一個 LLMOps 開發平台(Platform)應該算是當之無愧的原因了。

串接到 LINE Bot

那在Dify上開發測試完之後,我們當然希望能夠整合在 LINE Bot(或是隨便其它什麼 Bot)上面。前面說過,由於 Dify 有提供一組API,我們可以透過該API輕易地進行串接,API的部分,我們在先前這一篇介紹過。因此,我們底下要更進一步,談談如何實際跟 LINE Bot 整合在一起。

其實架構上不複雜,我們只需要建立一個 LINE Bot 的 WebHook ,並且在其中呼叫 Dify API即可:
圖片

也就是上圖中,紅色虛線框起來的這一段。

這部分該如何進行呢? 倒也容易,我們看底下 WebHook 的部分:

var responseMsg = "";
//準備回覆訊息
if (LineEvent != null && LineEvent.type.ToLower() == "message" && LineEvent.message.type == "text")
{
    //取得對話ID(from cache)
    var Conversation_id = _cacheService.GetCache(LineEvent.source.userId);
    //如果用戶輸入 /forget 則把 Conversation_id 清空,重啟對話
    if (LineEvent.message.text.Trim().ToLower() == "/forget")
    {
        _cacheService.RemoveCache(LineEvent.source.userId);
        Conversation_id = null;
        responseMsg = "我已經忘記之前所有對話了";
    }
    else
    {
        //👇建立呼叫 Dify API 所需的 requestData 參數
        var requestData = new
        {
            inputs = new { },
            query = LineEvent.message.text, //👉取得使用者輸入文字
            response_mode = "blocking",
            conversation_id = Conversation_id is null ? "" : Conversation_id.ToString(), //👉取得對話ID
            user = LineEvent.source.userId //👉取得使用者ID
        };
        var response = Dify.CallDifyChatMessagesAPI(DifyAPIKey, requestData);
        responseMsg = response.answer;
        //儲存對話ID(to cache)
        _cacheService.SetCache(LineEvent.source.userId, response.conversation_id);
    }
}
else if (LineEvent != null && LineEvent.type.ToLower() == "message")
    responseMsg = $"收到 event : {LineEvent.type} type: {LineEvent.message.type} ";
else
    responseMsg = $"收到 event : {LineEvent.type} ";
//回覆訊息
this.ReplyMessage(LineEvent.replyToken, responseMsg);
//response OK
return Ok();

其中的重點在於,回覆用戶的訊息時,我們呼叫了 Dify 的 ChatMessage API,它被包裹在我們撰寫的 Dify.CallDifyChatMessagesAPI() 當中。

這個方法的程式碼如下:

public static dynamic CallDifyChatMessagesAPI(string apiKey, object requestData)
{
    var client = new HttpClient();

    // 設定 API 網址
    var apiUrl = $"https://api.dify.ai/v1/chat-messages";

    // 設定 HTTP request headers
    client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); //👉Dify API key
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));//ACCEPT header
    // 將 requestData 物件序列化成 JSON 字串
    string jsonRequestData = Newtonsoft.Json.JsonConvert.SerializeObject(requestData);
    // 建立 HTTP request 內容
    var content = new StringContent(jsonRequestData, Encoding.UTF8, "application/json");
    // 傳送 HTTP POST request
    var response = client.PostAsync(apiUrl, content).Result;
    // 取得 HTTP response 內容
    var responseContent = response.Content.ReadAsStringAsync().Result;
    var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(responseContent);
    return obj;
}

沒啥特別的,典型的POST API呼叫,帶入 Dify API的key,以及 requestData。

但其中有一個細節要稍微注意, requestData 包含了一個 conversation_id 屬性:

//👇建立呼叫 Dify API 所需的 requestData 參數
var requestData = new
{
    inputs = new { },
    query = LineEvent.message.text, //👉取得使用者輸入文字
    response_mode = "blocking",
    conversation_id = Conversation_id is null ? "" : Conversation_id.ToString(), //👉取得對話ID
    user = LineEvent.source.userId //👉取得使用者ID
};

這個屬性頗為重要,因為它是我們之所以能夠讓 Chat Bot 有記憶和前後文理解的關鍵因素。因此,我們必須在每次Chat Bot與用戶對談時,保持這個 conversation_id 屬性具有相同的值,conversation_id 的值會在第一次與 Chat Bot 對談時被建立起來,這是 Dify 框架的功能,這部分我們在先前這一篇介紹過。

採用 MemoryCache 保存 conversation_id

為了讓 conversation_id 的值在每一個我們的 WebHook 被呼叫的過程中,都能夠 by 對談User 維持一致,因此我們又設計了一個機制,採用 MemoryCache ,以 key value pair 的形式,by User id來保存每一個用戶的 conversation_id,這就是你之所以在前面看到

//取得對話ID(from cache)
var  Conversation_id  =  _cacheService.GetCache(LineEvent.source.userId);

//儲存對話ID(to cache)
_cacheService.SetCache(LineEvent.source.userId, response.conversation_id);

的原因。

這兩段程式碼,把 conversation_id 依照個別用戶的不同,以 key value pair 的形式妥善的保存在記憶體中,只要 WebHook 不被重啟,用戶就可以透過Dify內建的功能,維持持續的對談(也就是保有前後文記憶的意思)。

採用 MemoryCache 而非資料庫很單純是因為我不希望範例變得複雜,正式環境中,採用類似SQL DB這種長時間儲存體(persistent storage),的了儲存體依舊是比較理想的選擇

至於 MemoryCacheService 這個類別,我就不特別介紹了,完整的程式碼都在我的 github repo:

https://github.com/isdaviddong/ex_Dify2LineBotWebHookController.git

我們在對談中還做額外做了一個機制,只要用戶說了『/forget』 關鍵字,我們就會清空該用戶的對談紀錄,這段程式碼如下:

//如果用戶輸入 /forget 則把 Conversation_id 清空,重啟對話
if (LineEvent.message.text.Trim().ToLower() == "/forget")
{
    _cacheService.RemoveCache(LineEvent.source.userId);
    Conversation_id = null;
    responseMsg = "我已經忘記之前所有對話了";
}

這是為了方便我們測試使用。

Demo實際執行

好啦,簡單介紹(更詳細的說明在我們的課程當中)完程式碼之後,我們來實際運行看看:
(底下的gif動畫,從我輸入 “你好” 開始)
enter image description here

你會發現,透過串接 Dify ,我們的 AI助理 輕鬆的擁有記憶、前後文處理的功能,也有大語言模型來處理對話的回應,這部分我們可以完全不用撰寫任何程式碼,交給Dify來搞定。

同樣的,比照這個邏輯,我們可以透過 Dify 的開發環境,以所視即所得的方式,用滑鼠拖拖拉拉,就可以設計出一個完善的 AI 助理,不管是用來請假、售票、處理客訴、整合RAG查詢企業內的資料…都不是問題,然後直接跟 LINE 串接在一起,隨時要抽換後端的大語言模型(LLM)從 OpenAI 到 Azure OpenAI 或 Gemini、Llama 也都不是難事。

真是前所未有的便捷、前所未有的愉快啊!!!


參考 課程:
ChatGPT(Azure OpenAI) 對談機器人開發實戰

留言

這個網誌中的熱門文章

使用LM Studio輕鬆在本地端以API呼叫大語言模型(LLM)

VS Code的字體大小

使用C#開發LineBot(3) - 使用LineBotSDK發送Line訊息

使用 Dify 建立企業請假機器人

使用Qdrant向量資料庫實作語意相似度比對