AI服务的可观测性与运维

AI服务的可观测性与运维

当 AI 服务从开发环境走向生产,可观测性(Observability)成为运维的基石。传统的监控(CPU、内存、请求量)已不足以应对 AI 系统的复杂性,我们需要深入追踪 每个 AI 交互的细节 :Token 消耗、模型延迟、函数调用路径、成本分布等。同时,AI 模型的持续迭代要求我们具备 A/B 测试灰度发布 能力,以安全地引入新模型或提示词。

将构建一套完整的 AI 可观测性体系,涵盖 OpenTelemetry 埋点、实验框架以及成本分析工具。

1 OpenTelemetry 在 AI 链路中的埋点(Token 消耗、延迟)

1.1 为什么需要 OpenTelemetry?

OpenTelemetry(OTel)是云原生可观测性的行业标准,提供统一的 API 和 SDK 来收集 追踪(Traces)指标(Metrics)日志(Logs)。对于 AI 应用,我们需要将 LLM 调用、向量检索、插件执行等环节作为 span 串联起来,形成完整的调用链,从而定位性能瓶颈和异常。

10.1.2 在 .NET 中集成 OpenTelemetry

首先安装必要的 NuGet 包:

bash 复制代码
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol   # 导出到 OTLP 兼容后端

在 Program.cs 中配置

csharp 复制代码
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;

var serviceName = "EnterpriseAI";
var serviceVersion = "1.0.0";

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(serviceName)
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("Microsoft.SemanticKernel")   // 自动捕获 SK 的 spans
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName, serviceVersion: serviceVersion))
        .AddOtlpExporter())   // 发送到 Jaeger、Aspire Dashboard 等
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddMeter(serviceName)
        .AddPrometheusExporter());   // 可选:暴露 Prometheus 端点
1.3 为 AI 组件添加自定义 Span

我们需要在关键位置创建自定义 Span,记录模型调用、Token 用量等关键信息。

创建 ActivitySource

csharp 复制代码
public static class AIDiagnostics
{
    public static readonly ActivitySource ActivitySource = new("EnterpriseAI.AI", "1.0.0");
}

在 Semantic Kernel 调用中添加 Span

由于 Semantic Kernel 内部已支持 OpenTelemetry(通过 HttpClient 的自动追踪),但我们可以包装 Invoke 方法以添加更细粒度的信息。

csharp 复制代码
public class ObservableKernel
{
    private readonly Kernel _kernel;

    public async Task<string> InvokePromptWithTelemetryAsync(string prompt, KernelArguments? arguments = null)
    {
        using var activity = AIDiagnostics.ActivitySource.StartActivity("AI Invoke", ActivityKind.Server);
        activity?.SetTag("prompt", prompt);
        activity?.SetTag("arguments", arguments?.ToString());

        var stopwatch = Stopwatch.StartNew();
        try
        {
            var result = await _kernel.InvokePromptAsync(prompt, arguments);
            stopwatch.Stop();
            activity?.SetTag("duration_ms", stopwatch.ElapsedMilliseconds);
            activity?.SetTag("output_length", result.Length);
            // 记录 Token 用量(需从响应中提取)
            // 注意:OpenAI SDK 返回的 Completions 对象包含 Usage 信息
            // 这里仅示例,实际需要从 Kernel 返回的结果中解析
            return result;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

对于 ONNX Runtime 推理,也可以添加类似的 Span。

在插件方法中添加 Span

csharp 复制代码
public class WeatherPlugin
{
    [KernelFunction]
    public async Task<string> GetWeatherAsync(string city)
    {
        using var activity = AIDiagnostics.ActivitySource.StartActivity("WeatherPlugin.GetWeather");
        activity?.SetTag("city", city);
        try
        {
            var weather = await _weatherService.GetAsync(city);
            activity?.SetTag("result", weather);
            return weather;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}
1.4 记录 Token 消耗与成本

Token 消耗是 LLM 调用的核心指标。Azure OpenAI 和 OpenAI 的响应中都包含 Usage 对象。在 Semantic Kernel 中,可以通过配置 OpenAIPromptExecutionSettings 并捕获响应来提取。

自定义 DelegatingHandler 捕获响应

csharp 复制代码
public class TokenUsageHandler : DelegatingHandler
{
    public static readonly Meter Meter = new("EnterpriseAI.AI");
    public static readonly Counter<int> InputTokenCounter = Meter.CreateCounter<int>("ai.input_tokens");
    public static readonly Counter<int> OutputTokenCounter = Meter.CreateCounter<int>("ai.output_tokens");
    public static readonly Histogram<double> RequestDuration = Meter.CreateHistogram<double>("ai.request_duration");

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        var response = await base.SendAsync(request, cancellationToken);
        stopwatch.Stop();

        // 尝试解析响应中的 usage
        var content = await response.Content.ReadAsStringAsync();
        if (response.IsSuccessStatusCode && TryExtractUsage(content, out var inputTokens, out var outputTokens))
        {
            InputTokenCounter.Add(inputTokens);
            OutputTokenCounter.Add(outputTokens);
            RequestDuration.Record(stopwatch.Elapsed.TotalSeconds);
        }
        // 重新包装内容流,因为已经读取了一次
        response.Content = new StringContent(content);
        return response;
    }

    private bool TryExtractUsage(string json, out int inputTokens, out int outputTokens)
    {
        // 解析 OpenAI/Azure OpenAI 响应格式
        // 示例:{"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}
        inputTokens = outputTokens = 0;
        try
        {
            var doc = JsonDocument.Parse(json);
            if (doc.RootElement.TryGetProperty("usage", out var usage))
            {
                inputTokens = usage.GetProperty("prompt_tokens").GetInt32();
                outputTokens = usage.GetProperty("completion_tokens").GetInt32();
                return true;
            }
        }
        catch { }
        return false;
    }
}

在注册 HttpClient 时添加此 Handler:

csharp 复制代码
builder.Services.AddHttpClient("AzureOpenAI")
    .AddHttpMessageHandler<TokenUsageHandler>();
1.5 集成可观测性后端
  • 本地开发:使用 .NET Aspire Dashboard 或 Jaeger 查看 traces。
  • 生产环境:导出到 Azure Application Insights、AWS X-Ray、Datadog 或 Prometheus + Grafana。

导出到 Application Insights(Azure Monitor):

bash 复制代码
dotnet add package OpenTelemetry.Exporter.AzureMonitor

配置:

csharp 复制代码
builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing
    .AddAzureMonitorTraceExporter(options =>
    {
        options.ConnectionString = "<Your Application Insights Connection String>";
    }));

2 模型的 A/B 测试与灰度发布策略

2.1 为什么需要 A/B 测试?

模型迭代速度快,新模型可能在准确率、延迟、成本上有差异。直接全量替换风险极高。通过 A/B 测试,我们可以:

  • 比较不同模型版本的效果(如 GPT-3.5 vs GPT-4)
  • 验证新提示词模板的效果
  • 评估不同部署后端(如本地 vs 云端)的性能
2.2 构建实验框架

设计一个简单的实验框架,支持按用户、租户或随机流量分配变体。

定义实验配置

csharp 复制代码
public class ExperimentConfig
{
    public string Name { get; set; } = "model_ab_test";
    public List<Variant> Variants { get; set; } = new();
    public string DefaultVariant { get; set; }
}

public class Variant
{
    public string Name { get; set; }
    public int Weight { get; set; }  // 流量权重百分比
    public string ModelName { get; set; }
    public string PromptTemplate { get; set; }
    public Dictionary<string, object> Parameters { get; set; }
}

配置存储在 appsettings.json

json 复制代码
{
  "Experiments": {
    "model_ab_test": {
      "Variants": [
        { "Name": "gpt35", "Weight": 50, "ModelName": "gpt-3.5-turbo", "PromptTemplate": "default" },
        { "Name": "gpt4", "Weight": 50, "ModelName": "gpt-4o", "PromptTemplate": "default" }
      ],
      "DefaultVariant": "gpt35"
    }
  }
}

实验分配器

csharp 复制代码
public interface IExperimentAssigner
{
    Variant GetVariant(string experimentName, string userId);
}

public class RandomExperimentAssigner : IExperimentAssigner
{
    private readonly IConfiguration _config;
    private readonly Random _random = new();

    public Variant GetVariant(string experimentName, string userId)
    {
        var expConfig = _config.GetSection($"Experiments:{experimentName}").Get<ExperimentConfig>();
        if (expConfig == null) return null;

        // 可以基于 userId 实现确定性哈希,保证同一用户始终看到同一变体
        var hash = (userId?.GetHashCode() ?? 0) % 100;
        int cumulative = 0;
        foreach (var variant in expConfig.Variants)
        {
            cumulative += variant.Weight;
            if (hash < cumulative)
                return variant;
        }
        return expConfig.Variants.FirstOrDefault(v => v.Name == expConfig.DefaultVariant);
    }
}

在服务中使用

csharp 复制代码
public class ChatService
{
    private readonly IExperimentAssigner _assigner;
    private readonly KernelFactory _kernelFactory;

    public async Task<string> ChatAsync(string userMessage, string userId)
    {
        var variant = _assigner.GetVariant("model_ab_test", userId);
        var kernel = _kernelFactory.Create(variant);
        var result = await kernel.InvokePromptAsync(variant.PromptTemplate, new() { ["input"] = userMessage });
        // 记录实验数据(见下节)
        return result;
    }
}
2.3 灰度发布策略

灰度发布是逐步将流量切换到新版本的过程。与 A/B 测试不同,灰度通常有一个明确的目标(全量切换)。我们可以利用相同的实验框架,通过调整权重实现:

  1. 初始:新模型权重 0%,旧模型 100%。
  2. 灰度 1%:新模型 1%,旧模型 99%。
  3. 灰度 10%:新模型 10%,旧模型 90%。
  4. ...直至 100%。

自动化灰度:结合监控指标(错误率、延迟),当新版本指标异常时自动回滚。

2.4 实验数据收集与评估

A/B 测试需要收集关键指标,如:

  • 业务指标:用户满意度(点赞/点踩)、任务完成率
  • 技术指标:延迟、Token 消耗、错误率
  • 成本指标:每次请求的成本

将实验变体信息添加到 OpenTelemetry span 的标签中:

csharp 复制代码
using var activity = AIDiagnostics.ActivitySource.StartActivity("Chat");
activity?.SetTag("experiment", "model_ab_test");
activity?.SetTag("variant", variant.Name);

在后端(如 Application Insights)中,可以通过这些标签进行分组分析,比较不同变体的表现。

3 成本控制:Token 经济学与缓存策略

3.1 Token 成本构成

LLM 的成本与 Token 数量直接相关:

  • 输入 Token:提示词、历史对话、检索的上下文
  • 输出 Token:模型生成的回答
3.2 缓存策略

1. 精确缓存(Exact Match Cache)

对于完全相同的用户问题,直接返回缓存的结果。适用于 FAQ 类场景。

csharp 复制代码
public class ExactMatchCache
{
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _ttl;

    public async Task<string> GetOrAddAsync(string userInput, Func<Task<string>> factory)
    {
        return await _cache.GetOrCreateAsync(userInput, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _ttl;
            return await factory();
        });
    }
}

2. 语义缓存(Semantic Cache)

对于语义相似但表述不同的问题,可以基于向量相似度判断是否命中缓存。将问题向量化,计算与缓存项的相似度,超过阈值则返回。

csharp 复制代码
public class SemanticCache
{
    private readonly IVectorDatabase _vectorDb;
    private readonly ITextEmbeddingGenerationService _embeddingService;

    public async Task<string?> GetAsync(string userInput, float similarityThreshold = 0.95f)
    {
        var embedding = await _embeddingService.GenerateEmbeddingAsync(userInput);
        var results = await _vectorDb.SearchAsync(embedding.ToArray(), topK: 1);
        if (results.Any() && results[0].Score >= similarityThreshold)
        {
            return results[0].Metadata["response"];
        }
        return null;
    }

    public async Task SetAsync(string userInput, string response)
    {
        var embedding = await _embeddingService.GenerateEmbeddingAsync(userInput);
        await _vectorDb.AddAsync(Guid.NewGuid().ToString(), response, embedding.ToArray(),
            metadata: new Dictionary<string, object> { ["response"] = response });
    }
}

3. 提示词压缩

对于包含大量上下文的 RAG 应用,可以对检索到的内容进行摘要或提取关键句子,减少输入 Token。

csharp 复制代码
public async Task<string> SummarizeContextAsync(string context, int maxTokens)
{
    var prompt = $"Summarize the following context in less than {maxTokens} tokens:\n{context}";
    var summary = await _kernel.InvokePromptAsync(prompt);
    return summary;
}
3.3 模型选择策略

根据任务复杂度动态选择模型:

  • 简单任务(如情感分析、分类):使用轻量级模型(GPT-3.5-turbo 或本地小模型)
  • 复杂任务(如代码生成、逻辑推理):使用 GPT-4o
  • 可批处理的任务:使用异步批处理模式,降低单次调用成本
3.4 成本监控与告警

在 OpenTelemetry 指标中增加成本指标:

csharp 复制代码
public static class CostMetrics
{
    public static readonly Histogram<double> RequestCost = Meter.CreateHistogram<double>("ai.request_cost", "USD");
}

// 在 TokenUsageHandler 中计算成本并记录
var cost = (inputTokens * 0.000005) + (outputTokens * 0.000015); // 示例单价
CostMetrics.RequestCost.Record(cost, new TagList { { "model", modelName } });

在 Prometheus/Grafana 中建立成本仪表板,并设置预算告警(如日成本超过 $100 时触发)。

3.5 限流与配额管理

防止单个用户或租户滥用导致成本激增:

csharp 复制代码
public class RateLimiter
{
    private readonly IMemoryCache _cache;

    public bool IsAllowed(string userId, int maxTokensPerDay)
    {
        var key = $"user_tokens_{userId}";
        var todayTokens = _cache.Get<int>(key);
        if (todayTokens > maxTokensPerDay) return false;

        // 在请求后增加 Token 计数
        return true;
    }

    public void RecordTokens(string userId, int tokens)
    {
        var key = $"user_tokens_{userId}";
        _cache.Set(key, _cache.Get<int>(key) + tokens, TimeSpan.FromDays(1));
    }
}

总结

  • OpenTelemetry 埋点:为 AI 链路添加分布式追踪和指标,记录 Token 消耗和延迟。
  • A/B 测试与灰度发布:通过实验框架安全地迭代模型和提示词,降低变更风险。
  • 成本控制:通过缓存、模型选择、限流等策略,在保证体验的前提下优化成本。
相关推荐
小超同学你好2 小时前
面向 LLM 的程序设计 4:API 版本化与演进——在「模型会记忆旧文档」前提下的兼容策略
人工智能·语言模型
guslegend2 小时前
系统整体设计方案
人工智能·大模型·知识图谱
deephub2 小时前
ADK 多智能体编排:SequentialAgent、ParallelAgent 与 LoopAgent 解析
人工智能·python·大语言模型·agent
HcreateLabelView2 小时前
引领RFID电子标签打印新时代,打造标识打印系统新标杆
大数据·人工智能
似水এ᭄往昔2 小时前
【Linxu】--进程优先级和进程切换
linux·运维·服务器
wjcroom2 小时前
以太缄默-理论分析
人工智能·物理学
guslegend2 小时前
4月5日(大语言模型训练原理)
人工智能·大模型
数智化管理手记2 小时前
精益生产合理化建议核心解读:本质、价值与提报规范
大数据·网络·人工智能·低代码·制造
QfC92C02p2 小时前
Hagicode 多 AI 提供者切换与互操作实现方案
人工智能