基於GPT搭建私有知識庫聊天機器人(六)仿chatGPT打字機效果

2023-07-21 18:00:30

文章連結:

基於GPT搭建私有知識庫聊天機器人(一)實現原理

基於GPT搭建私有知識庫聊天機器人(二)環境安裝

基於GPT搭建私有知識庫聊天機器人(三)向量資料訓練

基於GPT搭建私有知識庫聊天機器人(四)問答實現

基於GPT搭建私有知識庫聊天機器人(五)函數呼叫


在前幾篇文章中,我們已經瞭解瞭如何使用 GPT 模型來搭建一個簡單的聊天機器人,並在後端使用私有知識庫來提供答案。

現在,我們將繼續改進我們的聊天介面,實現類似chatGPT打字機的效果聊天,避免長時間等待介面資料返回,以提升使用者體驗。

1、效果展示

PS:一本正經的胡說八道

2、Server-Sent Events (SSE) 技術簡介

在本篇文章中,我們將使用 SSE 技術來實現打字機效果輸出。SSE 是一種 HTML5 技術,允許伺服器向用戶端推播資料,而不需要使用者端主動請求。通過 SSE,我們可以在伺服器端有新訊息時,實時將訊息推播到前端,從而實現動態的聊天效果。

3、前端程式碼

首先,我們需要編寫前端的JavaScript 程式碼,以便使用 SSE 技術與伺服器進行實時通訊。

<!DOCTYPE html>
<html>
<head>
    <title>ChatGPT-like Interface</title>
    <link rel="stylesheet" href="static/styles.css">
</head>
<body>
    <div class="chat-container">
        <div class="chat-history" id="chatHistory">
            <!-- Chat messages will be dynamically added here -->
        </div>
        <div class="user-input">
            <input type="text" id="userInput" placeholder="請輸入您的問題...">
            <button id="sendButton">傳送</button>
        </div>
    </div>

    <script>
        // Your existing chat interface code here...

        // Server communication code
        var eventSource; // Declare the eventSource variable outside the click handler

        document.getElementById("sendButton").addEventListener("click", function () {
            var userMessage = document.getElementById("userInput").value.trim();
            if (userMessage === '') {
                alert('Please enter a message!');
                return;
            }

            appendMessage('user', userMessage); // Add the user's message to the chat history

            // Close the previous SSE connection (if exists)
            if (eventSource) {
                eventSource.close();
            }

            // Establish SSE connection with the user's message as a parameter
            eventSource = new EventSource(`/print_stream?question=${encodeURIComponent(userMessage)}`);

            eventSource.onmessage = function (event) {
                var botMessage = event.data;
                appendMessage('bot', botMessage);
            };

            eventSource.onerror = function (error) {
                console.error("Error occurred with SSE connection:", error);
                // Handle the error if necessary
                isFirstToken = true;
                eventSource.close();
            };
            document.getElementById("userInput").value = '';
        });
        var chatHistoryDiv = document.getElementById("chatHistory"); // 獲取 chatHistory 的元素
        var isFirstToken = true; // 用於跟蹤是否是第一次返回 token
        function appendMessage(sender, message) {
            if (isFirstToken) {
                // 如果是第一次返回 token,建立新的 <div> 元素,並將 isFirstToken 設定為 false
                var messageDiv = document.createElement('div');
                messageDiv.className = `chat-message ${sender === 'user' ? 'user-message' : 'bot-message'}`;
                chatHistoryDiv.appendChild(messageDiv);
                if(sender === 'bot') {
                    isFirstToken = false;
                }
            } else {
                // 如果不是第一次返回 token,直接獲取最後一個 <div> 元素,將新的訊息內容追加到現有的元素中
                var messageDiv = chatHistoryDiv.lastElementChild;
            }
            messageDiv.innerText += message; // 將新的訊息內容追加到 <div> 中
            chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight; // 將卷軸捲動到最底部
        }
    </script>
</body>
</html>

為了實現對話效果,我們需要調整 CSS 樣式表中的部分樣式。以下是 CSS 樣式表:

body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}

