Gradio.NET 是 Gradio 的.NET 移植版本。它是一個能夠助力迅速搭建機器學習模型示範界面的庫,其提供了簡潔的 API,僅需寥寥數行代碼就能建立出一個具備互動性的界面。在本篇文章中,我們将會闡述如何借助 Gradio.NET 為 LLamaWorker 快捷地建立一個大型模型示範界面。
1. 背景
前面一篇文章我們認識了 LLamaWorker[1] 項目,它是一個專為 .NET 開發者設計的大型語言模型服務。LLamaWorker 提供了與 OpenAI 類似的 API,支援多模型切換、流式響應、嵌入支援等特性。此外,LLamaWorker 還提供了一個基于 Gradio.NET[2] 的 UI 示範,使得開發者能夠更快地體驗和調試模型。
2. Gradio.NET 簡介
Gradio.NET 是 Gradio 的.NET 移植版本。Gradio 作為一個開源 Python 包,允許為機器學習模型、API 或任何任意 Python 函數快速建構示範或 Web 應用程式,無需具備 JavaScript、CSS 經驗。使用 Gradio,能夠基于機器學習模型或資料科學工作流迅速建立一個使用者友好的界面,讓使用者可以通過浏覽器進行諸如拖放圖像、粘貼文本、錄制聲音等操作,并與示範程式進行互動。
3. 為什麼選擇 Gradio.NET
LLamaWorker 是提供有 swagger 頁面的 API 服務,但是 swagger 頁面并不能直覺地展示模型的效果。提供一個直覺的示範界面,能夠讓使用者更快地了解模型的效果,同時也能夠幫助開發者更快地調試模型,是我一直在考慮的問題。
當然,選擇技術架構是一個關鍵的決策。起初,我考慮使用 Vue3 從零開始搭建,但這需要耗費大量的時間和精力。恰好在這個時候,我發現了社群新推出的開源項目 Gradio.NET。
我抱着學習新技術的心态嘗試了一下,同時也想為開發者們測試一下這個新架構,發現問題并提出改進的建議。對于初次接觸 Gradio 的人,比如我來說,可能會在初期感到有些吃力。然而,如果之前就熟悉 Python 的 Gradio,那麼使用 Gradio.NET 将會變得非常輕松。
需要注意的是,目前 Gradio.NET 仍在不斷完善之中,還有許多庫尚未完成遷移。但我相信,隻要大家共同努力,積極參與建設,一定能夠讓 Gradio.NET 變得更加完善和強大。
4. 為 LLamaWorker 建立示範界面
接下來,我們将會為 LLamaWorker 建立一個簡單的示範界面。整體代碼包含注釋不過 300 行,但卻能夠實作一個具有互動性的界面。在這個界面中,我們可以輸入文本,然後點選“生成”按鈕,即可擷取模型的回複。
在 ChatUI 項目中,我們使用了 Gradio.NET 多個元件和相關功能,期間也發現并送出了多個 issues 到 Gradio.NET。對于學習 Gradio.NET 的同學來說,這個實際的使用案例将會非常有幫助。特别是重新整理
Dropdown
,網絡請求,以及流式響應的處理等。
4.1. 服務設定
LLamaWorker 提供了API Key 的支援,并提供了模型配置資訊擷取的接口,在 ChatUI 項目中,我們将會使用這些接口來擷取模型的配置資訊。
在頁面的頂部,我們設定了一個輸入框用于輸入 LLamaWorker 服務的 URL,一個輸入框用于輸入 API Key,一個按鈕用于擷取模型配置資訊,以及一個下拉框用于選擇模型。
```cs gr.Markdown("# LLamaWorker"); Textbox input,token; Dropdown model; Button btnset; using (gr.Row()) { input = gr.Textbox("http://localhost:5000", placeholder: "LLamaWorker Server URL", label: "Server"); token = gr.Textbox(placeholder: "API Key", label: "API Key", maxLines:1, type:TextboxType.Password); btnset = gr.Button("Get Models", variant: ButtonVariant.Primary); model = gr.Dropdown(choices: [], label: "Model Select", allowCustomValue:true); }
在上面的代碼中,我們設定了一個輸入框用于輸入 API Key,并驚奇設定為密碼輸入框
TextboxType.Password
,以便隐藏輸入的内容。這裡的
Dropdown
元件我們沒有設定選項,并且允許其可以擷取使用者的自定義值
allowCustomValue:true
,友善使用者輸入自定義的模型名稱,同時也可以使 ChatUI 項目調用其他的服務,比如阿裡靈積的大模型服務等。
服務設定
上圖展示的是在移動端的界面,Gradio.NET 會自動處理流式布局,使得界面在不同裝置上都能夠正常顯示。
在設定好基礎界面後,我們需要為按鈕添加點選事件,以便擷取模型配置資訊。在 Gradio.NET 中,可以通過
Button
的
Click
事件來實作。
btnset?.Click(update_models, inputs: [input, token], outputs: [model]);
在點選按鈕後,會調用
update_models
方法,該方法會向 LLamaWorker 服務發送請求,擷取模型配置資訊,并更新下拉框的選項。
static async Task<Output> update_models(Input input) { string server = Textbox.Payload(input.Data[0]); string token = Textbox.Payload(input.Data[1]); if (server == "") { throw new Exception("Server URL cannot be empty."); } if (!string.IsOrWhiteSpace(token)) { Utils.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } var res = await Utils.client.GetFromJsonAsync<ConfigModels>(server + "/models/config"); if (res?.Models == || res.Models.Count==0) { throw new Exception("Failed to fetch models from the server."); } Utils.config = res; var models = res.Models.Select(x => x.Name).ToList(); return gr.Output(gr.Dropdown(choices: models,value: models[res.Current], interactive: true)); }
在
update_models
方法中,我們首先擷取輸入的服務 URL 和 API Key,然後向服務發送請求擷取模型配置資訊。如果請求成功,我們将會更新下拉框的選項。在這個過程中,我們還會根據服務傳回的目前模型,設定下拉框的預設值。
這裡的網絡請求使用了
Utils
類中
HttpClient
的單例模式,以便在整個項目中共享一個
HttpClient
執行個體。
HttpClient
執行個體是設計為可以被多個請求重用的,這有助于減少資源消耗和提高應用程式的性能。
4.2. Dropdown 元件的模型切換
在擷取到模型配置資訊後,我們需要為下拉框的選項添加點選事件,以便切換模型。在 Gradio.NET 中,可以通過
Dropdown
的
Change
事件來實作。
model?.Change(change_models, inputs: [input, model], outputs: [model]);
在點選下拉框選項後,會調用
change_models
方法,該方法會向 LLamaWorker 服務發送請求,切換模型。
static async Task<Output> change_models(Input input) { string server = Textbox.Payload(input.Data[0]); string model = Dropdown.Payload(input.Data[1]).Single(); var models = Utils.config?.Models?.Select(x => x.Name).ToList(); // 未使用服務端模型配置,允許自定義模型 if (models == ) { return gr.Output(gr.Dropdown(choices: [model], value: model, interactive: true, allowCustomValue: true)); } if (server == "") { throw new Exception("Server URL cannot be empty."); } // 取得模型是第幾個 var index = models.IndexOf(model); if (index == -1) { throw new Exception("Model not found in the list of available models."); } if (Utils.config.Current == index) { // 沒有切換模型 return gr.Output(gr.Dropdown(choices: models, value: model, interactive: true)); } var res = await Utils.client.PutAsync($"{server}/models/{index}/switch", ); // 請求失敗 if (!res.IsSuccessStatusCode) { // 錯誤資訊未傳回 gr.Warning("Failed to switch model."); await Task.Delay(2000); return gr.Output(gr.Dropdown(choices: models, value: models[Utils.config.Current], interactive: true)); } Utils.config.Current = index; return gr.Output(gr.Dropdown(choices: models, value: model, interactive: true)); }
在
change_models
方法中,我們首先擷取模型配置資訊,然後擷取輸入的服務 URL 和模型名稱,向服務發送請求切換模型。如果請求成功,我們将會更新下拉框的選項。同時在不存在服務端模型配置的情況下,我們允許使用者自定義模型。
這裡需要注意的是,在切換失敗的情況下,我們會展示一個警告資訊,并在2秒後恢複下拉框的選項。但是,恢複下拉框的選項會重複調用
Change
事件,這樣會造成
Warning
提示框不顯示,是以需要在
Warning
提示框顯示後延遲2秒再恢複下拉框的選項,重複調用倒是不算大問題。
4.3. 模型互動
在設定好服務和模型切換後,我們添加一個Tab元件,用于展示模型的不同能力對話和文本生成。
using (gr.Tab("Chat")) { // Chat 互動界面元件 } using (gr.Tab("Completion")) { // Completion 互動界面元件 }
在 Chat 互動界面中,我們可以直接使用
Chatbot
元件,用于展示對話消息清單,并添加一個輸入框用于輸入文本,同時提供三個按鈕用于發送文本、重新生成和清空對話。
Chatbot chatBot = gr.Chatbot(label: "LLamaWorker Chat", showCopyButton: true, placeholder: "Chat history",height:520); Textbox userInput = gr.Textbox(label: "Input", placeholder: "Type a message..."); Button sendButton, resetButton, regenerateButton; using (gr.Row()) { sendButton = gr.Button("✉️ Send", variant: ButtonVariant.Primary); regenerateButton = gr.Button("🔃 Retry", variant: ButtonVariant.Secondary); resetButton = gr.Button("🗑️ Clear", variant: ButtonVariant.Stop); }
接下來我們添加三個按鈕的點選事件,以便發送文本、重新生成和清空對話。
sendButton?.Click(streamingFn: i => { string server = Textbox.Payload(i.Data[0]); string token = Textbox.Payload(i.Data[3]); string model = Dropdown.Payload(i.Data[4]).Single(); IList<ChatbotMessagePair> chatHistory = Chatbot.Payload(i.Data[1]); string userInput = Textbox.Payload(i.Data[2]); return ProcessChatMessages(server, token, model, chatHistory, userInput); }, inputs: [input, chatBot, userInput, token, model], outputs: [userInput, chatBot]); regenerateButton?.Click(streamingFn: i => { string server = Textbox.Payload(i.Data[0]); string token = Textbox.Payload(i.Data[2]); string model = Dropdown.Payload(i.Data[3]).Single(); IList<ChatbotMessagePair> chatHistory = Chatbot.Payload(i.Data[1]); if (chatHistory.Count == 0) { throw new Exception("No chat history available for regeneration."); } string userInput = chatHistory[^1].HumanMessage.TextMessage; chatHistory.RemoveAt(chatHistory.Count - 1); return ProcessChatMessages(server, token, model, chatHistory, userInput); }, inputs: [input, chatBot, token, model], outputs: [userInput, chatBot]); resetButton?.Click(i => Task.FromResult(gr.Output(Array.Empty<ChatbotMessagePair>(), "")), outputs: [chatBot, userInput]);
在點選按鈕後,會調用
ProcessChatMessages
方法,該方法會向 LLamaWorker 服務發送請求,擷取模型的回複,并更新對話消息清單。
static async IAsyncEnumerable<Output> ProcessChatMessages(string server, string token, string model, IList<ChatbotMessagePair> chatHistory, string message) { if (message == "") { yield return gr.Output("", chatHistory); yield break; } // 添加使用者輸入到曆史記錄 chatHistory.Add(new ChatbotMessagePair(message, "")); // sse 請求 var request = new HttpRequestMessage(HttpMethod.Post, $"{server}/v1/chat/completions"); request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream")); if (!string.IsOrWhiteSpace(token)) { Utils.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } var messages =new List<ChatCompletionMessage>(); foreach (var item in chatHistory) { messages.Add(new ChatCompletionMessage { role = "user", content = item.HumanMessage.TextMessage }); messages.Add(new ChatCompletionMessage { role = "assistant", content = item.AiMessage.TextMessage }); } messages.Add(new ChatCompletionMessage { role = "user", content = message }); request.Content = new StringContent(JsonSerializer.Serialize(new ChatCompletionRequest { stream = true, messages = messages.ToArray(), model = model, max_tokens = 1024, temperature = 0.9f, top_p = 0.9f, }), Encoding.UTF8, "application/json"); using var response = await Utils.client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) using (var reader = new System.IO.StreamReader(stream)) { while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); if (line.StartsWith("data:")) { var data = line.Substring(5).Trim(); // 結束 if(data == "[DONE]") { yield break; } // 解析傳回的資料 var completionResponse = JsonSerializer.Deserialize<ChatCompletionChunkResponse>(data); var text = completionResponse?.choices[0]?.delta?.content; if (string.IsOrEmpty(text)) { continue; } chatHistory[^1].AiMessage.TextMessage += text; yield return gr.Output("", chatHistory); } } } }
在
ProcessChatMessages
方法中,我們首先擷取輸入的服務 URL、API Key、對話消息清單和文本,然後向服務發送請求擷取模型的回複。在這個過程中,我們使用了 SSE 請求,以便實作流式響應。在擷取到模型的回複後,我們将會更新對話消息清單。
對于文本生成界面,我們可以直接使用
Textbox
元件,用于輸入文本,同時添加一個按鈕用于生成文本。其相關的事件處理和流程與 Chat 互動界面類似,這裡不再贅述。完整的代碼可以在 LLamaWorker[2] 項目的 ChatUI 中檢視。
5. 效果
在運作 LLamaWorker 服務後,我們可以在 ChatUI 項目中輸入服務 URL 和 API Key(若有配置),然後點選“Get Models”按鈕,即可擷取模型配置資訊。接着,我們可以選擇模型,然後在 Chat 互動界面中輸入文本,點選“Send”按鈕,即可擷取模型的回複。
當然你也可以選擇其他服務,比如阿裡靈積的大模型服務,隻需要修改服務 URL:
https://dashscope.aliyuncs.com/compatible-mode
和 API Key,通過手動輸入你要體驗的模型,如 “qwen-long” 即可體驗阿裡靈積的大模型服務。
qwen-long
6. 總結
在本篇文章中,我們闡述了如何使用 Gradio.NET 為 LLamaWorker 快捷地建立一個大型模型示範界面。通過 Gradio.NET,我們可以快速搭建一個具備互動性的界面,幫助開發者更快地了解和體驗模型的效果。同時,我們還展示了如何使用 Gradio.NET 的多個元件和相關功能,以及如何處理網絡請求和流式響應。希望這個實際的使用案例能夠幫助大家更好地學習和使用 Gradio.NET。
References
[1]
LLamaWorker: https://github.com/sangyuxiaowu/LLamaWorker?wt.mc_id=DT-MVP-5005195
[2]
Gradio.NET: https://github.com/feiyun0112/Gradio.Net?wt.mc_id=DT-MVP-5005195