可观察性
OpenTelemetry 的出现是为了应对微服务的挑战,这一套也可以用在 AI 应用上。随着 AI 应用的迅速发展,Agent 逻辑和链路也变得尤为复杂,所以为了能够了解 Agent 的内部行为,帮助开发者解决 AI 应用的监控、调试、测试问题,关注 AI 应用的安全性和可靠性,所以在 AI 应用的观测性就变得尤为重要。
MAF 接入了 OpenTelemetry(OTel)标准,把 Agent 内部的行为,每一轮调用、每一次模型请求、每一次工具执行,都打成遥测数据(Span / 指标 / 日志)。
为了演示可观察性,笔者部署了 ClickStack 平台,读者也可以根据笔者的 《万字长文:企业可观察性平台的建设方案实践》部署自己的监控架构,文章地址:
https://www.cnblogs.com/whuanle/p/19117957
从最小 demo 开始
本小节演示的是使用控制台程序手动配置和注入做 OpenTelemetry ,如果你是 ASP.NET Core 则不需要这么麻烦,只需要 .AddSource("Experimental.Microsoft.Agents.AI") 即可。
先装包:
OpenTelemetry
OpenTelemetry.Exporter.OpenTelemetryProtocol
完整代码,复制即可跑:
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenTelemetry;
using OpenTelemetry.Trace;
using System.ClientModel;
using System.ComponentModel;
[Description("获取指定地点的天气。")]
static string GetWeather([Description("地点名称")] string location)
=> $"{location} 今天多云,最高 15°C。";
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("Experimental.Microsoft.Agents.AI")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://192.168.50.199:4317");
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
o.HttpClientFactory = () =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "5c3e497a-aaa3-416d-8e80-31c427fbdf3d");
return client;
};
}
)
.Build();
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential(key: "1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient(model: "qwen/qwen3.5-9b")
.AsAIAgent(
instructions: "你是一个乐于助人的助手,必要时调用工具。",
name: "Helper",
tools: [AIFunctionFactory.Create(GetWeather)])
.AsBuilder()
.UseOpenTelemetry() // 启用 OpenTelemetry 服务
.Build();
// 第 2 轮:触发工具调用(模型会自动调 GetWeather),模型仍记得第 1 轮
Console.WriteLine();
await foreach (var u in agent.RunStreamingAsync("阿姆斯特丹天气怎么样?"))
Console.Write(u);
运行程序后,SDK 会推送链路追踪数据到服务器。


OpenTelemetryAgent 详细配置
UseOpenTelemetry 内部就是 new OpenTelemetryAgent(...),所以配置项 = OpenTelemetryAgent 的构造参数 + 公开属性(含从 DelegatingAIAgent / AIAgent 继承的)。
完整清单:
| 成员 | 类型 | 默认值 | 可写 | 说明 |
|---|---|---|---|---|
EnableSensitiveData | bool | false | ✅ | 是否把消息正文(输入/回复/工具入参出参/指令/schema)打进 Span |
Id | string | 随机 GUID | ❌ | 代理唯一标识, gen_ai.agent.id 标签 |
Name | string? | null | ❌ | 代理名, gen_ai.agent.name 标签,也用于 Span 显示名 invoke_agent <name>(<id>) |
Description | string? | null | ❌ | 代理描述, gen_ai.agent.description 标签 |
CurrentRunContext (静态) | AgentRunContext? | null | — | 当前异步调用链的运行上下文(agent/session/options),让底层中间件能反查运行时信息 |
真正需要你配的只有两个:构造时的 sourceName、构造后(configure 回调里)的 EnableSensitiveData。其余要么是框架透传内层 Agent 的只读元数据(Id/Name/Description → 变成 Span 标签),要么是基类通用方法。
几个要点:
sourceName的「空白即默认」:传null、""、纯空白效果相同,都走默认值。- **
EnableSensitiveData** 默认是 false,开启后 EnableSensitiveData联动自动接线:改它不只影响invoke_agentSpan,框架会同步把值传给自动接线的内层 chat client 插槽,让chat/execute_tool的敏感数据开关保持一致,所以只需在configure里设一次。Id/Name/Description不是在这里配的:它们取自内层 Agent,你应在创建内层ChatClientAgent时通过AsAIAgent(name:..., instructions:...)等指定,OpenTelemetryAgent只是读出来打标签。
sourceName 要保持一致,默认为 "Experimental.Microsoft.Agents.AI",例如都填 AAA,这样 OpenTelemetry 才会监听这个事件源。
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("AAA")
...
AIAgent agent = new OpenAIClient(
...
.UseOpenTelemetry(sourceName: "AAA", configure: cfg =>
{
cfg.EnableSensitiveData = true;
})

如果 EnableSensitiveData = true;,SDK 会把每次会话的请求和响应内容都输出到链路中,在调试和了解细节时非常有用,但是要注意敏感信息泄露。
.UseOpenTelemetry(sourceName: null, configure: cfg =>
{
cfg.EnableSensitiveData = true;
})
链路追踪中会显示每次对话的详细内容,只建议调试模式时使用,否则整个链路会非常庞大,并且容易泄露敏感信息。

完整可观察性案例
由于 Microsoft.Agents.AI 由 Microsoft.Agents.AI.AIAgentBuilder和 Microsoft.Extensions.AI.ChatClientBuilder 组成,所以埋点也需要在 Microsoft.Extensions.AI 里面加上。
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential(key: "1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient(model: "qwen/qwen3.5-9b")
.AsAIAgent(
instructions: "你是一个乐于助人的助手,必要时调用工具。",
name: "Helper",
tools: [AIFunctionFactory.Create(GetWeather)],
clientFactory: client => client // Chat 层埋点
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg => cfg.EnableSensitiveData = true)
.Build())
.AsBuilder()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg =>
{
cfg.EnableSensitiveData = true; // Agent 层埋点
})
.Build();
完整可观察性应该包括链路追踪、指标监控、日志,不过日志一般是输出到控制台,由 Logstarsh 或者 fileeat 这些日志收集工具批处理再推送,一般很少通过 OpenTelemetry 收集日志。
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.ClientModel;
using System.ComponentModel;
[Description("获取指定地点的天气。")]
static string GetWeather([Description("地点名称")] string location)
=> $"{location} 今天多云,最高 15°C。";
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("AAA")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://192.168.50.199:4317");
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
o.HttpClientFactory = () =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "5c3e497a-aaa3-416d-8e80-31c427fbdf3d");
return client;
};
}
)
.Build();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName: "MyAIAgent", "1.0.0"))
.AddMeter("AAA")
.AddMeter("Microsoft.Extensions.AI")
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://192.168.50.199:4317");
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
o.HttpClientFactory = () =>
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "5c3e497a-aaa3-416d-8e80-31c427fbdf3d");
return client;
};
})
.Build();
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential(key: "1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient(model: "qwen/qwen3.5-9b")
.AsAIAgent(
instructions: "你是一个乐于助人的助手,必要时调用工具。",
name: "Helper",
tools: [AIFunctionFactory.Create(GetWeather)],
clientFactory: client => client // Chat 层埋点
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg => cfg.EnableSensitiveData = true)
.Build())
.AsBuilder()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg => // Agent 层埋点
{
cfg.EnableSensitiveData = true;
})
.Build();
Console.WriteLine();
await foreach (var u in agent.RunStreamingAsync("阿姆斯特丹天气怎么样?"))
Console.Write(u);
不过 ClickStack 看指标监控需要自己设计面板,这里笔者懒得设计,官方可能只有 grafana 的面板,所以这里不作更多演示。