.chat-container {
    width: 800px;
    border: 1px solid #ccc;
    border-radius: 5px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
    background-color: #fff;
    overflow: hidden;
}

.chat-history {
    max-height: 800px;
    overflow-y: auto;
    padding: 10px;
}

.chat-message {
    margin-bottom: 10px;
    padding: 8px 12px;
    border-radius: 20px;
    max-width: 70%; /* 設定最大寬度,使得訊息在一行中不會過長 */
    align-self: flex-end; /* 靠右顯示 */
    word-wrap: break-word; /* 處理長文字的自動換行 */
    overflow-wrap: break-word; /* 處理長文字的自動換行 */
}

.user-message {
    color: #007bff;
    background-color: #e6e6e6; /* 使用者訊息氣泡背景色 */
    text-align: right; /* 靠右顯示文字內容 */
    align-self: flex-end; /* 靠右顯示氣泡 */
    margin-left: auto; /* 新增額外的間距,讓氣泡靠右 */
}

.bot-message {
    color: #555;
    background-color: #d9edf7; /* 機器人訊息氣泡背景色 */
    text-align: left; /* 靠左顯示文字內容 */
    align-self: flex-start; /* 靠左顯示氣泡 */
    margin-right: auto; /* 新增額外的間距,讓氣泡靠左 */
}


.user-input {
    display: flex;
    align-items: center;
    padding: 10px;
}

#userInput {
    flex-grow: 1;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 5px;
    margin-right: 10px;
}

#sendButton {
    padding: 8px 15px;
    border: none;
    border-radius: 5px;
    background-color: #007bff;
    color: #fff;
    cursor: pointer;
}

#sendButton:hover {
    background-color: #0056b3;
}

4、後端程式碼

本文依舊使用的langchain框架實現存取openAI,以及利用回撥函數接收token資料。

首先,是API入口:

from flask import Flask, request, Response, stream_with_context

@app.route("/print_stream")
def print_stream():
    question = request.args.get('question')
    ans = search_schedule(question)

    return Response(stream_with_context(ans), content_type='text/event-stream')

其次,是存取openAI程式碼(不太瞭解的可以看下前幾篇文章):

def search_schedule(query: str) -> str:
    stream_to_web = StreamToWeb()
    llm = ChatOpenAI(temperature=0,
                     model="gpt-3.5-turbo-0613",
                     callback_manager=CallbackManager([stream_to_web]),
                     streaming=True
                     )
    bus_tools = [BusTool()]
    open_ai_agent = initialize_agent(bus_tools,
                                     llm,
                                     agent=AgentType.OPENAI_FUNCTIONS,
                                     verbose=True)
    chain_thread = threading.Thread(target=process_query,
                                    kwargs={"question": query,
                                            "open_ai_agent": open_ai_agent})
    chain_thread.start()
    resp = stream_to_web.generate_tokens()
    return resp

注意:上面呼叫openai部分程式碼必須使用非同步執行,才能做到一邊接收返回token,一邊返回前端,否則無法實現打字機效果。

最後,打字機效果核心程式碼:

class StreamToWeb(StreamingStdOutCallbackHandler):
    def __init__(self):
        self.tokens = []
        # 記得結束後這裡置true
        self.finish = False

    def on_llm_new_token(self, token: str, **kwargs):
        self.tokens.append(token)

    def on_llm_end(self, response: any, **kwargs: any) -> None:
        self.finish = 1

    def on_llm_error(self, error: Exception, **kwargs: any) -> None:
        print(str(error))
        self.tokens.append(str(error))

    def generate_tokens(self):
        while not self.finish or self.tokens:
            if self.tokens:
                data = self.tokens.pop(0)
                yield f"data: {data}\n\n"
            else:
                pass

注意:yield f"data: {data}\n\n" ,data是前端接受資料的引數,\n\n在SSE要求中必須新增。

5、總結

通過使用 SSE 技術和打字機樣式輸出,我們成功改進了聊天機器人的介面,實現了更加動態和流暢的聊天體驗。這樣的使用者介面使得聊天機器人更加接近真實對話,提升了使用者體驗。