文末附全部源代码
使用 .NET 开发 AI 应用(2):引入 Semantic Kernel 框架
使用 .NET 开发 AI 应用(3): 创建第一个 AI 应用
使用 .NET 开发 AI 应用(5): 创建第一个 AI 聊天机器人应用
在上一章中,我们创建了一个 AI 聊天机器人应用。但是在使用过程中,你会发现 AI 模型是没有“记忆”的。
举个例子,我们要AI记住我的名字,AI 也回复记住了,但当我们再次询问时,它却不记得了:
这是因为 AI 模型是无状态的。 AI 模型旨在同时服务于成千上万的用户,而为每个用户维护一个持久的状态将是一个资源密集型的任务。
无状态的设计允许模型轻松地扩展到多个实例,而不需要同步用户状态,从而提高了效率和可靠性。
这种设计使得模型能够高效地服务于大量用户,但也限制了对话的连贯性。
然而,通过提示工程,我们可以巧妙地为 AI 添加“记忆”。
提示工程
提示工程是一种技术,它通过精心设计的提示词,引导模型提供更准确、更相关的信息,使其更加符合特定的应用场景和研究领域。
提示词不仅包含指令或问题,还可以包含上下文、输入或示例等详细信息。这些元素共同作用,帮助模型更好地理解请求的意图,从而生成更加满意的答案。
例如,通过将对话历史转化为提示词,我们可以使模型在生成响应时考虑之前的交互。
这就像是给模型一个记忆的窗口,让它能够在一定程度上“记住”与用户的对话。
更多提示工程的相关知识,大家可以通过《面向开发者的 ChatGPT 提示词工程》这门免费课程(https://github.com/GitHubDaily/ChatGPT-Prompt-Engineering-for-Developers-in-Chinese)进行学习。
代码实现
我们使用的Gradio.Net已经记录下了对话历史,我们只需将其作为上下文传递给模型。这样,每次用户与AI交互时,模型都会接收到之前的对话,从而生成更加连贯和个性化的回答。
修改后的GenerateResponse
方法使用提示模板传入之前的对话(和当前对话userPrompt):
static async IAsyncEnumerable<Output> GenerateResponse(Input input, Kernel kernel)
{
var userPrompt = Textbox.Payload(input.Data[0]);
var chatHistory = Chatbot.Payload(input.Data[1]);
var chatMessages = ChatHistoryToPrompt(chatHistory);
var promptTemplate = @"{{ $chatMessages }}
user:{{ $userPrompt }}
assistant:
";
chatHistory.Add(new ChatbotMessagePair(new ChatMessage { TextMessage = userPrompt }, new ChatMessage { TextMessage = "" }));
await foreach (var responseMessage in kernel.InvokePromptStreamingAsync<string>(promptTemplate, new() { { "chatMessages", chatMessages }, { "userPrompt", userPrompt } }))
{
if (!string.IsNullOrEmpty(responseMessage))
{
chatHistory.Last().AiMessage.TextMessage += responseMessage;
yield return gr.Output("", chatHistory);
}
await Task.Delay(50);
}
}
现在,我们再运行看下效果:
结论
虽然LLM本身可能不具备记忆功能,但通过提示工程和适当的工具,我们可以克服LLM的无状态限制,为用户提供更加丰富和深入的对话体验。
Gradio.NET(https://github.com/feiyun0112/Gradio.Net/)的目标是成为用于开发 Web 应用的 .NET 开发者的首选框架。它的设计理念是让开发变得更加简单,让每个人都能够参与到Web应用的创造中来。
添加微信GradioDotNet,通过加入技术讨论群,开发者们可以分享经验,解决问题,并共同推动.NET的发展。
完整源代码:
using Gradio.Net;
using Microsoft.SemanticKernel;
using System.Text;
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_GPT_NAME");
string apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
apiKey: apiKey)
.Build();
App.Launch(await CreateBlocks(kernel));
static async Task<Blocks> CreateBlocks(Kernel kernel)
{
using (var blocks = gr.Blocks())
{
var chatbot = gr.Chatbot();
var txt = gr.Textbox(showLabel: false,
placeholder: "输入文本并按回车键"
);
txt.Submit(streamingFn: (input) => GenerateResponse(input, kernel), inputs: new Gradio.Net.Component[] { txt, chatbot }, outputs: new Gradio.Net.Component[] { txt, chatbot });
return blocks;
}
}
static async IAsyncEnumerable<Output> GenerateResponse(Input input, Kernel kernel)
{
var userPrompt = Textbox.Payload(input.Data[0]);
var chatHistory = Chatbot.Payload(input.Data[1]);
var chatMessages = ChatHistoryToPrompt(chatHistory);
var promptTemplate = @"{{ $chatMessages }}
user:{{ $userPrompt }}
assistant:
";
chatHistory.Add(new ChatbotMessagePair(new ChatMessage { TextMessage = userPrompt }, new ChatMessage { TextMessage = "" }));
await foreach (var responseMessage in kernel.InvokePromptStreamingAsync<string>(promptTemplate, new() { { "chatMessages", chatMessages }, { "userPrompt", userPrompt } }))
{
if (!string.IsNullOrEmpty(responseMessage))
{
chatHistory.Last().AiMessage.TextMessage += responseMessage;
yield return gr.Output("", chatHistory);
}
await Task.Delay(50);
}
}
static string ChatHistoryToPrompt(IList<ChatbotMessagePair> chatHistory)
{
var sb = new StringBuilder();
foreach (var chatbotMessagePair in chatHistory)
{
sb.AppendLine($"user:{chatbotMessagePair.HumanMessage.TextMessage}");
sb.AppendLine($"assistant:{chatbotMessagePair.AiMessage.TextMessage}");
}
return sb.ToString();
}