😊

普通と違う感じの Semantic Kernel 入門 008「DI コンテナとの統合」

に公開

これまでの記事

本文

ここでは、Semantic Kernel の DI コンテナとの統合について解説します。
これまでは KernelBuilder を使って Kernel を構築していました。そして、KernelBuilderIServiceCollection を持っているため、そこにサービスを登録していました。しかし、一般的な .NET のアプリケーションでは DI コンテナである IServiceCollection は汎用ホストから提供されます。このようなアプリケーションで、どのように Kernel を構築するのかといったことを解説します。

手組で Kernel を構築する

DI コンテナでオブジェクトを組み立てるには、まずそのオブジェクトを組み立てるために何が必要なのかを知る必要があります。Kernel クラスをインスタンス化するだけであれば new Kernel() と書くことでインスタンスの作成が出来ます。AI などのサービスを使わない場合はプラグインの追加やプラグインに登録された関数の呼び出しも可能です。

Kernel を作成してプラグインを登録し、関数を呼び出すコードは以下のようになります。

using Microsoft.SemanticKernel;

// AI が不要なら Kernel の作成は簡単
var kernel = new Kernel();
// Kernel 作成後にプラグインを登録することもできる
kernel.Plugins.AddFromFunctions("TimePlugin",
    [
        KernelFunctionFactory.CreateFromMethod(
            () => TimeProvider.System.GetLocalNow(),
            functionName: "GetLocalNow",
            description: "現在のローカル時間を取得します。"),
    ]);

// プラグインの関数を呼び出す
var result = await kernel.InvokeAsync("TimePlugin", "GetLocalNow");
Console.WriteLine(result.GetValue<DateTimeOffset>());

実行結果は、現在時刻が表示されるだけなので割愛します。
AI などのサービスを使う場合は、Kernel のコンストラクタに明示的に渡す必要があります。実際に Kernel のコンストラクタは 2 つの引数を受け取ります。どちらのパラメーターにもデフォルト値に null が設定されているため引数無しでインスタンス化できているのです。実際の Kernel のコンストラクタは以下のようになっています。

Kernel(IServiceProvider? services = null, KernelPluginCollection? plugins = null)

services パラメーターは DI コンテナの IServiceProvider を受け取ります。plugins パラメーターはプラグインのコレクションを受け取ります。プラグインのコレクションは KernelPluginCollection クラスのインスタンスです。KernelPluginCollectionKernelPlugin のコレクションで、プラグインの名前から KernelPlugin を取得する機能がある以外は普通のコレクションです。

実際には KernelPluginCollection を明示的に指定することはレアケースです。plugins パラメーターが null の場合は KernelPluginCollectionservices パラメーターから取得します。services にも KernelPluginCollection が登録されていない場合は、services から IEnumerable<KernelPlugin> を取得し、KernelPluginCollection を作成します。つまり、IServiceProviderKernelPlugin を登録しておけば、よしなに KernelPluginCollection が作成されます。

実際に KernelPlugin を DI コンテナに登録して動きを確認してみましょう。まずは以下のようなプラグインのクラスを作成します。どちらも TimeProvider を DI コンテナから取得して、現在のローカル時間を取得したり天気予報を返す関数を持っています。

// Kernel に登録するプラグイン
class TimePlugin(TimeProvider timerProvider)
{
    [KernelFunction]
    public DateTimeOffset GetLocalNow() => timerProvider.GetLocalNow();
}

class WeatherForecastPlugin(TimeProvider timerProvider)
{
    [KernelFunction]
    public string GetWeatherForecast(string location)
    {
        var now = timerProvider.GetLocalNow();
        return $"[{now:yyyy-MM-dd HH:mm}] The weather in {location} is sunny.";
    }

    [KernelFunction]
    public string GetWeatherAdvice(string location)
    {
        var now = timerProvider.GetLocalNow();
        return $"[{now:yyyy-MM-dd HH:mm}] It's a great day to be outside in {location}!";
    }
}

このプラグインを DI コンテナに KernelPlugin として登録します。以下のように IServiceCollection に登録するコードを書きます。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;

var services = new ServiceCollection();

// プラグインで使う TimeProvider を登録
services.AddSingleton(TimeProvider.System);

