当前位置: 欣欣网 > 码农

在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技术相关的精彩内容。

相关文章: