📑

普通と違う感じの Semantic Kernel 入門 002「テンプレートエンジン」

に公開

これまでの記事

本文

前回の最後で AI を呼び出すための関数を説明をすると書きましたが、その前にテンプレートエンジンについてやろうと思います。
というのも、このテンプレートエンジンによって生成された文字列が AI の呼び出しに渡されるため切っても切れない関係だからです。

Semantic Kernel のテンプレートエンジン

Semantic Kernel では、AI を呼び出す関数経由では基本的にテンプレートエンジンを使用してプロンプトが生成されます。
Semantic Kernel のテンプレートエンジンは IPromptTemplateFactory によって、なんのテンプレートエンジンを使うかを差し替えることが出来るようになっています。

例えば以下のようなテンプレートエンジンの実装が用意されています。

  • EchoPromptTemplateFactory: 引数をそのまま返すテンプレートエンジン
  • KernelPromptTemplateFactory: Semantic Kernel の既定のテンプレートエンジン。基本的な変数の展開や関数呼び出し機能を持つ。
  • HandlebarsPromptTemplateFactory: Handlebars のテンプレートエンジン。ループや分岐などもサポートしていて多機能。
  • LiquidPromptTemplateFactory: Liquid のテンプレートエンジン。Handlebars と同様に多機能。この中では一番新しい。
  • AggregatorPromptTemplateFactory: 複数のテンプレートエンジンを組み合わせて使うためのテンプレートエンジン。

昔は AI に送信するテンプレートを駆使して、システムプロンプトに外部のリソースからとってきた情報を埋め込むといったことをしていました。
そのために、ループや分岐といった機能を持つ Handlebars を使って色々やることも多かったと思います。ですが、最近は AI の関数呼び出し機能を使って外部のリソースの情報を必要に応じて AI が取得してくることも増えてきました。そのため若干出番は減っていると思います。そのためここでは既定の KernelPromptTemplateFactory のみを少し深堀していこうと思います。

KernelPromptTemplateFactory の使い方

では、KernelPromptTemplateFactory の使い方を見ていきましょう。
まずは、KernelPromptTemplateFactory を使ってテンプレートを作成し、変数を設定してレンダリングする基本的な例を見ていきます。

using Microsoft.SemanticKernel;

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new KernelPromptTemplateFactory();
// テンプレートを作成
IPromptTemplate template = templateFactory.Create(new PromptTemplateConfig("""
    Hello, {{$name}}!
    """));

// テンプレートに変数を設定してレンダリング
string prompt = await template.RenderAsync(new Kernel(), new KernelArguments
{
    ["name"] = "Kazuki",
});

// レンダリングされたプロンプトを表示
Console.WriteLine(prompt);

普通に Semantic Kernel を使っていると、ここらへんの API を直接使うことはありませんが、大体中でこんなことが行われています。
TemplateFactoryCreate メソッドでテンプレートを作成し、RenderAsync メソッドで変数を設定してレンダリングしています。
KernelArguments を使って変数を設定し、テンプレート内で {{$name}} のように変数を参照しています。
この例では、Hello, Kazuki! という文字列が出力されます。

Create メソッド以外にも TryCreate メソッドもあります。こちらはよくある戻り値が bool のパターンで、テンプレートの作成に失敗した場合は false を返します。IPromptTemplateout 引数で返されます。

この単純なコード内にも色々と暗黙のルールがあったりします。例えば LiquidPromptTemplateFactory を使うように書き換えると例外が出てしまいます。試してみましょう。
以下の NuGet パッケージをインストールします。

  • Microsoft.SemanticKernel.PromptTemplates.Liquid

そしてコードを以下のように書き換えます。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.PromptTemplates.Liquid;

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new LiquidPromptTemplateFactory();
// テンプレートを作成
IPromptTemplate template = templateFactory.Create(new PromptTemplateConfig("""
    Hello, {{$name}}!
    """));

// テンプレートに変数を設定してレンダリング
string prompt = await template.RenderAsync(new Kernel(), new KernelArguments
{
    ["name"] = "Kazuki",
});

// レンダリングされたプロンプトを表示
Console.WriteLine(prompt);

実行すると以下のような例外が発生します。

Microsoft.SemanticKernel.KernelException: Prompt template format semantic-kernel is not supported.
   at Microsoft.SemanticKernel.PromptTemplateFactoryExtensions.Create(IPromptTemplateFactory factory, PromptTemplateConfig templateConfig)
   at Program.<Main>$(String[] args) in D:\Repos\runceel\sk-edu\SK-Edu\002-PromptTemplate\PromptTemplate001\Program.cs:line 7

