💭

利用者目線での Semantic Kernel v1 入門

2024/04/02に公開

変更履歴

  • 2024/04/02: 初版を公開
  • 2024/04/03: 「Planner で正しい答えを出すようにしてみよう」を追加

はじめに

Microsoft が OSS で開発している Semantic Kernel を入門してみようと思います。

前提条件

この記事は 2024/04/01 時点の情報を元に作成しています。
.NET 版の Semantic Kernel の v1.6.3 をベースに記載しています。

公式ソース

公式ドキュメントは以下になります。

https://learn.microsoft.com/en-us/semantic-kernel/overview/

GitHub で実際にソースコードを見ることも出来ます。
そこまで複雑な感じではないと思うので実際にコードを clone してみてみると面白いと思います。

https://github.com/microsoft/semantic-kernel

特に dotnet/samples/KernslSyntaxExamples は機能ごとのサンプルになっているのでお勧めです。

Azure OpenAI Service か OpenAI が必要

Azure OpenAI Service か OpenAI のどちらかで API を呼べるようにしておく必要があります。

Semantic Kernel とは

Semantic Kernel とは Microsoft の提唱する Copilot Stack の AI orchestration 部分を作成するために作られたライブラリです。


上記図は「マイクロソフト、AI アプリや Copilot の構築フレームワークを明らかにし、AI プラグインエコシステムを拡充」より引用しています。

この Semantic Kernel は .NET 版、Python 版、Java 版が開発されています。この記事では .NET 版についてしか言及しませんが、他の言語も各言語ごとの方言はあるものの基本的に同じような感じで使えるように作られていると思うので参考にはなると思います。

この記事では利用者の目線から Semantic Kernel の使い方を紹介していく形で Semantic Kernel を解説していこうと思います。

Semantic Kernel の使い方

個人的な主観になりますが Semantic Kernel の使い方には、以下のような 3 通りの使い方があります。

  1. 簡易的に OpenAI の API を呼び出すために使う
  2. テンプレート エンジンを兼ね備えた OpenAI を呼び出すための便利ライブラリとして使う
  3. プラグインを使って AI にある程度自発的に問題解決をしてもらうエージェントっぽく使う

下に行くほど、Semantic Kernel の機能を使いこなしていると思います。ただ、1 の使い方だけでも地味に便利ではあるので、まずは 1 から始めてみましょう。

簡易的に OpenAI の API を呼び出すために使う

OpenAI の API の一番簡単な使い方は、文字列を渡してそれに対する応答を AI に生成してもらうという使い方だと思います。Semantic Kernel も、そのような使い方をすることが出来ます。やってみましょう。

コンソールアプリケーションを作成して、以下の NuGet パッケージを追加します。

  • Microsoft.SemanticKernel

そして以下のようなコードを書いてください。Azure OpenAI Service の場合と OpenAI の場合ですこしコードが異なるので 2 通りのコードを記載しています。これ以降は Azure OpenAI Service のみ記載するので OpenAI の方を使う人は読み替えてください。

Azureの場合
using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");

var kernel = builder.Build();
var result = await kernel.InvokePromptAsync(
    """
    Hello, world! と表示する C# のプログラムを書いてください。
    """);
Console.WriteLine(result.GetValue<string>());
OpenAIの場合
using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
    "gpt-3.5-turbo",
    "<< ここに OpenAI の API Key を記載する >>");

var kernel = builder.Build();
var result = await kernel.InvokePromptAsync(
    """
    Hello, world! と表示する C# のプログラムを書いてください。
    """);
Console.WriteLine(result.GetValue<string>());

このコードは、Kernel のインスタンスを作成して、InvokePromptAsync で AI にプロンプトを渡して結果を取得して表示しています。
Kernel のインスタンスは、Kernel.CreateBuilder()KernelBuilder のインスタンスを作成して、AddAzureOpenAIChatCompletion または AddOpenAIChatCompletion を呼び出して OpenAI の API を追加し、Build()Kernel のインスタンスを作成しています。

