しばやん雑記

Azure とメイドさんが大好きなプログラマーのブログ

Agent Framework で作成した Agent を A2A で公開して Durable Agents から利用する

少し前に GA したこともあり、そろそろ Agent Framework をちゃんと触っておかないといけなくなったので、基本的に最小コードで動くようなものをベースに検証することにしました。

公式のサンプルやドキュメントは古いバージョンのコードが混ざっていることが多く、本質的ではない実装が混ざっていることもあったので、最小の依存関係と最小の実装で Agent Framework の本質的な部分を確認する目的です。

Agent Framework としての機能を確認するので、LLM のプロバイダーに関するコードは説明しません。今回は全てのコードで Azure OpenAI を API キーで使うようにしているので、Foundry Project などが使いたい場合は以下のドキュメントを参照してください。

シンプルな Agent を作る場合には難しいことはないと考えているので、今回は Agent Framework の機能の中でも A2A 周りと Durable Agents に注目して検証を行っています。サラッと調べた感じではドキュメントは例によって分散していて分かりにくく、A2A に関しては使う側のドキュメントは見当たらなかったという経緯もあります。

従ってこれから先のサンプルコードでは Agent Framework を使った Agent の定義についての説明は全く出てこないので、必要に応じて公式ドキュメントを参照してください。

A2A を使って Agent を公開する

1 年前は A2A を使って Agent を公開して、他の Agent から利用可能にすること自体を個人的には考えなかったのですが、最近の LLM と Agent の進化や実行したいタスクが専門的かつ重くなってきているため、A2A で公開するメリットが出てきたと感じています。

Agent Framework は A2A を使った Agent の公開と利用の両方に対応しているので、ASP.NET Core Web API の Minimal APIs の知識が少しあれば簡単に作成した Agent を公開可能です。もちろん認証などは考える必要がありますが、ASP.NET Core Web API に乗っかっているので難しくはないです。

公式ドキュメントやサンプルコードは依存関係を含めかなりごちゃごちゃしていますが、A2A での公開を行うために実際に必要なのは以下のパッケージのみです。もちろん LLM のプロバイダーは必要ですが、それ以外はこのパッケージのみです。

このパッケージをインストールすると MapA2AHttpJsonMapA2AJsonRpc という拡張メソッドが利用可能になるので、Minimal APIs と同様にマッピングするパスの情報を渡して呼び出せば A2A での公開が完了します。今回は以下のように SNS 投稿用の内容を考える Agent とその内容をレビューする Agent を例として用意しました。

using A2A;
using A2A.AspNetCore;

using Azure.AI.OpenAI;

using OpenAI.Chat;

var builder = WebApplication.CreateBuilder(args);

var chatClient = new AzureOpenAIClient(new Uri(builder.Configuration["AOAI_ENDPOINT"]), new System.ClientModel.ApiKeyCredential(builder.Configuration["AOAI_API_KEY"]))
    .GetChatClient("gpt-5.4");

var writerAgent = chatClient.AsAIAgent(instructions: """
    You are an SNS post writer agent.
    Create a short and engaging Japanese social media post based on the theme provided by the user.
    Make the post natural, easy to read, and appropriate for a broad audience.
    Do not invent facts that are not implied by the user's theme.
    Return only the post text without explanations, headings, or quotation marks.
    """, name: "WriterAgent");

var reviewerAgent = chatClient.AsAIAgent(instructions: """
    You are an SNS post reviewer agent.
    Review the Japanese social media post for clarity, natural tone, engagement, appropriateness for a broad audience, and whether it is 120 characters or fewer.
    If the post is acceptable, respond with only: APPROVED
    If the post needs improvement, provide a short revision request in Japanese.
    """, name: "ReviewerAgent");

builder.AddA2AServer(writerAgent);
builder.AddA2AServer(reviewerAgent);

var app = builder.Build();

app.MapA2AJsonRpc(writerAgent, "/writer");
app.MapA2AJsonRpc(reviewerAgent, "/reviewer");

app.MapWellKnownAgentCard(new AgentCard
{
    Name = "WriterAgent",
    Description = "Creates a Japanese SNS post from a user-provided theme",
    Version = "1.0.0",
    SupportedInterfaces =
    [
       new AgentInterface
       {
           Url = "http://localhost:5062/writer",
           ProtocolBinding = "JSONRPC",
           ProtocolVersion = "1.0"
       }
    ]
}, "/writer");

app.MapWellKnownAgentCard(new AgentCard
{
    Name = "ReviewerAgent",
    Description = "Reviews a Japanese SNS post for clarity, tone, and suitability",
    Version = "1.0.0",
    SupportedInterfaces =
    [
        new AgentInterface
        {
            Url = "http://localhost:5062/reviewer",
            ProtocolBinding = "JSONRPC",
            ProtocolVersion = "1.0"
        }
    ]
}, "/reviewer");

app.Run();

