プラグインとしてネイティブ コードを追加する
ネイティブでサポートされていない機能を AI エージェントに提供する最も簡単な方法は、ネイティブ コードをプラグインにラップすることです。 これにより、アプリ開発者としての既存のスキルを活用して、AI エージェントの機能を拡張できます。
その後、セマンティック カーネルは、提供した説明とリフレクションを使用して、AI エージェントにプラグインを意味的に記述します。 これにより、AI エージェントはプラグインの機能とその操作方法を理解できます。
LLM に適切な情報を提供する
プラグインを作成するときは、プラグインとその機能の機能を理解するための適切な情報を AI エージェントに提供する必要があります。 これには、次のものが含まれます。
- プラグインの名前
- 関数の名前
- 関数の説明
- 関数のパラメーター
- パラメーターのスキーマ
- 戻り値のスキーマ
セマンティック カーネルの値は、コード自体からこの情報の大部分を自動的に生成できることです。 開発者は、これは、AI エージェントがそれらを理解できるように、関数とパラメーターのセマンティック記述を提供する必要があることを意味します。 ただし、コードに適切にコメントを付け、注釈を付ける場合は、既にこの情報が手元にある可能性があります。
以下では、ネイティブ コードを AI エージェントに提供する 2 つの異なる方法と、このセマンティック情報を提供する方法について説明します。
クラスを使用したプラグインの定義
ネイティブ プラグインを作成する最も簡単な方法は、クラスから始めて、 KernelFunction
属性で注釈が付けられたメソッドを追加することです。 また、 Description
注釈を自由に使用して、機能を理解するために必要な情報を AI エージェントに提供することもお勧めします。
public class LightsPlugin
{
private readonly List<LightModel> _lights;
public LightsPlugin(LoggerFactory loggerFactory, List<LightModel> lights)
{
_lights = lights;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
return _lights;
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
// Find the light to change
var light = _lights.FirstOrDefault(l => l.Id == changeState.Id);
// If the light does not exist, return null
if (light == null)
{
return null;
}
// Update the light state
light.IsOn = changeState.IsOn;
light.Brightness = changeState.Brightness;
light.Color = changeState.Color;
return light;
}
}
from typing import List, Optional, Annotated
class LightsPlugin:
def __init__(self, lights: List[LightModel]):
self._lights = lights
@kernel_function
async def get_lights(self) -> List[LightModel]:
"""Gets a list of lights and their current state."""
return self._lights
@kernel_function
async def change_state(
self,
change_state: LightModel
) -> Optional[LightModel]:
"""Changes the state of the light."""
for light in self._lights:
if light["id"] == change_state["id"]:
light["is_on"] = change_state.get("is_on", light["is_on"])
light["brightness"] = change_state.get("brightness", light["brightness"])
light["hex"] = change_state.get("hex", light["hex"])
return light
return None
public class LightsPlugin {
// Mock data for the lights
private final Map<Integer, LightModel> lights = new HashMap<>();
public LightsPlugin() {
lights.put(1, new LightModel(1, "Table Lamp", false, LightModel.Brightness.MEDIUM, "#FFFFFF"));
lights.put(2, new LightModel(2, "Porch light", false, LightModel.Brightness.HIGH, "#FF0000"));
lights.put(3, new LightModel(3, "Chandelier", true, LightModel.Brightness.LOW, "#FFFF00"));
}
@DefineKernelFunction(name = "get_lights", description = "Gets a list of lights and their current state")
public List<LightModel> getLights() {
System.out.println("Getting lights");
return new ArrayList<>(lights.values());
}
@DefineKernelFunction(name = "change_state", description = "Changes the state of the light")
public LightModel changeState(
@KernelFunctionParameter(
name = "model",
description = "The new state of the model to set. Example model: " +
"{\"id\":99,\"name\":\"Head Lamp\",\"isOn\":false,\"brightness\":\"MEDIUM\",\"color\":\"#FFFFFF\"}",
type = LightModel.class) LightModel model
) {
System.out.println("Changing light " + model.getId() + " " + model.getIsOn());
if (!lights.containsKey(model.getId())) {
throw new IllegalArgumentException("Light not found");
}
lights.put(model.getId(), model);
return lights.get(model.getId());
}
}
ヒント
LLM は主に Python コードでトレーニングされるため、関数名とパラメーターにsnake_caseを使用することをお勧めします (C# または Java を使用している場合でも)。 これは、AI エージェントが関数とそのパラメーターをより深く理解するのに役立ちます。
ヒント
関数は、パラメーターとして Kernel
、KernelArguments
、ILoggerFactory
、ILogger
、IAIServiceSelector
、CultureInfo
、IFormatProvider
、CancellationToken
を指定でき、これらは LLM にアドバタイズされず、関数が呼び出されたときに自動的に設定されます。
明示的な入力引数ではなく KernelArguments
に依存している場合、コードは型変換を実行する必要があります。
関数に入力変数として複雑なオブジェクトがある場合、セマンティック カーネルはそのオブジェクトのスキーマも生成し、それを AI エージェントに渡します。 関数と同様に、AI には明らかではないプロパティに対して Description
注釈を指定する必要があります。
LightState
クラスとBrightness
列挙型の定義を次に示します。
using System.Text.Json.Serialization;
public class LightModel
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Brightness
{
Low,
Medium,
High
}
from typing import TypedDict
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: int | None
hex: str | None
public class LightModel {
private int id;
private String name;
private Boolean isOn;
private Brightness brightness;
private String color;
public enum Brightness {
LOW,
MEDIUM,
HIGH
}
public LightModel(int id, String name, Boolean isOn, Brightness brightness, String color) {
this.id = id;
this.name = name;
this.isOn = isOn;
this.brightness = brightness;
this.color = color;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getIsOn() {
return isOn;
}
public void setIsOn(Boolean isOn) {
this.isOn = isOn;
}
public Brightness getBrightness() {
return brightness;
}
public void setBrightness(Brightness brightness) {
this.brightness = brightness;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
Note
これは 「楽しい」例ですが、プラグインのパラメーターがどれだけ複雑であるかを示す良い仕事です。 この 1 つのケースでは、 four さまざまな種類のプロパティ (整数、文字列、ブール値、列挙型) を持つ複合オブジェクトがあります。 セマンティック カーネルの値は、このオブジェクトのスキーマを自動的に生成し、それを AI エージェントに渡し、AI エージェントによって生成されたパラメーターを正しいオブジェクトにマーシャリングできることです。
プラグイン クラスの作成が完了したら、 AddFromType<>
メソッドまたは AddFromObject
メソッドを使用してカーネルに追加できます。
ヒント
関数を作成するときは、常に「この関数を使用するために AI に追加のヘルプを提供するにはどうすればよいですか?」と自問してください。これには、特定の入力型の使用 (可能な場合は文字列を避ける)、説明の提供、例が含まれます。
AddFromObject
メソッドを使用したプラグインの追加
AddFromObject
メソッドを使用すると、プラグインの構築方法を直接制御する場合に備えて、プラグイン クラスのインスタンスをプラグイン コレクションに直接追加できます。
たとえば、 LightsPlugin
クラスのコンストラクターにはライトの一覧が必要です。 この場合は、プラグイン クラスのインスタンスを作成し、プラグイン コレクションに追加できます。
List<LightModel> lights = new()
{
new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = Brightness.Medium, Color = "#FFFFFF" },
new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = Brightness.High, Color = "#FF0000" },
new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = Brightness.Low, Color = "#FFFF00" }
};
kernel.Plugins.AddFromObject(new LightsPlugin(lights));
AddFromType<>
メソッドを使用したプラグインの追加
AddFromType<>
メソッドを使用すると、カーネルは自動的に依存関係の挿入を使用してプラグイン クラスのインスタンスを作成し、プラグイン コレクションに追加します。
これは、コンストラクターでサービスまたはその他の依存関係をプラグインに挿入する必要がある場合に役立ちます。 たとえば、 LightsPlugin
クラスでは、ライトのリストではなく、ロガーとライト サービスを挿入する必要があります。
public class LightsPlugin
{
private readonly Logger _logger;
private readonly LightService _lightService;
public LightsPlugin(LoggerFactory loggerFactory, LightService lightService)
{
_logger = loggerFactory.CreateLogger<LightsPlugin>();
_lightService = lightService;
}
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
public async Task<List<LightModel>> GetLightsAsync()
{
_logger.LogInformation("Getting lights");
return lightService.GetLights();
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
_logger.LogInformation("Changing light state");
return lightService.ChangeState(changeState);
}
}
Dependency Injection を使用すると、カーネルをビルドする前に、必要なサービスとプラグインをカーネル ビルダーに追加できます。
var builder = Kernel.CreateBuilder();
// Add dependencies for the plugin
builder.Services.AddLogging(loggingBuilder => loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Trace));
builder.Services.AddSingleton<LightService>();
// Add the plugin to the kernel
builder.Plugins.AddFromType<LightsPlugin>("Lights");
// Build the kernel
Kernel kernel = builder.Build();
関数のコレクションを使用したプラグインの定義
あまり一般的ではありませんが、それでも便利なのは、関数のコレクションを使用してプラグインを定義することです。 これは、実行時に一連の関数からプラグインを動的に作成する必要がある場合に特に便利です。
このプロセスを使用するには、関数ファクトリを使用して、プラグインに追加する前に個々の関数を作成する必要があります。
kernel.Plugins.AddFromFunctions("time_plugin",
[
KernelFunctionFactory.CreateFromMethod(
method: () => DateTime.Now,
functionName: "get_time",
description: "Get the current time"
),
KernelFunctionFactory.CreateFromMethod(
method: (DateTime start, DateTime end) => (end - start).TotalSeconds,
functionName: "diff_time",
description: "Get the difference between two times in seconds"
)
]);
依存関係の挿入を使用してネイティブ コードを追加するための追加の戦略
依存関係の挿入を使用している場合は、カーネルにプラグインを作成して追加するために実行できる追加の戦略があります。 依存関係挿入を使用してプラグインを追加する方法の例を次に示します。
プラグイン コレクションを挿入する
ヒント
プラグイン コレクションは変更可能であるため、使用するたびに破棄されるように、プラグイン コレクションを一時的なサービスにすることをお勧めします。 使用ごとに新しいプラグイン コレクションを作成するのは安価であるため、パフォーマンスの問題ではありません。
var builder = Host.CreateApplicationBuilder(args);
// Create native plugin collection
builder.Services.AddTransient((serviceProvider)=>{
KernelPluginCollection pluginCollection = [];
pluginCollection.AddFromType<LightsPlugin>("Lights");
return pluginCollection;
});
// Create the kernel service
builder.Services.AddTransient<Kernel>((serviceProvider)=> {
KernelPluginCollection pluginCollection = serviceProvider.GetRequiredService<KernelPluginCollection>();
return new Kernel(serviceProvider, pluginCollection);
});
ヒント
カーネルの記事で説明したようにカーネルは非常に軽量であるため、一時的な使用ごとに新しいカーネルを作成することはパフォーマンスの問題ではありません。
シングルトンとしてプラグインを生成する
プラグインは変更できないため、通常はシングルトンとして作成しても安全です。 これを行うには、プラグイン ファクトリを使用し、結果のプラグインをサービス コレクションに追加します。
var builder = Host.CreateApplicationBuilder(args);
// Create singletons of your plugin
builder.Services.AddKeyedSingleton("LightPlugin", (serviceProvider, key) => {
return KernelPluginFactory.CreateFromType<LightsPlugin>();
});
// Create a kernel service with singleton plugin
builder.Services.AddTransient((serviceProvider)=> {
KernelPluginCollection pluginCollection = [
serviceProvider.GetRequiredKeyedService<KernelPlugin>("LightPlugin")
];
return new Kernel(serviceProvider, pluginCollection);
});
add_plugin
メソッドを使用したプラグインの追加
add_plugin
メソッドを使用すると、プラグイン インスタンスをカーネルに追加できます。
LightsPlugin
クラスを構築してカーネルに追加する方法の例を次に示します。
# Create the kernel
kernel = Kernel()
# Create dependencies for the plugin
lights = [
{"id": 1, "name": "Table Lamp", "is_on": False, "brightness": 100, "hex": "FF0000"},
{"id": 2, "name": "Porch light", "is_on": False, "brightness": 50, "hex": "00FF00"},
{"id": 3, "name": "Chandelier", "is_on": True, "brightness": 75, "hex": "0000FF"},
]
# Create the plugin
lights_plugin = LightsPlugin(lights)
# Add the plugin to the kernel
kernel.add_plugin(lights_plugin)
createFromObject
メソッドを使用したプラグインの追加
createFromObject
メソッドを使用すると、注釈付きメソッドを使用して Object からカーネル プラグインをビルドできます。
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
その後、このプラグインをカーネルに追加できます。
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
関数の戻り値の型スキーマを LLM に提供する
現時点では、AI モデルに関数の戻り値の型メタデータを提供するための、業界全体で明確に定義された標準はありません。 このような標準が確立されるまでは、戻り値の型プロパティの名前が、LLM がコンテンツを推論するのに不十分なシナリオや、シナリオをモデル化または強化するために、追加のコンテキストまたは処理命令を戻り値の型に関連付ける必要がある場合に、次の手法を検討できます。
これらの手法のいずれかを使用する前に、戻り値の型プロパティにわかりやすい名前を付けることをお勧めします。これは、LLM の戻り値の型の理解を向上させる最も簡単な方法であり、トークンの使用に関してもコスト効率が高いためです。
関数の説明で関数の戻り値の型情報を指定する
この手法を適用するには、関数の description 属性に戻り値の型スキーマを含めます。 スキーマでは、次の例に示すように、プロパティ名、説明、および型について詳しく説明する必要があります。
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
{
"type": "object",
"properties": {
"id": { "type": "integer", "description": "Light ID" },
"name": { "type": "string", "description": "Light name" },
"is_on": { "type": "boolean", "description": "Is light on" },
"brightness": { "type": "string", "enum": ["Low", "Medium", "High"], "description": "Brightness level" },
"color": { "type": "string", "description": "Hex color code" }
},
"required": ["id", "name"]
}
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
一部のモデルでは、関数の説明のサイズに制限があるため、スキーマを簡潔に保ち、重要な情報のみを含めるのが推奨されます。
型情報が重要ではなく、トークンの消費を最小限に抑えることが優先される場合は、完全なスキーマではなく、関数の description 属性に戻り値の型の簡単な説明を指定することを検討してください。
public class LightsPlugin
{
[KernelFunction("change_state")]
[Description("""Changes the state of the light and returns:
id: light ID,
name: light name,
is_on: is light on,
brightness: brightness level (Low, Medium, High),
color: Hex color code.
""")]
public async Task<LightModel?> ChangeStateAsync(LightModel changeState)
{
...
}
}
上記のどちらの方法でも、戻り値の型スキーマを手動で追加し、戻り値の型が変更されるたびに更新する必要があります。 これを回避するには、次の手法を検討してください。
関数の戻り値の一部として関数の戻り値の型スキーマを提供する
この手法では、戻り値だけでなく、関数の戻り値とそのスキーマの両方を LLM に提供します。 これにより、LLM はスキーマを使用して戻り値のプロパティを推論できます。
この手法を実装するには、自動関数呼び出しフィルターを作成して登録する必要があります。 詳細については、自動関数呼び出しフィルターの に関する記事を参照してください。 このフィルターは、元の戻り値とそのスキーマの両方を含むカスタム オブジェクトで関数の戻り値をラップする必要があります。 次に例を示します。
private sealed class AddReturnTypeSchemaFilter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context); // Invoke the original function
// Crete the result with the schema
FunctionResultWithSchema resultWithSchema = new()
{
Value = context.Result.GetValue<object>(), // Get the original result
Schema = context.Function.Metadata.ReturnParameter?.Schema // Get the function return type schema
};
// Return the result with the schema instead of the original one
context.Result = new FunctionResult(context.Result, resultWithSchema);
}
private sealed class FunctionResultWithSchema
{
public object? Value { get; set; }
public KernelJsonSchema? Schema { get; set; }
}
}
// Register the filter
Kernel kernel = new Kernel();
kernel.AutoFunctionInvocationFilters.Add(new AddReturnTypeSchemaFilter());
フィルターを登録すると、戻り値の型とそのプロパティの説明を指定できるようになりました。これはセマンティック カーネルによって自動的に抽出されます。
[Description("The state of the light")] // Equivalent to annotating the function with the [return: Description("The state of the light")] attribute
public class LightModel
{
[JsonPropertyName("id")]
[Description("The ID of the light")]
public int Id { get; set; }
[JsonPropertyName("name")]
[Description("The name of the light")]
public string? Name { get; set; }
[JsonPropertyName("is_on")]
[Description("Indicates whether the light is on")]
public bool? IsOn { get; set; }
[JsonPropertyName("brightness")]
[Description("The brightness level of the light")]
public Brightness? Brightness { get; set; }
[JsonPropertyName("color")]
[Description("The color of the light with a hex code (ensure you include the # symbol)")]
public string? Color { get; set; }
}
この方法では、セマンティック カーネルによってスキーマが自動的に抽出されるため、戻り値の型が変更されるたびに戻り値の型スキーマを手動で指定して更新する必要がなくなります。
次のステップ
プラグインを作成する方法がわかったら、AI エージェントでプラグインを使用する方法を学習できるようになりました。 プラグインに追加した関数の種類に応じて、従う必要があるパターンが異なります。 取得関数については、 取得関数の使用に関する記事 参照してください。 タスク自動化関数については、 タスク自動化関数の使用に関する記事 参照してください。