このコードを実行すると以下のような結果が表示されます。

以下は、C#でHello, world!を表示するプログラムの例です。

`​``csharp
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello, world!");
    }
}
`​``

上記のコードをコンパイルし実行すると、コンソールに "Hello, world!" が表示されます

非常に簡単ですね。最終的には InvokePromptAsync に渡された文字列は OpenAI の Chat Completion API のユーザーメッセージとして渡されています。この挙動を理解していれば、非常に簡単なものを作るときであれば便利ですね。

そうはいっても Chat Completion API をもう少し踏み込んで使いたい場合は

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// Chat の履歴を作成
var chatHistory = new ChatHistory();
chatHistory.AddSystemMessage("""
    ソースコードジェネレーターとして振舞ってください。
    ユーザーが指示する内容の C# のソースコードを生成してください。
    生成結果には C# のコード以外を含まないようにしてください。
    """);

// few shots
chatHistory.AddUserMessage("Hello, world! と表示するプログラムを書いてください。");
chatHistory.AddAssistantMessage("""
    using System;

    class Program
    {
        static void Main()
        {
            Console.WriteLine("Hello, world!");
        }
    }
    """);
chatHistory.AddUserMessage("10 * 300 の結果を表示するプログラムを書いてください。");
chatHistory.AddAssistantMessage("""
    using System;

    class Program
    {
        static void Main()
        {
            Console.WriteLine($"10 * 300 = {10 * 300}");
        }
    }
    """);

// 本番!
chatHistory.AddUserMessage("与えられた数字が素数かどうか判定するプログラムを書いてください。");

// IChatCompletionService を使って Chat Completion API を呼び出す
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var response = await chatService.GetChatMessageContentAsync(chatHistory);

// 結果からテキストを取り出して表示
if (response.Items.FirstOrDefault() is TextContent responseText)
{
    Console.WriteLine(responseText.Text);
}
else
{
    Console.WriteLine("None");
}

いくつか few shots を追加して、AI に期待する挙動を把握してもらったうえで実際のコード生成に挑むような流れになっています。実行すると以下のような結果になります。

using System;

class Program
{
    static void Main()
    {
        int number = 13; // 判定する数字

        bool isPrime = true; // 素数かどうかを判定するフラグ

        if (number <= 1)
        {
            isPrime = false; // 1以下の数字は素数ではない
        }
        else
        {
            for (int i = 2; i <= Math.Sqrt(number); i++)
            {
                if (number % i == 0)
                {
                    isPrime = false; // 素数でないことがわかった時点でループを抜ける
                    break;
                }
            }
        }

        if (isPrime)
        {
            Console.WriteLine($"{number} is a prime number");
        }
        else
        {
            Console.WriteLine($"{number} is not a prime number");
        }
    }
}

ぱっと見ちゃんとしてそうですね。
ChatHisotry クラスでチャット履歴を管理して IChatCompletionService を使って Chat Completion API を呼び出しています。結果の Items プロパティに結果が入っていて文字列の生成結果は TextContent 型として格納されているので、それを取り出して表示するといった流れでの使い方になります。

画像生成系の場合は ImageContent 型などがあります。

因みに InvokePromptAsync メソッドでもチャットを扱うことが出来ます。文字列が message というタグで記載されている場合は内部的に ChatHistory に変換されて Chat Completion API に渡されます。上記のコードと同じよう処理を InvokePromptAsync を使ってやると以下のようになります。

using Microsoft.SemanticKernel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// message タグで ChatHistory を定義することでチャットの履歴を表すことが出来る
var response = await kernel.InvokePromptAsync("""
    <message role="system">
        ソースコードジェネレーターとして振舞ってください。
        ユーザーが指示する内容の C# のソースコードを生成してください。
        生成結果には C# のコード以外を含まないようにしてください。
    </message>
    <message role="user">
        Hello, world! と表示するプログラムを書いてください。
    </message>
    <message role="assistant">
        using System;

        class Program
        {
            static void Main()
            {
                Console.WriteLine("Hello, world!");
            }
        }
    </message>
    <message role="user">
        10 * 300 の結果を表示するプログラムを書いてください。
    </message>
    <message role="assistant">
        using System;

        class Program
        {
            static void Main()
            {
                Console.WriteLine($"10 * 300 = {10 * 300}");
            }
        }
    </message>
    <message role="user">
        与えられた数字が素数かどうか判定するプログラムを書いてください。
    </message>
    """);

// 結果を表示
Console.WriteLine(response.GetValue<string>());

実行結果は同じなので結果は省略します。

このように Semantic Kernel を使うことで生の Azure.AI.OpenAI パッケージを使うよりも、簡単に OpenAI の API を使うことが出来ます。また、Semantic Kernel の API は色々な AI モデルでも使うことが出来るように抽象化されているため別の生成 AI のモデルでも同じコードで呼び出せるようになる(と期待できる)のも 1 つのメリットになると思います。

テンプレート エンジンを兼ね備えた OpenAI を呼び出すための便利ライブラリとして使う

これまでの使い方でも、簡単に OpenAI の Chat Completion API を呼び出して結果を受け取るのには十分な感じです。ここから少しずつ Semantic Kernel の機能を使うようにしていきましょう。
そこで、1 つ注意点なのですが、この段階では Semantic Kernel のプラグイン機能を使って、その呼び出し結果をプロンプトに埋め込むといったことをやります。

まぁ便利と言えば便利なのですが、この使い方だと C# の文字列補間などで関数の実行結果などを埋め込むだけでも同じことができます。あんまり便利さは感じ無いと思います。この段階は次へ進むための前準備的な立ち位置になります。

プロンプトに変数を埋め込んで AI に問い合わせる

では簡単な所からやっていきます。InvokePromptAsync メソッドですが arguments という省略可能な引数があります。この引数は名前のとおりプロンプト呼び出しのための引数を渡すためのものです。ここに渡すのは KernelArguments 型です。このクラスは IDictionary<string, object?> を実装しているので端的に言うとディクショナリでキーの文字列は変数名で値の object? に変数の値を設定します。プロンプト内で {{$変数名}} という書き方で変数を埋め込むことが出来ます。

では試してみましょう。

using Microsoft.SemanticKernel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// name 変数を持ったプロンプト
var response = await kernel.InvokePromptAsync("""
    私の名前の最初の文字から始まる短歌を作成してください。

    ### 私の名前
    {{$name}}
    """,
    // arguments 引数で name のパラメーターの値を指定
    arguments: new KernelArguments
    {
        ["name"] = "大田 一希",
    });

Console.WriteLine(response.GetValue<string>());

実行すると以下のような結果になりました。

大地を踏み 空を仰ぐ心 揺れる一希

プロンプトに、関数の呼び出し結果を埋め込む

変数の埋め込みの次は関数の呼び出し結果の埋め込みをしてみたいと思います。
Semantic Kernel にはプラグインという仕組みがあります。プラグインは関連する機能を纏めた関数の集まりです。
関数は C# のネイティブのコードやプロンプトを関数として登録することが出来ます。

まずは簡単なネイティブの関数を使ったプラグインを使ってみます。

ネイティブ関数を持ったプラグインは KernelFunction 属性を付けたメソッドを持ったクラスを定義することで作成できます。例えば現在時間を返すような関数と足し算をするような関数をもった Utils プラグインを定義してみます。

// ネイティブ関数を持ったプラグインの定義
class Utils(TimeProvider timeProvider)
{
    [KernelFunction]
    public string LocalNow() => timeProvider.GetLocalNow().ToString("u");

    [KernelFunction]
    public int Add(int x, int y) => x + y;
}

作成したプラグインをプロンプト内で使えるようにするには Kernel に対してプラグインの登録を行う必要があります。プラグインの登録は KernelPlugins.AddFromXXXX (XXXX の部分に、どこからプラグインを取り込むかが入る) というメソッドを使って行います。今回のケースでは Plugins.AddFromObject メソッドで Utils クラスのインスタンスをプラグインとして登録して使うことができます。

登録されたプラグインは {{ プラグイン名.関数名 引数 }} といった形で記載できます。複数個の引数がある場合は {{ プラグイン名.関数名 引数名='値' 引数名='値'}} といった形で指定できます。

先ほど作成した Utils クラスをプラグインとして登録をしてプロンプト内で使用しているコード例になります。処理としては特に意味はありませんがプラグインの登録方法や関数の呼び出し方法の例として参考にしてください。

using Microsoft.SemanticKernel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// プラグインの登録
kernel.Plugins.AddFromObject(new Utils(TimeProvider.System));

// 関数呼び出しも含んだプロンプト
var response = await kernel.InvokePromptAsync("""
    参考情報の内容をすべて使って短文を生成してください。

    ### 参考情報
    - 今日の日付: {{ Utils.LocalNow }}
    - あなたの名前: {{ $name }}
    - 1 + 2 の計算結果: {{ Utils.Add x='1' y='2' }}
    """,
    // arguments 引数で name のパラメーターの値を指定
    arguments: new KernelArguments
    {
        ["name"] = "大田 一希",
    });

Console.WriteLine(response.GetValue<string>());

また、プロンプトも関数化してプラグインとして登録してプロンプト内で呼び出すことも出来ます。プロンプトから作られた関数はプロンプト関数と呼ばれます。

using Microsoft.SemanticKernel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// プロンプトから関数を作成
var func1 = kernel.CreateFunctionFromPrompt(
    new PromptTemplateConfig("""
        与えられた季節の季語を 3 つ挙げてください。

        ### 季節
        {{ $season }}
        """)
    {
        Name = "Generate",
        InputVariables = [
            new InputVariable { Name = "season", IsRequired = true },
        ],
    });
// 生成した関数をプラグインとして登録
kernel.Plugins.AddFromFunctions(
    "TestPlugin",
    [func1]); // 関数は複数個を纏めて登録も可能

// 関数呼び出しも含んだプロンプト
var response = await kernel.InvokePromptAsync("""
    与えられたテーマに関連する俳句を1つ作成してください。
    俳句の作成にはテーマに加えて参考情報にある季語も1つ組み込んで作成してください。

    ### テーマ
    {{ $season }}の朝

    ### 季語
    {{ TestPlugin.Generate $season }}
    """,
    arguments: new KernelArguments
    {
        ["season"] = "春",
    });

// 結果を表示
Console.WriteLine(response.GetValue<string>());

季語を生成するプロンプト関数を作成して、それを俳句を生成するプロンプト内から {{ TestPlugin.Generate }} として呼び出しています。

実行すると以下のような結果になりました。嫌な感じの俳句ですね…。

桜咲き 花粉を感じつつ 春の朝

このように、プロンプトにプロンプトや C# のメソッドの結果を埋め込んで作成した結果をプロンプトとして AI に問い合わせるといったことが出来ます。これくらいだったら文字列補間でもいいんですけどね…。

この他にもプロンプト関数を使ったプラグインの作成方法にはディレクトリ内に特定のルールに従って配置されたファイルをプラグインとして登録する方法もあります。詳細については、この記事以外で気力がわいたら書いてみようと思います。このようなケースでは C# の文字列補間では出来ないので、便利度が増してくると思います。また組み込みのメソッドは無いのですが今回使用した Plugins.AddFromFunctions を使えば、任意の場所に保存していたプロンプトからプロンプト関数を生成してプラグインとして登録することが出来ます。

プラグインを使って AI にある程度自発的に問題解決をしてもらうエージェントっぽく使う

最後に、Semantic Kernel のプラグイン機能を使って AI にある程度自発的に問題解決をしてもらうエージェントっぽく使う方法を紹介します。
恐らく、ここら辺から Semantic Kernel を使っている感が出てくるところだと思います。とはいえ、ここら辺の機能は Planner と呼ばれる機能を使うのが恐らく本来のやり方になります。

