SemanticKernel之Chat
去年写过几过几篇关于SemanticKernel的文章,由于正式发布的版本与之前的版本变化较大,加上前的东京《生成式AI应用开发》活动,想把演示的Demo逐一分享出来,的以再次开启SemanticKernel系统。
下面是一个Chat的例子,用户提问,如果本地有固定数据能对应,直接返回,如果没有,就进行查询时间段,返回mock数据。第一部分是把本地数据向量化后存储,然后在用户提问时,查询向量化的结果,找出评分最高的一条记录返回。第二部分调用GPT的API,从提问中寻找时间段,然后返回mock的数据。
引入Nuget包
<ItemGroup> <PackageReference Include="Bogus" Version="35.5.0" /> <PackageReference Include="Microsoft.SemanticKernel" Version="1.6.3" /> <PackageReference Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.6.3-alpha" /> </ItemGroup>
后端CSharp代码如下:
using Bogus;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Plugins.Memory;
using System.Text.Json;
using System.Text.Unicode;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles();
const string MemoryCollectionName = "works";
var key = File.ReadAllText(@"C:\GPT\key.txt");
var chatModelId = "gpt-4-0125-preview";
#pragma warning disable SKEXP0001
#pragma warning disable SKEXP0050
#pragma warning disable SKEXP0010
//为向量化作准备
var kernel = Kernel.CreateBuilder()
.AddOpenAITextEmbeddingGeneration("text-embedding-ada-002", key)
.Build();
var embeddingGenerator = new OpenAITextEmbeddingGenerationService("text-embedding-ada-002", key);
SemanticTextMemory memory = new(new VolatileMemoryStore(), embeddingGenerator);
var memoryPlugin = kernel.ImportPluginFromObject(new TextMemoryPlugin(memory));
//利用Embedding,把本地数据向量化存储
await StoreData();
app.MapGet("/query", async (string question) =>
{
app.Logger.LogInformation("question:{0}", question);
var (message, relevance) = await SearchMemoryAsync(memory, question);
if (relevance < 0.83)
{
var result = await QueryAsync(key, question);
result.Message += "【热度:" + relevance + "】";
return result;
}
else
{
return new BackData { Message = message + "【热度:" + relevance + "】" };
}
});
app.Run();
//从向量化存储中查询用户提问,找出相关的内容,注意参数limit,ninRelevanceScore等参数
async Task<(string, double)> SearchMemoryAsync(ISemanticTextMemory memory, string query)
{
await foreach (var answer in memory.SearchAsync(
collection: MemoryCollectionName,
query: "问题:" + query,
limit: 1,
minRelevanceScore: 0.8,
withEmbeddings: true))
{
return (answer.Metadata.Text, answer.Relevance);
};
return ("非常抱歉,我没有找到你要的问题!", 0);
}
//向量化本地数据后存储
async Task StoreData()
{
var dic = new Dictionary<string, string>
{
["info1-1"] = "我叫Bot",
["info1-2"] = "我的名字叫Bot",
["info2-1"] = "我是属于ABCDE公司的",
["info2-2"] = "我来自于ABCDE公司的",
["info3-1"] = "我的功能:交易查询,交易统计,结算查询等",
["info3-2"] = "我的主要作是:交易查询,交易统计,结算查询等",
};
foreach (var (key, value) in dic)
{
await memory.SaveInformationAsync(MemoryCollectionName, id: key, text: value);
}
}
//利用用户的提示信息,找出时间段的内容,然后按时间段查询,数据是用Bogus来mock的
async Task<BackData> QueryAsync(string key, string data)
{
var arr = await GetDatesAsync(key, data);
if (arr.Length == 1)
{
arr = arr[0].Split("至");
}
//判断有没有时间段
if (arr.Length != 2)
{
Console.WriteLine("非常抱歉,我没有找到你要的问题!");
return new BackData { Message = "非常抱歉,我没有找到你要的问题!" };
}
else
{
Console.WriteLine($"正在为你查询{string.Join("到", arr)}的数据,请稍候……");
var rand = new Random();
var options = new JsonSerializerOptions();
//组装mock的数据
options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(UnicodeRanges.All);
var orders = new List<Order>();
for (var i = 0; i < rand.Next(5, 10); i++)
{
var orderFaker = new Faker<Order>("zh_CN")
.RuleFor(x => x.ID, x => x.Random.Int(1, 1000))
.RuleFor(x => x.OrderNumber, x => x.Random.Guid().ToString("N").ToUpper().Substring(0, 8))
.RuleFor(x => x.TotalAmount, x => x.Random.Int(100, 900))
.RuleFor(x => x.OrderDate, x => x.Date.Past(1))
.RuleFor(x => x.Status, x => x.PickRandom("已下单", "已发货", "已完成", "已取消"));
orders.Add(orderFaker.Generate());
}
return new BackData { Message = string.Join("到", arr), Data = orders };
}
}
//从提示中获取固定日期
async Task<string[]> GetDatesAsync(string key, string data)
{
var chatGPT = new OpenAIChatCompletionService(chatModelId, key);
var chatHistory = new ChatHistory($"今天是{DateTime.Now}。");
chatHistory.AddUserMessage($"请注意上面给出的当前时间,然后分多行给出下面句子中的日期或时间。如果遇到今年,去年,本月等信息,转换成一个开始日期和结束日期,并且分行显示,");
chatHistory.AddUserMessage(data);
var reply = await chatGPT.GetChatMessageContentAsync(chatHistory);
Console.WriteLine(reply.Content);
return reply.Content.Split('\r', '\n');
}
public class Order
{
public int ID { get; set; }
public string OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public int TotalAmount { get; set; }
public string Status { get; set; }
}
public class BackData
{
public string Message { get; set; }
public List<Order> Data { get; set; }
}
前端代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Bot</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<div class="row" style="margin:5px">
<div class="col">
<textarea rows="5" class="form-control" id="question"></textarea>
</div>
</div>
<div class="row" style="margin:5px">
<div class="col-md-8">
</div>
<div class="col">
</div>
<div class="col">
</div>
<div class="col" style="text-align:right">
<button class="btn btn-info" id="submit" onclick="answer()">发送</button>
</div>
</div>
<div class="row" style="font-weight:900">
<span id="result"></span>
</div>
<div class="row" id="tablediv">
</div>
</div>
<script>
function answer() {
$('#result').css('color', 'black');
$("#result").html("思考中……");
//$("#tablediv").html("")
var type = ''
$.ajax({
url: '/query?question=' + $('#question').val(),
type: 'GET',
success: function (data) {
$("#result").html(data.message);
if (data.data.length == 0) {
return
}
$('#question').val("")
var table = '<table class="table" id="orderTable" border="1">'
+ '<thead>'
+ ' <tr>'
+ ' <th>ID</th>'
+ ' <th>Order Number</th>'
+ ' <th>Order Date</th>'
+ ' <th>Total Amount</th>'
+ ' <th>Status</th>'
+ ' </tr>'
+ ' </thead>'
+ ' <tbody id="tbody">'
$.each(data.data, function (index, item) {
var row = '<tr>' +
'<td>' + item.id + '</td>' +
'<td>' + item.orderNumber + '</td>' +
'<td>' + item.orderDate + '</td>' +
'<td>' + item.totalAmount + '</td>' +
'<td>' + item.status + '</td>' +
'</tr>';
table += row
});
table += '</tbody></table>'
$("#tablediv").html(table)
},
error: function (xhr, status, error) {
alter(error)
}
});
}
</script>
</body>
</html>
本例只是简单的功能演示,作为SK的开胃菜,不具体商业务使用价值。更通用的做法是:从向量库里找出结果后,再传给GPT的ChatCompletion,进行更友好的加工后返回。关于自己和自己的应用结合,后面的文章会作介绍。
文章来源微信公众号
想要更快更方便的了解相关知识,可以关注微信公众号
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2019-02-25 第一课了解SQL