プラグインとは
プラグインはセマンティック カーネルの重要なコンポーネントです。 Microsoft 365 で ChatGPT または Copilot 拡張機能のプラグインを既に使用している場合は、既に使い慣れていると思います。 プラグインを使用すると、AI で使用できるコレクションに既存の API をカプセル化できます。 これにより、それ以外では実行できないアクションを実行する機能を AI に提供できます。
セマンティック カーネルはバックグラウンドで、最新の LLM のほとんどのネイティブ機能である 関数呼び出しを利用して、LLM を許可し、計画 を実行し、API を呼び出します。 関数呼び出しでは、LLM は特定の関数を要求 (つまり呼び出し) できます。 その後、セマンティック カーネルは、コードベース内の適切な関数に要求をマーシャリングし、LLM が最終的な応答を生成できるように結果を LLM に返します。
すべての AI SDK がプラグインに似た概念を持っているわけではありません (ほとんどの場合、関数やツールを持っているだけです)。 ただし、エンタープライズ シナリオでは、プラグインは、エンタープライズ開発者が既にサービスと API を開発する方法を反映する一連の機能をカプセル化しているため、価値があります。 プラグインは依存関係の挿入でもうまく機能します。 プラグインのコンストラクター内では、プラグインの処理を実行するために必要なサービス (データベース接続、HTTP クライアントなど) を挿入できます。 これは、プラグインがない他の SDK では実現が困難です。
プラグインの構造
大まかに言えば、プラグインは、AI アプリやサービスに公開できる 関数 のグループです。 プラグイン内の関数は、AI アプリケーションによって調整され、ユーザー要求を実現できます。 セマンティック カーネル内では、関数呼び出しでこれらの関数を自動的に呼び出すことができます。
手記
他のプラットフォームでは、関数は多くの場合、"ツール" または "アクション" と呼ばれます。 セマンティック カーネルでは、通常はコードベースでネイティブ関数として定義されるため、"functions" という用語を使用します。
ただし、関数を提供するだけでは、プラグインを作成するだけでは不十分です。 関数呼び出しによる自動オーケストレーションを行うために、プラグインは、その動作を意味的に記述する詳細も提供する必要があります。 関数の入力、出力、および副作用のすべてが、AI が理解できる方法で記述する必要があります。そうしないと、AI は関数を正しく呼び出しません。
たとえば、右側のサンプル WriterPlugin
プラグインには、各関数の動作を記述するセマンティック記述を含む関数があります。 LLM では、これらの説明を使用して、ユーザーの質問を満たすために呼び出す最適な関数を選択できます。
右側の図では、LLM は、ShortPoem
関数と StoryGen
関数を呼び出して、提供されたセマンティックの説明に感謝するユーザーの要求を満たす可能性があります。
さまざまな種類のプラグインをインポートする
セマンティック カーネルにプラグインをインポートするには、ネイティブ コード を使用するか、OpenAPI 仕様使用する 2 つの主な方法があります。 前者では、既存のコードベースでプラグインを作成し、既に持っている依存関係とサービスを活用できます。 後者では、異なるプログラミング言語とプラットフォーム間で共有できる OpenAPI 仕様からプラグインをインポートできます。
次に、ネイティブ プラグインのインポートと使用の簡単な例を示します。 これらのさまざまな種類のプラグインをインポートする方法の詳細については、次の記事を参照してください。
ヒント
作業を開始するときは、ネイティブ コード プラグインを使用することをお勧めします。 アプリケーションが成熟し、クロスプラットフォーム チーム間で作業する際に、OpenAPI 仕様を使用して、さまざまなプログラミング言語とプラットフォーム間でプラグインを共有することを検討できます。
さまざまな種類のプラグイン関数
プラグイン内には、通常、拡張生成 (RAG) を取得するためのデータを取得する関数とタスクを自動化する関数の 2 種類があります。 各型は機能的には同じですが、通常はセマンティック カーネルを使用するアプリケーション内で異なる方法で使用されます。
たとえば、取得関数を使用する場合、パフォーマンスを向上させるために戦略を使用できます (たとえば、キャッシュや、要約のために安価な中間モデルを使用する)。 一方、タスク自動化関数では、タスクが正しく完了していることを確認するために、人間のループ内承認プロセスを実装する必要があります。
さまざまな種類のプラグイン関数の詳細については、次の記事を参照してください。
プラグインの開始ガイド
セマンティック カーネル内でプラグインを使用することは、常に次の 3 つのステップのプロセスです。
- プラグインの を定義する
- カーネル にプラグインを追加する
- 次に、いずれかのプロンプトでプラグインの関数を呼び出して、機能呼び出しを行います
以下では、セマンティック カーネル内でプラグインを使用する方法の概要を示します。 プラグインを作成して使用する方法の詳細については、上記のリンクを参照してください。
1) プラグインを定義する
プラグインを作成する最も簡単な方法は、クラスを定義し、KernelFunction
属性を使用してそのメソッドに注釈を付ける方法です。 これはセマンティック カーネルで、これが AI によって呼び出されるか、プロンプトで参照できる関数であることを知らせましょう。
OpenAPI 仕様からプラグインをインポートすることもできます。
以下では、ライトの状態を取得してその状態を変更できるプラグインを作成します。
ヒント
ほとんどの LLM は関数呼び出しのために Python でトレーニングされているため、C# または Java SDK を使用している場合でも、関数名とプロパティ名にはスネーク ケースを使用することをお勧めします。
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class LightsPlugin
{
// Mock data for the lights
private readonly List<LightModel> lights = new()
{
new LightModel { Id = 1, Name = "Table Lamp", IsOn = false, Brightness = 100, Hex = "FF0000" },
new LightModel { Id = 2, Name = "Porch light", IsOn = false, Brightness = 50, Hex = "00FF00" },
new LightModel { Id = 3, Name = "Chandelier", IsOn = true, Brightness = 75, Hex = "0000FF" }
};
[KernelFunction("get_lights")]
[Description("Gets a list of lights and their current state")]
[return: Description("An array of lights")]
public async Task<List<LightModel>> GetLightsAsync()
{
return lights
}
[KernelFunction("get_state")]
[Description("Gets the state of a particular light")]
[return: Description("The state of the light")]
public async Task<LightModel?> GetStateAsync([Description("The ID of the light")] int id)
{
// Get the state of the light with the specified ID
return lights.FirstOrDefault(light => light.Id == id);
}
[KernelFunction("change_state")]
[Description("Changes the state of the light")]
[return: Description("The updated state of the light; will return null if the light does not exist")]
public async Task<LightModel?> ChangeStateAsync(int id, LightModel LightModel)
{
var light = lights.FirstOrDefault(light => light.Id == id);
if (light == null)
{
return null;
}
// Update the light with the new state
light.IsOn = LightModel.IsOn;
light.Brightness = LightModel.Brightness;
light.Hex = LightModel.Hex;
return light;
}
}
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 byte? Brightness { get; set; }
[JsonPropertyName("hex")]
public string? Hex { get; set; }
}
from typing import TypedDict, Annotated
class LightModel(TypedDict):
id: int
name: str
is_on: bool | None
brightness: int | None
hex: str | None
class LightsPlugin:
lights: list[LightModel] = [
{"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"},
]
@kernel_function
async def get_lights(self) -> Annotated[list[LightModel], "An array of lights"]:
"""Gets a list of lights and their current state."""
return self.lights
@kernel_function
async def get_state(
self,
id: Annotated[int, "The ID of the light"]
) -> Annotated[LightModel | None], "The state of the light"]:
"""Gets the state of a particular light."""
for light in self.lights:
if light["id"] == id:
return light
return None
@kernel_function
async def change_state(
self,
id: Annotated[int, "The ID of the light"],
new_state: LightModel
) -> Annotated[Optional[LightModel], "The updated state of the light; will return null if the light does not exist"]:
"""Changes the state of the light."""
for light in self.lights:
if light["id"] == id:
light["is_on"] = new_state.get("is_on", light["is_on"])
light["brightness"] = new_state.get("brightness", light["brightness"])
light["hex"] = new_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));
lights.put(2, new LightModel(2, "Porch light", false));
lights.put(3, new LightModel(3, "Chandelier", true));
}
@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 = "id", description = "The ID of the light to change") int id,
@KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) {
System.out.println("Changing light " + id + " " + isOn);
if (!lights.containsKey(id)) {
throw new IllegalArgumentException("Light not found");
}
lights.get(id).setIsOn(isOn);
return lights.get(id);
}
}
関数、戻り値、およびパラメーターの説明を提供していることに注意してください。 これは、AI が関数の機能とその使用方法を理解するために重要です。
ヒント
AI が関数の呼び出しに問題がある場合は、関数の詳細な説明を提供することを恐れないでください。 いくつかの例、関数を使用する (使用しない) 場合の推奨事項、必要なパラメーターを取得する場所に関するガイダンスがすべて役立ちます。
2) プラグインをカーネルに追加する
プラグインを定義したら、プラグインの新しいインスタンスを作成し、カーネルのプラグイン コレクションに追加することで、プラグインをカーネルに追加できます。
この例では、AddFromType
メソッドを使用してプラグインとしてクラスを追加する最も簡単な方法を示します。 プラグインを追加するその他の方法については、ネイティブ プラグインの追加 記事を参照してください。
var builder = new KernelBuilder();
builder.Plugins.AddFromType<LightsPlugin>("Lights")
Kernel kernel = builder.Build();
kernel = Kernel()
kernel.add_plugin(
LightsPlugin(),
plugin_name="Lights",
)
// 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();
3) プラグインの関数を呼び出す
最後に、関数呼び出しを使用して、AI でプラグインの関数を呼び出すことができます。 次に示す例では、change_state
関数を呼び出してライトをオンにする前に、Lights
プラグインから get_lights
関数を呼び出すように AI を同調させる方法を示します。
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
// Create a kernel with Azure OpenAI chat completion
var builder = Kernel.CreateBuilder().AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey);
// Build the kernel
Kernel kernel = builder.Build();
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
// Add a plugin (the LightsPlugin class is defined below)
kernel.Plugins.AddFromType<LightsPlugin>("Lights");
// Enable planning
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
// Create a history store the conversation
var history = new ChatHistory();
history.AddUserMessage("Please turn on the lamp");
// Get the response from the AI
var result = await chatCompletionService.GetChatMessageContentAsync(
history,
executionSettings: openAIPromptExecutionSettings,
kernel: kernel);
// Print the results
Console.WriteLine("Assistant > " + result);
// Add the message from the agent to the chat history
history.AddAssistantMessage(result);
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.azure_chat_prompt_execution_settings import (
AzureChatPromptExecutionSettings,
)
async def main():
# Initialize the kernel
kernel = Kernel()
# Add Azure OpenAI chat completion
chat_completion = AzureChatCompletion(
deployment_name="your_models_deployment_name",
api_key="your_api_key",
base_url="your_base_url",
)
kernel.add_service(chat_completion)
# Add a plugin (the LightsPlugin class is defined below)
kernel.add_plugin(
LightsPlugin(),
plugin_name="Lights",
)
# Enable planning
execution_settings = AzureChatPromptExecutionSettings()
execution_settings.function_call_behavior = FunctionChoiceBehavior.Auto()
# Create a history of the conversation
history = ChatHistory()
history.add_message("Please turn on the lamp")
# Get the response from the AI
result = await chat_completion.get_chat_message_content(
chat_history=history,
settings=execution_settings,
kernel=kernel,
)
# Print the results
print("Assistant > " + str(result))
# Add the message from the agent to the chat history
history.add_message(result)
# Run the main function
if __name__ == "__main__":
asyncio.run(main())
// Enable planning
InvocationContext invocationContext = new InvocationContext.Builder()
.withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY)
.withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
.build();
// Create a history to store the conversation
ChatHistory history = new ChatHistory();
history.addUserMessage("Turn on light 2");
List<ChatMessageContent<?>> results = chatCompletionService
.getChatMessageContentsAsync(history, kernel, invocationContext)
.block();
System.out.println("Assistant > " + results.get(0));
上記のコードでは、次のような応答が返されます。
役割 | メッセージ |
---|---|
🔵 ユーザー | ランプをオンにしてください |
🔴 Assistant (関数呼び出し) | Lights.get_lights() |
🟢 ツール | [{ "id": 1, "name": "Table Lamp", "isOn": false, "brightness": 100, "hex": "FF0000" }, { "id": 2, "name": "Porch light", "isOn": false, "brightness": 50, "hex": "00FF00" }, { "id": 3, "name": "Chandelier", "isOn": true, "brightness": 75, "hex": "0000FF" }] |
🔴 Assistant (関数呼び出し) | Lights.change_state(1, { "isOn": true }) |
🟢 ツール | { "id": 1, "name": "Table Lamp", "isOn": true, "brightness": 100, "hex": "FF0000" } |
🔴 アシスタント | ランプがオンになりました |
ヒント
プラグイン関数を直接呼び出すことができますが、呼び出す関数を決定するのは AI である必要があるため、これはお勧めしません。 呼び出される関数を明示的に制御する必要がある場合は、プラグインではなくコードベースで標準メソッドを使用することを検討してください。
プラグインの作成に関する一般的な推奨事項
各シナリオには固有の要件があり、個別のプラグイン設計を利用し、複数の LLM が組み込まれる可能性があることを考慮すると、プラグインの設計に関する 1 つのサイズに適合するガイドを提供することは困難です。 ただし、プラグインが AI に優しく、LLM で簡単かつ効率的に使用できるようにするための一般的な推奨事項とガイドラインを以下に示します。
必要なプラグインのみをインポートする
特定のシナリオに必要な関数を含むプラグインのみをインポートします。 この方法では、使用される入力トークンの数を減らすだけでなく、シナリオで使用されていない関数に対する関数の誤呼呼び出しの発生を最小限に抑えます。 全体として、この戦略では、関数呼び出しの精度を高め、誤検知の数を減らす必要があります。
さらに、OpenAI では、1 回の API 呼び出しで 20 個以下のツールを使用することをお勧めします。理想的には、10 個以下のツールです。 OpenAI で述べたように
プラグインを AI に対応させる
プラグインを理解して利用する LLM の機能を強化するには、次のガイドラインに従うことをお勧めします。
説明的で簡潔な関数名を使用する: 各関数を選択するタイミングをモデルが理解するのに役立つ目的が関数名によって明確に伝わるようにします。 関数名があいまいな場合は、わかりやすくするために名前を変更することを検討してください。 省略形や頭字語を使用して関数名を短くしないでください。
DescriptionAttribute
を使用して、必要な場合にのみ追加のコンテキストと命令を提供し、トークンの消費を最小限に抑えます。関数パラメーターの最小化: 関数パラメーターの数を制限し、可能な限りプリミティブ型を使用します。 この方法により、トークンの消費量が削減され、関数シグネチャが簡略化され、LLM が関数パラメーターと効果的に一致しやすくなります。
関数パラメーターにわかりやすい名前を付ける: 関数パラメーターにわかりやすい名前を割り当てて目的を明確にします。 省略形や頭字語を使用してパラメーター名を短くすることは避けてください。これは、LLM がパラメーターを推論し、正確な値を提供するのに役立ちます。 関数名と同様に、トークンの使用量を最小限に抑えるために必要な場合にのみ、
DescriptionAttribute
を使用します。
関数の数とその責任の適切なバランスを見いだす
一方で、1 つの責任で機能を持つことは、複数のシナリオで関数をシンプルかつ再利用可能に保つことを可能にする良い方法です。 一方、各関数呼び出しでは、ネットワークラウンドトリップ待機時間と消費される入力トークンと出力トークンの数という点でオーバーヘッドが発生します。入力トークンは、関数定義と呼び出し結果を LLM に送信するために使用され、出力トークンはモデルから関数呼び出しを受信するときに使用されます。
また、複数の責任を持つ 1 つの関数を実装して、使用されるトークンの数を減らし、ネットワークオーバーヘッドを削減することもできますが、他のシナリオでは再利用性が低下します。
ただし、多くの責任を 1 つの関数に統合すると、関数パラメーターとその戻り値の型の数と複雑さが増す可能性があります。 この複雑さが原因で、モデルが関数パラメーターと正しく一致できない場合があり、パラメーターが見落とされたり、型が正しくない場合があります。 そのため、ネットワーク オーバーヘッドを減らす関数の数と各関数が持つ責任の数との間で適切なバランスを取り、モデルが関数パラメーターと正確に一致できるようにすることが不可欠です。
セマンティック カーネル関数の変換
セマンティック カーネル関数の変換の に関するブログ記事の説明に従って、セマンティック カーネル関数 変換手法を利用します。
関数の動作の変更: 関数の既定の動作が目的の結果と一致せず、元の関数の実装を変更できない場合があります。 このような場合は、元の関数をラップし、それに応じて動作を変更する新しい関数を作成できます。
コンテキスト情報を提供する: 関数には、LLM が推論できないパラメーターまたは推論すべきでないパラメーターが必要な場合があります。 たとえば、関数が現在のユーザーに代わって動作する必要がある場合や認証情報が必要な場合、通常、このコンテキストはホスト アプリケーションで使用できますが、LLM では使用できません。 このような場合は、LLM によって提供される引数と共に、ホスティング アプリケーションから必要なコンテキスト情報を提供しながら、元の関数を呼び出すように関数を変換できます。
パラメーターの一覧、型、名前の変更: 元の関数に、LLM が解釈に苦労する複雑なシグネチャがある場合は、LLM がより簡単に理解できる単純なシグネチャを持つ関数に変換できます。 これには、パラメーター名、型、パラメーターの数の変更、複雑なパラメーターのフラット化やフラット化解除などの調整が含まれる場合があります。
ローカル状態の使用率
ドキュメント、記事、機密情報を含む電子メールなど、比較的大きなデータセットまたは機密データセットを操作するプラグインを設計する場合は、ローカル状態を利用して、LLM に送信する必要のない元のデータまたは中間結果を格納することを検討してください。 このようなシナリオの関数は状態 ID を受け入れて返すことができます。これにより、実際のデータを LLM に渡す代わりに、データを検索してローカルでアクセスできます。これは、次の関数呼び出しの引数として受け取るだけです。
データをローカルに格納することで、関数呼び出し中に不要なトークンの消費を回避しながら、情報をプライベートで安全に保つことができます。 このアプローチにより、データのプライバシーが向上するだけでなく、大規模または機密性の高いデータセットを処理する全体的な効率も向上します。