首页 > 基础资料 博客日记

[MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控

2026-06-12 08:30:02基础资料围观3

极客资料网推荐[MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控这篇文章给大家,欢迎收藏极客资料网享受知识的乐趣

赋予部署的应用和服务可观测性已经是一个基本的需求,在这方面,OpenTelemetry无疑已经称为了事实上的标准。OpenTelemetryChatClient是一个预定义的IChatClient中间件,它利用重写的GetResponseAsyncGetResponseStreamAsync方法,为LLM的调用添加了对于的链路和性能计数的输出。结合OpenTelemetry框架,开发者可以轻松地将这些数据发送到各种后端系统,如PrometheusJaeger等,以实现对LLM调用的深入分析和监控。本篇文件通过一个简单的例子在本地搭建一个这样的监控环境,展示针对Agent调用的链路跟踪和性能指标。

1. 构建基础设施

为了显示基于调用链的跟踪信息,我们在本地安装了Jaeger。为了收集和展示性能指标,我们使用了PrometheusGrafana。我们采用最简单的方式,通过在本地创建相映的Docker容器来搭建这些服务。如果希望在Windows上执行相应的命令,将换行符从\改为 ^即可。

Jageer:

docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  jaegertracing/all-in-one:latest

Prometheus:

docker run -d --name prometheus \
  -p 9090:9090 \
  -v /c/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus:latest

其中c:\prometheus\prometheus.yml的内容如下:

global:
  scrape_interval: 5s 

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'csharp_console_app'
    static_configs:
      - targets: ['192.168.1.166:9464'] 
  • 192.168.1.166是本地的IP地址,9464是接下来创建的应用暴露的端口,用于输出性能指标信息。

Grafana:

docker run -d --name grafana \
  -p 3000:3000 \
  grafana/grafana:latest

然后添加针对Prometheushttp://192.168.1.166:9090的连接。我针对OpenTelemetryChatClient输出的指标创建了一个简单Dashboard,可以通过这里下载并导入。

2. 构建一个简单的Agent应用

我们创建一个简单的Console应用,并添加针对OpenTelemetry.NET相关的NuGet包:

  • OpenTelemetry
  • OpenTelemetry.Exporter.Console
  • OpenTelemetry.Exporter.OpenTelemetryProtocol
  • OpenTelemetry.Exporter.Prometheus.HttpListener
  • OpenTelemetry.Extensions.Hosting

如下所示的是完整的演示程序。最外层的两个using块分别创建了TracerProviderMeterProvider,前者用于链路跟踪,后者用于性能指标的收集,两者设置了相同的服务名称(AIApp)和版本(1.0.0)。对于Trace,我们添加了ConsoleOTLP两种Exporter,后者将数据发送到Jaeger。对于Metrics,我们添加了ConsolePrometheusHttpListener两种Exporter,后者在http://192.168.1.166:9464/暴露性能指标,供Prometheus收集。

using Azure;
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Diagnostics;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var serviceName = "AIApp";
var servceVersion = "1.0.0";

using (Sdk.CreateTracerProviderBuilder()
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion))
        .AddSource(serviceName)
        .AddConsoleExporter()
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:4317");
            options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
        })
    .Build())
using (Sdk.CreateMeterProviderBuilder()
    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName, serviceVersion: servceVersion))
    .AddMeter(serviceName)
    .AddConsoleExporter()
    .AddPrometheusHttpListener(options =>
    {
        options.UriPrefixes = ["http://192.168.1.166:9464/"];
    }).Build())
{
    var chatClient = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model: model)
    .AsIChatClient()
    .AsBuilder()
    .UseOpenTelemetry(sourceName: serviceName)
    .Build();

    string[] queries =
    [
        "What is the capital of France?",
    "Who won the FIFA World Cup in 2018?",
    "What is the largest mammal on Earth?"
    ];

    var random = new Random();
    var source = new ActivitySource(serviceName);
    for (int i = 0; i < 30; i++)
    {
        using (source.StartActivity("Agent-Server", kind: ActivityKind.Server, parentContext: default))
        {
            await Task.Delay(random.Next(100, 1000));
            using (source.StartActivity("Foo"))
            {
                await Task.Delay(random.Next(100, 1000));
                using (source.StartActivity("Bar"))
                {
                    await Task.Delay(random.Next(100, 1000));
                    await chatClient.GetResponseAsync(queries[random.Next(queries.Length)]);
                }
            }
        }
        await Task.Delay(random.Next(3000, 5000));
    }
    Console.ReadLine();
}

using块中,我们创建了用来调用LLM的IChatClient对象。具体来说,我们首先创建了一个OpenAIClient,并通过GetChatClient方法获取了一个针对聊天模型的客户端。然后我们将其转换为IChatClient,并使用AsBuilder方法创建了一个可配置的构建器。在构建器上,我们调用UseOpenTelemetry方法,指定了与TracerProviderMeterProvider相同的sourceName(AIApp),以启用链路跟踪和性能指标的收集。最后,我们调用Build方法构建了最终的IChatClient对象。

为了模拟一段持续的调用,我们在一个循环中随机选择了三个问题,并调用了GetResponseAsync方法。为了模拟一段完整的调用链,我们利用创建的ActivitySource(将服务名称作为sourceName)手动创建了三个不同层级的Activity,分别命名为Agent-ServerFooBar,它们表示LLM调用外层的操作。

3. 结果展示

运行程序之后,我们可以在控制台上看到链路跟踪和性能指标的输出。同时,在Jaeger的UI界面http://localhost:16686/上,我们可以看到针对Agent-Server操作的调用链信息,如下图所示:

Alternative Text

打开Grafana的Dashboard,我们可以看到针对LLM调用的性能指标,其中包括请求和响应Token的消耗、调用LLM的延时、成功调用的比例和错误分布等。

Alternative Text

4. OpenTelemetryChatClient

和我们演示的程序一样,OpenTelemetryChatClient也是使用ActivitySource创建的Activity来表示针对LLM的调用。创建这个ActivitySource指定的名称来源于OpenTelemetryChatClient构造函数中的sourceName参数,在OpenTelemetry的语境中将它视为服务名称。如果没有显示指定sourceNameOpenTelemetryChatClient会使用默认的名称"Experimental.Microsoft.Extensions.AI"。

public sealed partial class OpenTelemetryChatClient : DelegatingChatClient
{
    public OpenTelemetryChatClient(
        IChatClient innerClient, 
        ILogger? logger = null, 
        string? sourceName = null);

    public JsonSerializerOptions JsonSerializerOptions { get; set; }

    public bool EnableSensitiveData { get; set; } = TelemetryHelpers.EnableSensitiveDataDefault;
    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default);
    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default);
}

EnableSensitiveData属性用于控制是否允许在Trace数据中包含一些敏感数据,这个属性的默认值来源于针对环境变量OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT的设置。如果两者均为设置,默认不捕获敏感数据。如果这个属性被设置为true,调用LLM的请求和响应消息会被序列化并作为操作的标签进行输出,JsonSerializerOptions属性就是用来控制这个序列化过程的行为的。

在重写的GetResponseAsyncGetStreamingResponseAsync方法中,OpenTelemetryChatClient会创建一个新的Activity来表示针对IChatClient的调用。如果能够从ChatOptions中提取名称的名称(对应ModelId属性),此操作被命名为“chat {model-name}”,否则被命名为“chat”。创建的Activity会被设置一系列丰富的标签来描述此次调用。对于我们前面的演示程序,OpenTelemetryChatClient创建的跟踪操作包含的标签体现在如下这张针对Jaeger的截图上。

Alternative Text

对.NET的诊断跟踪具有了解的开发者应该知道,性能指标是通过Meter计算和输出的。所以OpenTelemetryChatClient在构造函数中创建了一个Meter对象,此对象依然使用sourceName作为它的名称。它会创建如下的性能指标

  • gen_ai.client.operation.duration:整个AI请求从发送到接收完成的总持续时间,计量单位为秒。它记录了端到端(End-to-End)的延迟,包括了网络传输、模型在云端的排队等待、模型推理(计算)以及数据返回的完整时间,用于评估系统的整体性能。如果这个值突然变高,说明用户等待回答的时间变长了。通常结合分位数(如P95, P99)来监控长尾延迟;
  • gen_ai.client.token.usage:单次AI请求所消耗的Token数量。该指标通常包含多个维度(Attributes),如gen_ai.token.type,会细分为input(提示词Token)和output(模型生成的Token)。通过统计这个指标,你可以精准计算出应用消耗了多少费用,或者分析是否存在异常的“长文本”请求耗尽了配额;
  • gen_ai.client.operation.time_to_first_chunk:在使用流式传输时,从发出请求到收到第一个数据块(Chunk)所消耗的时间。计量单位为秒 。这就是业界常说的TTFT (Time to First Token),它反映了模型的启动速度网络建连的延迟,而与模型最终生成多少个字无关。这是一个体现用户体验(UX)的核心指标。在聊天界面中,只要第一个字蹦出来,用户就会觉得系统在工作。如果TTFT过高,用户会感到明显的卡顿和焦虑;
  • gen_ai.client.operation.time_per_output_chunk:在使用流式传输时,模型生成和传输每一个后续数据块(Chunk)的平均间隔时间,计量单位为秒。它反映了模型的推理吞吐量(类似常说的Tokens Per Second)。数值越低,意味着流式打字机的输出速度越快、越流畅。这个指标用于监控流式输出的流畅度。如果这个值偏高,用户会看到打字机效果非常慢,甚至出现一卡一卡(抖动)的现象。通常与模型的并发负载、云端实例的算力有关;

我制作Grafana Dashboard比较简单,只用到了前面两个指标。至于后面两个流式传输相关的指标,有兴趣的读者可以自己动手添加一下。

5. UseOpenTelemetry

OpenTelemetryChatClient的注册可以如下这个针对ChatClientBuilder的扩展方法UseOpenTelemetry来完成。它接受三个参数,loggerFactory用于创建日志记录器,sourceName用于指定ActivitySourceMeter的名称,configure是一个可选的委托,用于进一步配置OpenTelemetryChatClient实例。

public static class OpenTelemetryChatClientBuilderExtensions
{
    public static ChatClientBuilder UseOpenTelemetry(
        this ChatClientBuilder builder,
        ILoggerFactory? loggerFactory = null,
        string? sourceName = null,
        Action<OpenTelemetryChatClient>? configure = null);
}

文章来源:https://www.cnblogs.com/artech/p/20464876/chat-client-pipeline-08
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云