可观察性

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 会推送链路追踪数据到服务器。

image-20260717093744085

image-20260717093759440


OpenTelemetryAgent 详细配置

UseOpenTelemetry 内部就是 new OpenTelemetryAgent(...),所以配置项 = OpenTelemetryAgent 的构造参数 + 公开属性(含从 DelegatingAIAgent / AIAgent 继承的)。

完整清单:

成员类型默认值可写说明
EnableSensitiveDataboolfalse是否把消息正文(输入/回复/工具入参出参/指令/schema)打进 Span
Idstring随机 GUID代理唯一标识, gen_ai.agent.id 标签
Namestring?null代理名, gen_ai.agent.name 标签,也用于 Span 显示名 invoke_agent <name>(<id>)
Descriptionstring?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_agent Span,框架会同步把值传给自动接线的内层 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;
    })

image-20260717100325653


如果 EnableSensitiveData = true;,SDK 会把每次会话的请求和响应内容都输出到链路中,在调试和了解细节时非常有用,但是要注意敏感信息泄露。

    .UseOpenTelemetry(sourceName: null, configure: cfg =>
    {
        cfg.EnableSensitiveData = true;
    })

链路追踪中会显示每次对话的详细内容,只建议调试模式时使用,否则整个链路会非常庞大,并且容易泄露敏感信息。

image-20260717095711453


完整可观察性案例

由于 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 的面板,所以这里不作更多演示。

image-20260717102054667


日志

前面说到,日志一般不会通过 OpenTelemetry 收集,一般都是通过控制台打印日志后,由 logstash 或者 Filebeat 等批处理收集后解析消息结构,再推送到 Kafka,由下游服务二次处理再推送到 ES 里面,如果日志量比较大,还可能上 Hbase 等分布式文件系统。

所以 MAF 的设计也是如此。

MAF 用 ILogger 输出日志的方式,和「埋点」是两套独立的东西,它走的是 .NET 标准的 ILogger 管道,你只要配 appsettings.jsonLogging: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();

image-20260717105812837


以下是 MAF 框架的日志源,它们都需要在 Debug 级别才会打印日志。

Category(全限定,示例)组件
Microsoft.Agents.AI.ChatClientAgent核心 Agent 运行
Microsoft.Agents.AI.CompactionProvider上下文压缩
Microsoft.Agents.AI.AgentSkillsProviderSkills 加载
Microsoft.Agents.AI.ChatHistoryMemoryProvider历史存储
Microsoft.Agents.AI.TextSearchProvider搜索工具
Microsoft.Agents.AI.Harness.Loop.LoopAgentHarness 循环代理
Microsoft.Agents.AI.Mcp.Skills.AgentMcpSkillsSourceMCP 技能源

指标名称

前面只介绍了 MAF 框架的链路追踪和日志,里面有一些标签记录的信息需要理解,这里使用了 AI 帮助梳理这部分信息。

Span 名谁产生关键标签
invoke_agent <name>(<id>)MAF(OpenTelemetryAgentgen_ai.operation.namegen_ai.agent.id/.name/.descriptiongen_ai.provider.name
chat <model>Microsoft.Extensions.AIgen_ai.request.modelgen_ai.usage.input_tokens/output_tokensserver.address/.port
execute_tool <name>Microsoft.Extensions.AI(FICC)gen_ai.tool.name


标签遵循 OTel Gen-AI 语义约定(目前 experimental,名字带 gen_ai. 前缀)。

要收指标得另外建 MeterProviderAddMeter

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.durationHistogram单次模型调用耗时分布
gen_ai.client.token.usageCounter/Histogramtoken 用量(输入/输出)

默认 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覆盖环境变量。