0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第七編:OpenTelemetryでMicrosoft Agent Frameworkに「可観測性」を追加する

0
Posted at

エージェントアプリを作るときに最も怖いのは、エラーが出ることではなく、「エラーは出ていないように見えるのに、なんだかおかしい」ことです。たとえば次のような場合です。

  • ユーザーが一言入力したとき、Agent はいったいどのモデルを呼び出したのか?
  • ツールは発火したのか?
  • ツールの実行がどこで遅いのか?
  • Prompt と Response は期待どおりに流れているのか?
  • 本番で問題が起きたとき、どうやって素早く特定するのか?

これが可観測性で解決すべき問題です。

Microsoft Agent Framework にはすでに OpenTelemetry のサポートが組み込まれており、Agent の実行プロセスを trace、log、metric の形式で出力できます。つまり、自分で大量のログを書かなくても、Agent の呼び出しチェーンで何が起きたのかを確認できます。公式ドキュメントでも、Agent Framework は OpenTelemetry GenAI のセマンティック規約に基づいてトレース、ログ、メトリクスを出力すると説明されています。

今回のサンプルで何をするのか?

公式の AgentOpenTelemetry サンプルを参考にしました。このデモは主に 3 つの部分で構成されています。1つのコンソール Agent アプリ、Agent Framework の OpenTelemetry 計測、そしてローカルでテレメトリデータを確認するための Aspire Dashboard です。

全体の流れはとてもシンプルです。

  1. Aspire Dashboard を起動する
  2. OTLP endpoint を設定する
  3. Agent アプリを実行する
  4. Dashboard で trace、ログ、メトリクスを確認する

公式サンプルでは、Aspire Dashboard を Docker で起動し、OTLP のテレメトリポートを公開します。アプリは http://localhost:4317 にデータを送信し、ブラウザで結果を確認します。

コアの考え方

C# では、Agent Framework の可観測性は主に 2 層あります。

  • 1層目は Chat Client に OpenTelemetry を付与すること
  • 2層目は Agent 自体に OpenTelemetry を付与すること

公式ドキュメントのサンプルでは .UseOpenTelemetry(...).WithOpenTelemetry(...) が使われています。前者はより低レベルのモデル呼び出し寄りで、後者は Agent の実行プロセス寄りです。

ただし、ここでひとつ注意があります。両方を有効にすると、特に sensitive data も有効にしている場合、prompt、response、またはコンテキスト情報が重複して見えることがあります。公式でも、実際のニーズに応じて Chat Client 側だけ、または Agent 側だけを有効にし、データの重複を避けるよう推奨しています。

完全なコード

// Copyright (c) Microsoft. All rights reserved.

using Azure.AI.OpenAI;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.ClientModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Metrics;

#region テレメトリの設定
// このサンプルのカスタム ActivitySource と Meter のソース名。その他の計測はそれぞれのソース/カテゴリを使用する。
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
const string ServiceName = "ObservabilityDemo";
// Aspire ダッシュボード用の OpenTelemetry を設定
var otlpEndpoint = "http://localhost:4317";
// Azure で AgentApplicationInsights を作成し、Connection string を貼り付ければよい
var applicationInsightsConnectionString = "InstrumentationKey=******************";
// このサービスを識別するためのリソースを作成
var resource = ResourceBuilder.CreateDefault()
    .AddService(ServiceName, serviceVersion: "1.0.0")
    .AddAttributes(new Dictionary<string, object>
    {
        ["service.instance.id"] = Environment.MachineName,
        ["deployment.environment"] = "development"
    })
    .Build();
// リソースを使ってトレースを構成
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
    .AddSource(SourceName) // 自作の ActivitySource
    .AddHttpClientInstrumentation() // OpenAI への HTTP 呼び出しを捕捉
    .AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
{
    tracerProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
}
using var tracerProvider = tracerProviderBuilder.Build();
// リソースと計測器名でフィルタリングしてメトリクスを構成
using var meterProvider = Sdk.CreateMeterProviderBuilder()
    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
    .AddMeter(SourceName) // 自作の Meter ソース
    .AddHttpClientInstrumentation() // HTTP クライアントのメトリクス
    .AddRuntimeInstrumentation() // .NET ランタイムのメトリクス
    .AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
    .Build();
