🧙‍♂️

Semantic Kernelを使ってC#でAI (6)

に公開

ベクターストアのデータを使ってSemantic KernelでRAG

前回(5)でベクターストアを使って類似文章を検索させる方法を書きました。
今回はそれを使って Semantic Kernel でRAGをやってみます。

そして、前回と同じように RAGって何 とかいうのには触れずにやっていきます!

注意

この記事は Semantic Kernel が 1.52.0 の際に書いたものです。
Semantic Kernel は更新速度が非常に早く、記載の表記から変更されていることが非常によくあります。
実際にコーディングする際には確認の上、作成してください。

RAGのプログラム

サンプルは こちら

今回は要点に絞って解説します。

ベクターストアのデータ

WagashiItem.cs
public class WagashiItem
{
    public const string CollectionName = "wagashi_items";

    [Description("ID")]
    [VectorStoreKey]
    public string Id { get; set; } = string.Empty;

    [Description("商品名")]
    [VectorStoreData(IsFullTextIndexed = true, IsIndexed = true)]
    public string Name { get; set; } = string.Empty;

    [Description("商品の種類名")]
    [VectorStoreData(IsFullTextIndexed = true, IsIndexed = true)]
    public string ItemType { get; set; } = string.Empty;

    [Description("商品の詳細な説明")]
    [VectorStoreData]
    public string Description { get; set; } = string.Empty;

    [Description("税込み価格")]
    [VectorStoreData]
    public int Price { get; set; } = 600;

    [VectorStoreVector(Dimensions: 768, DistanceFunction = DistanceFunction.CosineDistance)]
    [JsonIgnore]
    public ReadOnlyMemory<float> DescriptionVector { get; set; }
}

こちらは前回説明しているので、VectorStore関連の設定は前回参照ということで。
データの保存・復帰にJSONのファイル書き出しで対応するので、JSON関連のものを追加。
そして、初期データ生成にAIを使うので、そのための DescriptionAttribute を付加してあります。

初期化部

Program.cs
new EnvLoader().Load();

var builder = Kernel.CreateBuilder();

builder.Services
    .AddLogging(lb => lb.AddDebug().SetMinimumLevel(LogLevel.Trace))
    .AddInMemoryVectorStore()
    .AddAzureOpenAIChatCompletion(
        Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? string.Empty,
        apiVersion: Environment.GetEnvironmentVariable("AZURE_OPENAI_API_VERSION"),
        serviceId: "gpt"
        )
    ;

#pragma warning disable SKEXP0010 // 種類は、評価の目的でのみ提供されています。将来の更新で変更または削除されることがあります。続行するには、この診断を非表示にします。
builder.Services
    .AddOpenAIEmbeddingGenerator(
        "cl-nagoya/ruri-v3-310m",
        "-",
        httpClient: new HttpClient()
        {
            BaseAddress = new Uri("http://localhost:7997")
        })
    ;
#pragma warning restore SKEXP0010 // 種類は、評価の目的でのみ提供されています。将来の更新で変更または削除されることがあります。続行するには、この診断を非表示にします。

builder.Plugins.AddFromType<ItemSearchPlugin>("ItemSearch");

var kernel = builder.Build();

var embedding = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();
var chatComp = kernel.GetRequiredService<IChatCompletionService>();
var vectorStore = kernel.GetRequiredService<InMemoryVectorStore>();
var collection = vectorStore.GetCollection<string, WagashiItem>(WagashiItem.CollectionName);
await collection.EnsureCollectionExistsAsync();

ここらへんは、大体いつもの通りの流れです。

  1. .envファイル読み込んで環境変数に設定
  2. Kernel にいろいろ設定し
  3. 設定したものから取得

今回、特筆するところと言えば、プラグインを追加している、

builder.Plugins.AddFromType<ItemSearchPlugin>("ItemSearch");

ここですね。
ItemSearchPlugin クラスを ItemSearch という名前で登録しています。

プラグイン

プラグインは KernelFunctionAttribute がついている関数を含んだクラスです。
このソース内では ItemSearchPlugin クラスにも以下の関数を含んでいます。

ItemSearchPlugin.cs
[KernelFunction("search")]
[Description("query文字列から商品をピックアップする")]
public async Task<ItemSearchResult[]> SearchAsync(string query)
{
    var collection = _vectorStore.GetCollection<string, WagashiItem>(WagashiItem.CollectionName);

    var searchVector = await _embedding.GenerateAsync($"検索クエリ: {query}");
    var searchResult = collection.SearchAsync(searchVector, top: 5);

    return searchResult
        .ToBlockingEnumerable()
        .Select(i => new ItemSearchResult
        {
            Id = i.Record.Id,
            Name = i.Record.Name,
            ItemType = i.Record.ItemType,
            Description = i.Record.Description,
            Price = i.Record.Price,
        })
        .ToArray();
}

KernelFunction の引数で指定されている文字列が呼ばれる関数名になります。
ここでは search になります。

プロンプトテンプレート

今回はプロンプトを作成するためにプラグインを使います。
サンプルでは、以下のテンプレートから

Program.cs
    var promptTemplate = @"query はお客様からの商品検索文です。
返答文には検索文に対する確認の文章を記載してください。
<query>
{{query}}
</query>

また、下記はお客様にお勧めする候補に挙がった商品です。
見やすい適切な一覧にしてお客様にお勧めする文章を作成してください。
以下の注意を守って文書を作成してください。
・情報として記載されている価格は、税込み金額であること
・税別金額も併記すること

<items>
{{#with (ItemSearch-search query)}}
  {{#each this}}
  Name: {{Name}}
  Description: {{Description}}
  Price: {{Price}}
  ------
  {{/each}}
{{/with}}
</items>
";

このテンプレートを、以下でプロンプト文字列に変換します。

Program.cs
var arguments = new KernelArguments() {
    { "query", query }
};
var promptFactory = new HandlebarsPromptTemplateFactory();
var prompt = await kernel.InvokePromptAsync(
    promptTemplate,
    arguments,
    HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat,
    promptFactory);

これで、 (ItemSearch-search query) から ItemSearch という名前のプラグインの search という関数を、引数 query で呼び出す、ということになります。
そして、 {{}} で囲まれているのがテンプレートフォーマットという整形式です。
今回は Handlebar式のフォーマット になっていて、上記 HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat で指定しています。

…まあ、だいたい見てるとわかりますよね。

まとめ

今回はプロンプトテンプレートからプラグインを呼び出すようにして、RAGを実現しました。
テンプレートフォーマットもいくつかあり、なかなか奥が深い部分ですね。

Discussion