在今天的技術分享中,我們將深入探討如何在 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將格式統一歸整一下。
//全部處理完後再處理一次Markdown
info!.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技術相關的精彩內容。
相關文章: