當前位置: 妍妍網 > 碼農

在Blazor中套用Semantic Kernel打造流暢的流式輸出與多輪對話

2024-02-17碼農

在今天的技術分享中,我們將深入探討如何在 Blazor 套用中運用 Semantic Kernel 來實作流式輸出和多輪對話的高效方案,為使用者帶來更快捷的互動體驗。這一技術的套用開啟了一個全新的篇章,在我們的 AntSK 計畫中就得到了生動的實踐和積極的反饋。現在,我們就來詳細解讀實作這一功能的步驟和技巧。

流式輸出:快速響應的秘訣

首先,我們需要了解什麽是流式輸出。在傳統的請求-響應模式中,使用者發送請求後往往需要等待整個響應內容返回後才能看到結果,這在多數據或復雜查詢場景下會造成顯著的等待時間。流式輸出正是為解決這一問題而生,它能夠實作逐步返回數據,讓使用者能夠更早地看到部份結果,提高整體的響應效能。

Blazor 中實作流式輸出,我們需要將標準的 _kernel.InvokeAsync 方法替換為 _kernel.InvokeStreamingAsync 。這一替換意味著我們開始逐步、連續地返回內容,而非一次性返回全部數據。

AntSK 計畫為例,以下是實作流式輸出的關鍵程式碼:

var func = _kernel.CreateFunctionFromPrompt(app.Prompt, new OpenAIPromptExecutionSettings());var chatResult = _kernel.InvokeStreamingAsync<StreamingChatMessageContent>(function: func, arguments: newKernelArguments() { ["input"] = msg });MessageInfo info = null;var markdown = new Markdown();await foreach (var content in chatResult){if (info == null) { info = new MessageInfo { ID = Guid.NewGuid().ToString(), Questions = questions, Answers = content.Content!, HtmlAnswers = content.Content!, CreateTime = DateTime.Now }; MessageList.Add(info); }else { info.HtmlAnswers += content.Content; }await InvokeAsync(StateHasChanged);}

在上述程式碼中,我們遍歷 chatResult ,這是一個我們從 Semantic Kernel 流式呼叫返回的異步列舉元。我們需要透過 await InvokeAsync(StateHasChanged) 來確保每次接收到新內容時UI都能及時更新,保持與使用者的互動流暢。

讓我們一起來看看效果吧!

在這裏,我們看到雖然實作了流式輸出,但很明顯,它並不流暢。是一次出來了一批字,然後頓幾秒後又出來一批。於是我便研究了一下 到底是Blazor元件渲染還是流式返回的問題。

我透過打印foreach中的日誌發現。SK的流式返回雖然在迴圈裏是一次返回一個字,但是他是在同一時間內返回了10多個字,然後會停頓幾秒 然後接著返回十幾個字。我們為了讓效果更佳流暢,便在迴圈中適當加入了一點點延遲。

awaitforeach (var content in chatResult) {if (info == null) { info = new MessageInfo(); info.ID = Guid.NewGuid().ToString(); info.Questions = questions; info.Answers = content.Content!; info.HtmlAnswers = content.Content!; info.CreateTime = DateTime.Now; MessageList.Add(info); }else { info.HtmlAnswers += content.Content;await Task.Delay(50); Console.WriteLine(DateTime.Now + " " + content.Content); }await InvokeAsync(StateHasChanged); }

修改後的程式碼如上,每個字加入了50毫秒的延遲。然後我們在來看一看執行效果

在這個視訊中,我們可以看到回復的結果很明顯的流暢了許多。基本是比較平順的,到此我們的流式輸出就完成了。然後我們在全部輸出完以後,想用markdown將格式統一歸整一下。

//全部處理完後再處理一次Markdowninfo!.HtmlAnswers = markdown.Transform(info.HtmlAnswers);await InvokeAsync(StateHasChanged);

這樣效果就非常不錯了。

多輪對話:智慧互動的核心

有了流式輸出之後,下一步我們要實作的就是多輪對話功能。多輪對話意味著系統不僅能處理一個簡單的查詢,還能記住之前的互動,形成對話上下文,從而提供更為連貫和精準的回答。

為此,我們設計了 HistorySummarize 方法,它的作用是對之前的對話歷史進行匯總,確保語意上的連續性,同時減少對諸如API令牌這類資源的消耗。

在Blazor中,實作這一功能需要借助 Semantic Kernel 提供的 ConversationSummaryPlugin 。下面是 HistorySummarize 的實作:

privateasync Task<string> HistorySummarize(string questions, string msg){ StringBuilder history = new StringBuilder();foreach (var item in MessageList) { history.Append($"user:{item.Questions}{Environment.NewLine}"); history.Append($"assistant:{item.Answers}{Environment.NewLine}"); } KernelFunction sunFun = _kernel.Plugins.GetFunction("ConversationSummaryPlugin", "SummarizeConversation");var summary = await _kernel.InvokeAsync(sunFun, new() { ["input"] = $"內容是:{history.ToString()}{Environment.NewLine}請註意用中文總結" });string his = summary.GetValue<string>(); msg = $"歷史對話:{his}{Environment.NewLine}{questions}";return msg;}

在上面的程式碼中,我們首先拼接歷史訊息,然後利用 SummarizeConversation 功能進行匯總,並且確保輸出內容為中文,這對於中文使用者來說是非常重要的細節。

為了使匯總更加適合中文環境,我們透過更改提示中的內容為中文( 如:「請註意用中文總結」 ),以引導生成的總結也是中文形式。

在整合好流式輸出和會話總結之後, SendAsync 方法負責處理使用者輸入和確保多輪對話的順暢進行。這個方法根據套用的不同型別(如普通會話或知識庫問答),執行相應的處理邏輯:

protectedasync Task<bool> SendAsync(string questions){string msg = questions;// 處理多輪會話if (MessageList.Count > 0) { msg = await HistorySummarize(questions, msg); }// 根據套用型別處理不同邏輯 Apps app = _apps_Repositories.GetFirst(p => p.Id == AppId);switch (app.Type) {case"chat":// 普通會話await SendChat(questions, msg, app);break;case"kms":// 知識庫問答await SendKms(questions, msg, app);break; }returnawait Task.FromResult(true);}

透過以上深入的探討和程式碼範例,我們了解了如何在 Blazor 套用中透過 Semantic Kernel 實作流式輸出和多輪對話,為使用者提供更加流暢的互動體驗。無論是及時的響應還是上下文的連續性,在即時互動的套用場景中都是至關重要的。而在 AntSK 計畫中,這些技術的套用讓我們的產品更加智慧、高效,得到了使用者的一致好評。

現在,我們希望這篇文章能給你帶來啟發,讓你的Blazor套用也能夠透過流式輸出和多輪對話技術,提升使用者體驗,驅動業務的發展。如果你對這些技術有任何疑問或想要更深一步的討論,歡迎在評論區展開交流,我們將樂於解答你的問題。另外,請記得關註我們的公眾號,獲取更多.Net技術相關的精彩內容。

相關文章: