🐥

普通と違う感じの Semantic Kernel 入門 005「Chat Completions API を使おう」

に公開

これまでの記事

本文

これまで 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 を使うためのクライアントを取得します。以下のように KernelAddAzureOpenAIChatClient メソッドを使って、クライアントを登録すると KernelServices プロパティ (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);

IChatClientGetResponseAsync メソッドを使って、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 属性でメソッドの説明を付けておくだけで良いです。そして、IChatClientGetResponseAsync メソッドに ChatOptions を渡すオーバーロードがあるので、そこで Tools プロパティに呼び出し候補のツールを渡し、ToolModeChatToolMode.Auto を指定することで、AI がツールを自動的に選択して呼び出すことができるようになります。

Tools プロパティに渡すための AITool クラスは AIFunctionFactory.Create メソッドを使って Delegate から作ることができます。AIFunctionFactory で作成されるクラスは AIFunction なのですが AIFunctionAITool のサブクラスなので 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 を相互に変換することができます。

AIFunctionKernelFunction に変換するには 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 つは PromptExecutionSettingsChatOptions に変換する方法です。これは PromptExecutionSettingsToChatOptions 拡張メソッドを使うことで簡単に変換できます。もう 1 つは KernelFunctionAIFunction に変換する方法です。これは AsAIFunctions 拡張メソッドを使うことで、プラグインの中の KernelFunctionAIFunction に変換してツールとして登録できます。

PromptExecutionSettingsChatOptions に変換する方法はプレビューではないので、普通に使えます。KernelFunctionAIFunction に変換する方法は、プレビュー機能のため SKEXP0001 を disable にしないと使えません。

以下のコードは、今日の日付を聞いている方では PromptExecutionSettingsChatOptions に変換して、AI に関数を自動で呼び出させています。明日の日付を聞いている方では、KernelFunctionAIFunction に変換してツールとして登録し、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 が関数を呼び出して、今日の日付を取得していることがわかります。

内部動作

PromptExecutionSettingsChatOptions に変換する方法は、内部で KernelChatOptions という型に変換されています。そして関数を呼び出す処理のハンドリングで KernelChatOptions の場合には Kernel にある Plugins から関数を探して呼び出すような特別実装が入っています。

結構動作がややこしいのですが、このようにして PromptExecutionSettingsFunctionChoiceBehavior で設定された内容を判別できるようになっています。

まとめ

今回は Semantic Kernel の Chat Completions API を使う方法について見てきました。IChatClient を使うことで、より細かな制御が可能になり、チャットの履歴を管理しながら AI と対話することができます。また、プラグインと AIFunction の相互変換も可能で、柔軟な開発ができるようになっています。

まだ、一部プレビュー機能になっていますが API を慎重に選べばプレビューを避けて通れるくらいにはなっています。今後の Semantic Kernel の発展に期待しましょう。

目次

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

Microsoft (有志)

Discussion