このメッセージにある通りテンプレートを指定している PromptTemplateConfig をデフォルトのまま使うと Semantic Kernel の既定のテンプレートエンジンである KernelPromptTemplateFactory が指定されます。
LiquidPromptTemplateFactory を使う場合は、テンプレートのフォーマットを明示的に指定する必要があります。これをするには PromptTemplateConfigTemplateFormat プロパティを設定することで使用する必要があります。TemplateFormat に指定する値は各 PromptTemplateFactory の実装に static 変数として定義されています。LiquidPromptTemplateFactory の場合は LiquidPromptTemplateFactory.LiquidTemplateFormat になります。

ということで LiquidPromptTemplateFactory を使うように以下のようにするには以下のように書き換えが必要です。ついでにテンプレートエンジンも Liquid のものに書き換えます。変数の指定は単純に {{name}} になります。$ を付ける必要はありません。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.PromptTemplates.Liquid;

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new LiquidPromptTemplateFactory();
// テンプレートを作成
IPromptTemplate template = templateFactory.Create(
    new PromptTemplateConfig("""
        Hello, {{name}}!
        """)
    {
        // テンプレートのフォーマットをLiquidに設定
        TemplateFormat = LiquidPromptTemplateFactory.LiquidTemplateFormat,
    });

// テンプレートに変数を設定してレンダリング
string prompt = await template.RenderAsync(new Kernel(), new KernelArguments
{
    ["name"] = "Kazuki",
});

// レンダリングされたプロンプトを表示
Console.WriteLine(prompt);

今度は実行すると、ちゃんと以下のように表示されます。テンプレートエンジンが動いていることがわかります。

Hello, Kazuki!

テンプレートエンジンの切り替え

このままでは、1つのテンプレートにしか対応できません。不便です。
1つのアプリ内で使用するテンプレートエンジンが複数あるケースも考えられます。そのような状況に対応するためには AggregatorPromptTemplateFactory を使います。これは複数のテンプレートエンジンをまとめて、1 つのテンプレートエンジンとして扱うことができます。PromptTemplateConfigTemplateFormat プロパティに指定されたテンプレートエンジンのフォーマットに応じて、適切なテンプレートエンジンを選択してくれます。

では、LiquidPromptTemplateFactoryKernelPromptTemplateFactoryEchoPromptTemplateFactory の 3 つを使えるように書き換えてみましょう。イメージとしては liquid と Semantic Kernel のテンプレートエンジンを使いつつ、無効なフォーマットが指定された場合は EchoPromptTemplateFactory を使うようにします。

以下のようにコードを書き換えます。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.PromptTemplates.Liquid;

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new AggregatorPromptTemplateFactory(
    // 複数のテンプレートファクトリを登録
    new LiquidPromptTemplateFactory(),
    new KernelPromptTemplateFactory(),
    new EchoPromptTemplateFactory());

// 何も指定していないので KernelPromptTemplateFactory がデフォルトで使用される
IPromptTemplate kernelTemplate = templateFactory.Create(
    new PromptTemplateConfig("""
        Kernel, {{$name}}!
        """));
// TemplateFormat が設定されているため LiquidPromptTemplateFactory が使用される
IPromptTemplate liquidTemplate = templateFactory.Create(
    new PromptTemplateConfig("""
        Liquid, {{name}}!
        """)
    {
        // テンプレートのフォーマットをLiquidに設定
        TemplateFormat = LiquidPromptTemplateFactory.LiquidTemplateFormat,
    });
// TemplateFormat が設定されているため LiquidPromptTemplateFactory が使用される
IPromptTemplate echoTemplate = templateFactory.Create(
    new PromptTemplateConfig("""
        Echo, {{name}}!
        """)
    {
        // 無効なテンプレートのフォーマットを設定
        TemplateFormat = "犬派×きのこ派",
    });

// テンプレートに渡す変数
var arguments = new KernelArguments
{
    ["name"] = "Kazuki",
};
// ダミーの空のカーネル
var kernel = new Kernel();

// レンダリングされたプロンプトを表示
Console.WriteLine($"""
    {await kernelTemplate.RenderAsync(kernel, arguments)}
    {await liquidTemplate.RenderAsync(kernel, arguments)}
    {await echoTemplate.RenderAsync(kernel, arguments)}
    """);

TemplateFormat 無し(デフォルト)の場合は KernelPromptTemplateFactory が使用されます。
TemplateFormatLiquidPromptTemplateFactory.LiquidTemplateFormat を指定した場合は LiquidPromptTemplateFactory が使用されます。
TemplateFormat に無効な値を指定した場合は EchoPromptTemplateFactory が使用されます。
実行すると以下のように表示されます。

Kernel, Kazuki!
Liquid, Kazuki!
Echo, {{name}}!

Echo の場合は変数が展開されずにそのまま表示されます。ちゃんと思った通りに動いていますね。このようにして複数のテンプレートエンジンを使うことが出来ます。