// KernelPluginFactory を使ってプラグインを登録
services.AddSingleton(sp =>
    KernelPluginFactory.CreateFromType<TimePlugin>("TimePlugin", sp));
services.AddSingleton(sp =>
    KernelPluginFactory.CreateFromType<WeatherForecastPlugin>("WeatherForecastPlugin", sp));

登録が終わったら、IServiceProvider を使って Kernel を作成します。以下のように Kernel をインスタンス化し、プラグインの関数を呼び出すコードを書きます。呼び出した結果は標準出力に出力しましょう。

// IServiceProvider を使って Kernel を作成
var kernel = new Kernel(services.BuildServiceProvider());

// プラグインの関数を呼び出す
var now = await kernel.InvokeAsync("TimePlugin", "GetLocalNow");
var arguments = new KernelArguments
{
    ["location"] = "Tokyo",
};
var weatherForecast = await kernel.InvokeAsync("WeatherForecastPlugin", "GetWeatherForecast", arguments);
var weatherAdvice = await kernel.InvokeAsync("WeatherForecastPlugin", "GetWeatherAdvice", arguments);

// 結果を出力
Console.WriteLine(now.GetValue<DateTimeOffset>());
Console.WriteLine(weatherForecast.GetValue<string>());
Console.WriteLine(weatherAdvice.GetValue<string>());

このコードを実行すると以下のような結果が得られます。

2025/06/01 12:15:10 +09:00
[2025-06-01 12:15] The weather in Tokyo is sunny.
[2025-06-01 12:15] It's a great day to be outside in Tokyo!

このように、DI コンテナを使って Kernel を構築し、プラグインの関数を呼び出すことができます。さらに Kernel も DI コンテナに登録することが出来ます。このための拡張メソッドとして AddKernel メソッドが提供されています。これを使うと以下のように書けます。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;

var services = new ServiceCollection();

// プラグインで使う TimeProvider を登録
services.AddSingleton(TimeProvider.System);

// KernelPluginFactory を使ってプラグインを登録
services.AddSingleton(sp =>
    KernelPluginFactory.CreateFromType<TimePlugin>("TimePlugin", sp));
services.AddSingleton(sp =>
    KernelPluginFactory.CreateFromType<WeatherForecastPlugin>("WeatherForecastPlugin", sp));

// DI コンテナに Kernel を登録
services.AddKernel();

var sp = services.BuildServiceProvider();

// DI コンテナから Kernel を作成
var kernel = sp.GetRequiredService<Kernel>();

// プラグインの関数を呼び出す
var now = await kernel.InvokeAsync("TimePlugin", "GetLocalNow");
var arguments = new KernelArguments
{
    ["location"] = "Tokyo",
};
var weatherForecast = await kernel.InvokeAsync("WeatherForecastPlugin", "GetWeatherForecast", arguments);
var weatherAdvice = await kernel.InvokeAsync("WeatherForecastPlugin", "GetWeatherAdvice", arguments);

// 結果を出力
Console.WriteLine(now.GetValue<DateTimeOffset>());
Console.WriteLine(weatherForecast.GetValue<string>());
Console.WriteLine(weatherAdvice.GetValue<string>());

// Kernel に登録するプラグイン
class TimePlugin(TimeProvider timerProvider)
{
    [KernelFunction]
    public DateTimeOffset GetLocalNow() => timerProvider.GetLocalNow();
}

class WeatherForecastPlugin(TimeProvider timerProvider)
{
    [KernelFunction]
    public string GetWeatherForecast(string location)
    {
        var now = timerProvider.GetLocalNow();
        return $"[{now:yyyy-MM-dd HH:mm}] The weather in {location} is sunny.";
    }

    [KernelFunction]
    public string GetWeatherAdvice(string location)
    {
        var now = timerProvider.GetLocalNow();
        return $"[{now:yyyy-MM-dd HH:mm}] It's a great day to be outside in {location}!";
    }
}

実行結果は先ほどと同じなので割愛します。

AI サービスを使う場合

ここまでは AI を呼び出さない場合の Kernel の構築方法を解説しました。Semantic Kernel は AI を呼び出してなんぼのフレームワークです。ということで、AI サービスを使う場合の Kernel の構築方法を解説します。AI サービスを呼び出す場合も所定のサービスを DI コンテナに登録しておくだけです。若干厄介なのが 2025 年 6 月 1 日時点では、新しい IChatClient を使用する場合のメソッドがプレビュー版であることです。IChatClient の前に使われていた(現在の現役ですが) IChatCompletionService はプレビュー版ではないので過渡期だなぁと感じますね。

