🐕

Microsoft Agent Framework (C#) を見てみよう その11 エージェントを見てみよう

に公開

シリーズ記事

はじめに

今回は、Microsoft Agent Framework のエージェント機能について見ていこうと思います。
Microsoft Agent Framework のエージェントは様々なものに対応しています。なんとなく透けて見えるものとして AI Foundry の Agent Service を使ってほしそうな感じがありますが、今回は基本的にローカルでカスタマイズ可能なエージェントを作成する感じで行こうと思います。

対応しているエージェントの種類としては以下のようなものがあります。

  • A2A
  • Azure AI Foundry Agent Service
  • OpenAI Chat Completion API(Azure OpenAI 含む)
  • OpenAI Responses API(Azure OpenAI 含む)
  • OpenAI Assistant API(Azure OpenAI 含む)
  • Microsoft.Extensions.AI の IChatClient
    • Ollama
    • OpenAI
    • ONNX
    • Bedrock (公式サンプルには無いけど AWSSDK.Extensions.Bedrock.MEAI があるので多分いける)
    • etc...

また、Agent のベースクラスも提供されているので、それを実装してカスタムのエージェントを作成することも可能です。メジャー所は割と対応していますね。
早速試してみましょう Azure OpenAI を使う場合は、以下のようなパッケージを追加します。

<PackageReference Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-preview.251016.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251016.1" />

エージェントを作成するには以下のように OpenAI.Chat.ChatClient の拡張メソッドの CreateAIAgent を使うと簡単にエージェントを作成できます。

using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI;

// Azure OpenAI クライアントの作成
var azureClient = new AzureOpenAIClient(
    new Uri("<<AI の エンドポイント>>"),
    new AzureCliCredential());
// エージェントの作成
var agent = azureClient.GetChatClient("gpt-4.1")
    .CreateAIAgent(
        // agent への支持
        instructions: """
        あなたはネコ型アシスタントです。
        ネコらしく振舞うために語尾はかならず「にゃん」にしてください。
        """,
        // エージェントの名前
        name: "Cat");

// エージェントの実行をして結果を表示する
var result = await agent.RunAsync("品川の天気を教えて");
Console.WriteLine(result.Text);

因みに同じように使える CreateAIAgent 拡張メソッドが Microsoft.Extensions.AIIChatClient にも用意されています。因みに OpenAI.Chat.ChatClientCreateAIAgent は内部で AsIChatClient 拡張メソッドを使って IChatClient に変換してから CreateAIAgent を呼び出しているので、基本的には同じものと考えて良いです。

このコードを実行すると、以下のような出力が得られます。

2024年6月現在の情報だと、品川の天気は曇りがちで、気温はだいたい25度くらいにゃん。午後には少し雨が降る可能性もあるから、傘を持っていくと安心にゃん!

※最新の天気情報は、天気予報サイトやアプリで確認してにゃん

もちろん最新の情報は知らないので、天気予報サイトなどを参照するように促していますね。
ツール呼び出しにも対応しています。ツールの指定は CreateAIAgent の引数で tools パラメーターに Microsoft.Extensions.AIAITool の配列を渡すことで指定できます。例えば、天気予報のツールを作成してエージェントに渡す場合は以下のようになります。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ComponentModel;

var azureClient = new AzureOpenAIClient(
    new Uri("<<AI の エンドポイント>>"),
    new AzureCliCredential());
var agent = azureClient.GetChatClient("gpt-4.1")
    .CreateAIAgent(
        instructions: """
        あなたはネコ型アシスタントです。
        ネコらしく振舞うために語尾はかならず「にゃん」にしてください。
        """,
        name: "Cat",
        // ツールを渡せば自動で使ってくれる
        tools: [
            AIFunctionFactory.Create(
                ([Description("場所")]string location) => $"{location} の天気は空からカエルが降る異常気象です。", 
                description: "指定した場所の天気を取得します。")
        ]);

var result = await agent.RunAsync("品川の天気を教えて");
Console.WriteLine(result.Text);

このコードを実行すると、以下のような出力が得られます。

品川の天気は、空からカエルが降る異常気象にゃん!お出かけの時は傘じゃなくてカエル避けの帽子がいるかもしれないにゃん。

ちゃんと、ツールを呼び出して、その情報をもとに回答を生成してくれていることがわかりますね。
これは、Agent Framework の内部で自動的に Microsoft.Extensions.AI の自動ツール呼び出しのミドルウェアを組み込んでくれるためです。自分でミドルウェアを追加する必要はありません。もちろん他のミドルウェアを追加したい場合は一度自分で IChatClient にして自分でミドルウェアを追加することで対応できます。例えば Microsoft.Extensions.Logging.Console パッケージを追加して以下のようにコードを変えることでログ出力を追加できます。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using System.ComponentModel;

var azureClient = new AzureOpenAIClient(
    new Uri("<<AI の エンドポイント>>"),
    new AzureCliCredential());

// トレースレベル以上のログをコンソールに出力するロガー
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Trace));

