第七篇:用 OpenTelemetry 给 Microsoft Agent Framework 加上“可观测性”

做 Agent 应用时,最怕的不是它报错,而是它“看起来没报错,但就是不对劲”。比如:

用户问了一句话,Agent 到底调用了哪个模型?

有没有触发工具?

工具执行慢在哪里?

Prompt 和 Response 有没有按预期流转?

线上出了问题,怎么快速定位?

这就是可观测性要解决的问题。

Microsoft Agent Framework 已经内置了 OpenTelemetry 支持,可以把 Agent 的执行过程以 trace、log、metric 的形式导出来。也就是说,我们不用自己手写一堆日志,就能看到 Agent 调用链路里发生了什么。官方文档也提到,Agent Framework 会基于 OpenTelemetry GenAI 语义约定输出跟踪、日志和指标。

这次示例做什么?

我参考了官方的AgentOpenTelemetry 示例。这个 demo 主要由三部分组成:一个控制台 Agent 应用、Agent Framework 的 OpenTelemetry 埋点,以及本地用来查看遥测数据的 Aspire Dashboard。

整体流程很简单:

启动 Aspire Dashboard

配置 OTLP endpoint

运行 Agent 应用

在 Dashboard 里查看 trace、日志和指标

官方示例里,Aspire Dashboard 会通过 Docker 启动,并把 OTLP 遥测端口暴露出来,应用把数据发到 http://localhost:4317,然后我们在浏览器里看效果。

核心思路

在 C# 里,Agent Framework 的可观测性主要有两层:

第一层是给 Chat Client 加 OpenTelemetry。

第二层是给 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
{
[“service.instance.id”] = Environment.MachineName,
[“deployment.environment”] = “development”
})
.Build();
// 使用资源配置追踪
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: “1.0.0”))
.AddSource(SourceName) // 我们自定义的活动源
.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) // 我们自定义的计量器源
.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(“agent_interactions_total”, description: “Agent 交互总次数”);
var responseTimeHistogram = meter.CreateHistogram(“agent_response_time_seconds”, description: “Agent 响应时间(秒)”);

endregion

var serviceProvider = serviceCollection.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService();
var appLogger = loggerFactory.CreateLogger();
Console.WriteLine(“””
=== OpenTelemetry Aspire 演示 ===
此演示展示了 OpenTelemetry 与 Agent Framework 的集成。
你可以在 Aspire 仪表板中查看遥测数据。
请输入消息并按回车。输入“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 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);
// 为整个 Agent 会话创建父 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 { [“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(“status”, “success”));
responseTimeHistogram.Record(responseTime,
new KeyValuePair(“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(“status”, “error”));
responseTimeHistogram.Record(responseTime,
new KeyValuePair(“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 调用了模型,会有类似 chat 的 span。

如果 Agent 调用了工具,也会看到类似 execute_tool 的 span。官方文档里也说明了这些 span 会在配置后自动生成。

这对调试 Agent 很有帮助。比如工具慢、模型慢、某一步参数不对,都可以直接从 trace 里看出来。

注意 sensitive data

开发环境里,为了调试方便,可以打开 sensitive data,这样能看到 prompt、response、函数参数和工具返回值。

但生产环境不要随便开。

因为这些内容里可能有用户输入、业务数据,甚至敏感信息。官方文档也明确提醒,敏感数据只建议在开发或测试环境启用。

小结

Agent 应用不是普通 API。它中间有模型调用、工具调用、多轮上下文、甚至外部服务依赖。

所以,只靠 Console.WriteLine 很快就不够用了。

用 Agent Framework 自带的 OpenTelemetry 支持,再配合 Aspire Dashboard,本地就能很直观地看到 Agent 的完整执行链路。开发阶段排查问题会轻松很多,后面要接 Application Insights 或其他观测平台,也比较自然。

声明:来自硅基-桂迹,仅代表创作者观点。链接:https://eyangzhen.com/7753.html

硅基-桂迹的头像硅基-桂迹

相关推荐

添加微信
添加微信
Ai学习群
返回顶部