SemanticKernelのChatについて


昨年、SemanticKernelに関する数篇の記事を書いたが、正式リリースバージョンは以前のバージョンと大きく異なるため、東京で開催された「生成型AIアプリケーション開発」イベントでのデモを一つずつ共有し、SemanticKernelシステムを再開することにした。

以下はChatの例で、ユーザーが質問をすると、ローカルに対応する固定データがあれば、直接返答し、なければ時間帯を問い合わせてモックデータを返す。一部はローカルデータをベクトル化して保存し、ユーザーが質問したときにベクトル化された結果を検索し、最も高いスコアの記録を返す。二部はGPTのAPIを呼び出し、質問から時間帯を見つけ出して、モックデータを返す。

##### Nugetパッケージの導入

```xml
<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コード

```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; }
}
```

##### フロントエンドコード

```html
<!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)

元のリンク：https://mp.weixin.qq.com/s?__biz=MzA3NDM1MzIyMQ==&mid=2247487943&idx=1&sn=23f893d856fab6380ddd7d6f4c8b7bf7&chksm=9f004eeda877c7fbce952f9e8b11fc5bce26a91dc31db0a51d4f31158224fe260ad43c574f75&token=1672532928&lang=zh_CN#rd