var agent = azureClient.GetChatClient("gpt-4.1")
    // IChatClient にして
    .AsIChatClient()
    // 一度 Builder にして
    .AsBuilder()
    // ログのミドルウェアを仕込んで
    .UseLogging(loggerFactory)
    // Build して CreateAIAgent を呼ぶのと同じ効果をもつ BuildAIAgent を使ってエージェントにする
    .BuildAIAgent(
        instructions: """
        あなたはネコ型アシスタントです。
        ネコらしく振舞うために語尾はかならず「にゃん」にしてください。
        """,
        name: "Cat",
        // ツールを渡せば自動で使ってくれる
        tools: [
            AIFunctionFactory.Create(
                ([Description("場所")]string location) => $"{location} の天気は空からカエルが降る異常気象です。", 
                description: "指定した場所の天気を取得します。")
        ]);

var result = await agent.RunAsync("品川の天気を教えて");
Console.WriteLine(result.Text);

これを実行すると以下のようにログが出力されます。

trce: Microsoft.Extensions.AI.LoggingChatClient[805843669]
      GetResponseAsync invoked: [
        {
          "role": "user",
          "contents": [
            {
              "$type": "text",
              "text": "品川の天気を教えて"
            }
          ]
        }
      ]. Options: {
        "instructions": "あなたはネコ型アシスタントです。\r\nネコらしく振舞うために語尾はかならず「にゃん」にしてください。"
      }. Metadata: {
        "providerName": "openai",
        "providerUri": "<<AI の エンドポイント>>",
        "defaultModelId": "gpt-4.1"
      }.
trce: Microsoft.Extensions.AI.LoggingChatClient[384896670]
      GetResponseAsync completed: {
        "messages": [
          {
            "createdAt": "2025-10-21T14:06:45+00:00",
            "role": "assistant",
            "contents": [
              {
                "$type": "functionCall",
                "callId": "call_I0qZJwqB4k3YddaPM6sE1hlB",
                "name": "_Main_b_0_1",
                "arguments": {
                  "location": "品川"
                }
              }
            ],
            "messageId": "chatcmpl-CT7IbUJFYis5EnlvIAwRktmT4tpzh"
          }
        ],
        "responseId": "chatcmpl-CT7IbUJFYis5EnlvIAwRktmT4tpzh",
        "modelId": "gpt-4.1-2025-04-14",
        "createdAt": "2025-10-21T14:06:45+00:00",
        "finishReason": "tool_calls",
        "usage": {
          "inputTokenCount": 99,
          "outputTokenCount": 21,
          "totalTokenCount": 120,
          "additionalCounts": {
            "InputTokenDetails.AudioTokenCount": 0,
            "InputTokenDetails.CachedTokenCount": 0,
            "OutputTokenDetails.ReasoningTokenCount": 0,
            "OutputTokenDetails.AudioTokenCount": 0,
            "OutputTokenDetails.AcceptedPredictionTokenCount": 0,
            "OutputTokenDetails.RejectedPredictionTokenCount": 0
          }
        }
      }.
trce: Microsoft.Extensions.AI.LoggingChatClient[805843669]
      GetResponseAsync invoked: [
        {
          "role": "user",
          "contents": [
            {
              "$type": "text",
              "text": "品川の天気を教えて"
            }
          ]
        },
        {
          "createdAt": "2025-10-21T14:06:45+00:00",
          "role": "assistant",
          "contents": [
            {
              "$type": "functionCall",
              "callId": "call_I0qZJwqB4k3YddaPM6sE1hlB",
              "name": "_Main_b_0_1",
              "arguments": {
                "location": "品川"
              }
            }
          ],
          "messageId": "chatcmpl-CT7IbUJFYis5EnlvIAwRktmT4tpzh"
        },
        {
          "role": "tool",
          "contents": [
            {
              "$type": "functionResult",
              "callId": "call_I0qZJwqB4k3YddaPM6sE1hlB",
              "result": "品川 の天気は空からカエルが降る異常気象です。"
            }
          ]
        }
      ]. Options: {
        "instructions": "あなたはネコ型アシスタントです。\r\nネコらしく振舞うために語尾はかならず「にゃん」にしてください。"
      }. Metadata: {
        "providerName": "openai",
        "providerUri": "<<AI の エンドポイント>>",
        "defaultModelId": "gpt-4.1"
      }.
trce: Microsoft.Extensions.AI.LoggingChatClient[384896670]
      GetResponseAsync completed: {
        "messages": [
          {
            "createdAt": "2025-10-21T14:06:45+00:00",
            "role": "assistant",
            "contents": [
              {
                "$type": "text",
                "text": "品川の天気は、空からカエルが降る異常気象らしいにゃん!不思議な天気なので、お出かけは気をつけるにゃん!"
              }
            ],
            "messageId": "chatcmpl-CT7IbBiQP9tE15rkM79otdMmFlUi5"
          }
        ],
        "responseId": "chatcmpl-CT7IbBiQP9tE15rkM79otdMmFlUi5",
        "modelId": "gpt-4.1-2025-04-14",
        "createdAt": "2025-10-21T14:06:45+00:00",
        "finishReason": "stop",
        "usage": {
          "inputTokenCount": 152,
          "outputTokenCount": 48,
          "totalTokenCount": 200,
          "additionalCounts": {
            "InputTokenDetails.AudioTokenCount": 0,
            "InputTokenDetails.CachedTokenCount": 0,
            "OutputTokenDetails.ReasoningTokenCount": 0,
            "OutputTokenDetails.AudioTokenCount": 0,
            "OutputTokenDetails.AcceptedPredictionTokenCount": 0,
            "OutputTokenDetails.RejectedPredictionTokenCount": 0
          }
        }
      }.
品川の天気は、空からカエルが降る異常気象らしいにゃん!不思議な天気なので、お出かけは気をつけるにゃん!

ちゃんとログが出てますね!Microsoft.Extensions.AI の上に構築されているので、そこのために作られた仕組みがそのまま使えるのは便利です。

Structured output に対応する

Microsoft Agent Framework のエージェントは Structured output にも対応しています。Structured output とは、AI の応答を JSON などの構造化された形式で受け取る機能です。これにより、AI の応答をプログラムで簡単に解析・処理できるようになります。Structured output も Microsoft.Extensions.AI の機能として提供されているため、同様にエージェントでも利用可能です。具体的にはエージェントを作成する際に渡せる ChatClientAgentOptions クラスの ChatOptions プロパティに ChatOptions オブジェクトが渡せるので ResponseFormat プロパティを設定することで Structured output を指定できます。例えば、以下のように JSON 形式で応答を受け取るように指定できます。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using System.ComponentModel;

var azureClient = new AzureOpenAIClient(
    new Uri("<<AI の エンドポイント>>"),
    new AzureCliCredential());

// トレースレベル以上のログをコンソールに出力するロガー
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Trace));

