新しいワークフローの勉強1 - Microsoft Agent Framework (C#) その18
シリーズ記事
- その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
はじめに
久しぶりに Microsoft Agent Framework のワークフローについて書いてみようと思います。しばらく前にワークフローを結構掘り下げたのですが、見事に破壊的変更が入っていています。ということで、ここら辺でワークフローについて改めて入門的な内容を書いてみようと思います。
ワークフローとは?
Agent Framework のワークフローは ExecutorBinding 同士を繋ぐ Edge で構成されるグラフ構造になっています。ノードは ExecutorBinding というクラスで表されています。このグラフ構造を組み立てるための WorkflowBuilder クラスが提供されていて、これを使ってワークフローを構築します。
ExecutorBinding 同士はメッセージ (端的に言うと任意のオブジェクト) を source の ExecutorBinding から target の ExecutorBinding へ渡すことができます。
Edge のつなぎ方にはいくつかのパターンがあり、基本的なものとしては以下のようなものがあります。
-
AddEdge: 1つのExecutorBindingから別のExecutorBindingへメッセージを渡す基本的な接続- メッセージを渡すための条件を指定することも出来る
-
AddFanOutEdge: 1つのExecutorBindingから複数のExecutorBindingへメッセージを分岐させる- Fan-out の先の
ExecutorBindingに渡すメッセージは同一ものもが渡されます。配列を分解してくれたりはしません。そこは自分で頑張るところになります。
- Fan-out の先の
-
AddFanInEdge: 複数のExecutorBindingから1つのExecutorBindingへメッセージを集約する- Fan-in も複数の source から target へ渡されるメッセージを配列にまとめたりしてくれません。自分で頑張る必要になります。
他にも WorkflowBuilder の拡張メソッドとして AddSwitch や AddChain などのメソッドもありますが、これらは AddEdge や AddFan(Out/In)Edge を組み合わせて実装されているので基本はこの 3 つを使いこなせれば大丈夫です。
さらに、これらの上に AIAgent で構成されるワークフローパターンを簡単に作れる AgentWorkflowBuilder クラスも提供されていますが、これも、これらの基本的な機能を使って実装されています。そのため、まずは基本的なワークフローの構築方法を押さえておくことが重要です。
簡単なワークフロー
サクッと簡単なワークフローを作ってみましょう。
まずは AI とか関係なく文字列操作をするだけのワークフローを作ってみます。具体的には入力された文字列を大文字に変換して反転させるワークフローを作成します。
このワークフローを実装してみましょう。コンソールアプリを新規作成して以下のパッケージ(両方とも 2025/11/24 時点でプレビューです) を追加します。
Microsoft.Agents.AIMicrosoft.Agents.AI.Workflows
そして以下のようなコードを書きます。
using Microsoft.Agents.AI.Workflows;
// 各ワークフローでやる処理
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
Func<string, string> reverseFunc = s => string.Concat(s.Reverse());
// ExecutorBinding 型に変換
var uppercase = uppercaseFunc.BindAsExecutor(nameof(uppercaseFunc));
var reverse = reverseFunc.BindAsExecutor(nameof(reverseFunc));
// uppercase -> reverse の順で実行されるワークフローを構築
// 出力は reverse のものを使う
var workflow = new WorkflowBuilder(uppercase)
.AddEdge(uppercase, reverse)
.WithOutputFrom(reverse)
.Build();
// ワークフローを実行
var run = await InProcessExecution.RunAsync(workflow, "hello");
// NewEvents プロパティからイベントを取得して表示
foreach (var evt in run.NewEvents)
{
Console.WriteLine(evt);
}
これで大文字にして反転するワークフローが定義出来ました。そして、実行するとワークフローから実行に伴ってイベントが発行されます。InProcessExecution クラスの RunAsync メソッドを使うとワークフローを実行して、NewEvents プロパティから発行されたイベントを取得できます。
実行結果は以下のようになります。
SuperStepStartedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = uppercaseFunc, Data: System.String = hello)
ExecutorCompletedEvent(Executor = uppercaseFunc, Data: System.String = HELLO)
SuperStepCompletedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
SuperStepStartedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = reverseFunc, Data: System.String = HELLO)
ExecutorCompletedEvent(Executor = reverseFunc, Data: System.String = OLLEH)
WorkflowOutputEvent(Data: System.String = OLLEH)
SuperStepCompletedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
SuperStep というのがワークフロー内の各ステップを表していて、その中で ExecutorInvokedEvent と ExecutorCompletedEvent が発行されています。その SuperStep で実行可能な ExecutorBinding が全て実行されると SuperStepCompletedEvent が発行され、次の SuperStep が開始されます。その過程でイベントが発行されると、それもイベントとして取得できます。WorkflowOutputEvent はワークフローの出力が発行されたことを表しています。WorkflowOutputEvent は WithOutputFrom メソッドで指定した ExecutorBinding の出力が発行されます。1 回のワークフローの実行で WorkflowOutputEvent は複数回発行されることがあります。
そのため以下のように WithOutputFrom メソッドで複数の ExecutorBinding を指定することもできます。最初の例では全部のイベントを表示していましたが WorkflowOutputEvent のみを表示して出力だけを確認するようにしてみましょう。
WorkflowOutputEvent は As<T> メソッドで出力の型を指定して取得できるので、以下のようにして出力だけを取得して表示してみます。
using Microsoft.Agents.AI.Workflows;
// 各ワークフローでやる処理
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
Func<string, string> reverseFunc = s => string.Concat(s.Reverse());
// ExecutorBinding 型に変換
var uppercase = uppercaseFunc.BindAsExecutor(nameof(uppercaseFunc));
var reverse = reverseFunc.BindAsExecutor(nameof(reverseFunc));
// uppercase -> reverse の順で実行されるワークフローを構築
// 出力は reverse のものを使う
var workflow = new WorkflowBuilder(uppercase)
.AddEdge(uppercase, reverse)
// 両方の実行結果を出力に含める
.WithOutputFrom(uppercase, reverse)
.Build();
// ワークフローを実行
var run = await InProcessExecution.RunAsync(workflow, "hello");
// 出力を取得して表示
var outputs = run.NewEvents.OfType<WorkflowOutputEvent>().Select(x => x.As<string>());
Console.WriteLine(string.Join(", ", outputs));
実行結果は以下のようになります。
HELLO, OLLEH
いい感じですね。今回は 2 ステップだけですが、WorkflowBuilder クラスを使うことで複雑なワークフローも構築できます。さらに ExecutorBinding には AIAgent や Executor クラスの派生クラスや string も指定できます。Func<T, U> は BindAsExecutor 拡張メソッドで ExecutorBinding に変換しないといけませんが、これはワークフロー内で ExecutorBinding を識別するための id を指定するためです。AIAgent や Executor クラスの派生クラスはそれぞれ固有の ID を持っているので、暗黙的型変換でそのまま ExecutorBinding として利用できます。
Executor を実装してみよう
実際にワークフローを構築するときには Func や Executor<TInput, TOutput> クラスや AIAgent クラスを使うことが多いですが、一番ベースのクラスの Executor クラスを継承して独自の Executor を実装することもできます。一番根っこの処理の動きを見るために、先ほど実装した大文字変換と反転を行う Executor を実装してみましょう。
Executor クラスは ConfigureRoutes メソッドをオーバーライドして Executor でハンドルする型と処理を登録します。ConfigureRoutes メソッドでは RouteBuilder クラスのインスタンスが渡されるので、RouteBuilder クラスの AddHandler<TInput> メソッドを使って型ごとのハンドラを登録します。ハンドラは以下のように定義されています。
RouteBuilder AddHandler<TInput>(
Func<TInput, IWorkflowContext, CancellationToken, ValueTask> handler,
bool overwrite = false)
handler 引数には 3 つの引数を持つデリゲートを指定します。1 つ目の引数は入力値、2 つ目の引数はワークフローのコンテキスト、3 つ目の引数はキャンセレーション用のトークンです。戻り値は ValueTask 型です。第 2 引数の overwrite 引数は同じ型のハンドラが既に登録されている場合に上書きするかどうかを指定します。デフォルトは false です。ちなみに、上記の定義は一番引数が多いパターンで Action<TInput, IWorkflowContext> handler のように引数が少ないパターンも用意されています。
さて、これを使って大文字変換と反転を行う Executor を実装してみましょう。Executor では、次の Executor へメッセージを送るための SendMessageAsync メソッドが用意されているので、これを使ってメッセージを送信します。
まずは SendMessageAsync だけを使った基本的な実装から見てみます:
以下のように実装します:
class UppercaseExecutor() : Executor(nameof(UppercaseExecutor))
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder.AddHandler<string>(ToUpperInvariantAsync);
}
private static async ValueTask ToUpperInvariantAsync(string s, IWorkflowContext context)
{
var uppercased = s.ToUpperInvariant();
await context.SendMessageAsync(uppercased);
}
}
同じ要領で反転を行う Executor も実装します。
class ReverseExecutor() : Executor(nameof(ReverseExecutor))
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder.AddHandler<string>(ReverseAsync);
}
private static async ValueTask ReverseAsync(string s, IWorkflowContext context)
{
var reversed = string.Concat(s.Reverse());
await context.SendMessageAsync(reversed);
}
}
これを使ってワークフローを構築してみましょう。
using Microsoft.Agents.AI.Workflows;
// Executor をインスタンス化
var uppercase = new UppercaseExecutor();
var reverse = new ReverseExecutor();
// uppercase -> reverse の順で実行されるワークフローを構築
// 出力は uppercase と reverse のものを使う
var workflow = new WorkflowBuilder(uppercase)
.AddEdge(uppercase, reverse)
// 両方の実行結果を出力に含める
.WithOutputFrom(uppercase, reverse)
.Build();
// ワークフローを実行
var run = await InProcessExecution.RunAsync(workflow, "hello");
foreach (var evt in run.NewEvents)
{
Console.WriteLine(evt);
}
実行すると以下のようになります。因みに、この時点では WorkflowOutputEvent が発行されません。これは Executor 内で WorkflowOutputEvent が発行されないためです。WithOutputFrom メソッドで指定した ExecutorBinding の出力が発行されるためには、Executor 側で IWorkflowContext.YieldOutputAsync メソッドを呼び出してメッセージを送信する必要があります。上記の Executor 実装ではこれを行っていないので、WorkflowOutputEvent が発行されていません。
SuperStepStartedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = UppercaseExecutor, Data: System.String = hello)
ExecutorCompletedEvent(Executor = UppercaseExecutor)
SuperStepCompletedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
SuperStepStartedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = ReverseExecutor, Data: System.String = HELLO)
ExecutorCompletedEvent(Executor = ReverseExecutor)
SuperStepCompletedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
WorkflowOutputEvent を発行するには Executor を以下のように修正します。YieldOutputAsync メソッドを追加することで、WorkflowOutputEvent が発行されるようになります:
実装は以下のようになります。
class UppercaseExecutor() : Executor(nameof(UppercaseExecutor))
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder.AddHandler<string>(ToUpperInvariantAsync);
}
private static async ValueTask ToUpperInvariantAsync(string s, IWorkflowContext context)
{
var uppercased = s.ToUpperInvariant();
await context.SendMessageAsync(uppercased);
await context.YieldOutputAsync(uppercased); // ここで出力を指定
}
}
class ReverseExecutor() : Executor(nameof(ReverseExecutor))
{
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
return routeBuilder.AddHandler<string>(ReverseAsync);
}
private static async ValueTask ReverseAsync(string s, IWorkflowContext context)
{
var reversed = string.Concat(s.Reverse());
await context.SendMessageAsync(reversed);
await context.YieldOutputAsync(reversed); // ここで出力を指定
}
}
この状態で実行すると以下のようになります。ちゃんと WorkflowOutputEvent が発行されていることが分かります。
SuperStepStartedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = UppercaseExecutor, Data: System.String = hello)
WorkflowOutputEvent(Data: System.String = HELLO)
ExecutorCompletedEvent(Executor = UppercaseExecutor)
SuperStepCompletedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
SuperStepStartedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = ReverseExecutor, Data: System.String = HELLO)
WorkflowOutputEvent(Data: System.String = OLLEH)
ExecutorCompletedEvent(Executor = ReverseExecutor)
SuperStepCompletedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
ここで大事なことは WorkflowBuilder.WithOutputFrom メソッドで指定した ExecutorBinding の出力は Executor 側で IWorkflowContext.YieldOutputAsync メソッドを呼び出してメッセージを送信しないと発行されないということです。Func<TInput, TOutput> や Executor<TInput, TOutput> クラスなどは、この YieldOutputAsync メソッドを呼び出してくれるので、特に意識しなくても WorkflowOutputEvent が発行されます。
一応、YieldOutputAsync を呼び出していても WithOutputFrom メソッドで指定していない ExecutorBinding の出力は発行されないということも確認しておきましょう。上記の例で WithOutputFrom メソッドを以下のように修正して reverse のみを指定してみます。
// uppercase -> reverse の順で実行されるワークフローを構築
// 出力は reverse のものを使う
var workflow = new WorkflowBuilder(uppercase)
.AddEdge(uppercase, reverse)
// 両方の実行結果を出力に含める
.WithOutputFrom(reverse)
.Build();
実行すると以下のような結果になります。
SuperStepStartedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = UppercaseExecutor, Data: System.String = hello)
ExecutorCompletedEvent(Executor = UppercaseExecutor)
SuperStepCompletedEvent(Step = 0, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
SuperStepStartedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepStartInfo = Microsoft.Agents.AI.Workflows.SuperStepStartInfo)
ExecutorInvokedEvent(Executor = ReverseExecutor, Data: System.String = HELLO)
WorkflowOutputEvent(Data: System.String = OLLEH)
ExecutorCompletedEvent(Executor = ReverseExecutor)
SuperStepCompletedEvent(Step = 1, Data: Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo = Microsoft.Agents.AI.Workflows.SuperStepCompletionInfo)
AIAgent の場合には AddEventAsync でエージェント関連のイベントは発行しますが、出力のイベントは発行しないので注意が必要です。
文字列を WorkflowBuilder で指定するケース
ExecutorBinding には string 型も指定できます。string 型を指定した場合には、その文字列が ExecutorBinding の ID として扱われます。string 型を指定した場合には、その ID に対応する Executor がワークフローの実行前に実際の Executor に紐づけを行う必要があります。紐づけを行うには WorkflowBuilder の BindExecutor メソッドを使います。以下のように string 型で指定した ID に対して Executor を紐づけることができます。
この紐付けを行うコードは以下のようになります。
// 先に ID 指定でワークフローを組み立てて、後で Executor をバインドする
var workflow = new WorkflowBuilder("UppercaseExecutor")
.AddEdge("UppercaseExecutor", "ReverseExecutor")
.WithOutputFrom("ReverseExecutor")
// ワークフローに Executor をバインド
.BindExecutor(uppercase)
.BindExecutor(reverse)
.Build();
この機能を使うことでワークフローの構築と、実際に使う Executor の紐づけを分離することができます。例えば、同じワークフロー構造を使って異なる Executor を使い回したい場合などに便利です。
まとめ
Microsoft Agent Framework のワークフローについて入門的な内容を見てきました。
WorkflowBuilder クラスを使うことで柔軟にワークフローを構築でき、Executor クラスを継承して独自の処理を実装することもできます。普段は Executor を直接実装することはあまりないかもしれませんが、基礎的な動作を理解する上では役立つと思います。
ワークフロー内でのメッセージの流れやイベントの発行について理解することで、より高度なエージェントの動作を実現できるようになります。次回は、気が向いたらワークフローの応用的な使い方や、実際のエージェントでの活用例について掘り下げてみたいと思います。
余談
「わかりにくいなー」って思いながら、めっちゃ頑張って読み解いた ExecutorIsh は消えてしまい ExecutorBinding になって、多少わかりやすくなりました。めでたしめでたし。
Discussion