// OpenTelemetry で構造化ログを構成
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(loggingBuilder => loggingBuilder
    .SetMinimumLevel(LogLevel.Debug)
    .AddOpenTelemetry(options =>
    {
        options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"));
        options.AddOtlpExporter(otlpOptions => otlpOptions.Endpoint = new Uri(otlpEndpoint));
        if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
        {
            options.AddAzureMonitorLogExporter(options => options.ConnectionString = applicationInsightsConnectionString);
        }
        options.IncludeScopes = true;
        options.IncludeFormattedMessage = true;
    }));
using var activitySource = new ActivitySource(SourceName);
using var meter = new Meter(SourceName);
// カスタムメトリクスを作成
var interactionCounter = meter.CreateCounter<int>("agent_interactions_total", description: "Agent 交互总次数");
var responseTimeHistogram = meter.CreateHistogram<double>("agent_response_time_seconds", description: "Agent 响应时间(秒)");
#endregion

var serviceProvider = serviceCollection.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var appLogger = loggerFactory.CreateLogger<Program>();

Console.WriteLine("""
    === OpenTelemetry Aspire デモ ===
    このデモは OpenTelemetry  Agent Framework の統合を示します。
    テレメトリデタは Aspire ダッシュボドで確認できます。
    メッセジを入力して Enter を押してください。"exit" または空メッセジで終了します。
    """);

var arr = File.ReadAllLines("C:/gpt/azure_gpt5.4_mini.txt");
var endpoint = arr[1];
var deploymentName = arr[0];
// アプリケーションの起動を記録
appLogger.LogInformation("OpenTelemetry Aspire デモアプリケーションが起動しました");

[Description("指定した場所の天気を取得します。")]
static async Task<string> GetWeatherAsync([Description("天気を取得する場所。")] string location)
{
    await Task.Delay(2000);
    return $"ご指定の {location} の天気は、曇り、最高気温 15°C です。";
}

var credential = new ApiKeyCredential(arr[2]);
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), credential)
    .GetChatClient(deploymentName)
    .AsIChatClient()
    .AsBuilder()
    .UseFunctionInvocation()
    .UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // チャットクライアントレベルでテレメトリを有効化
    .Build();

appLogger.LogInformation("OpenTelemetry 計測を有効にした Agent を作成しています");
// 計測済みのチャットクライアントを使って Agent を作成
var agent = new ChatClientAgent(instrumentedChatClient,
    name: "OpenTelemetryDemoAgent",
    instructions: "あなたは役に立つアシスタントです。簡潔で十分な情報を含む回答をしてください。",
    tools: [AIFunctionFactory.Create(GetWeatherAsync)])
    .AsBuilder()
    .UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // Agent レベルでテレメトリを有効化
    .Build();

var session = await agent.CreateSessionAsync();
appLogger.LogInformation("Agent の作成に成功しました。ID: {AgentId}", agent.Id);
// セッション全体の親 Span を作成
using var sessionActivity = activitySource.StartActivity("Agent Session");
Console.WriteLine($"Trace ID: {sessionActivity?.TraceId} ");
var sessionId = Guid.NewGuid().ToString("N");
sessionActivity?
    .SetTag("agent.name", "OpenTelemetryDemoAgent")
    .SetTag("session.id", sessionId)
    .SetTag("session.start_time", DateTimeOffset.UtcNow.ToString("O"));
appLogger.LogInformation("Agent セッションを開始しています。セッション ID: {SessionId}", sessionId);

