普通と違う感じの Semantic Kernel 入門 005「Chat Completions API を使おう」
これまでの記事
- 普通と違う感じの Semantic Kernel 入門 001「関数」
- 普通と違う感じの Semantic Kernel 入門 002「テンプレートエンジン」
- 普通と違う感じの Semantic Kernel 入門 003「AI を呼ぶ関数」
- 普通と違う感じの Semantic Kernel 入門 004「プラグイン」
本文
これまで Semantic Kernel の関数やテンプレートエンジン、AI を呼ぶ関数、プラグインについて見てきましたが、今回は Chat Completions API を使ってみます。003 の「AI を呼ぶ関数」で AddAzureOpenAIChatCompletion
メソッドと AddAzureOpenAIChatClient
メソッドの話を少ししましたが、現状過渡期のような状態で AddAzureOpenAIChatCompletion
が古くからあるメソッドで、AddAzureOpenAIChatClient
が新しいメソッドです。どちらも Azure OpenAI Service の Chat Completions API を使うためのサービスを追加するメソッドですが、AddAzureOpenAIChatClient
の方が .NET の共通の AI クライアントのインターフェースである Microsoft.Extensions.AI
の API のため、より汎用的に使えるようになっています。
ここでは、これを使用して、Chat Completions API を使う方法を見ていきます。
Chat Completions API を使うためのクライアントを取得する
まずは、Chat Completions API を使うためのクライアントを取得します。以下のように Kernel
の AddAzureOpenAIChatClient
メソッドを使って、クライアントを登録すると Kernel
の Services
プロパティ (IServiceProvider
) から取得できるようになっています。そのため、以下のように Kernel
を作成して、IChatClient
を取得することで Semantic Kernel にラップされていない IChatClient
の API を直接呼び出すことができます。
例えば IChatClient
を使って AI にメッセージを送信するには、以下のようにします。
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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();
// Azure OpenAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// Kernel を作成
var kernel = builder.Build();
// Kernel のサービスから IChatClient を取得
var chatClient = kernel.Services.GetRequiredService<IChatClient>();
// ChatClient を使用してメッセージを送信
var response = await chatClient.GetResponseAsync("こんにちは!");
// 結果を表示
Console.WriteLine(response.Text);
IChatClient
の GetResponseAsync
メソッドを使って、AI にメッセージを送信しています。ここでは、こんにちは!
というメッセージを送信し、その応答を表示しています。実行すると以下のような結果になります。
こんにちは!?? 今日はどんなことをお手伝いできますか?
IChatClient を使うメリット
Semantic Kernel で、これまで紹介してきた機能は、割と抽象度が高めの機能でしたが IChatClient
を使うことで、より細かな制御が可能になります。一番顕著なのがチャットの履歴を管理できることです。IChatClient
を使うことで ChatMessage
のリスト形式でチャットの履歴を管理できるようになります。
ChatMessage
のリストは JSON 形式でシリアライズされているため、履歴を保存しておいて、後で読み込むこともできます。以下のように ChatMessage
のリストを作成して、IChatClient
に渡すことで、チャットの履歴を管理できます。さらに、チャットの履歴を chatlog.json
というファイルに保存しておくことで、次回起動時にその履歴を読み込むこともできます。
以下のコードでは実行するたびに 1 ターンのチャットを行い、その履歴を chatlog.json
に保存します。次回起動時にはその履歴を読み込んで、前回の続きからチャットを始めることができます。
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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();
// Azure OpenAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// Kernel を作成
var kernel = builder.Build();
// Kernel のサービスから IChatClient を取得
var chatClient = kernel.Services.GetRequiredService<IChatClient>();
List<ChatMessage> messages;
if (File.Exists("chatlog.json"))
{
// chatlog.json が存在する場合は、そこからメッセージを読み込む
await using var stream = File.OpenRead("chatlog.json");
messages = await JsonSerializer.DeserializeAsync<List<ChatMessage>>(stream)
?? throw new InvalidOperationException("Failed to deserialize chat log.");
}
else
{
// chatlog.json が存在しない場合は、初期メッセージを設定
messages = [new ChatMessage(ChatRole.System, "あなたは猫型アシスタントです。猫らしく振舞うために語尾は「にゃん」にしてください。")];
}
// ユーザーからの入力を受け付ける
Console.Write("User > ");
var userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput))
{
Console.WriteLine("No input provided. Exiting.");
return;
}
// ユーザーの入力をメッセージに追加し、AIからの応答を取得
messages.Add(new ChatMessage(ChatRole.User, userInput));
var response = await chatClient.GetResponseAsync(messages);
Console.WriteLine($"AI > {response.Text}");
// AIの応答をメッセージに追加し、chatlog.jsonに保存
messages.AddRange(response.Messages);
await using var fileStream = File.Create("chatlog.json");
await JsonSerializer.SerializeAsync(fileStream, messages, new JsonSerializerOptions
{
// 日本語がエスケープされないように、UnicodeRanges.All を指定
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
// インデントを有効にして、読みやすい形式で保存
WriteIndented = true,
});
最初の実行で以下のように私の名前を伝えます。そうすると以下のような結果になります。
User > 私の名前は Kazuki です。
AI > Kazukiさん、よろしくにゃん!何かお手伝いすることがあったら、いつでも言ってにゃん!
次に再度実行すると、前回の続きからチャットが始まります。試しに私の名前を聞いてみると、ちゃんと回答してくれます。
User > 私の名前を教えてください。
AI > Kazukiさんのお名前は「Kazuki」だにゃん!かわいいお名前だにゃん?。
chatlog.json
の内容は以下のようになっています。ちゃんと会話履歴が保存されています。
[
{
"AuthorName": null,
"Role": "system",
"Contents": [
{
"$type": "text",
"Text": "あなたは猫型アシスタントです。猫らしく振舞うために語尾は「にゃん」にしてください。",
"AdditionalProperties": null
}
],
"MessageId": null,
"AdditionalProperties": null
},
{
"AuthorName": null,
"Role": "user",
"Contents": [
{
"$type": "text",
"Text": "私の名前は Kazuki です。",
"AdditionalProperties": null
}
],
"MessageId": null,
"AdditionalProperties": null
},
{
"AuthorName": null,
"Role": "assistant",
"Contents": [
{
"$type": "text",
"Text": "Kazukiさん、よろしくにゃん!何かお手伝いすることがあったら、いつでも言ってにゃん!",
"AdditionalProperties": null
}
],
"MessageId": "chatcmpl-BbkgkGHZKbzwpiLGtL3u3tb8jwr2O",
"AdditionalProperties": null
},
{
"AuthorName": null,
"Role": "user",
"Contents": [
{
"$type": "text",
"Text": "私の名前を教えてください。",
"AdditionalProperties": null
}
],
"MessageId": null,
"AdditionalProperties": null
},
{
"AuthorName": null,
"Role": "assistant",
"Contents": [
{
"$type": "text",
"Text": "Kazukiさんのお名前は「Kazuki」だにゃん!かわいいお名前だにゃん〜。",
"AdditionalProperties": null
}
],
"MessageId": "chatcmpl-BbkhExfWg1rC1QWUtYTXa25Csi3lD",
"AdditionalProperties": null
}
]]
このように、IChatClient
を使うことで、チャットの履歴を管理しながら、AI と対話することができます。さらに、ChatMessage
のリストを JSON 形式で保存することで、後で再利用することも可能です。こういう細かいことをしたい場合は IChatClient
を使った方が便利です。
IChatClient
にも Function calling の機能があり、こちらもメソッドをツールとして渡して呼び出すことが出来ます。Semantic Kernel のプラグインのような関数の集合体を扱うような概念は無く、単純に Description
属性でメソッドの説明を付けておくだけで良いです。そして、IChatClient
の GetResponseAsync
メソッドに ChatOptions
を渡すオーバーロードがあるので、そこで Tools
プロパティに呼び出し候補のツールを渡し、ToolMode
に ChatToolMode.Auto
を指定することで、AI がツールを自動的に選択して呼び出すことができるようになります。
Tools
プロパティに渡すための AITool
クラスは AIFunctionFactory.Create
メソッドを使って Delegate
から作ることができます。AIFunctionFactory
で作成されるクラスは AIFunction
なのですが AIFunction
は AITool
のサブクラスなので AITool
に渡せます。
以下のように、IChatClient
を使って今日の日付を取得するツールを呼び出す例を示します。
using System.ComponentModel;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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();
// Azure OpenAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// Kernel を作成
var kernel = builder.Build();
// Kernel のサービスから IChatClient を取得
var chatClient = kernel.Services.GetRequiredService<IChatClient>();
// ChatClient を使用してメッセージを送信
var response = await chatClient.GetResponseAsync("今日は何日ですか?",
// 関数の自動呼出しを有効にするためのオプションを指定
new ChatOptions
{
// 呼び出し可能な関数を指定
Tools = [AIFunctionFactory.Create(TimeTools.GetCurrentTime)],
// 関数の自動呼出しを有効にする
ToolMode = ChatToolMode.Auto,
});
// 結果を表示
Console.WriteLine(response.Text);
// AI から呼ぶための関数を定義
class TimeTools
{
[Description("Get the current local time.")]
public static DateTimeOffset GetCurrentTime() => TimeProvider.System.GetLocalNow();
}
実行すると以下のような結果になります。
今日は2025年5月27日です。
ちゃんとツールを呼んで今日の日付が取れていることがわかります。
プラグインの AIFunction 化 / AIFunction のプラグイン化
ここまでの内容を見てプラグインと AIFunction
で同じようなことが出来るけど違う API になっていてややこしい。なんならプラグインを AIFunction
として扱いたいといったことや、逆に AIFunction
をプラグインとして扱いたいといったことがあるかもしれません。
安心してください Semantic Kernel では、そのようなことが出来るように実装されています。具体的には KernelFunction
自体が AIFunction
を継承しているため、ほぼシームレスにプラグインと AIFunction
を相互に変換することができます。
AIFunction
を KernelFunction
に変換するには AsKernelFunction
拡張メソッドを使います。非常に簡単です。ただ、この機能はまだプレビュー機能のため使用するためにはコードに #pragma warning disable SKEXP0001
を追加する必要があります。
やってみましょう。今日の日付を取得する関数を AIFunction
にして、そこから KernelFunction
を作成して、プラグインとして登録してみます。
using System.ComponentModel;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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();
// Azure OpenAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// AsKernelFunction はまだプレビュー機能なので SKEXP0001 を disable にしないと使えない
#pragma warning disable SKEXP0001
// AITool (AIFunction) を KernelFunction に変換してプラグインとして登録
builder.Plugins.AddFromFunctions("TimePlugin",
[AIFunctionFactory.Create(TimeTools.GetCurrentTime).AsKernelFunction()]);
#pragma warning restore SKEXP0001
// Kernel を作成
var kernel = builder.Build();
// Semantic Kernel の API で Function calling
var result = await kernel.InvokePromptAsync(
"今日は何日?",
new KernelArguments(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
}));
Console.WriteLine(result.GetValue<string>());
// AI から呼ぶための関数を定義
class TimeTools
{
[Description("Get the current local time.")]
public static DateTimeOffset GetCurrentTime() => TimeProvider.System.GetLocalNow();
}
実行すると以下のような結果になります。ちゃんとプラグインとして登録されていることがわかります。
今日は2025年5月27日です。
逆方向もやってみましょう。逆方向には大きく分けて 2 通りのやり方があります。1 つは PromptExecutionSettings
を ChatOptions
に変換する方法です。これは PromptExecutionSettings
の ToChatOptions
拡張メソッドを使うことで簡単に変換できます。もう 1 つは KernelFunction
を AIFunction
に変換する方法です。これは AsAIFunctions
拡張メソッドを使うことで、プラグインの中の KernelFunction
を AIFunction
に変換してツールとして登録できます。
PromptExecutionSettings
を ChatOptions
に変換する方法はプレビューではないので、普通に使えます。KernelFunction
を AIFunction
に変換する方法は、プレビュー機能のため SKEXP0001
を disable にしないと使えません。
以下のコードは、今日の日付を聞いている方では PromptExecutionSettings
を ChatOptions
に変換して、AI に関数を自動で呼び出させています。明日の日付を聞いている方では、KernelFunction
を AIFunction
に変換してツールとして登録し、AI が自動で呼び出すようにしています。
using System.ComponentModel;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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();
// Azure OpenAI 用の Chat Client を登録
builder.AddAzureOpenAIChatClient(
modelDeploymentName,
endpoint,
new AzureCliCredential());
// TimePlugin を登録
builder.Plugins.AddFromType<TimePlugin>();
// Kernel を作成
var kernel = builder.Build();
// Kernel のサービスから IChatClient を取得
var chatClient = kernel.Services.GetRequiredService<IChatClient>();
// IChatClient を使って関数を呼び出す
// PromptExecutionSettings から ChatOptions への変換を使って関数を自動で呼び出す
var response1 = await chatClient.GetResponseAsync("今日は何日?",
new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
}.ToChatOptions(kernel));
#pragma warning disable SKEXP0001
// KernelFunction を AIFunction に変換してツールとして登録
// プレビューなので SKEXP0001 を disable にしないと使えない
var response2 = await chatClient.GetResponseAsync("明日は何日?",
new ChatOptions
{
ToolMode = ChatToolMode.Auto,
Tools = [.. kernel.Plugins.SelectMany(x => x.AsAIFunctions(kernel))]
});
#pragma warning restore SKEXP0001
Console.WriteLine($"今日は何日?→{response1.Text}");
Console.WriteLine($"明日は何日?→{response2.Text}");
// クラスでプラグインを定義
[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();
}
実行すると以下のような結果になります。
今日は何日?→今日は2025年5月27日です。
明日は何日?→今日は2025年5月27日なので、明日は2025年5月28日です。
どちらもちゃんと AI が関数を呼び出して、今日の日付を取得していることがわかります。
内部動作
PromptExecutionSettings
を ChatOptions
に変換する方法は、内部で KernelChatOptions
という型に変換されています。そして関数を呼び出す処理のハンドリングで KernelChatOptions
の場合には Kernel
にある Plugins
から関数を探して呼び出すような特別実装が入っています。
結構動作がややこしいのですが、このようにして PromptExecutionSettings
の FunctionChoiceBehavior
で設定された内容を判別できるようになっています。
まとめ
今回は Semantic Kernel の Chat Completions API を使う方法について見てきました。IChatClient
を使うことで、より細かな制御が可能になり、チャットの履歴を管理しながら AI と対話することができます。また、プラグインと AIFunction
の相互変換も可能で、柔軟な開発ができるようになっています。
まだ、一部プレビュー機能になっていますが API を慎重に選べばプレビューを避けて通れるくらいにはなっています。今後の Semantic Kernel の発展に期待しましょう。
Discussion