昨年、SemanticKernelに関する数篇の記事を書いたが、正式リリースバージョンは以前のバージョンと大きく異なるため、東京で開催された「生成型AIアプリケーション開発」イベントでのデモを一つずつ共有し、SemanticKernelシステムを再開することにした。
以下はChatの例で、ユーザーが質問をすると、ローカルに対応する固定データがあれば、直接返答し、なければ時間帯を問い合わせてモックデータを返す。一部はローカルデータをベクトル化して保存し、ユーザーが質問したときにベクトル化された結果を検索し、最も高いスコアの記録を返す。二部はGPTのAPIを呼び出し、質問から時間帯を見つけ出して、モックデータを返す。
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));
// ローカルデータをベクトル化して保存
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();
// ユーザー質問をベクトル化ストレージから検索し、関連するコンテンツを見つける
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でモック
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();
// モックデータを組み立てる
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("思考中……");
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に渡して、より友好的に加工してから返すことだ。自分のアプリケーションとの組み合わせについては、後の記事で紹介する。
(Translated by GPT)