ただ、現状 Planner はプレビュー機能なため今回は使わないで行こうと思います。

Planner を使わない場合は OpenAI の関数呼び出し (Function calling) を使って似たようなことが出来ます。関数呼び出しは Chat Completion API の tools パラメーターに AI が使うことが出来るツールの仕様を一緒に渡すことで AI が必要に応じて、ユーザーからの問いに対して、この tool を使いたい!といったような返答をしてくれるようになる機能です。
その返答をハンドリングして対象の tool に対応する処理をプログラムで実行して、その結果を再度 AI に渡すことで、続きの処理を AI がやってくれるといった感じの使い方をする機能になります。

Semantic Kernel を使うと、この tools のパラメーターにプラグインに登録されている関数を自動的に設定してくれる機能があります。さらに通常であれば AI からの応答に対して tool の呼び出しが求められた場合は、自分で対応する tool の呼び出しや tool の実行結果を AI に返す処理などを書かないといけないところを Semantic Kernel が自動でやってくれるようにするオプションがあります。
これを使うことで、プラグインにある機能を使って自動的に AI がやりたいことを自発的にやってくれるような動きを実現できます。

ということで、やってみましょう。

まずは、プラグインを tools パラメーターに渡すために必要な下準備を行います。
これまで作成してきたプラグインではあえて追加していなかったのですが、本来であればプラグインには Description を追加してあげる必要があります。ネイティブ関数のプラグインであれば Description 属性を使ってクラスやメソッドや引数や戻り値に対して、それがどんな意味を持つものなのかを記述します。プロンプト関数の場合はプロンプト関数を生成するメソッドに description といった引数や、パラメーターに対しても description を追加するような引数があります。

一般的に以下のような情報を付与することになります。

  • プラグインの概要
  • 関数の概要
    • 関数自体が何をするものなのかを説明した概要
    • パラメーターの概要
    • パラメーターが必須かどうか
    • 戻り値の概要

では手始めに基礎的な計算をする機能を持ったネイティブ関数ベースのプラグインを作成して AI が苦手とする計算問題を解いてもらおうと思います。

まずは Description 属性でガチガチに概要を追記した MathPlugin というクラスを以下のように定義します。

[Description("四則演算プラグイン")]
class MathPlugin
{
    [KernelFunction]
    [Description("足し算を行います。")]
    [return: Description("計算結果")]
    public double Add(
        [Description("左辺値")] double a, 
        [Description("右辺値")] double b)
    {
        Console.WriteLine($"{a} + {b} を計算中…");
        return a + b;
    }

    [KernelFunction]
    [Description("引き算を行います。")]
    [return: Description("計算結果")]
    public double Sub(
        [Description("左辺値")] double a, 
        [Description("右辺値")] double b)
    {
        Console.WriteLine($"{a} - {b} を計算中…");
        return a - b;
    }

    [KernelFunction]
    [Description("掛け算を行います。")]
    [return: Description("計算結果")]
    public double Mul(
        [Description("左辺値")] double a,
        [Description("右辺値")] double b)
    {
        Console.WriteLine($"{a} * {b} を計算中…");
        return a * b;
    }

    [KernelFunction]
    [Description("割り算を行います。")]
    [return: Description("計算結果")]
    public double Div(
        [Description("左辺値")] double a,
        [Description("右辺値")] double b)
    {
        Console.WriteLine($"{a} / {b} を計算中…");
        return a / b;
    }
}

では、このプラグインを使って AI に計算問題を解いてもらうプログラムを作成してみましょう。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

// 今回の MathPlugin は普通にインスタンス化するだけでいいので AddFromObject ではなく
// AddFromType で登録
kernel.Plugins.AddFromType<MathPlugin>();

var func = kernel.CreateFunctionFromPrompt("""
    以下の式の計算結果を求めてください。
    計算は一気に行わずに、1つずつ計算してください。

    ## 式
    4 / 2 + 1000 - 1000 * 2 + 30000
    """,
    executionSettings: new OpenAIPromptExecutionSettings
    {
        // これで自動でプラグインを tools パラメーターに設定したうえで自動で呼び出しまでしてくれるようになる
        // 現状自動呼出しを行ってくれる回数は 5 回まで
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    });

