逐句回答,流式返回,ChatGPT採用的Server-sent events後端實時推送協議Python3.10實現,基於Tornado6.1

劉悅的技術部落格發表於2023-03-08

善於觀察的朋友一定會敏銳地發現ChatGPT網頁端是逐句給出問題答案的,同樣,ChatGPT後臺Api介面請求中,如果將Stream引數設定為True後,Api介面也可以實現和ChatGPT網頁端一樣的流式返回,進而更快地給到前端使用者反饋,同時也可以緩解連線超時的問題。

Server-sent events(SSE)是一種用於實現伺服器到客戶端的單向通訊的協議。使用SSE,伺服器可以向客戶端推送實時資料,而無需客戶端發出請求。

SSE建立在HTTP協議上,使用基於文字的資料格式(通常是JSON)進行通訊。客戶端透過建立一個EventSource物件來與伺服器建立連線,然後可以監聽伺服器傳送的事件。伺服器端可以隨時將事件推送給客戶端,客戶端透過監聽事件來接收這些資料。

ChatGPT的Server-sent events應用

首先開啟ChatGPT網頁端,隨便問一個問題,然後進入網路選單,清空歷史請求記錄後,進行網路抓包監聽:

可以看到,在觸發了回答按鈕之後,頁面會往後端的backend-api/conversation對話介面發起請求,但這個介面的通訊方式並非傳統的http介面或者Websocket持久化連結協議,而是基於EventSteam的事件流一段一段地返回ChatGPT後端模型的返回資料。

為什麼ChatGPT會選擇這種方式和後端Server進行通訊?ChatGPT網頁端使用Server-sent events通訊是因為這種通訊方式可以實現伺服器向客戶端推送資料,而無需客戶端不斷地向伺服器傳送請求。這種推送模式可以提高應用程式的效能和響應速度,減少了不必要的網路流量。

與其他實時通訊協議(如WebSocket)相比,Server-sent events通訊是一種輕量級協議,易於實現和部署。此外,它也具有廣泛的瀏覽器相容性,並且可以在不需要特殊網路配置的情況下使用。

在ChatGPT中,伺服器會將新的聊天訊息推送到網頁端,以便實時顯示新的聊天內容。使用Server-sent events通訊,可以輕鬆地實現這種實時更新功能,並確保網頁端與伺服器之間的通訊效率和穩定性。

說白了,降低成本,提高效率,ChatGPT是一個基於深度學習的大型語言模型,處理自然語言文字需要大量的計算資源和時間。因此,返回響應的速度肯定比普通的讀資料庫要慢的多,Http介面顯然並不合適,因為Http是一次性返回,等待時間過長,而Websocket又過重,因為全雙工通訊並不適合這種單項對話場景,所謂單項對話場景,就是對話雙方並不會併發對話,而是序列的一問一答邏輯,同時持久化連結也會佔用伺服器資源,要知道ChatGPT幾乎可以算是日均活躍使用者數全球最高的Web應用了。

效率層面,大型語言模型沒辦法一下子返回所有計算資料,但是可以透過Server-sent events將前面計算出的資料先“推送”到前端,這樣使用者也不會因為等待時間過長而關閉頁面,所以ChatGPT的前端觀感就是像打字機一樣,一段一段的返回答案,這種“邊計算邊返回”的生成器模式也提高了ChatGPT的回答效率。

Python3.10實現Server-sent events應用

這裡我們使用基於Python3.10的Tornado非同步非阻塞框架來實現Server-sent events通訊。

首先安裝Tornado框架

pip3 install tornado==6.1

隨後編寫sse_server.py:

import tornado.ioloop  
import tornado.web  
  
  
push_flag = True  
  
from asyncio import sleep  
  
  
class ServerSentEvent(tornado.web.RequestHandler):  
  
    def __init__(self, *args, **kwargs):  
        super(ServerSentEvent, self).__init__(*args, **kwargs)  
        self.set_header('Content-Type', 'text/event-stream')  
        self.set_header('Access-Control-Allow-Origin', "*")  
        self.set_header("Access-Control-Allow-Headers","*")  
        # 請求方式  
        self.set_header("Access-Control-Allow-Methods","*")  
  
    # 斷開連線  
    def on_finish(self):  
        print("斷開連線")  
        return super().on_finish()  
  
    async def get(self):  
        print("建立連結")  
        while True:  
            if push_flag:  
                print("開始")  
                self.write("event: message\n");  
                self.write("data:" + "push data" + "\n\n");  
                self.flush()  
                await sleep(2)

建立好推送路由類ServerSentEvent,它繼承Tornado內建的檢視類tornado.web.RequestHandler,首先利用super方法呼叫父類的初始化方法,設定跨域,如果不使用super,會將父類同名方法重寫,隨後建立非同步的get方法用來連結和推送訊息,這裡使用Python原生非同步的寫法,每隔兩秒往前端推送一個事件message,內容為push data。

注意,這裡只是簡單的推送演示,真實場景下如果涉及IO操作,比如資料庫讀寫或者網路請求之類,還需要單獨封裝非同步方法。

另外這裡假定前端onmessage處理程式的事件名稱為message。如果想使用其他事件名稱,可以使用前端addEventListener來訂閱事件,最後訊息後必須以兩個換行為結尾。

隨後編寫路由和服務例項:

def make_app():  
    return tornado.web.Application([  
        (r"/sse/data/", ServerSentEvent),  
    ])  
  
if __name__ == "__main__":  
    app = make_app()  
    app.listen(8000)  
    print("sse服務啟動")  
    tornado.ioloop.IOLoop.current().start()

隨後在後臺執行命令:

python3 sse_server.py

程式返回:

PS C:\Users\liuyue\www\videosite> python .\sse_server.py  
sse服務啟動

至此,基於Tornado的Server-sent events服務就搭建好了。

前端Vue.js3連結Server-sent events服務

客戶端我們使用目前最流行的Vue.js3框架:

sse_init:function(){  
  
  
          var push_data = new EventSource("http://localhost:8000/sse/data/")  
        push_data.onopen = function (event) {  
            // open事件  
            console.log("EventSource連線成功");  
        };  
         
  
        push_data.onmessage = function (event) {  
    try {  
        console.log(event);  
    } catch (error) {  
        console.log('EventSource結束訊息異常', error);  
    }  
};  
  
  
       push_data.onerror = function (error) {  
    console.log('EventSource連線異常', error);  
};  
  
  
      }

這裡在前端的初始化方法內建立EventSource例項,透過onmessage方法來監聽後端的主動推送:

可以看到,每隔兩秒鐘就可以訂閱到後端的message事件推送的訊息,同時,SSE預設支援斷線重連,而全雙工的WebSocket協議則需要自己在前端實現,高下立判。

結語

不僅僅可以實現ChatGPT的流式返回功能,SSE在Web應用程式中的使用場景非常廣泛,例如實時的新聞推送、實時股票報價、線上遊戲等等,比起輪詢和長輪詢,SSE更加高效,因為只有在有新資料到達時才會傳送;同時SSE支援自定義事件和資料,具有更高的靈活性和複用性,為流式資料返回保駕護航,ChatGPT的最愛,誰不愛?最後奉上專案地址,與眾鄉親同饗:github.com/zcxey2911/sse_tornado6_vuejs3

相關文章