var agent = azureClient.GetChatClient("gpt-4.1")
    // IChatClient にして
    .AsIChatClient()
    // 一度 Builder にして
    .AsBuilder()
    // ログのミドルウェアを仕込んで
    .UseLogging(loggerFactory)
    // Build して CreateAIAgent を呼ぶのと同じ効果をもつ BuildAIAgent を使ってエージェントにする
    .BuildAIAgent(new ChatClientAgentOptions
    {
        Instructions = """
            あなたはネコ型アシスタントです。
            ネコらしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        Name = "Cat",
        ChatOptions = new()
        {
            Tools = [
                AIFunctionFactory.Create(
                    ([Description("場所")]string location) => $"{location} の天気は空からカエルが降る異常気象です。",
                    description: "指定した場所の天気を取得します。")
            ],
            // ここで Structured Output を指定
            ResponseFormat = ChatResponseFormat.ForJsonSchema<WeatherOutput>(),
        },
    });

// 戻り値の型を指定する (注意: 型引数を指定できるのは ChatClientAgent の場合のみ)
var result = await agent.RunAsync<WeatherOutput>("品川の天気を教えて");
// Result プロパティにデシリアライズされた結果が入ってる
Console.WriteLine(result.Result);

// この形式で返してもらう
record WeatherOutput(
    [Description("場所")]
    string Location,
    [Description("天気")]
    string Weather,
    [Description("ユーザー向けの回答メッセージ")]
    string Message);

コメントにも書いていますが、戻り値の型を指定できるのは ChatClientAgent の場合のみです。他のエージェントの場合は戻り値を文字列で受け取って自分でデシリアライズする必要があります。
このコードを実行すると、以下のような出力が得られます。ログも出ているのですが長くなるので最終結果だけ示します。

WeatherOutput { Location = 品川, Weather = 空からカエルが降る異常気象, Message = 品川の今日は、なんと空からカエルが降る異常気象にゃん!お出かけの際はカエル対策を忘れずににゃん。 }

ちゃんと Structured output が得られていますね。
このように ChatOptions が渡せるので、大体何でもできちゃいます。

チャット履歴

チャット履歴については初回で触れたので「雑感」とハローワールドの記事を参照してください。

ミドルウェア

IChatClient にも AI への呼び出しの前後に処理を挟み込むミドルウェアの仕組みが用意されています。その他に Agent Framework のエージェントのレイヤーにもミドルウェアがあります。ミドルウェアを作成するには DelegatingAIAgent クラスを継承して前後に処理を入れたい部分をオーバーライドします。例えば、以下のようにミドルウェアを作成してエージェントに組み込むことができます。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.ComponentModel;

var azureClient = new AzureOpenAIClient(
    new Uri("<<AI の エンドポイント>>"),
    new AzureCliCredential());

var agent = azureClient.GetChatClient("gpt-4.1")
    .AsIChatClient()
    .CreateAIAgent(new ChatClientAgentOptions
    {
        Instructions = """
            あなたはネコ型アシスタントです。
            ネコらしく振舞うために語尾はかならず「にゃん」にしてください。
            """,
        Name = "Cat",
        ChatOptions = new()
        {
            Tools = [
                AIFunctionFactory.Create(
                    ([Description("場所")]string location) => $"{location} の天気は空からカエルが降る異常気象です。",
                    description: "指定した場所の天気を取得します。")
            ],
            // ここで Structured Output を指定
            ResponseFormat = ChatResponseFormat.ForJsonSchema<WeatherOutput>(),
        },
    })
    // Agent をビルダーにして
    .AsBuilder()
    // ミドルウェアを仕込んで
    .Use(innerAgent => new DogMiddleware(innerAgent))
    // ビルドする
    .Build();

// この場合は agent は AIAgent 型なので RunAsync の型引数は指定できない
var result = await agent.RunAsync("品川の天気を教えて");
Console.WriteLine(result.Text);

// この形式で返してもらう
record WeatherOutput(
    [Description("場所")]
    string Location,
    [Description("天気")]
    string Weather,
    [Description("ユーザー向けの回答メッセージ")]
    string Message);

// 絶対犬にするミドルウェア
class DogMiddleware(AIAgent innerAgent) : DelegatingAIAgent(innerAgent)
{
    public override async Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
    {
        messages = [new ChatMessage(ChatRole.System, "実はあなたは犬です。ここから先は語尾に「わん」とつけてください。"), .. messages, ];
        return await InnerAgent.RunAsync(messages, thread, options, cancellationToken);
    }
}

システムメッセージとして犬であることを伝えて、語尾に「わん」とつけるように指示しています。ネコ型アシスタントなのに犬にしたいっていう魂胆です。これを実行すると以下のような出力が得られます。

{"location":"品川","weather":"空からカエルが降る異常気象","message":"品川の今日の天気は「空からカエルが降る異常気象」ですわん。不思議な日なので外出する際は気をつけてくださいわん。"}

ちゃんと犬になってますね!ミドルウェアの存在を知っているとカスタマイズの幅がひろがるので覚えておくと良いでしょう。

まとめ

ひとまず、軽くエージェント機能を見てみました。Microsoft.Extensions.AI の上に構築されているため、IChatClient のミドルウェアや Structured output などの仕組みがそのまま使えるのは便利ですね。エージェントのミドルウェアもあるので、より高度なカスタマイズも可能です。
次回は、Agent Framework の公式サンプルやコードを眺めてみて気になったところを書いてみようと思います。

Microsoft (有志)

Discussion