随着对话系统和聊天机器人技术的不断演进,我们如何使得对话流程更加灵活、可重用成为了开发者们研究的热点。在上一篇文章中,介绍了如何创建一个固定回答选项的聊天提示(prompt)。然而,这种方法的可重用性并不高,因为回答选项是硬编码的。可以通过动态地创建提示字符串来解决这个问题,但有一个更好的方法:prompt模板功能。接下来我们将采用Semantic Kernel来实现这一技术。
Semantic Kernel提供了一种模板语言,允许我们添加变量,这些变量会在输入参数时自动替换。首先,我们构建一个简单的提示,使用Semantic Kernel模板语法语言包含足够的信息以便agent能够回复用户。
// 使用Semantic Kernel模板创建聊天提示
var chat = kernel.CreateFunctionFromPrompt(
@"{{$history}}
User: {{$request}}
Assistant: "
);
新的提示使用了request和history变量,这样我们可以在运行提示时包括这些值。为了测试我们的提示,我们可以创建一个新的kernel并开始一个聊天循环,以便我们可以与我们的agent来回交谈。当我们调用提示时,我们可以传递request和history变量作为参数。
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
// 创建kernel
var builder = Kernel.CreateBuilder();
// 添加文本或聊天完成服务,使用:
// builder.Services.AddAzureOpenAIChatCompletion()
// builder.Services.AddAzureOpenAITextGeneration()
// builder.Services.AddOpenAIChatCompletion()
// builder.Services.AddOpenAITextGeneration()
var kernel = builder.Build();
// 创建聊天历史
ChatHistory history = new List<ChatMessageContent>();
// 开始聊天循环
while (true)
{
// 获取用户输入
Console.Write("User > ");
var request = Console.ReadLine();
// 获取聊天响应
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
chat,
new() {
{ "request", request },
{ "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
}
);
//输出响应流
string message = "";
await foreach (var chunk in chatResult)
{
if (chunk.Role.HasValue) Console.Write(chunk.Role + " > ");
message += chunk.Content;
Console.Write(chunk.Content);
}
Console.WriteLine();
// 添加到历史记录
history.Add(new ChatMessageContent(AuthorRole.User, request));
history.Add(new ChatMessageContent(AuthorRole.Assistant, message));
}
此外,Semantic Kernel还支持C# SDK中的Handlebars模板语言。要使用Handlebars,首先要在项目中添加Handlebars包。
dotnet add package Microsoft.SemanticKernel.PromptTemplate.Handlebars --prerelease
然后导入Handlebars模板引擎包。
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
之后,您可以使用HandlebarsPromptTemplateFactory创建新的提示。由于Handlebars支持循环,我们可以使用它来循环处理例如示例和聊天历史记录等元素。这使得它非常适合我们在前一篇文章中创建的getIntent提示。
// 创建Handlebars模板以获取意图
var getIntent = kernel.CreateFunctionFromPrompt(
new()
{
Template = @"
<message role=""system"">说明:这个请求的意图是什么?
不要解释原因,只需回复意图。如果不确定,则回复{{choices[0]}}。
选项:{{choices}}。</message>
{{#each fewShotExamples}}
{{#each this}}
<message role=""{{role}}"">{{content}}</message>
{{/each}}
{{/each}}
{{#each useHistory}}
<message role=""{{role}}"">{{content}}</message>
{{/each}}
<message role=""user"">{{request}}</message>
<message role=""system"">意图:</message>",
TemplateFormat = "handlebars"
},
new HandlebarsPromptTemplateFactory()
);
然后我们可以创建将被模板使用的choice和example对象。在这个例子中,我们可以用我们的提示来结束对话。为此,我们将提供两个有效的意图选项:ContinueConversation和EndConversation。
// 创建选择列表
List<string> choices = new List<string> { "ContinueConversation", "EndConversation" };
// 创建少数示例
List<ChatHistory> fewShotExamples = new List<ChatHistory> {
new ChatHistory {
new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"),
new ChatMessageContent(AuthorRole.System, "Intent:"),
new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation")
},
new ChatHistory {
new ChatMessageContent(AuthorRole.User, "Thanks, I'm done for now"),
new ChatMessageContent(AuthorRole.System, "Intent:"),
new ChatMessageContent(AuthorRole.Assistant, "EndConversation")
}
};
最后,您可以使用kernel运行提示。在主聊天循环中添加以下代码,以便一旦意图是EndConversation就终止循环。
// 调用提示
var intentResult = await kernel.InvokeAsync(
getIntent,
new() {
{ "request", request },
{ "choices", choices },
{ "chatHistory", history },
{ "fewShotExamples", fewShotExamples }
}
);
// 如果意图是"EndConversation"则结束聊天
if (intentResult.ToString() == "EndConversation")
{
break;
}
但是使用过多的会话历史可能导致令牌数过多,影响系统效率。为了避免这种情况,我们可以在询问意图之前,使用ConversationSummaryPlugin插件来总结会话历史。
var chat = kernel.CreateFunctionFromPrompt(
@"{{ConversationSummaryPlugin.SummarizeConversation $history}}
User: {{$request}}
Assistant: "
);
在上面的示例中,通过嵌套调用SummarizeConversation
函数,能够在请求用户意图之前,对之前的对话历史进行摘要处理。这不仅可以提升处理效率,还能避免因历史信息过长而影响模型的响应。
接着,我们需要确保插件被载入到Semantic Kernel中。可以通过以下代码来添加必要的服务和插件:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Plugins.Core;
// 创建Kernel实例
var builder = Kernel.CreateBuilder();
// 添加文本或者聊天完成功能服务
// builder.Services.AddAzureOpenAIChatCompletion()
// builder.Services.AddAzureOpenAITextGeneration()
// builder.Services.AddOpenAIChatCompletion()
// builder.Services.AddOpenAITextGeneration()
// 注入ConversationSummaryPlugin插件
builder.Plugins.AddFromType<ConversationSummaryPlugin>();
var kernel = builder.Build();
一旦插件和函数就绪,我们就可以开始测试更新后的prompt了。通过创建一个聊天循环,使得每次循环都会逐渐增加历史信息的长度,如下所示:
// 创建聊天历史对象
ChatHistory history = [];
// 开始聊天循环
while (true)
{
// 获取用户输入
Console.Write("User > ");
var request = Console.ReadLine();
// 调用kernel以获取聊天响应
var chatResult = kernel.InvokeStreamingAsync<StreamingChatMessageContent>(
chat,
new() {
{ "request", request },
{ "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) }
}
);
// 迭代输出响应内容
string message = "";
await foreach (var chunk in chatResult)
{
if (chunk.Role.HasValue) Console.Write(chunk.Role + " > ");
message += chunk;
Console.Write(chunk);
}
Console.WriteLine();
// 将聊天记录添加到历史对象中
history.AddUserMessage(request!);
history.AddAssistantMessage(message);
}
通过以上示例代码,我们可以创建出功能丰富、可根据实际情况动态应变的聊天agent。这种方式不仅提升了聊天的自然性和连贯性,还提高了代码的复用性和扩展性。在构建自己的智能聊天机器人时,不妨考虑Semantic Kernel的模板技术,让聊天交互更加智能化。