AI 系のサービスを DI コンテナに登録する場合も、Semantic Kernel から提供されている IServiceCollection に対する拡張メソッドを使うことで簡単に登録できます。さらに、適切なサービスを自分で登録をすれば Semantic Kernel で提供されている拡張メソッドを使わなくても AI サービスを使うことができます。例えばチャット系機能は以下のように IChatClient を DI コンテナに登録することで使えるようになります。

using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;

var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new EchoChatClient("default"));

var kernel = new Kernel(services.BuildServiceProvider());
var promptFunction = kernel.CreateFunctionFromPrompt("Hello");

// デフォルトの IChatClient が使われる
var result = await kernel.InvokeAsync(promptFunction);
Console.WriteLine(result.GetValue<string>());

// ダミーの IChatClient 実装
class EchoChatClient(string name) : IChatClient
{
    public void Dispose()
    {
        throw new NotImplementedException();
    }

    public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        var lastMessage = messages.LastOrDefault()?.Text ?? "";
        return Task.FromResult(
            new ChatResponse(new ChatMessage(ChatRole.Assistant, $"Echo: {lastMessage} by {name}")));
    }

    public object? GetService(Type serviceType, object? serviceKey = null)
    {
        if (serviceType == typeof(ChatClientMetadata))
        {
            return new ChatClientMetadata("mock");
        }

        return null;
    }

    public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}

ダミーのエコーを返す IChatClient を実装して、IServiceCollection に登録しています。この IChatClient が Semantic Kernel が AI (Chat Completions API) を使う場所で使われます。今回の例では Hello というプロンプトを実行する関数を作成して呼び出しています。実行結果は以下のようになります。

Echo: Hello by default

DI コンテナに AddKeyedSingleton を使って serviceKey を指定して登録することで、複数の IChatClient を登録することもできます。以下のように serviceKey を指定して登録します。

services.AddKeyedSingleton<IChatClient>("key1", new EchoChatClient("key1"));
services.AddKeyedSingleton<IChatClient>("key2", new EchoChatClient("key2"));

この key1key2PromptExecutionSettingsServiceId で指定します。
以下のようなコードになります。

// キーを指定して使用する IChatClient を指定する
var result1 = await kernel.InvokeAsync(promptFunction,
    arguments: new(new PromptExecutionSettings { ServiceId = "key1" }));
Console.WriteLine(result1.GetValue<string>());
var result2 = await kernel.InvokeAsync(promptFunction,
    arguments: new(new PromptExecutionSettings { ServiceId = "key2" }));
Console.WriteLine(result2.GetValue<string>());

実行結果は以下のようになります。

Echo: Hello by key1
Echo: Hello by key2

その他の AI サービスも同様に DI コンテナに登録して使う形になります。現時点では、まだプレビューですがベクトルを生成するような場合には IEmbeddingGenerator を DI コンテナに登録して使います。このパターンを覚えておくと Semantic Kernel を使う際に使い方の当たりをつけやすいと思います。

次は Semantic Kernel で提供されているメソッドを使っていこうと思います。こちらはプレビュー機能なので気を付けてください。
IServiceCollection にも Semantic Kernel の拡張メソッドの AddAzureOpenAIChatClient があります。これを使うと Azure OpenAI Service の Chat Completions API を簡単に使えるようになります。試しに先ほどやった Hello を送るコードの IChatClient を Azure OpenAI Service の Chat Completions API を使うように変更してみます。

コードは以下のようになります。

using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;

var configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();
// AOAI にデプロイしているモデル名
var modelDeploymentName = configuration["AOAI:ModelDeploymentName"]
    ?? throw new ArgumentNullException("AOAI:ModelDeploymentName is not set in the configuration.");
// AOAI のエンドポイント
var endpoint = configuration["AOAI:Endpoint"]
    ?? throw new ArgumentNullException("AOAI:Endpoint is not set in the configuration.");


var services = new ServiceCollection();
#pragma warning disable SKEXP0010 // preview なので警告が出るが無視する
services.AddAzureOpenAIChatClient(modelDeploymentName, endpoint, new AzureCliCredential());
#pragma warning restore SKEXP0010