KernelPromptTemplateFactory の機能

KernelPromptTemplateFactory は Semantic Kernel の既定のテンプレートエンジンで、基本的な変数の展開や関数呼び出し機能を持っています。
変数の展開は、{{$name}} のように {{}} で囲んで $ を指定します。その他にテンプレート内から関数を呼ぶことも出来ます。$ が無いと関数として認識されます。

テンプレートから呼び出す関数は Kernel の中にプラグインとして登録されている必要があります。プラグインについては別の記事で説明しますが、簡単に言うと関数の集合体がプラグインです。
とりあえず現在時刻を返す関数を登録して、テンプレートから呼び出してみましょう。

以下のようにコードを書き換えます。

using Microsoft.SemanticKernel;

var kernel = new Kernel();
// Kernel にも実は CreateFunctionFromMethod がある
// 中では KernelFunctionFactory が呼ばれているだけなので実質的には同じ
var getLocalNowFunction = kernel.CreateFunctionFromMethod(
    TimeProvider.System.GetLocalNow,
    "GetLocalNow");
// Kernel に関数をプラグインとして登録する
kernel.Plugins.AddFromFunctions("TimePlugin", [getLocalNowFunction]);

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new KernelPromptTemplateFactory();
// テンプレートを作成
IPromptTemplate template = templateFactory.Create(new PromptTemplateConfig("""
    Hello, {{$name}}!The current local time is {{TimePlugin.GetLocalNow}}.
    """));

// テンプレートに変数を設定してレンダリング
string prompt = await template.RenderAsync(kernel, new KernelArguments
{
    ["name"] = "Kazuki",
});

// レンダリングされたプロンプトを表示
Console.WriteLine(prompt);

最初の方のコードで関数を作成してプラグインとして登録しています。テンプレート内での関数呼び出しは {{TimePlugin.GetLocalNow}} のようにプラグイン名と関数名をドットで区切って指定します。忘れてはいかないのが RenderAsync メソッドの第一引数にプラグインが登録されている Kernel を指定することです。これを忘れると関数が呼び出されません。
実行すると以下のように表示されます。ちゃんと時間が表示されるので関数が呼び出されていることが確認できます。

Hello, Kazuki!The current local time is 05/26/2025 23:09:23 +09:00.

関数には引数を渡すことも出来ます。引数は 1 つ目だけは引数名を指定しないで呼び出せます。2 つ目以降の引数は 名前=値 の形式で指定します。
例えば、フルネームを取得する関数を作成して、テンプレートから呼び出してみましょう。
以下のようにコードを書き換えます。

using Microsoft.SemanticKernel;

var kernel = new Kernel();
// 引数を受け取る関数を作成
var getFullNameFunction = kernel.CreateFunctionFromMethod(
    (string firstName, string lastName) => $"{firstName} {lastName}",
    "GetFullName");
// Kernel に関数をプラグインとして登録する
kernel.Plugins.AddFromFunctions("GreetingPlugin", [getFullNameFunction]);

// テンプレートのファクトリを作成
IPromptTemplateFactory templateFactory = new KernelPromptTemplateFactory();
// テンプレートを作成
IPromptTemplate template = templateFactory.Create(new PromptTemplateConfig("""
    Hello, {{GreetingPlugin.GetFullName firstName=$firstName lastName='Ota'}}!
    """));

// テンプレートに変数を設定してレンダリング
string prompt = await template.RenderAsync(kernel, new KernelArguments
{
    ["firstName"] = "Kazuki",
});

// レンダリングされたプロンプトを表示
Console.WriteLine(prompt);

実行すると以下のように表示されます。ちゃんと GetFullName 関数が呼び出されていることが確認できます。

Hello, Kazuki Ota!

因みに Semantic Kernel のデフォルトのテンプレートエンジンでは関数の引数内で関数を呼び出すことは出来ません。あまり複雑なことは出来ないようになっています。

まとめ

ここでは Semantic Kernel のテンプレートエンジンについて、基本的な使い方を紹介しました。
テンプレートエンジンは AI を呼び出すためのプロンプトを生成するために使用されます。
Semantic Kernel では IPromptTemplateFactory を使ってテンプレートエンジンを差し替えることが出来ます。
KernelPromptTemplateFactory は Semantic Kernel の既定のテンプレートエンジンで、基本的な変数の展開や関数呼び出し機能を持っています。
テンプレートエンジンを使うことで、AI を呼び出すためのプロンプトを柔軟に生成することが出来ます。
次回は AI を呼び出すための関数について説明します。テンプレートエンジンと関数は密接に関連しているので、テンプレートエンジンを理解した上で関数を学ぶとより理解が深まると思います。

目次

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

Microsoft (有志)

Discussion