大まかに説明すると Agent を作成した後は AddA2AServer を公開する Agent の数だけ呼び出した後に、MapA2AHttpJsonMapA2AJsonRpc を使ってパスと Agent をマッピングし、最後に MapWellKnownAgentCard を呼び出して Agent Card を公開するという流れです。今回のサンプルでは Agent Card は /writer/.well-known/agent-card.json/reviewer/.well-known/agent-card.json で公開されているので、これを使う形になります。

Agent Card を作成する部分が若干めんどくさいですが、作成した Agent を公開するのは非常に簡単なコードで実現できます。API を見ていると A2A は 1 つのホストで複数の Agent を公開する前提になっていない感があります。

A2A で公開された Agent を利用する

Agent Framework を使うことで作成した Agent を A2A で公開可能になったので、次は実際に Agent Framework を使って A2A で Agent を呼び出すようにしていきます。何故か公式ドキュメントやサンプルには A2A で他の Agent を呼び出す方法が見当たらなかったので、若干手探りになりましたが以下のパッケージをインストールするだけで可能になります。

余談ですが Agent Framework の NuGet パッケージ名は規約ベースで付けられているので、パッケージ名を見れば用途が一目でわかるようになっていて良い感じです。

今回のサンプルはシンプルに A2A で Agent を呼び出すだけなので、OpenAI への参照が存在しません。決定論的なワークフローで十分な処理の場合もあるはずなので、Agent Framework には LLM が必須ではないと認識しておくとよいです。A2A を使う場合には A2ACardResolver から始まることを覚えておけば、あとは GetAIAgentAsync メソッドを呼べば AIAgent のインスタンスが返ってくるので、Agent Framework の世界に落とし込めます。

using A2A;

var writerResolver = new A2ACardResolver(new Uri("http://localhost:5062/writer/"));
var reviewerResolver = new A2ACardResolver(new Uri("http://localhost:5062/reviewer/"));

var writerAgent = await writerResolver.GetAIAgentAsync();
var reviewerAgent = await reviewerResolver.GetAIAgentAsync();

var writerResponse = await writerAgent.RunAsync<string>("Claude Opus 4.7 の特徴とメリットについて書いてください");

var reviewerResponse = await reviewerAgent.RunAsync<string>(writerResponse.Text);

Console.WriteLine($"Writer: {writerResponse.Text}");
Console.WriteLine($"Reviewer: {reviewerResponse.Text}");

少しわかりにくい部分が A2ACardResolver 周りで、この時の URL は最後に / を付けないと正しく Agent Card がダウンロードされませんでした。ルートで 1 つだけ Agent を公開する場合は影響しないですが、今回は複数の Agent を公開しているので注意が必要です。

このコードを実行してみると 2 つの Agent が順番に呼び出され、それぞれの Agent の結果が出力されます。

A2A を利用する側も Agent Card をどのように定義し、利用していくかが課題ではありますが、実装自体は非常にシンプルなことが分かります。Agent Framework が Agent の実装を AIAgent として抽象化しているので、Local か A2A かを意識することなく利用できるのは大きなメリットです。

A2A Agent を Tool として利用する

それぞれの Agent を固定で呼び出す場合には LLM すら必要なかったですが、実際には LLM を使ってどの Agent を利用するかも判断させたいことが多いと思います。Agent Framework を使うと Agent 自体を Tools として呼び出し可能になるので、それを使うと自律性の高い処理を簡単に実現できます。

実装としては取得した AIAgent のインスタンスに対して AsAIFunction 拡張メソッドを呼び出すと、Agent の Tools として呼び出し可能なインターフェースに変換できます。あとは Orchestrator として動作する Agent の Tools として指定するだけで、Agent から利用可能になります。

using A2A;

using Azure.AI.OpenAI;

using Microsoft.Agents.AI;

using OpenAI.Chat;

var chatClient = new AzureOpenAIClient(new Uri("AOAI_ENDPOINT"), new System.ClientModel.ApiKeyCredential("AOAI_API_KEY"))
    .GetChatClient("gpt-5.4");

var writerResolver = new A2ACardResolver(new Uri("http://localhost:5062/writer/"));
var reviewerResolver = new A2ACardResolver(new Uri("http://localhost:5062/reviewer/"));

var writerAgent = await writerResolver.GetAIAgentAsync();
var reviewerAgent = await reviewerResolver.GetAIAgentAsync();

var agent = chatClient.AsAIAgent(
    instructions: """
        You are a specialized agent for creating social media post messages.
        Based on the topic provided by the user, use the registered tools to create a short, engaging message suitable for posting publicly.

        Always follow these rules:
        - First, use the writer tool to create a first draft of the social media post based on the topic.
        - Next, use the reviewer tool to review the draft for clarity, tone, appeal, and any unnatural phrasing.
        - If the reviewer provides any feedback, issues, or suggested improvements, send that feedback back to the writer tool and ask for a revised draft.
        - Repeat the review and revision cycle as needed until the draft is good enough for a final answer.
        - After the final revision, produce the final post.
        - Output only the final social media post. Do not include intermediate steps, tool usage details, or explanations.
        - The final message must be written in Japanese.
        - Keep it concise and natural for a single social media post.
        - You may use emojis and hashtags when appropriate, but do not overuse them.
        """,
    name: "SocialPostAgent",
    tools: [writerAgent.AsAIFunction(), reviewerAgent.AsAIFunction()]);