日志
前面说到,日志一般不会通过 OpenTelemetry 收集,一般都是通过控制台打印日志后,由 logstash 或者 Filebeat 等批处理收集后解析消息结构,再推送到 Kafka,由下游服务二次处理再推送到 ES 里面,如果日志量比较大,还可能上 Hbase 等分布式文件系统。
所以 MAF 的设计也是如此。
MAF 用 ILogger 输出日志的方式,和「埋点」是两套独立的东西,它走的是 .NET 标准的 ILogger 管道,你只要配 appsettings.json 的 Logging:LogLevel 就能控制。同样分两层,和埋点层级一一对应:
// Agent 层日志
AIAgent agent = ...GetChatClient(...)
.AsAIAgent(...)
.AsBuilder()
.UseOpenTelemetry()
.UseLogging() // ← 日志中间件,category = "LoggingAgent"
.Build();
// Chat 层日志(和 OTel 一起,常配在 clientFactory 里)
.AsAIAgent(...,
clientFactory: client => client
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry()
.UseLogging() // ← category = "Microsoft.Extensions.AI"
.Build())
这里笔者使用 Serilog 演示日志配置,演示控制台程序日志配置过程,如果你是 ASP.NET Core 就不需要这么麻烦。
Microsoft.Extensions.Configuration.Json
Serilog
Serilog.Settings.Configuration
Serilog.Sinks.Console
Serilog.Extensions.Logging
定义 serilog.json 日志模板。
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console" ],
"MinimumLevel": {
"Default": "Debug",
"Override": {
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{SourceContext} {Scope} {Timestamp:HH:mm} [{Level}]{NewLine}{Properties:j}{NewLine}{Message:lj} {Exception} {NewLine}"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
}
}
static Serilog.ILogger GetJsonLogger()
{
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(path: "serilog.json", optional: true, reloadOnChange: true)
.Build();
if (configuration == null)
{
throw new ArgumentNullException($"未能找到 serilog.json 日志配置文件");
}
var logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
return logger;
}
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddSerilog(GetJsonLogger())
.SetMinimumLevel( LogLevel.Debug) ;
});
AIAgent agent = new OpenAIClient(
credential: new ApiKeyCredential(key: "1234"),
options: new OpenAIClientOptions { Endpoint = new Uri("http://127.0.0.1:1234/v1") })
.GetChatClient(model: "qwen/qwen3.5-9b")
.AsAIAgent(
instructions: "你是一个乐于助人的助手,必要时调用工具。",
name: "Helper",
tools: [AIFunctionFactory.Create(GetWeather)],
clientFactory: client => client // Chat 层埋点 和日志
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg => cfg.EnableSensitiveData = true)
.UseLogging(loggerFactory: loggerFactory)
.Build())
.AsBuilder()
.UseOpenTelemetry(sourceName: "AAA", configure: cfg =>
{
cfg.EnableSensitiveData = true;
})
.UseLogging(loggerFactory: loggerFactory)
.Build();

