在今天的技术分享中,我们将深入探讨如何在 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技术相关的精彩内容。
相关文章: