Microsoft Copilot 由多個開源工具提供支持,例如:SignalR、自適應卡片、Markdown 和物件盆地,以解決大規模構建支持 AI 的應用程式的獨特挑戰。在本文中,我們將分享設計註意事項以及如何整合各種工具。更具體地說,本文重點介紹我們如何將訊息和響應流式傳輸到前端 UI 的各個方面,同時概述伺服器端發生的情況。隨著越來越多的團隊和企業考慮利用大型語言模型 (LLMs),我們希望本文有助於擴充套件 AI 生態系。
概述
Microsoft Copilot 可在移動應用程式、必應等許多表面上使用,並且它內建於 Microsoft Edge 側資訊看板中,為 Edge 使用者提供輕鬆存取。它提供了流暢的對話體驗,增強了傳統的網路搜尋和瀏覽體驗。由於 Microsoft Copilot 由 AI 提供支持並駐留在 Edge 瀏覽器中,因此使用者可以提出以自然人類語言形成的復雜問題,甚至可以在瀏覽 Web 時需要獲得創意源泉時獲得幫助。在幕後,最先進的 AI 模型透過考慮使用者當前的瀏覽上下文並合成 Web 來生成響應。除了文本輸入和輸出之外,使用者還可以使用語音和影像與 Microsoft Copilot 進行通訊,以實作更豐富的互動。
大綱
使用 SignalR 建立低延遲通訊通道
使用自適應卡片 + Markdown 描述和呈現 UI
申請流程
深入探討:我們如何使用 SignalR?
下一步是什麽?
使用 SignalR 建立低延遲通訊通道
等待 AI 模型生成完整響應後再將其發送回客戶端是不切實際的。我們需要的是一種讓伺服器以塊形式流式傳輸響應的方法,以便使用者可以盡快看到部份結果。雖然我們無法更改生成完整響應所需的時間,但我們希望提供近乎即時的感知響應時間。我們首先研究了伺服器發送事件,因為它是 OpenAI 選擇的協定,我們的一些團隊成員在 Python 中也有使用它的經驗。但是,我們最終決定不這樣做,原因有兩個:
我們從從事 Azure 服務的同事那裏了解到,他們已經看到有關 SSE 被代理阻止的報告。
我們想添加一項功能,允許使用者打斷 AI 模型的進一步響應。當從 AI 模型生成極長的響應時,這會派上用場。SSE 是一種單向協定,在這裏幫不上什麽忙。
初步調查使我們對需要實作的目標有了更深入的了解 - 客戶端和伺服器之間的通訊通道具有四個關鍵特征:
低延遲
網路容錯
易於擴充套件
雙向(伺服器和客戶端都可以在需要時來回發送訊息)
SignalR 似乎幾乎完全符合描述。它會自動檢測並在各種 Web 標準協定和技術中選擇最佳傳輸方法。預設情況下,WebSocket 用作傳輸方法,它優雅地回退到伺服器發送的事件和長輪詢技術,以滿足客戶端的能力。
使用 SignalR 庫,我們無需擔心使用哪種傳輸協定,我們可以超越網路級別的問題,專註於實作核心業務邏輯,該邏輯以各種數據格式生成有用的響應。我們的團隊可以在安全的假設下運作,即即時通訊渠道已經建立並得到照顧。對我們來說,另一個方便的好處是 SignalR 是 ASP.NET Core 的一部份,我們不需要新的後端依賴項。
使用自適應卡片 + Markdown 描述和呈現 UI
大多數訊息都使用自適應卡片,因為可以有 Markdown。這有助於我們支持和控制如何以標準方式顯示許多內容,例如程式碼塊、連結、影像和樣式文本。「自適應卡片是與平台無關的 UI 片段,以 JSON 編寫,套用和服務可以公開交換。當交付到特定套用時,JSON 將轉換為自動適應其周圍環境的本機 UI。它有助於為所有主要平台和框架設計和整合輕量級 UI。(摘自 https://adaptivecards.io)。
使用自適應卡片的響應範例,其中包含顯示自適應卡片的 JSON 的程式碼塊。
我們在後端使用標準的自適應卡片架構物件,以便各種元件可以控制與前端共享的內容。例如,在模型生成響應後,我們後端的元件可以輕松地在生成的文本上方添加影像。
申請流程
文本輸入 -> 文本輸出
使用者可以使用關鍵字鍵入其輸入訊息。該頁面使用 SignalR 透過 WebSocket 與名為
/ChatHub
的終結點進行通訊。
/ChatHub
終結點接受一個請求,其中包含有關使用者及其訊息的後設資料。
/ChatHub
發回帶有響應的事件,供客戶端處理或顯示。
當您說「我可以為只吃橙色食物的挑剔的幼兒做些什麽飯菜」時,事件透過 WebSocket 連線流式傳輸。第一行是來自客戶端(前端)的請求。以下內容是來自服務的響應。它們包括要在聊天視窗中顯示的每個計畫的訊息物件。我們將在本文後面詳細介紹它們。
該頁面使用 FAST 在生成和接收訊息時有效地更新頁面
/ChatHub
。
語音輸入 - >語音輸出
使用者還可以使用語音提供輸入,Microsoft Copilot 將讀出響應。
為了處理語音輸入,我們使用與必應相同的語音辨識服務,並且多年來一直在改進。一開始,在客戶端和我們的後端之間,我們只是放置了語音辨識服務,它充當反向代理。該服務將語音轉換為文本,並將其作為提示發送到我們的後端。從後端返回響應後,語音辨識服務將執行文本轉語音,並將文本和音訊發送到前端。
透過這種簡單的方法,我們很快意識到向終端使用者顯示文本存在延遲,因為代理正在等待響應文本轉換為音訊。因此,我們改用讓對話管理服務異步呼叫語音服務,告訴它開始計算響應的音訊,並為要播放的每個音訊段返回確定生成的 URL。UI 使用這些 URL 從語音服務獲取要播放的音訊。在音訊準備就緒之前,UI 可能需要稍等片刻,但至少終端使用者會看到一些結果,而不必等待音訊段準備就緒。
此最佳化還有助於減少語音服務的負載,因為對語音服務的請求要短得多,因為它們不需要在向對話管理層發出請求的整個持續時間內持續。
您可能已經註意到,輸出是分段播放的。我們仍在嘗試不同的方法將文本拆分為多個段,以最大程度地減少段之間的延遲。我們可能會決定按句子拆分,並且可能會在它們自己的段中放置前幾個單詞,以便我們可以盡快開始播放音訊,而不是等待生成第一個完整的句子。
深入探討:如何使用 SignalR?
我們為每個使用者的訊息使用一個連線
對於具有使用 SignalR 或 WebSockets 經驗的開發人員來說,這可能會讓他們感到奇怪。對於遊戲,應保持 WebSocket 連線處於開啟狀態,因為您希望最大程度地減少使用者輸入的延遲。然而,在我們的例子中,使用者發送新訊息可能需要幾秒鐘或幾分鐘,如果他們這樣做的話。因此,保持連線開啟是不值得的。無論如何,該模型都需要一段時間才能完全生成響應,這使得重用同一連線所節省的費用微不足道。
除了這個考慮之外,還有其他原因使這個決定適合我們。客戶端更容易管理,因為它們不需要保留對連線的參照,也不需要在發送新訊息之前檢查其狀態以檢視它是否已在處理連線。對聊天中的每條訊息使用單獨的連線,可以在測試時更輕松地偵錯我們的程式碼,因為您可以在 DevTools 中開啟瀏覽器的「網路」索引標籤,並在其自己的行😉中清楚地看到每個測試請求。當每個 WebSocket 連線的生存期非常短時,管理後端負載要簡單得多。使用簡單的負載均衡方案,每個 SignalR 連線都可以在後端使用不同的節點。這樣可以防止某些節點提供過多長時間執行的連線,而其他節點則具有短執行連線的不平衡。
利用現有功能以必應本機方式顯示訊息
當你在必應中查詢天氣或新聞時,必應有特殊的卡片來顯示此類資訊,並具有豐富的互動性。我們的團隊利用了這些出色的現有功能。Microsoft Copilot 前端以自訂方式解釋或處理某些訊息,以必應使用者熟悉的方式顯示資訊。
在這些情況下,後端會生成一個查詢供客戶端使用。某些訊息顯示處理資訊,例如模型的搜尋 Web 決策、搜尋查詢、搜尋結果以及生成的響應建議,您可以對模型說出這些建議。
響應流式處理的兩種型別的操作
覆蓋信件
如果您檢視過網路流量,您會註意到我們有時會在每次更新中發送完整的響應。
在「update」事件的訊息中發送整個響應的範例。
你可能會問,為什麽我們不能在每次更新中發送一些文本來附加?在模型輸出到達使用者之前,我們會對模型的輸出進行大量內部處理,使用者也無法看到所有輸出。我們尋找特殊的訊號來確定要返回的響應型別,很好地格式化響應,添加引文連結,重新排序引文連結,緩沖區,以便我們不會在連結建立完成之前顯示連結,檢查冒犯性,檢查有害內容,計算要大聲說出的文本等。使用者看到的是模型流輸出的修改版本,因此,我們不能總是像您在某些產品中看到的那樣以僅追加的方式進行更新。
目前,我們通常在流式處理響應時依賴覆蓋訊息,主要是為了利用這種方法為我們提供的簡單實作。我們的模型會生成 Markdown,但尚未將 Markdown 流式傳輸到渲染中。相反,我們會發送整個 Markdown 響應,並在為每次修改訊息重新呈現自適應卡片時將其轉換為 HTML。如果我們確實流式傳輸文本而不是整個物件,那麽理想情況下我們會做一些特殊的事情,比如將新的 Markdown 流式傳輸到一個元件,該元件可以在流中處理 Markdown 並將其添加到已經構建的 HTML 中。
另一個優點是容錯:如果未收到更新,或由於某種原因未處理更新,我們將在處理以後的更新或訊息的最終版本時進行恢復。
高效更新發送到客戶端的訊息物件
每條訊息都有一個唯一的
messageId
.每次更新都包括
messageId
要修改的訊息。我們使用該
"w"
事件來有效地更新訊息物件中的數據,例如自適應卡片中的內容。
雖然熟悉的 JSON 修補程式格式提供了一些更新物件或列出 JSON 物件的功能,但它僅支持覆蓋字串,不支持對字串進行簡潔的修改,例如在字串中附加或插入文本。我們的團隊為JavaScript / TypeScript和.NET建立了一個名為object-basin的新庫,以提供Microsoft Copilot所需的JSON物件的精確更新。object-basin 有助於將各種更新套用於流訊息物件,並允許我們有效地修改已發送到前端的文本。大多數更新只是要附加的文本,但我們可以從後端服務控制更多內容。例如,我們可以在任何字串中插入文本、刪除文本、建立新物件或列表、修改列表等。其工作原理是將遊標聲明為 JSONPath(例如,
$['a7dea828-4be2-4528-bd92-118e9c12dbc6'].adaptiveCards[0].body[0].text
)並行送文本、物件或列表以寫入該遊標上的值。在流式傳輸這些更簡潔的更新時,我們仍會重新處理整個 Markdown 和自適應卡片。
此文章來源 Microsoft .Net Blog
https://devblogs.microsoft.com/dotnet/building-ai-powered-bing-chat-with-signalr-and-other-open-source-tools/