以下是 MAF 框架的日志源,它们都需要在 Debug 级别才会打印日志。
| Category(全限定,示例) | 组件 |
|---|---|
Microsoft.Agents.AI.ChatClientAgent | 核心 Agent 运行 |
Microsoft.Agents.AI.CompactionProvider | 上下文压缩 |
Microsoft.Agents.AI.AgentSkillsProvider | Skills 加载 |
Microsoft.Agents.AI.ChatHistoryMemoryProvider | 历史存储 |
Microsoft.Agents.AI.TextSearchProvider | 搜索工具 |
Microsoft.Agents.AI.Harness.Loop.LoopAgent | Harness 循环代理 |
Microsoft.Agents.AI.Mcp.Skills.AgentMcpSkillsSource | MCP 技能源 |
指标名称
前面只介绍了 MAF 框架的链路追踪和日志,里面有一些标签记录的信息需要理解,这里使用了 AI 帮助梳理这部分信息。
| Span 名 | 谁产生 | 关键标签 |
|---|---|---|
invoke_agent <name>(<id>) | MAF(OpenTelemetryAgent) | gen_ai.operation.name、gen_ai.agent.id/.name/.description、gen_ai.provider.name |
chat <model> | Microsoft.Extensions.AI | gen_ai.request.model、gen_ai.usage.input_tokens/output_tokens、server.address/.port |
execute_tool <name> | Microsoft.Extensions.AI(FICC) | gen_ai.tool.name 等 |
标签遵循 OTel Gen-AI 语义约定(目前 experimental,名字带 gen_ai. 前缀)。
要收指标得另外建 MeterProvider 并 AddMeter:
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("MyApp.Agent")
.AddMeter("Microsoft.Extensions.AI") // chat 层指标来自这个 meter
.AddOtlpExporter(o => o.Endpoint = new Uri("http://192.168.50.199:4317"))
.Build();
| 指标名 | 类型 | 含义 |
|---|---|---|
gen_ai.client.operation.duration | Histogram | 单次模型调用耗时分布 |
gen_ai.client.token.usage | Counter/Histogram | token 用量(输入/输出) |
默认 Span 里只有元信息(agent 名、模型名、token 数、耗时),不含消息正文,这是为了安全,避免 PII / 密钥外泄。
但排错时你往往需要看「模型到底收到了什么、回了什么」,升级 demo 里那句 configure: cfg => cfg.EnableSensitiveData = true 就是干这个的。开启后 Span 会多出这些标签:
| 标签 | 内容 |
|---|---|
gen_ai.input.messages | 发给模型的完整消息列表(含你的 prompt) |
gen_ai.output.messages | 模型的完整回复 |
gen_ai.system_instructions | 系统指令(instructions) |
gen_ai.tool.definitions | 工具的 schema 定义 |
| 工具入参 / 返回值 | execute_tool Span 里的函数调用入参出参 |
也可用环境变量全局打开(本地调试方便,不用改代码):
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
⚠️ 生产慎用:消息原文含 PII / 敏感信息,一旦导出到外部后端可能被缓存、被有权限者查看。打开前确认遥测链路有合适的访问控制和保留策略。代码里显式设
EnableSensitiveData会覆盖环境变量。