var functionResult = await kernel.InvokeAsync(func);
Console.WriteLine(functionResult.GetValue<string>());

ポイントは CreateFunctionFromPrompt メソッドの executionSettings 引数で指定している OpenAIPromptExecutionSettingsToolBallBehaviorToolCallBehavior.AutoInvokeKernelFunctions を指定しているところです。これを指定することで自動的にプラグインを tools パラメーターに渡して、なおかつ必要があれば自動で呼び出しを行い結果を使って AI に継続の処理をやってくれます。便利!

実行すると以下のような結果になります。

4 / 2 を計算中…
2 + 1000 を計算中…
1000 * 2 を計算中…
1002 - 2000 を計算中…
-998 + 30000 を計算中…
式 4 / 2 + 1000 - 1000 * 2 + 30000 の計算結果は 29002 です。

ちゃんとプラグインの関数の中で Console.WriteLine を使って計算の途中経過を表示するようにしているので、計算の途中経過も表示されています。プラグインがちゃんと呼ばれていますね。
そして、計算結果もバッチリです。

因みに ToolCallBehavior を設定している部分をコメントアウトすると以下のような結果になりました。

計算を1つずつ行います。

まず、4を2で割ります。
4 / 2 = 2

次に、2に1000を足します。
2 + 1000 = 1002

その後、1000を2倍します。
1000 * 2 = 2000

最後に、1002から2000を引きます。
1002 - 2000 = -998

最後の計算結果は-998です。

何回か実行するとたまに答えがあうこともありますが、なんとなく間違えることが多い印象です。

ということで適切な説明文が付与されたプラグインがあると AI が自動的に目的を達成するためにやってくれることが分かったと思います。

プランナーも使ってみよう

プレビューなので、使い方が変わるかもしれませんが、一応プランナーも使ってみましょう。
プランナーには Handlebars というテンプレートエンジンを使う HandlebarsPlanner と、上で使用したのと同じ Function calling を使う FunctionCallingStepwisePlanner という 2 種類のプランナーがあります。

HandlebarsPlanner は事前にこういう順番で処理を行うというプランを AI に立ててもらって、それを実行するという事が出来ます。プランの実行前に人やプログラムで確認する余地がある点が特徴ですね。
FunctionCallingStepwisePlanner は問題が解決するまで自動的に自立的に処理を行うプランナーです。目的を達成するか、指定した回数の試行のイテレーションに達するまで自動的に動きます。

細かいことを書いてもリリース版で変わるかもしれないのでサクッと使ってみましょう。まずは HandlebarsPlanner です。Microsoft.SemanticKernel.Planners.Handlebars のプレビュー版パッケージを追加してコードを以下のように書いてください。MathPlugin は引き続き同じものを使用します。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning.Handlebars;
using System.ComponentModel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

kernel.Plugins.AddFromType<MathPlugin>();

// 実験的機能なので警告が出る。それを無効化して使う必要がある。
#pragma warning disable SKEXP0060
var planner = new HandlebarsPlanner();
var plan = await planner.CreatePlanAsync(kernel, """
    以下の式の計算結果を求めてください。
    計算は一気に行わずに、1つずつ計算してください。
    
    ## 式
    4 / 2 + 1000 - 1000 * 2 + 30000
    """);

Console.WriteLine("## 実行するプラン");
Console.WriteLine(plan);

Console.WriteLine("## プランを実行中...");
var result = await plan.InvokeAsync(kernel);

Console.WriteLine("## プランの実行結果");
Console.WriteLine(result);

実行すると以下のような結果になりました。いくらステップバイステップで計算するように指示しても、AI が一気に答えを出すためのプランを組む必要があるので、やっぱり計算順番を間違えたりしていますね。

