プロンプトテンプレートエンジンを使う - Microsoft Agent Framework (C#) その17
シリーズ記事
- その1: 「雑感」とハローワールド
- その2: ざっとリポジトリを見てみる
- その3: ワークフローを見てみよう
- その4: ワークフローの Executor を掘り下げる
- その5: ワークフローで条件分岐とループを扱う
- その6: Executor のステータス管理
- その7: チェックポイントの永続化
- その8: Human in the loop を試してみよう
- その9: Semantic Kernel の Plugin の移行
- その10: Durable Functions でワークフロー
- その11: エージェントを見てみよう
- その12: A2A対応のエージェントを作ってみよう
- その13: .NET 10 用の Agent プロジェクトテンプレート
- その14: Durable Agent を試してみよう
- その15: Durable Agent で長時間ツール呼び出し
- その16: Durable Agent で静的変数アクセスを消す
- その17: プロンプトテンプレートエンジンを使う
- その18: 新しいワークフローの勉強1
はじめに
Semantic Kernel の後継として位置づけられている Microsoft Agent Framework ですが、Semantic Kernel で提供されていたプロンプトテンプレートエンジンが提供されていません。GitHub の Issue を見る限り計画はされているようですが、今の所積極的にテンプレートエンジン機能を組み込もうという動きは見られません。 そこで今回は、Microsoft Agent Framework でプロンプトテンプレートエンジンを使う方法を模索していこうと思います。
Semantic Kernel のプロンプトテンプレートエンジンを Agent Framework で使う
とりあえず組み込みのテンプレートエンジンがないので、ここでは Semantic Kernel のデフォルトのテンプレートエンジンを使う方法を見ていきます。Semantic Kernel のテンプレートエンジンは Microsoft.SemanticKernel.Core NuGet パッケージに含まれています。そのため Agent Framework を使ったプロジェクトにこのパッケージを追加します。
テンプレートエンジン自体の使い方は昔に記事を書いたので以下の記事を参照してください。
このテンプレートエンジンで AIAgent の Instructions や messages の部分のテキストを置き換えることが今回のゴールです。Agent Framework では IChatClient や Microsoft Foundry の Agent Service などの多くのエージェントを使用することが出来ます。そのため全うなアプローチでは AIAgent のミドルウェアとしてテンプレートエンジンを組み込む方法が考えられますが、自分が確認したところシステムプロンプトにまで介入する方法は見つかりませんでした。messages のテキストを置き換えるだけなら AIAgent のミドルウェアで可能です。
しかし、今回はシステムプロンプトまで含めて書き換えるアプローチをとりたいと思います。そして、システムプロンプトを自分で指定して使うのは IChatClient をベースとした ChatClientAIAgent になります。これに限定する場合は IChatClient のミドルウェアとしてテンプレートエンジンを適用するアプローチが一番何でも出来るので、その方法で試してみます。
IChatClient のミドルウェアでテンプレートエンジンを使う
ということで IChatClient のミドルウェアを作っていきます。IChatClient のミドルウェアは DelegatingChatClient を継承して、処理をオーバーライドして作成します。実際の Semantic Kernel のテンプレートエンジンでは関数を呼び出す方法もサポートしていますが、ここでは簡単にテキストの置き換えだけをサポートする形にします。ChatOptions に AdditionalProperties がある場合にテンプレートを適用する形にします。以下がサンプルコードです。
// Semantic Kernel のテンプレートエンジンを使用してプロンプトテンプレートを適用する ChatClient のミドルウェア
class SKTemplateEngineChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient)
{
private readonly Kernel _kernel = new();
private readonly KernelPromptTemplateFactory _kernelPromptTemplateFactory = new();
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
// テンプレートを適用するしてベースの処理を呼び出す
await ApplyTemplateAsync(messages, options);
return await base.GetResponseAsync(messages, options, cancellationToken);
}
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation]CancellationToken cancellationToken = default)
{
// テンプレートを適用するしてベースの処理を呼び出す
await ApplyTemplateAsync(messages, options);
await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
{
yield return update;
}
}
// テンプレートを適用する
private async Task ApplyTemplateAsync(IEnumerable<ChatMessage> messages, ChatOptions? options)
{
// テンプレートを適用するのは AdditionalProperties に値がある場合のみにする
if (options is { AdditionalProperties: { Count: > 0 } properties })
{
// InputVariable を作成する
List<InputVariable> inputVariables =
[
..properties.Select(x => new InputVariable
{
Name = x.Key,
})
];
// KernelArguments を作成する
KernelArguments arguments = new(properties);
// Instructions にテンプレートを適用する
if (!string.IsNullOrWhiteSpace(options.Instructions))
{
options.Instructions = await _kernelPromptTemplateFactory
.Create(new()
{
Template = options.Instructions,
InputVariables = inputVariables,
})
.RenderAsync(_kernel, arguments);
}
// 各メッセージの TextContent にテンプレートを適用する
foreach (var textContent in messages.SelectMany(m => m.Contents).OfType<Microsoft.Extensions.AI.TextContent>())
{
var template = _kernelPromptTemplateFactory.Create(new()
{
Template = textContent.Text,
InputVariables = inputVariables,
});
textContent.Text = await template.RenderAsync(_kernel, arguments);
}
}
}
}
では、これを使ってみましょう。天気を聞くシナリオで、天気の情報はテンプレートエンジンで埋め込むようにする例を見てみます。
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using System.Runtime.CompilerServices;
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
AIAgent catAgent = new AzureOpenAIClient(new(configuration["AOAI:Endpoint"]!), new DefaultAzureCredential())
.GetChatClient("gpt-4.1")
.AsIChatClient()
.AsBuilder()
// ここで Semantic Kernel のテンプレートエンジンを使用するミドルウェアを追加する
.Use(innerClient => new SKTemplateEngineChatClient(innerClient))
.Build()
.CreateAIAgent(
name: "cat-agent",
// プロンプトテンプレートを指定する
instructions: """
あなたはネコ型アシスタントです。
猫らしく振舞うために語尾は必ず「にゃん」にしてください。
回答の際には以下のコンテキストを参考にしてください。
### コンテキスト
今日の天気: {{$weather}}
""");
// テンプレート エンジンが実行されないとわからない質問を実行する
var result = await catAgent.RunAsync("今日の天気は?",
options: new ChatClientAgentRunOptions
{
ChatOptions = new()
{
// ここでテンプレートに渡す値を指定する
AdditionalProperties = new()
{
["weather"] = "雷雨",
}
}
});
// 結果を表示する
Console.WriteLine(result.Text);
テンプレートエンジンを実行することでシステムプロンプトに今日の天気が雷雨であるという情報が埋め込まれてエージェントが回答をするようになります。実行すると以下のような出力が得られます。
今日の天気は雷雨にゃん。お外に出るなら気をつけてにゃん!
ちゃんと動きましたね。テンプレートエンジンを使うことで、エージェントに動的なコンテキスト情報を渡すことができるようになりました。といっても、最近の多くのケースでは AI が自発的に Tool を呼び出して情報を取得することが一般的です。テンプレートエンジンで値を埋め込む必要があるケースは少ないでしょう。また、AI に追加情報を動的に渡す方法として Microsoft Agent Framework では AIContext という仕組みも提供されています。これは AIAgent が実際に処理を実行する前に Instructions や Messages や Tools に動的に値を追加することが出来る拡張ポイントです。
AIContextProvider を使う方法(こちらがお勧め)
作成方法は簡単で AIContextProvider クラスを継承して InvokingAsync メソッドをオーバーライドして AIContext を返すだけです。AIContext には Instructions プロパティがあるのでここに追加のコンテキスト情報を記載すれば、エージェントのプロンプトに追加されます。この他にも Messages プロパティでメッセージを追加したり、Tools プロパティでツールを追加したりすることも出来ます。
以下が AIContextProvider を使って天気情報をコンテキストとして追加する例です。
// コンテキスト プロバイダーの実装例
class WeatherAIContextProvider : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
// 天気情報を取得してコンテキストとして返す(ここでは固定値を返す)
return ValueTask.FromResult(new AIContext
{
Instructions = """
## コンテキスト
今日の天気: 雷雨
""",
});
}
}
この WeatherAIContextProvider をエージェントに登録して使う例は以下のようになります。
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
AIAgent catAgent = new AzureOpenAIClient(new(configuration["AOAI:Endpoint"]!), new DefaultAzureCredential())
.GetChatClient("gpt-4.1")
.AsIChatClient()
.CreateAIAgent(new ChatClientAgentOptions
{
Name = "cat-agent",
Instructions = """
あなたはネコ型アシスタントです。
猫らしく振舞うために語尾は必ず「にゃん」にしてください。
回答の際には以下のコンテキストを参考にしてください。
""",
// エージェントの作成時にコンテキスト プロバイダーを指定する
AIContextProviderFactory = ctx => new WeatherAIContextProvider(),
});
// テンプレート エンジンが実行されないとわからない質問を実行する
var result = await catAgent.RunAsync("今日の天気は?");
// 結果を表示する
Console.WriteLine(result.Text);
実行すると以下のような出力が得られました。
今日の天気は雷雨にゃん。お外に出るときは気をつけるにゃん!
ちゃんとコンテキストが渡されてエージェントが回答していますね。AIContextProvider を使う方法はテンプレートエンジンを使うよりもシンプルで、Agent Framework の設計思想にも合っているので、個人的には、可能であればこちらの方法を使うことをお勧めします。当然テンプレートエンジンは強力なツールなので、複雑なプロンプト生成が必要な場合には有用です。用途に応じて使い分けてください。
まとめ
今回は Microsoft Agent Framework でプロンプトテンプレートエンジンを使う方法を見てきました。Semantic Kernel のテンプレートエンジンを IChatClient のミドルウェアとして組み込む方法と、Agent Framework の AIContextProvider を使って動的にコンテキスト情報を追加する方法の2つを紹介しました。用途に応じて適切な方法を選んでエージェントのプロンプト生成に活用してください。
今回、使用したコードは以下の GitHub リポジトリで公開しています。興味がある方はぜひご覧ください。
Discussion