🧙‍♂️

最近のSemanticKernel

に公開

SemanticKernelがどう変わったか

以前にSemanticKernelの記事を書いたときは、バージョンが1.50くらいの時でした。
今は1.64(2025/09/07現在)になっています。
週一くらいでバージョンアップしてましたし。
いろいろ変わっていますので、どう変わっているのか見てみましょう。

DI対応がはっきりした

DIへの対応が少し変わりました。

サービス先登録の場合

var builder = Host.CreateApplicationBuilder();
builder.Services
    .AddAzureOpenAIChatCompletion(
        Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? string.Empty)
    .AddTransient(sp => new Kernel(sp))
    .AddTransient<ProgramMain>()
    ;
var host = builder.Build();

var prg = host.Services.GetRequiredService<ProgramMain>();
await prg.RunAsync();

return await Task.FromResult((int)0);

public class ProgramMain(
    IChatCompletionService chatCompletion
    )
{
    public async Task RunAsync()
    {
        var res = await chatCompletion.GetChatMessageContentAsync("今日の東京の天気を教えて");
        Console.WriteLine(res.Content);
    }
}

それまではKernelに各々のサービスを接続させてましたが、別のDIにサービス登録させておいて後でKernelがまとめて取得してしまう、という方法が使えるようになってます。

Kernel直接接続

これまではKernelBuilder作ってそこにサービス登録してましたが、

var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.Services
    .AddAzureOpenAIChatCompletion(
        Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? string.Empty,
        Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? string.Empty)
    ;
var kernel = kernelBuilder.Build();

KernelBuilderの中にもServiceができていて、その中に登録するようになっています。

各々のサービスへの登録方法

こういうところに書くと、 Azure OpenAI とか使って外につなぎに行くように書くことが多いわけですが、ローカルで動かすこともよくあるわけです。
その際に認証とか面倒なので省いているわけですが、そういうものに接続する際にどう書くか、忘備録的に例を挙げておきます。

LM Studio(疑似OpenAI)

builder.Services
    .AddOpenAIChatCompletion("openai/gpt-oss-20b", new OpenAI.OpenAIClient(new ApiKeyCredential("-"), new OpenAI.OpenAIClientOptions()
    {
        Endpoint = new Uri("http://localhost:1234/v1/"),
        NetworkTimeout = TimeSpan.FromSeconds(3000),
    }))
    ;

Infinity(テキスト埋め込み)

kernelBuilder.Services
    .AddOpenAIEmbeddingGenerator(
        "cl-nagoya/ruri-v3-310m",
        new OpenAI.OpenAIClient(new ApiKeyCredential("-"), new OpenAI.OpenAIClientOptions()
        {
            Endpoint = new Uri("http://localhost:7997"),
        }))
    ;

Qdrant(ベクターストア)

var qdrantUrl = new Uri(Environment.GetEnvironmentVariable("QDRANT_URL") ?? "http://localhost:6334");
builder.Services
    .AddQdrantCollection<ulong, QAItemModel>(
        nameof(QAItemModel),
        qdrantUrl.DnsSafeHost,
        qdrantUrl.Port,
        qdrantUrl.Scheme == "https")
    ;

InMemory(インメモリベクターストア)

builder.Services
    .AddInMemoryVectorStoreRecordCollection<ulong, QAItemModel>(nameof(QAItemModel))
    ;

各々のサービスの受け取り方

チャット完了

まあ、ChatCompletionなので、直訳するとチャット完了、なのでしょうが…。
うーん。

var chatComp = kernel.GetRequiredService<IChatCompletionService>();

これはわかりやすくていいですね。

テキスト埋め込み

テキスト埋め込みサービスでは、 IEmbeddingGenerator<string, Embedding<float>> というような型になりました。
上記でサービス登録は記載しましたが、受け取りの際には

var embed = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();

のように取得します。
型を指定しないといけないので、さっとは出てこないんですね。

ベクターストア

ベクターストアでは、サービスにコレクションを直接登録することができます。
上記のサンプルでは、直接コレクションを登録して、コレクションを操ることができます。
受け取りの際には、

var vectorStore = kernel.GetRequiredService<VectorStoreCollection<ulong, QAItemModel>>();

のようになります。
VectorStoreCollection<> 内は、<キーの型, モデルの型> になります。

基本、 すでにベクターストアにあるデータを使う と考えられているようです。

各々のサービスの使い方

チャット完了

使う際には、上記で取得したものに

  • プロンプトを直接設定
var chatRes = await chatCompletion.GetChatMessageContentAsync(
    "あなたは優秀な翻訳家です。次を日本語に翻訳してください。 This is a pen. This is an Apple.",
    new OpenAIPromptExecutionSettings
    {
        Temperature = 0.1f,
    },
    null,
    cancellationToken
);
Console.WriteLine($"chatRes: {chatRes.Content}");
  • 履歴変数を設定
var history = new ChatHistory();
history.AddSystemMessage("あなたは優秀な翻訳家です。");
history.AddUserMessage("次を日本語に翻訳してください。 This is a pen. This is an Apple.");
var chatRes = await chatCompletion.GetChatMessageContentAsync(
    history,
    new OpenAIPromptExecutionSettings
    {
        Temperature = 0.1f,
    },
    null,
    cancellationToken
);
Console.WriteLine($"chatRes: {chatRes.Content}");

テキスト埋め込み

文章をベクターに変更する際には以下。

var embed = await embeddingGenerator.GenerateAsync(text, null, cancellationToken);

また、ruri-v3とかで文章を検索したいときは以下。

var searchEmb = await embeddingGenerator.GenerateAsync($"検索クエリ: {text}", null, cancellationToken);
var searchRes = collection.SearchAsync(searchEmb, 5, null, cancellationToken)
var ret = searchRes
    .ToBlockingEnumerable()
    .Select(item => new QAItemScoreModel
    {
        Item = item.Record,
        Score = item.Score ?? 0.0f
    })
    .ToArray();

ベクターストア

コレクション内の文書を検索したいときは上記のように SearchAsync を使います。

基本的にはコレクションがある、またはインメモリストアなどはいきなり格納しても問題ないのですが、コレクションの型だけ設定しておいてプログラム中でコレクションを作りたい、とかいうパターンもあるかもしれません。
その場合は、以下。

var collection = kernel.GetRequiredService<VectorStoreCollection<ulong, QAItemModel>>();
await collection.EnsureCollectionExistsAsync();

まとめ

悩むパターンを網羅しておきました。
特に自分が使う際に使いやすいように列記しただけ、というのもありますが…。

Discussion