var kernel = new Kernel(services.BuildServiceProvider());
var promptFunction = kernel.CreateFunctionFromPrompt("Hello");

// デフォルトの IChatClient が使われる
var result = await kernel.InvokeAsync(promptFunction);
Console.WriteLine(result.GetValue<string>());

実行すると以下のようになります。

Hello! How can I help you today?

ASP.NET Core での使用方法

ASP.NET Core のように汎用ホストを使う場合は DI コンテナがフレームワーク側で提供されます。そのため、KernelBuilder を使うよりも IServiceCollection に各種サービスを登録して使うほうが自然に使えます。適当な GET リクエストの message パラメーターを受け取って AI に送信し、結果を返すような ASP.NET Core の Web API を作成してみます。

以下のようなコードになります。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SemanticKernel;

var builder = WebApplication.CreateBuilder(args);

// AOAI にアクセスする IChatClient を登録する
#pragma warning disable SKEXP0010 
builder.Services.AddAzureOpenAIChatClient(
    builder.Configuration["AOAI:ModelDeploymentName"]!,
    new AzureOpenAIClient(new(builder.Configuration["AOAI:Endpoint"]!),
    new AzureCliCredential()));
#pragma warning restore SKEXP0010 
// Kernel を DI コンテナに登録する
builder.Services.AddKernel();

// Kernel のプラグインを登録する
builder.Services.AddSingleton(sp =>
    KernelPluginFactory.CreateFromFunctions("AI",
    [
        KernelFunctionFactory.CreateFromPrompt(promptConfig: new("""
            <message role="system">
              あなたは猫型アシスタントです。ユーザーの問題解決を行ってください。
              猫らしく振舞うために語尾は「にゃん」にしてください。
            </message>
            <message>
              {{$message}}
            </message>
            """)
        {
            Name = "InvokeCat",
        }),
    ]));

var app = builder.Build();

app.MapGet("/ai", async ([FromQuery]string message, [FromServices]Kernel kernel) =>
{
    // DI コンテナから Kernel を取して処理を行う
    var result = await kernel.InvokeAsync(
        "AI", 
        "InvokeCat",
        arguments: new()
        {
            ["message"] = message,
        });
    return result.GetValue<string>();
});

app.Run();

このコードでは、AOAI の Chat Completions API を使うために AddAzureOpenAIChatClient を使って IChatClient を DI コンテナに登録しています。さらに、Kernel を DI コンテナに登録し、プラグインを登録しています。プラグインには、AOAI の Chat Completions API を使うための関数を登録しています。InvokeCat という関数は、ユーザーからのメッセージを受け取り、猫型アシスタントとして応答するように設定されています。

このコードを実行して、/ai?message=こんにちは のようにリクエストを送ると、以下のような応答が得られます。

にゃん!こんにちはにゃん。どんなお手伝いができるかにゃん?

このように、ASP.NET Core の Web API と Semantic Kernel を組み合わせて、AI を使ったアプリケーションを簡単に作成することができます。

Kernel の DI コンテナのライフタイム

Semantic Kernel の Kernel クラスを DI コンテナに登録する際には一般的に Transient (DI コンテナから取得されるたびにインスタンス化される) で登録します。これは、KernelPlugins プロパティなどに可変なコレクションを持っていて、それに要素を追加することで処理ごとに異なるプラグインを登録するような使い方をするケースがあるためです。このような使い方をする場合に KernelSingleton で登録してしまうと、プラグインの登録が上書きされてしまい、意図しない動作をする可能性があります。

まとめ

ここでは、Semantic Kernel の DI コンテナとの統合について解説しました。Kernel を手組で構築する方法や、DI コンテナを使って Kernel を構築する方法、AI サービスを使う場合の Kernel の構築方法などを紹介しました。また、ASP.NET Core での使用方法や Kernel の DI コンテナのライフタイムについても触れました。
Semantic Kernel は ASP.NET Core などの .NET のアプリケーションとの親和性が高くなるように作られていることがなんとなく感じ取ってもらえたかと思います。

次回は「Microsoft.Extensions.AI と Semantic Kernel の統合」について解説します。

目次

普通と違う感じの Semantic Kernel 入門の目次

Microsoft (有志)

Discussion