## 実行するプラン
{{!-- Step 0: Extract Key Values --}}
{{set "number1" 4}}
{{set "number2" 2}}
{{set "number3" 1000}}
{{set "number4" 2}}
{{set "number5" 30000}}

{{!-- Step 1: Perform Calculations --}}
{{set "result1" (MathPlugin-Div a=number1 b=number2)}}
{{set "result2" (MathPlugin-Mul a=number3 b=number4)}}
{{set "result3" (MathPlugin-Sub a=number2 b=result2)}}
{{set "result4" (MathPlugin-Add a=result3 b=number5)}}
{{set "finalResult" (MathPlugin-Add a=result1 b=result4)}}

{{!-- Step 2: Output the Result --}}
{{json finalResult}}
## プランを実行中...
4 / 2 を計算中…
1000 * 2 を計算中…
2 - 2000 を計算中…
-1998 + 30000 を計算中…
2 + 28002 を計算中…
## プランの実行結果
28004

続けて FunctionCallingStepwisePlanner を使ってみます。こちらは、1 ステップごとに AI に考えてもらうような動きになるので期待できます。Microsoft.SemanticKernel.Planners.OpenAI パッケージを追加して以下のようなコードに書き換えます。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning;
using System.ComponentModel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

kernel.Plugins.AddFromType<MathPlugin>();

// 実験的機能なので警告が出る。それを無効化して使う必要がある。
#pragma warning disable SKEXP0060
var planner = new FunctionCallingStepwisePlanner();
var result = await planner.ExecuteAsync(kernel, """
    以下の式の計算結果を求めてください。
    計算は一気に行わずに、1つずつ計算してください。
    
    ## 式
    4 / 2 + 1000 - 1000 * 2 + 30000
    """);

Console.WriteLine("## プランの実行結果");
Console.WriteLine(result.FinalAnswer);

実行すると以下のような結果になりました。

4 / 2 を計算中…
1000 * 2 を計算中…
2 + 2000 を計算中…
2002 - 1000 を計算中…
1002 + 30000 を計算中…
## プランの実行結果
31002

はい、間違えました。これは FunctionCallingStepwisePlanner が裏で1ステップずつ実行するためのプロンプトを独自に組んだりしているので、恐らくそれの影響もあるのだと思います。少なくとも今回、私が作った適当なプロンプトとの組み合わせが悪かっただけかもしれません。

Planner が裏で使っているプロンプトは引数で変更することも出来るので本気で使うなら、そこらへんの調整も必要になるかもしれません。

Planner で正しい答えを出すようにしてみよう

Planner は、元々ユーザーが指定したゴールを達成するために思考をするようなプロンプトが組み込まれていて、そこにユーザーがしたいことを埋め込んだプロンプトを生成して色々やってくれる部品です。なので私が組み込んでた適当なプロンプトのせいで精度が出てない可能性があるので、シンプルに計算だけしてほしいとお願いをしてみました。

まずは HandlebarsPlanner で試してみます。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning.Handlebars;
using System.ComponentModel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

kernel.Plugins.AddFromType<MathPlugin>();

// 実験的機能なので警告が出る。それを無効化して使う必要がある。
#pragma warning disable SKEXP0060
var planner = new HandlebarsPlanner();
var plan = await planner.CreatePlanAsync(kernel, """
    4 / 2 + 1000 - 1000 * 2 + 30000 を計算してください。
    """);
Console.WriteLine("## AI が考えたプラン");
Console.WriteLine(plan);

Console.WriteLine("## プランを実行");
var result = await plan.InvokeAsync(kernel);

Console.WriteLine("## 結果");
Console.WriteLine(result);

実行すると…、ダメでした。

## AI が考えたプラン
{{!-- Step 0: Extract key values --}}
{{set "a" 4}}
{{set "b" 2}}
{{set "c" 1000}}
{{set "d" 30000}}

{{!-- Step 1: Calculate the individual operations --}}
{{set "result1" (MathPlugin-Div a b)}}
{{set "result2" (MathPlugin-Mul c 2)}}
{{set "result3" (MathPlugin-Sub result1 result2)}}
{{set "result4" (MathPlugin-Add result3 d)}}

