普通と違う感じの Semantic Kernel 入門 004「プラグイン」
これまでの記事
- 普通と違う感じの Semantic Kernel 入門 001「関数」
- 普通と違う感じの Semantic Kernel 入門 002「テンプレートエンジン」
- 普通と違う感じの Semantic Kernel 入門 003「AI を呼ぶ関数」
本文
今回は Semantic Kernel のプラグインについて書いていきます。
Semantic Kernel のプラグインは、AI が外界とやり取りをするためのインターフェースになります。プラグインが Semantic Kernel でどのように実装されているものなのかという観点から少し見ていこうと思います。
プラグインの実体
Semantic Kernel のプラグインは KernelPlugin
クラスとして定義されています。
普通に Semantic Kernel を使う場合は、あまり触ることがないクラスですが KernelPlugin
がどういうものかを見ていくと楽しいので見ていきましょう。
KernelPlugin
クラスは以下のように定義されています。
public abstract class KernelPlugin : IEnumerable<KernelFunction>
{
public string Name { get; }
public string Description { get; }
public KernelFunction this[string functionName] { get; }
public abstract int FunctionCount { get; }
public abstract bool TryGetFunction(string name, [NotNullWhen(true)] out KernelFunction? function);
public IList<KernelFunctionMetadata> GetFunctionsMetadata();
public bool Contains(string functionName);
public bool Contains(KernelFunction function);
public abstract IEnumerator<KernelFunction> GetEnumerator();
}
この定義からわかるように KernelPlugin
は KernelFunction
のコレクションを表現するクラスです。それに加えてプラグイン名や説明、関数のメタデータを取得するためのメソッドも定義されています。つまり、ただの関数をグルーピングしたものがプラグインということになります。
KernelPlugin
クラスは抽象クラスで実装クラスが隠されているため KernelPlugin
を直接インスタンス化することはできません。KernelPlugin
をインスタンス化するには KernelPluginFactory
クラスを使います。例えば KernelPluginFactory.CreateFromFunctions
メソッドを使うと、複数の KernelFunction
をまとめてプラグイン化することができます。
以下のコードは、KernelPluginFactory.CreateFromFunctions
メソッドを使って、システムの現在時刻 (ローカルと UTC) を取得する 2 つの関数をプラグイン化する例です。
using Microsoft.SemanticKernel;
// KernelFunctionFactory を使用して関数を作成
var getLocalNow = KernelFunctionFactory.CreateFromMethod(TimeProvider.System.GetLocalNow);
var getUtcNow = KernelFunctionFactory.CreateFromMethod(TimeProvider.System.GetUtcNow);
// 関数をまとめてプラグイン化
var timePlugin = KernelPluginFactory.CreateFromFunctions(
"TimePlugin",
[getLocalNow, getUtcNow]);
// IEnumerable<KernelFunction> なので foreach でループ可能
foreach (var function in timePlugin)
{
Console.WriteLine($"Function Name: {function.Name}");
}
// プラグインから関数を取得して呼び出す
if (timePlugin.TryGetFunction("GetLocalNow", out var localNowFunction))
{
var localNowResult = await localNowFunction.InvokeAsync();
Console.WriteLine($"GetLocalNow: {localNowResult}");
}
実行すると以下のような結果になります。ちゃんと GetLocalNow
と GetUtcNow
の 2 つの関数が定義されていることがわかります。さらに GetLocalNow
関数を呼び出すと、システムのローカル時刻が取得できています。
Function Name: GetLocalNow
Function Name: GetUtcNow
GetLocalNow: 2025-05-27T14:50:00.161493+09:00
実際に AI が外界とやりとりするためにプラグインを使用する場合には、そのプラグインが何をするためのものなのか、その関数は何をするためのものなのかを明確にするために、プラグイン名や関数名、説明などを設定することが重要です。KernelFunction
と KernelPlugin
のメタデータを設定することで、AI がプラグインの意図を理解しやすくなります。実際に追加してみましょう。説明のために関数に引数も追加したバージョンも書いてみます。
using Microsoft.SemanticKernel;
// KernelFunctionFactory を使用して関数を作成
// システムの現在のローカル時間を取得する関数
var getLocalNow = KernelFunctionFactory.CreateFromMethod(
TimeProvider.System.GetLocalNow,
new KernelFunctionFromMethodOptions
{
Description = "Get the current local time.",
FunctionName = "GetLocalNow",
ReturnParameter = new KernelReturnParameterMetadata
{
Description = "The current local time as a DateTimeOffset object.",
ParameterType = typeof(DateTimeOffset),
}
});
// 日時をフォーマットする関数
var format = KernelFunctionFactory.CreateFromMethod((DateTimeOffset dateTime, string format) => dateTime.ToString(format),
new KernelFunctionFromMethodOptions
{
Description = "Format a DateTimeOffset object to a string.",
FunctionName = "FormatDateTime",
Parameters = [
new KernelParameterMetadata("dateTime")
{
Description = "The DateTimeOffset object to format.",
ParameterType = typeof(DateTimeOffset),
},
new KernelParameterMetadata("format")
{
Description = "The format string to use for formatting.",
ParameterType = typeof(string),
}
],
ReturnParameter = new KernelReturnParameterMetadata
{
Description = "The formatted date and time as a string.",
ParameterType = typeof(string),
}
});
// 関数をまとめてプラグイン化
var timePlugin = KernelPluginFactory.CreateFromFunctions(
pluginName: "TimePlugin",
description: "A plugin that provides time-related functions.",
functions: [getLocalNow, format]);
// 各種メタデータを表示
Console.WriteLine("====================================");
Console.WriteLine($"Plugin Name: {timePlugin.Name}, ({timePlugin.Description})");
foreach (var function in timePlugin)
{
// 関数のメタデータを表示
Console.WriteLine($" Function Name: {function.Name} ({function.Description})");
foreach (var parameter in function.Metadata.Parameters)
{
// パラメーターのメタデータを表示
Console.WriteLine($" Parameter: {parameter.Name} ({parameter.Description}) - Type: {parameter.ParameterType}");
}
// 戻り値のメタデータを表示
Console.WriteLine($" Return: ({function.Metadata.ReturnParameter.Description}) - Type: {function.Metadata.ReturnParameter.ParameterType}");
}
Console.WriteLine("====================================");
// プラグインから関数を取得して呼び出す
if (timePlugin.TryGetFunction("GetLocalNow", out var localNowFunction)
&& timePlugin.TryGetFunction("FormatDateTime", out var formatDateTime))
{
var localNowResult = await localNowFunction.InvokeAsync();
Console.WriteLine($"GetLocalNow: {localNowResult}");
var formattedDateTime = await formatDateTime.InvokeAsync(new KernelArguments
{
["dateTime"] = localNowResult,
["format"] = "yyyy-MM-dd HH:mm:ss zzz"
});
Console.WriteLine($"Formatted DateTime: {formattedDateTime}");
}
プラグインを作成して、そのメターデータを表示しています。さらに GetLocalNow
関数を呼び出して、現在のローカル時間を取得し、FormatDateTime
関数を使ってフォーマットしています。実行すると以下のような結果になります。
====================================
Plugin Name: TimePlugin, (A plugin that provides time-related functions.)
Function Name: GetLocalNow (Get the current local time.)
Return: (The current local time as a DateTimeOffset object.) - Type: System.DateTimeOffset
Function Name: FormatDateTime (Format a DateTimeOffset object to a string.)
Parameter: dateTime (The DateTimeOffset object to format.) - Type: System.DateTimeOffset
Parameter: format (The format string to use for formatting.) - Type: System.String
Return: (The formatted date and time as a string.) - Type: System.String
====================================
GetLocalNow: 2025-05-27T15:22:09.4890121+09:00
Formatted DateTime: 2025-05-27 15:22:09 +09:00
きちんとメタデータが表示されていて、関数の呼び出しも成功しています。これでプラグインの基本的な仕組みがわかりました。
クラスからプラグインの作成
先ほどは、関数を自作して、そこからプラグインを作成しましたが、Semantic Kernel にはクラスからプラグインを作成するための機能もあります。これを使うと、属性のついたクラスのメソッドを自動的にプラグイン化することができます。やってみましょう。
クラスのメソッドに KernelFunction
属性をつけることで、そのメソッドがプラグインの関数として認識されます。さらに、Description
属性をつけることで、関数や引数や戻り値の説明を設定することができます。
先ほど作成したものと同じプラグインをクラスから作成してみます。
using System.ComponentModel;
using Microsoft.SemanticKernel;
// クラスからプラグインを作成
var timePlugin = KernelPluginFactory.CreateFromType<TimePlugin>();
// 各種メタデータを表示
Console.WriteLine("====================================");
Console.WriteLine($"Plugin Name: {timePlugin.Name}, ({timePlugin.Description})");
foreach (var function in timePlugin)
{
// 関数のメタデータを表示
Console.WriteLine($" Function Name: {function.Name} ({function.Description})");
foreach (var parameter in function.Metadata.Parameters)
{
// パラメーターのメタデータを表示
Console.WriteLine($" Parameter: {parameter.Name} ({parameter.Description}) - Type: {parameter.ParameterType}");
}
// 戻り値のメタデータを表示
Console.WriteLine($" Return: ({function.Metadata.ReturnParameter.Description}) - Type: {function.Metadata.ReturnParameter.ParameterType}");
}
Console.WriteLine("====================================");
// プラグインから関数を取得して呼び出す
if (timePlugin.TryGetFunction("GetLocalNow", out var localNowFunction)
&& timePlugin.TryGetFunction("FormatDateTime", out var formatDateTime))
{
var localNowResult = await localNowFunction.InvokeAsync();
Console.WriteLine($"GetLocalNow: {localNowResult}");
var formattedDateTime = await formatDateTime.InvokeAsync(new KernelArguments
{
["dateTime"] = localNowResult,
["format"] = "yyyy-MM-dd HH:mm:ss zzz"
});
Console.WriteLine($"Formatted DateTime: {formattedDateTime}");
}
// クラスとして定義
[Description("A plugin that provides time-related functions.")]
class TimePlugin
{
[KernelFunction, Description("Get the current local time.")]
[return: Description("The current local time as a DateTimeOffset object.")]
public DateTimeOffset GetLocalNow() => TimeProvider.System.GetLocalNow();
[KernelFunction, Description("Format a DateTimeOffset object to a string.")]
[return: Description("The formatted date and time as a string.")]
public string FormatDateTime(
[Description("The DateTimeOffset object to format.")]
DateTimeOffset dateTime,
[Description("The format string to use for formatting.")]
string format) =>
dateTime.ToString(format);
}
出力結果は同じなので省略します。クラスからプラグインを作成することで、わかりやすくなりました。(個人的な感想)その他のプラグインの作成方法として、これまで説明していない機能にはなるのですが YAML ファイルで AI を呼び出す関数を定義する機能があります。これを使って所定のフォルダーにある YAML ファイルを読み込むことでプラグインを作成することもできます。これについては別の機会に説明したいと思います。
Kernel からの使用方法
作成した KernelPlugin
は Kernel
に登録して使用することができます。
代表的な Kernel
へのプラグインの登録方法は KernelBuilder
の Plugins
プロパティ経由で登録を行うか、Kernel
の Plugins
プロパティに直接登録することができます。
例えば KernelBuilder
を使ってプラグインを登録する場合は以下のようにします。
using System.ComponentModel;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
// User Secrets から設定を読み込む
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.");
// Builder を作成
var builder = Kernel.CreateBuilder();
// AOAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// Kernel に登録する Plugin を作成
builder.Plugins.AddFromType<TimePlugin>();
// Kernel を作成
var kernel = builder.Build();
// プロンプトを定義して、プラグインの関数を呼び出す
var result = await kernel.InvokePromptAsync("""
<message role="system">
あなたは猫型アシスタントです。猫らしく振舞うために語尾は「にゃん」にしてください。
ユーザーの質問に対して以下のコンテキストの内容を使って回答してください。
## コンテキスト
今日の日付: {{TimePlugin.GetLocalNow}}
</message>
<message role="user">
{{$userInput}}
</message>
""",
new KernelArguments
{
["userInput"] = "今日は何日ですか?",
});
// 結果を表示
Console.WriteLine(result.GetValue<string>());
// クラスでプラグインを定義
[Description("A plugin that provides time-related functions.")]
class TimePlugin
{
[KernelFunction, Description("Get the current local time.")]
[return: Description("The current local time as a DateTimeOffset object.")]
public DateTimeOffset GetLocalNow() => TimeProvider.System.GetLocalNow();
[KernelFunction, Description("Format a DateTimeOffset object to a string.")]
[return: Description("The formatted date and time as a string.")]
public string FormatDateTime(
[Description("The DateTimeOffset object to format.")]
DateTimeOffset dateTime,
[Description("The format string to use for formatting.")]
string format) =>
dateTime.ToString(format);
}
KernelBuilder
の Plugins
プロパティの AddFromType
メソッドを使うことで、クラスからプラグインを登録することができます。
// Kernel にプラグインを登録
kernel.Plugins.AddFromType<TimePlugin>();
実行すると以下のような結果になります。
今日は2025年5月27日だにゃん。
KernelBuilder
と Kernel
の両方でプラグインを登録することが出来ますが、KernelBuilder
は Kernel
のインスタンスを作成する前にプラグインを登録するためのものです。Kernel
を作成後に状況に応じて追加するプラグインを調整したい場合などは Kernel
の Plugins
プロパティに直接登録するといった使い分けが出来ます。
DI との統合
Semantic Kernel のプラグインの AddFromType
メソッドは DI コンテナと統合されています。
KernelBuilder
の方の AddFromType
メソッドは KernelBuilder
が内部で抱えている DI コンテナに登録されているサービスを使ってプラグインを作成します。Kernel
の方の AddFromType
メソッドは引数で IServiceProvider
を受け取って DI コンテナを指定することができます。
こうすることでプラグインのクラスも普通の .NET のプログラムでよく使われる DI を使った設計が可能になります。ちょっとやってみましょう。今回の TimePlugin
で使っている TimeProvider
を DI コンテナに登録して、プラグインのクラスで TimeProvider
を使うようにしてみます。
まずは TimePlugin
クラスを以下のように変更します。TimeProvider
をコンストラクタの引数で受け取るようにして、KernelFunction
属性をつけたメソッドで TimeProvider
を使うようにします。そして TimeProvider
の実装を追加します。ここでは固定の日時を返す FixedTimeProvider
を作成します。
// クラスでプラグインを定義
// TimeProvider は DI コンテナから取得
[Description("A plugin that provides time-related functions.")]
class TimePlugin(TimeProvider timeProvider)
{
[KernelFunction, Description("Get the current local time.")]
[return: Description("The current local time as a DateTimeOffset object.")]
public DateTimeOffset GetLocalNow() => timeProvider.GetLocalNow();
}
// 固定の日時を返す TimeProvider の実装
class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _specificDateTime =
new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public override DateTimeOffset GetUtcNow() => _specificDateTime;
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
}
次に KernelBuilder
を使ってプラグインを登録する部分で TimePlugin
をプラグインとして登録しつつ FixedTimeProvider
を DI コンテナに登録します。
// Builder を作成
var builder = Kernel.CreateBuilder();
// 固定の日時を返す TimeProvider の実装を DI コンテナに登録
builder.Services.AddSingleton<TimeProvider, FixedTimeProvider>();
// Kernel にプラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
この状態で実行すると以下のような結果になります。ちゃんと FixedTimeProvider
が使われて、固定の日時が返されていることがわかります。
今日は1969年7月21日だにゃん。
このように、Semantic Kernel のプラグインは DI コンテナと統合されているため、プラグインのクラスで DI を使った設計が可能になります。これにより、プラグインのクラスも他の .NET のクラスと同様に DI を使って依存関係を注入することができ、柔軟な設計が可能になります。
Function calling の対象としてのプラグイン
Semantic Kernel のプラグインですが、今までは明示的に自分で呼んでいましたが AI から呼んでもらうようにすることも出来ます。俗にいう Function calling と呼ばれる機能です。Semantic Kernel では AI を呼び出す時のパラメーターを PromptExecutionSettings
というクラスで指定します。これは AI を呼び出す時のパラメーターを指定する KernelArguments
のコンストラクターで設定できます。
PromptExecutionSettings
は以下のように定義されていて、大体の AI で共通して使えるパラメーターを指定することができます。
public class PromptExecutionSettings
{
public string? ServiceId { get; set; }
public string? ModelId { get; set; }
public FunctionChoiceBehavior? FunctionChoiceBehavior { get; set; }
public IDictionary<string, object>? ExtensionData { get; set; }
public bool IsFrozen { get; private set; }
}
PromptExecutionSettings
の FunctionChoiceBehavior
プロパティに FunctionChoiceBehavior.Auto()
を指定することで、プラグインの関数を AI が選択して、さらに選択した結果の呼び出しと、その結果の返却を自動的に行うことができます。これを使うと、AI がプラグインの関数を選択して呼び出すことができるようになります。
PromptExecutionSettings
には派生クラスがあり、個々の AI のモデルに特化したパラメーターをもったクラスがあります。こちらを使用することで、AI のモデルに特化したパラメーターを指定することができます。例えば Azure OpenAI の場合は AzureOpenAIPromptExecutionSettings
クラスがあり、おなじみの Temperature
や TopP
などのパラメーターを指定することができます。
実際にこれを使って AI にプラグインの関数を呼び出してもらう例を書いてみます。以下のコードは、TimePlugin
の GetLocalNow
関数を AI に呼び出してもらう例です。先ほどのコードをベースに InvokePromptAsync
メソッドの引数に PromptExecutionSettings
を指定して、AI にプラグインの関数を呼び出してもらうように変更しています。
using System.ComponentModel;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
// User Secrets から設定を読み込む
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.");
// Builder を作成
var builder = Kernel.CreateBuilder();
// 固定の日時を返す TimeProvider の実装を DI コンテナに登録
builder.Services.AddSingleton<TimeProvider, FixedTimeProvider>();
// Kernel にプラグインを登録
builder.Plugins.AddFromType<TimePlugin>();
// AOAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// Kernel を作成
var kernel = builder.Build();
// プロンプトの実行時のパラメーター
var promptExecutionSettings = new AzureOpenAIPromptExecutionSettings
{
// 関数は自動で呼び出す
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
// AOAI の gpt シリーズおなじみの温度パラメーターを設定
Temperature = 0,
};
// 関数の自動呼出しを有効にしたプロンプトを定義して、プラグインの関数を AI に呼び出してもらう
var result = await kernel.InvokePromptAsync("""
<message role="system">
あなたは猫型アシスタントです。猫らしく振舞うために語尾は「にゃん」にしてください。
</message>
<message role="user">
{{$userInput}}
</message>
""",
// KernelArguments のコンストラクタに実行設定を渡す
new KernelArguments(promptExecutionSettings)
{
["userInput"] = "今日は何日ですか?",
});
// 結果を表示
Console.WriteLine(result.GetValue<string>());
// クラスでプラグインを定義
// TimeProvider は DI コンテナから取得
[Description("A plugin that provides time-related functions.")]
class TimePlugin(TimeProvider timeProvider)
{
[KernelFunction, Description("Get the current local time.")]
[return: Description("The current local time as a DateTimeOffset object.")]
public DateTimeOffset GetLocalNow() => timeProvider.GetLocalNow();
}
// 固定の日時を返す TimeProvider の実装
class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _specificDateTime =
new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public override DateTimeOffset GetUtcNow() => _specificDateTime;
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
}
実行すると以下のような結果になります。プロンプトに今日の日付は入れていませんが AI が GetLocalNow
関数の結果を見て、ダミーのローカル時間を元に回答をしていることがわかります。
今日は1969年7月21日だにゃん!
このように、Semantic Kernel のプラグインは AI が関数を選択して呼び出すことができるため、AI がプラグインの関数を使って外界とやり取りすることができます。これにより、AI がプラグインの機能を活用して、より柔軟な応答を生成することが可能になります。
まとめ
今回は Semantic Kernel のプラグインについて、基本的な仕組みから DI との統合、AI からの呼び出しまでを見てきました。プラグインは AI が外界とやり取りするための重要な要素であり、Semantic Kernel の機能を拡張するために非常に有用です。
次回は、生の Chat Completion API を呼び出す場合について書いていこうと思います。(予定は未定)
Discussion