var response = await agent.RunAsync<string>("Claude Opus 4.7 の特徴とメリットについて書いてください");

Console.WriteLine(response.Text);

このサンプルコードでは A2A で公開されている Agent を利用して、 Writer と Reviewer を反復的に呼び出すように定義しています。シーケンシャルに呼び出すパターンでは Reviewer の応答によって処理を自前で書く必要がありますが、今回の例では LLM が必要に応じて複数回 Tools を呼び出すため最終的な結果を得られます。

実行結果よりも上の画像にある Tool Call の結果を見た方が分かりやすいです。今回の実行では Writer と Reviewer をそれぞれ呼び出して、Reviewer からのフィードバックを受けて再度 Writer を実行していることが分かります。

A2A であることを全く意識せずに Multi Agents な処理を実現出来るのは Agent Framework のメリットですね。

Durable Agents から利用する

最後に Agent Framework の拡張機能である Durable Agents からも利用してみます。本質的に処理時間が長くなりやすい Agent を安全にホストできる環境は実現が難しいですが、Durable Agents は信頼性の高い Durable Functions 上で実行されるため安全に利用できます。

Durable Agents を利用するには追加の NuGet パッケージが必要になります。現状のバージョンでは Durable Task Scheduler のパッケージも同時に追加しないとエラーになる不具合があるようでした。Agent の実行履歴を確認するのが容易なので DTS を使う前提で考えるのが良いでしょう。

これまでのサンプルコードをベースに Durable Agents に置き換えるには ConfigureDurableAgents を呼び出すだけになります。以下のように Agent 定義のタイミングの修正で済むのでわかりやすいです。

using A2A;

using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

var writerResolver = new A2ACardResolver(new Uri($"{builder.Configuration["AGENT_HOST"]}/writer/"));
var reviewerResolver = new A2ACardResolver(new Uri($"{builder.Configuration["AGENT_HOST"]}/reviewer/"));

var writerAgent = await writerResolver.GetAIAgentAsync();
var reviewerAgent = await reviewerResolver.GetAIAgentAsync();

builder.ConfigureDurableAgents(options =>
{
    options.AddAIAgent(writerAgent, enableHttpTrigger: false, enableMcpToolTrigger: false);
    options.AddAIAgent(reviewerAgent, enableHttpTrigger: false, enableMcpToolTrigger: false);
});

builder.Build().Run();

これだけでは呼び出しは行われないので、Durable Agents を使う場合は追加で Orchestrator を Durable Functions として追加します。Durable Functions についての説明はしないので、必要に応じて公式ドキュメントを参照してください。

今回は以下のようにループで 5 回まで Writer と Reviewer を呼び出す Orchestrator を実装しました。Reviewer が APPROVED を返すまでフィードバックを投げるというシンプルな処理ですが、セッション単位で履歴は自動的に管理されているので、Reviewer のフィードバックをそのまま Writer に渡す実装になっています。

public class Function
{
    [Function(nameof(RunOrchestrator))]
    public async Task<string> RunOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
    {
        var message = context.GetInput<string>();

        var writerAgent = context.GetAgent("WriterAgent");
        var writerSession = await writerAgent.CreateSessionAsync();

        var reviewerAgent = context.GetAgent("ReviewerAgent");
        var reviewerSession = await reviewerAgent.CreateSessionAsync();

        for (int i = 0; i < 5; i++)
        {
            var writerResponse = await writerAgent.RunAsync<string>(message, writerSession);

            var reviewerResponse = await reviewerAgent.RunAsync<string>(writerResponse.Text, reviewerSession);

            if (reviewerResponse.Text == "APPROVED")
            {
                return writerResponse.Text;
            }

            message = reviewerResponse.Text;
        }

        return "FAILED";
    }
}

これまでの実装とは異なり Session という概念が出てきますが、名前から想像がつくように Agent の履歴に相当するものです。Durable Agents の場合は Session が内部的に Durable Entities として管理されるようになっているので、強力に分離されているかつスケーラブルです。

HttpTrigger などを使って Orchestrator を実行すると Durable Task Scheduler でどのように Agent が呼ばれたかを簡単に確認可能です。DTS 自体の UI がかなり洗練されたこともあり、非常に分かりやすく表示されています。

それぞれの Agent でどのようなメッセージがやり取りされたかは、Agents からタイムラインとチャット形式の両方で確認できるので分かりやすいです。このあたりの Observability を確保するのは意外に大変ですが Durable Agents と DTS を使えばあっという間です。

個人的には Durable Agents を使って A2A Agent を Tool Call した場合の挙動について確認をしたかったのですが、かなり長くなってしまったので別で書きたいと思います。AI Agent はFlex Consumption や Container Apps との相性が非常に良いので、今後も Agent Framework のアップデートを含めて追っていきたいです。