{{!-- Step 2: Print the result --}}
{{json result4}}
## プランを実行
4 / 2 を計算中…
1000 * 2 を計算中…
2 - 2000 を計算中…
-1998 + 30000 を計算中…
## 結果
28002

エラーも良く出るし、答えも間違えてばかりでした。続けて FunctionCallingStepwisePlanner で試してみます。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning;
using System.ComponentModel;

// お決まりの初期化コード
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "<<gpt 3.5 turbo のモデルのデプロイ名>>",
    "https://<<Azure OpenAI Service のリソース名>>.openai.azure.com/",
    "<<Azure OpenAI Service の API キー>>");
var kernel = builder.Build();

kernel.Plugins.AddFromType<MathPlugin>();

// 実験的機能なので警告が出る。それを無効化して使う必要がある。
#pragma warning disable SKEXP0060
var planner = new FunctionCallingStepwisePlanner();
var result = await planner.ExecuteAsync(kernel, """
    4 / 2 + 1000 - 1000 * 2 + 30000 を計算してください。
    """);

Console.WriteLine("## プランの実行結果");
Console.WriteLine(result.FinalAnswer);

実行すると以下のような結果になりました。

1000 * 2 を計算中…
1002 - 2000 を計算中…
-998 + 30000 を計算中…
## プランの実行結果
29002

こっちは正解ですね!一部計算は AI が自分でやってるっぽいですが、それでも正解を出してくれました。

ということで Planner を使う場合は与える goal はやりたいことだけに絞って渡した方が良さそうでした。思考の過程をカスタマイズする場合は、その部分のプロンプトのカスタマイズを別途行った方が良さそうな雰囲気です。

その他、積み残し

さて、ということで、Semantic Kernel の使い方についてざっくりと紹介してみました。まだまだ使い方は色々あります。ぱっと思いつく説明していない内容だけでも以下のようなものがあります。

  • Kernel のインスタンス化方法
    • 素直に new する方法
    • DI コンテナを使ってインスタンス化する方法
  • プラグイン関連
    • フォルダーにあるプロンプトやメタデータが記載されたファイルをプラグインとして登録する方法
    • KernalBuilder にプラグインを登録する方法
    • DI コンテナを使ったときのプラグインの登録方法
    • 組込みのプラグイン
    • ベクトルDBとかを使うようなケース
  • Function calling 関連
    • 自動で tool を呼び出すのではなくマニュアルでハンドリングをする方法
  • Chat Completion API 以外の AI を使う方法
  • API キー認証ではなく Azure の Managed ID 認証をする
  • etc...

ここら辺は過去の記事でちょっと書いていたりするものもあれば、現状プレビュー版の機能なのでとりあえずいいかな…と思って放置しているものもあります。気が向いたら今度もこんな感じで書いていけたらいいなと思っています。

まとめ

ということで Semantic Kernel を以下の 3 つの視点でざっくりと紹介してみました。

  1. 簡易的に OpenAI の API を呼び出すために使う
  2. テンプレート エンジンを兼ね備えた OpenAI を呼び出すための便利ライブラリとして使う
  3. プラグインを使って AI にある程度自発的に問題解決をしてもらうエージェントっぽく使う

個人的には 2 の状態で使うことはあんまりなくて、使うなら 1 or 3 かなと思います。
なかなか、自分のコードのコアロジック部分に Semantic Kernel の API がにじみ出てると気持ち悪いのと、実際にそこまで複雑な処理をしていなかったり、素の SDK の機能で十分!!という程度のことしかまだしていないので Semantic Kernel をちゃんと使えていませんが Function calling は確実に素の SDK を使うよりは楽なので Function calling をしっかり使う機会があれば Semantic Kernel を使おうと虎視眈々と狙っていたりします。

ということで、Semantic Kernel についてざっくりと紹介してみました。今回はこの辺で。

Microsoft (有志)

Discussion