using (appLogger.BeginScope(new Dictionary<string, object> { ["SessionId"] = sessionId, ["AgentName"] = "OpenTelemetryDemoAgent" }))
{
    var interactionCount = 0;
    while (true)
    {
        Console.Write("あなた("exit" で終了): ");
        var userInput = Console.ReadLine();
        if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
        {
            appLogger.LogInformation("ユーザーがセッション終了を要求しました");
            break;
        }

        interactionCount++;
        appLogger.LogInformation("ユーザー操作 #{InteractionNumber} を処理しています: {UserInput}", interactionCount, userInput);
        // 各インタラクションごとに子 Span を作成
        using var activity = activitySource.StartActivity("Agent Interaction");
        activity?
            .SetTag("user.input", userInput)
            .SetTag("agent.name", "OpenTelemetryDemoAgent")
            .SetTag("interaction.number", interactionCount);

        var stopwatch = Stopwatch.StartNew();
        try
        {
            appLogger.LogDebug("インタラクション #{InteractionNumber} の Agent 実行を開始", interactionCount);
            Console.Write("Agent: ");
            // Agent を実行する(内部のテレメトリ Span が作成される)
            await foreach (var update in agent.RunStreamingAsync(userInput, session))
            {
                Console.Write(update.Text);
            }
            Console.WriteLine();
            stopwatch.Stop();
            var responseTime = stopwatch.Elapsed.TotalSeconds;
            // メトリクスを記録(Python サンプルと同様)
            interactionCounter.Add(1, new KeyValuePair<string, object?>("status", "success"));
            responseTimeHistogram.Record(responseTime,
                new KeyValuePair<string, object?>("status", "success"));
            activity?.SetTag("response.success", true);
            appLogger.LogInformation("Agent 交互 #{InteractionNumber} は正常に完了しました。所要時間 {ResponseTime:F2} 秒",
                interactionCount, responseTime);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラー: {ex.Message}");
            Console.WriteLine();
            stopwatch.Stop();
            var responseTime = stopwatch.Elapsed.TotalSeconds;
            // エラーメトリクスを記録
            interactionCounter.Add(1, new KeyValuePair<string, object?>("status", "error"));
            responseTimeHistogram.Record(responseTime,
                new KeyValuePair<string, object?>("status", "error"));
            activity?
                .SetTag("response.success", false)
                .SetTag("error.message", ex.Message)
                .SetStatus(ActivityStatusCode.Error, ex.Message);
            appLogger.LogError(ex, "Agent 交互 #{InteractionNumber} は {ResponseTime:F2} 秒後に失敗しました: {ErrorMessage}",
                interactionCount, responseTime, ex.Message);
        }
    }
    // セッションの要約を親 Span に追加
    sessionActivity?
        .SetTag("session.total_interactions", interactionCount)
        .SetTag("session.end_time", DateTimeOffset.UtcNow.ToString("O"));
    appLogger.LogInformation("Agent セッションが完了しました。総インタラクション数: {TotalInteractions}", interactionCount);
} // ログスコープ終了

appLogger.LogInformation("OpenTelemetry Aspire デモアプリケーションを終了しています");

ローカルでの実行方法

ローカルでデバッグする場合は、まず Aspire Dashboard を起動します。

docker run -d --name aspire-dashboard `
 -p 4318:18888 `
 -p 4317:18889 `
 -e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true `
 mcr.microsoft.com/dotnet/aspire-dashboard:latest

そのあと、上記の C# プロジェクトを実行するように設定します。ブラウザで http://localhost:4318 にアクセスします。

次に、コンソールで Agent と少し会話すると、Aspire Dashboard で trace を確認できます。

一括起動では、設定の確認、アプリのビルド、Dashboard の起動、ブラウザの起動、コンソールプログラムの実行まで含まれます。

ApplicationInsights の効果はさらに明確です。

何が見えるのか?

設定後、特に有用なのは次の情報です。

Agent 呼び出し全体のチェーン、たとえば invoke_agent <agent_name> を確認できます。

Agent がモデルを呼び出した場合、chat <model_name> のような span が表示されます。

Agent がツールを呼び出した場合も、execute_tool <function_name> のような span が表示されます。公式ドキュメントでも、これらの span は設定後に自動生成されると説明されています。

これは Agent のデバッグに非常に役立ちます。たとえば、ツールが遅い、モデルが遅い、あるステップのパラメータが正しくない、といった問題も trace から直接確認できます。

sensitive data に注意する

開発環境ではデバッグをしやすくするために sensitive data を有効にして、prompt、response、関数パラメータ、ツールの戻り値を確認できます。

ただし、本番環境では不用意に有効にしないでください。

これらの内容には、ユーザー入力、業務データ、さらには機密情報が含まれる可能性があります。公式ドキュメントでも、sensitive data は開発環境またはテスト環境でのみ有効化することを明確に推奨しています。

まとめ

Agent アプリは普通の API ではありません。モデル呼び出し、ツール呼び出し、複数ラウンドのコンテキスト、さらには外部サービス依存まで含まれます。

そのため、Console.WriteLine だけではすぐに足りなくなります。

Agent Framework に組み込まれた OpenTelemetry のサポートと Aspire Dashboard を組み合わせれば、ローカルで Agent の完全な実行チェーンを直感的に確認できます。開発段階での問題切り分けがかなり楽になり、のちに Application Insights やその他の観測プラットフォームへ接続するのも自然です。

(Translated by GPT)

元のリンク:https://mp.weixin.qq.com/s/KRCQrD1TAMZFVlIaClNCBQ?token=1435541481&lang=zh_CN&wt.mc_id=MVP_325642

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?