Semantic Kernel の Agent Framework で、これから何をやるのかを表示しつつ実行する
GitHub Copilot とかを使っていると実際に何かをする前に、これから何をするのかを表示してくれたりします。
Semantic Kernel の Agent Framework でもこういうことが出来ないのかなぁと思って考えたのでメモしておきます。
結論
そんな便利な機能はないので自分で作る。
やってみよう
やり方は簡単です。Semantic Kernel に限った話ではないですが、何をするのかを考えさせるということを先にやって、それを表示してから実行する Agent に処理を回すだけです。
試しに「今日の日付の現在地の天気を調べる」というシナリオで考えています。
そのためには、以下のように現在地を取得する、今日の日付を取得する、日付と場所から天気を返すという 3 つの関数を用意します。
[Description("Plugin class providing location, weather, and date/time information.")]
class MyPlugin
{
[KernelFunction, Description("Gets the current location.")]
public string GetCurrentLocation() => "Tokyo, Japan";
[KernelFunction, Description("Gets the current date and time.")]
public DateTimeOffset GetCurrentDateTime() =>
TimeProvider.System.GetLocalNow();
[KernelFunction, Description("Gets the weather for a specific date and location.")]
public string GetWeather(
[Description("The date for which to get the weather.")] DateTimeOffset date,
[Description("The location for which to get the weather.")] string location) =>
$"The weather in {location} on {date:yyyy-MM-dd} is sunny.";
}
これを使って以下のようにプランを立てる Agent と実際に処理を実行する Agent を用意します。
// プラン考える人
var plannerAgent = new ChatCompletionAgent
{
Name = "Planner",
Instructions = """
あなたはアシスタントが実行するべきことを考えるエージェントです。
ユーザーのやりたいことを聞いて、実行するための手順を考えてください。
その際に、一般的な知識は使用せずに与えられたツールをどのように組み合わせて実行するべきかを考えてください。
""",
Kernel = kernel,
Arguments = new(new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.None(),
ResponseFormat = typeof(PlanOutput),
}),
};
// 仕事する人
var executorAgent = new ChatCompletionAgent
{
Name = "Executor",
Instructions = """
あなたはアシスタントです。
ユーザーのやりたいことを実行するために、プランナーが考えた手順を実行してください。
""",
Kernel = kernel,
Arguments = new(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
}),
};
record PlanOutput(
[Description("後続で作業をするアシスタント向けの詳細なプラン。ツール名や渡す情報の詳細も含めてください。")]
string PlanDetails,
[Description("ユーザーに示すための簡易的な案内メッセージ")]
string PlanForUser);
プランの出力んは StructuredOutput を使って、ユーザー向けの簡易的なメッセージと、実行するための詳細なメッセージを分けて出力するようにしました。そして関数呼び出しのログは IAutoFunctionInvocationFilter
を使って出すようにします。以下のようなフィルターを容易して Kernel に登録しておきましょう。
class LogFileter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
var args = context.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [];
await next(context);
Console.WriteLine($"Invoke: {context.Function.PluginName}-{context.Function.Name}({string.Join(", ", args)}) -> {context.Result.GetValue<object>()}");
}
}
後は、2 つの Agent を順番に実行して表示したい内容を表示するだけです。
const string userRequest = "今日の現在地の天気を教えて";
Console.WriteLine("========== ユーザーリクエスト =================");
Console.WriteLine(userRequest);
Console.WriteLine();
AgentThread? thread = null;
var plan = await plannerAgent.InvokeAsync(userRequest, thread).FirstAsync();
var planOutput = JsonSerializer.Deserialize<PlanOutput>(plan.Message.Content ?? "");
Console.WriteLine("========== プラン =================");
if (planOutput == null)
{
Console.WriteLine(plan.Message.Content);
}
else
{
Console.WriteLine($"ユーザー向けメッセージ: {planOutput.PlanForUser}");
Console.WriteLine($"詳細プラン: {planOutput.PlanDetails}");
}
Console.WriteLine();
Console.WriteLine("========== 関数呼び出し =================");
thread = plan.Thread;
var result = await executorAgent.InvokeAsync(thread).FirstAsync();
Console.WriteLine();
Console.WriteLine("========== 結果 =================");
Console.WriteLine(result.Message.Content);
Console.WriteLine();
コードの全体は以下のようになります。
using Azure.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using System.ComponentModel;
using System.Text.Json;
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
"gpt-4.1",
"https://<<AOAIのリソース名>>.openai.azure.com/",
new AzureCliCredential());
builder.Plugins.AddFromType<MyPlugin>();
builder.Services.AddSingleton<IAutoFunctionInvocationFilter, LogFileter>();
var kernel = builder.Build();
// プラン考える人
var plannerAgent = new ChatCompletionAgent
{
Name = "Planner",
Instructions = """
あなたはアシスタントが実行するべきことを考えるエージェントです。
ユーザーのやりたいことを聞いて、実行pするための手順を考えてください。
その際に、一般的な知識は使用せずに与えられたツールをどのように組み合わせて実行するべきかを考えてください。
""",
Kernel = kernel,
Arguments = new(new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.None(),
ResponseFormat = typeof(PlanOutput),
}),
};
// 仕事する人
var executorAgent = new ChatCompletionAgent
{
Name = "Executor",
Instructions = """
あなたはアシスタントです。
ユーザーのやりたいことを実行するために、プランナーが考えた手順を実行してください。
""",
Kernel = kernel,
Arguments = new(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
}),
};
const string userRequest = "今日の現在地の天気を教えて";
Console.WriteLine("========== ユーザーリクエスト =================");
Console.WriteLine(userRequest);
Console.WriteLine();
AgentThread? thread = null;
var plan = await plannerAgent.InvokeAsync(userRequest, thread).FirstAsync();
var planOutput = JsonSerializer.Deserialize<PlanOutput>(plan.Message.Content ?? "");
Console.WriteLine("========== プラン =================");
if (planOutput == null)
{
Console.WriteLine(plan.Message.Content);
}
else
{
Console.WriteLine($"ユーザー向けメッセージ: {planOutput.PlanForUser}");
Console.WriteLine($"詳細プラン: {planOutput.PlanDetails}");
}
Console.WriteLine();
Console.WriteLine("========== 関数呼び出し =================");
thread = plan.Thread;
var result = await executorAgent.InvokeAsync(thread).FirstAsync();
Console.WriteLine();
Console.WriteLine("========== 結果 =================");
Console.WriteLine(result.Message.Content);
Console.WriteLine();
[Description("Plugin class providing location, weather, and date/time information.")]
class MyPlugin
{
[KernelFunction, Description("Gets the current location.")]
public string GetCurrentLocation() => "Tokyo, Japan";
[KernelFunction, Description("Gets the current date and time.")]
public DateTimeOffset GetCurrentDateTime() =>
TimeProvider.System.GetLocalNow();
[KernelFunction, Description("Gets the weather for a specific date and location.")]
public string GetWeather(
[Description("The date for which to get the weather.")] DateTimeOffset date,
[Description("The location for which to get the weather.")] string location) =>
$"The weather in {location} on {date:yyyy-MM-dd} is sunny.";
}
class LogFileter : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
var args = context.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [];
await next(context);
Console.WriteLine($"Invoke: {context.Function.PluginName}-{context.Function.Name}({string.Join(", ", args)}) -> {context.Result.GetValue<object>()}");
}
}
record PlanOutput(
[Description("後続で作業をするアシスタント向けの詳細なプラン。ツール名や渡す情報の詳細も含めてください。")]
string PlanDetails,
[Description("ユーザーに示すための簡易的な案内メッセージ")]
string PlanForUser);
実行すると以下のような結果になります。
========== ユーザーリクエスト =================
今日の現在地の天気を教えて
========== プラン =================
ユーザー向けメッセージ: あなたの現在地と今日の日付をもとに、天気を調べてお知らせします。
詳細プラン: まず、functions.MyPlugin-GetCurrentLocationでユーザーの現在地情報を取得し、その後、functions.MyPlugin-GetCurrentDateTimeで本日の日付・時刻を取得する。続いて、functions.MyPlugin-GetWeatherに両方の情報(location, date)を渡し、現在地と本日の日付を元に天気を取得する。すべて順番にツールを実行し、得られた天気情報をユーザーに提示する。
========== 関数呼び出し =================
Invoke: MyPlugin-GetCurrentLocation() -> Tokyo, Japan
Invoke: MyPlugin-GetCurrentDateTime() -> 2025/05/21 14:20:32 +09:00
Invoke: MyPlugin-GetWeather(date: 2025-05-21, location: Tokyo, Japan) -> The weather in Tokyo, Japan on 2025-05-21 is sunny.
========== 結果 =================
今日(2025年5月21日)のあなたの現在地(東京)の天気は「晴れ」です。何か他に知りたいことがあれば教えてください。
ちゃんといい感じに表示されてますね。
まとめ
Semantic Kernel の Agent Framework で、これから何をやるのかを表示しつつ実行する方法を考えてみました。
特別な機能はなく、純粋にプランを立てて実行するという 2 ステップにわけて実行することで実現できました。これを Web API 化するとなると、またちょっとめんどくさそうですが、AI がこれから何をするつもりなのかを宣言して関数を呼ぶような機能はないので、こういうのを自分で作るのが良いと思います。
もしくは、時間があったら次の記事で書こうと思いますが関数呼び出しを自動でやるのではなく、手動でハンドリングをして、そこでユーザーに通知をするような方法も良いと思います。
Discussion