Semantic Kernel 对接 Grafana/Jaeger + 微软OpenTelemetry -- 实现LLM 调用监控
声明:本篇博客并没有使用到OpenTelemetry及相关知识,这里只是记录如果需要对接Grafana/Jaeger ,就必须使用OpenTelemetry
✅ 对比:LoggingFilter vs OpenTelemetry
┌─────────────────────────────────────────────────────────┐ │ LoggingFilter 统计 vs OpenTelemetry │ ├─────────────────────────────────────────────────────────┤ │ │ │ LoggingFilter 静态字典(你现在的代码) │ │ ├─ 自己写的简易统计 │ │ ├─ 用 static Dictionary 累积数据 │ │ ├─ 控制台输出汇总 │ │ ├─ ✅ 简单、够用、好理解 │ │ └─ ❌ 只能在本程序用,无法对接外部系统 │ │ │ │ OpenTelemetry │ │ ├─ 行业标准化的可观测性框架 │ │ ├─ 自动采集 Trace/Metrics/Logs │ │ ├─ 可以对接 Jaeger/Prometheus/Grafana 等 │ │ ├─ ✅ 标准化、生态丰富、生产级 │ │ └─ ❌ 配置复杂、学习成本高 │ │ │ │ 关联:几乎没有! │ │ 两者是独立的方案,可以只用其中一个,也可以都用 │ │ │ └─────────────────────────────────────────────────────────┘
📊 实际关系图
┌─────────────────────────────────────────────────────────┐ │ 两种独立的监控方案 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 方案 A:LoggingFilter 静态字典 ✅ (你已实现) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Semantic Kernel │ │ │ │ ↓ (FunctionInvocationFilter) │ │ │ │ LoggingFilter │ │ │ │ ↓ (手动累加) │ │ │ │ static Dictionary _metrics │ │ │ │ ↓ (Console.WriteLine) │ │ │ │ 控制台输出 │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 方案 B:OpenTelemetry ⏭️ (可以缓一缓) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Semantic Kernel │ │ │ │ ↓ (AddSource) │ │ │ │ OpenTelemetry TracerProvider │ │ │ │ ↓ (自动采集) │ │ │ │ OTLP Exporter │ │ │ │ ↓ │ │ │ │ Jaeger / Prometheus / Grafana │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 两者没有依赖关系!可以只用 A,也可以只用 B,或都用 │ │ │ └─────────────────────────────────────────────────────────┘
💡 为什么我之前把它们放在一起讲?
我的错! 我之前的学习路线把它们都放在"日志与监控"章节,容易让人以为有关联。
实际上:
LoggingFilter静态字典 = 自己造的简易轮子 ✅OpenTelemetry= 行业标准轮子 ⏭️
你现在的 LoggingFilter 已经够用了! 不需要 OpenTelemetry 也能实现:
- ✅ 记录每次调用
- ✅ 统计成功率
- ✅ 统计平均耗时
- ✅ 统计 Token 用量
- ✅ 定期汇总输出
- ✅ 慢函数告警
- ✅ Token 超额告警
🎯 真实区别
| 特性 | LoggingFilter 静态字典 | OpenTelemetry |
|---|---|---|
| 实现难度 | ⭐ (简单) | ⭐⭐⭐⭐ (复杂) |
| 代码量 | 50 行 | 200 行+ |
| 依赖包 | 无 | 5-6 个 NuGet 包 |
| 输出目标 | 控制台 | Jaeger/Prometheus 等 |
| 标准化 | ❌ 自定义格式 | ✅ 行业标准 |
| 学习曲线 | 平缓 | 陡峭 |
| 学习阶段必要性 | ✅ 够用了 | ⏭️ 生产环境再说 |
✅ 我的建议
┌─────────────────────────────────────────────────────────┐ │ 现阶段建议 │ ├─────────────────────────────────────────────────────────┤ │ │ │ ✅ 继续用 LoggingFilter 静态字典 │ │ ├─ 简单、够用、好理解 │ │ ├─ 你想加什么检测就加什么 │ │ └─ 学习 static 变量、锁、字典等基础知识 │ │ │ │ ⏭️ OpenTelemetry 可以暂时放下 │ │ ├─ 现阶段用不到 │ │ ├─ 等以后做生产项目再学 │ │ └─ 或者等公司要求对接监控系统再学 │ │ │ │ 📚 下一步学更有价值的: │ │ ├─ Planner 任务规划器 │ │ ├─ Qdrant 向量检索 (RAG) │ │ └─ 结构化输出 │ │ │ └─────────────────────────────────────────────────────────┘
💬 真心话
你问"有什么关联",说明你在独立思考,这是好事! 很多教程喜欢堆砌名词,好像学得越多越好。 但真正的高手知道什么不该学! 你现在的 LoggingFilter 已经实现了: 指标收集 ✅ 汇总统计 ✅ 告警检测 ✅ 这就够了! 不需要为了"学新技术"而学 OpenTelemetry。 等哪天你的项目真的需要对接 Grafana/Jaeger 了,再学也不迟!
代码:
using ConsoleApp1; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Trace; using System.ComponentModel; using System.Diagnostics; using System.Timers; namespace ConsoleApp5 { class Program { static string ChatPluginName = "ChatPlugin"; //聊天插件 static async Task Main(string[] args) { Console.WriteLine("╔════════════════════════════════════════╗"); Console.WriteLine("║ LLM Gateway - 路由模块学习版 ║"); Console.WriteLine("║ 基于用户 Token 等级自动切换模型 ║"); Console.WriteLine("╚════════════════════════════════════════╝\n"); // 在 Program.cs 或 Startup.cs var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("Microsoft.SemanticKernel.*") // ⭐ 监听 SK 的所有操作 .AddConsoleExporter() // 输出到控制台 .AddOtlpExporter(options => // 或发送到 Jaeger/Zipkin { options.Endpoint = new Uri("http://localhost:4317"); }) .Build(); var kernel = CreateGatewayKernel(); // 注册插件-聊天插件 kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new ChatPlugin(kernel), ChatPluginName)); var Chat_funcName = "Chat";//插件函数 var result1 = await kernel.InvokeAsync(ChatPluginName, Chat_funcName, new KernelArguments { ["message"] = "你好" }); var result2 = await kernel.InvokeAsync(ChatPluginName, Chat_funcName, new KernelArguments { ["message"] = "请帮我模拟一个天气预报" }); var result3 = await kernel.InvokeAsync(ChatPluginName, Chat_funcName, new KernelArguments { ["message"] = "简单介绍一个小动物" }); Console.WriteLine($"✅ 结果:{result1}\n"); Console.WriteLine($"✅ 结果:{result2}\n"); Console.WriteLine($"✅ 结果:{result3}\n"); Console.Read(); } static Kernel CreateGatewayKernel() { var builder = Kernel.CreateBuilder(); // ========== 1. 注册模型服务(3 个模型) ========== var dashScopeApiKey = ConstParm.apiKey; var dashScopeEndpoint = ConstParm.endpoint; builder.AddOpenAIChatCompletion( serviceId: "dashscope_qwen-turbo", // Fast 级 modelId: "qwen-turbo", apiKey: dashScopeApiKey, endpoint: new Uri(dashScopeEndpoint)); builder.Services.AddLogging(b => b .AddConsole() .SetMinimumLevel(LogLevel.Error)); var kernel = builder.Build(); // ========== 4. 注册过滤器 ========== var logger = kernel.Services.GetRequiredService<ILogger<LoggingFilter>>(); kernel.FunctionInvocationFilters.Add(new LoggingFilter(logger)); return kernel; } } /// <summary> /// 演示插件(通用场景) /// 用于测试 LLM Gateway 的路由、限流、缓存、成本统计等功能 /// </summary> public class ChatPlugin { private readonly Kernel _kernel; public ChatPlugin(Kernel kernel) { _kernel = kernel; } [KernelFunction("Chat")] [Description("通用对话 - 日常聊天、问答、咨询")] public async Task<string> ChatMessage( [Description("用户消息")] string message, [Description("对话历史(可选)")] string? history = null) { var chatService = _kernel.GetRequiredService<IChatCompletionService>(); var chatHistory = new ChatHistory(); if (!string.IsNullOrEmpty(history)) { chatHistory.AddUserMessage(history); } chatHistory.AddUserMessage(message); var response = await chatService.GetChatMessageContentAsync( chatHistory, cancellationToken: default); // ✅ 新增:提取 Token 用量并注入 Kernel.Data if (response.Metadata != null) { // 尝试从 Metadata 中获取 Usage if (response.Metadata.TryGetValue("Usage", out var usageObj) && usageObj != null) { var usage = usageObj as OpenAI.Chat.ChatTokenUsage; if (usage != null) { _kernel.Data["input_tokens"] = usage.InputTokenCount; _kernel.Data["output_tokens"] = usage.OutputTokenCount; _kernel.Data["total_tokens"] = usage.TotalTokenCount; } } // 兼容其他格式(DashScope 可能用不同 key) else if (response.Metadata.TryGetValue("usage", out var usageObj2) && usageObj2 != null) { var usage = usageObj2 as OpenAI.Chat.ChatTokenUsage; if (usage != null) { _kernel.Data["input_tokens"] = usage.InputTokenCount; _kernel.Data["output_tokens"] = usage.OutputTokenCount; _kernel.Data["total_tokens"] = usage.TotalTokenCount; } } } return response.Content ?? "无回复"; } } public class LoggingFilter : IFunctionInvocationFilter { private readonly ILogger<LoggingFilter> _logger; private readonly Stopwatch _stopwatch; // ⭐ 新增:静态指标收集(用于汇总分析) private static readonly Dictionary<string, FunctionMetrics> _metrics = new(); private static readonly object _lock = new(); public LoggingFilter(ILogger<LoggingFilter> logger) { _logger = logger; _stopwatch = new Stopwatch(); } public async Task OnFunctionInvocationAsync( FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) { var functionName = $"{context.Function.PluginName}.{context.Function.Name}"; var arguments = string.Join(", ", context.Arguments.Select(kvp => $"{kvp.Key}={kvp.Value}")); var startTime = DateTime.Now; // 📝 开始执行函数 _logger.LogInformation("📝 开始执行函数:{FunctionName}({Arguments})", functionName, arguments); PrintColoredConsole($"📝 开始执行函数:{functionName}({arguments})", ConsoleColor.Cyan); _stopwatch.Restart(); try { // 执行函数 await next(context); _stopwatch.Stop(); var result = context.Result?.GetValue<object>()?.ToString() ?? "null"; // ⭐ 获取 Token 消耗量 var inputTokens = context.Kernel.Data.TryGetValue("input_tokens", out var inputObj) ? (int)inputObj : 0; var outputTokens = context.Kernel.Data.TryGetValue("output_tokens", out var outputObj) ? (int)outputObj : 0; var totalTokens = inputTokens + outputTokens; // ✅ 记录成功指标 RecordMetrics(functionName, success: true, _stopwatch.ElapsedMilliseconds, totalTokens); // ✅ 函数执行成功 - 彩色输出 _logger.LogInformation( "✅ 函数执行成功:{FunctionName},耗时 {ElapsedMs}ms,Token 消耗:输入={InputTokens}, 输出={OutputTokens}, 总计={TotalTokens},结果:{Result}", functionName, _stopwatch.ElapsedMilliseconds, inputTokens, outputTokens, totalTokens, Truncate(result, 100)); PrintColoredConsole( $"✅ 函数执行成功:{functionName},耗时 {_stopwatch.ElapsedMilliseconds}ms,📊 Token 消耗:输入={inputTokens}, 输出={outputTokens}, 总计={totalTokens}", ConsoleColor.Green); // ⭐ 新增检测 1:慢函数告警(> 3 秒) if (_stopwatch.ElapsedMilliseconds > 3000) { _logger.LogWarning("⚠️ 慢函数告警:{FunctionName} 耗时 {ElapsedMs}ms", functionName, _stopwatch.ElapsedMilliseconds); PrintColoredConsole($"⚠️ 慢函数告警:{functionName} 耗时 {_stopwatch.ElapsedMilliseconds}ms", ConsoleColor.Yellow); } // ⭐ 新增检测 2:Token 超额告警(单次 > 1 万) if (totalTokens > 10000) { _logger.LogWarning("⚠️ Token 超额告警:{FunctionName} 消耗 {TotalTokens} Token", functionName, totalTokens); PrintColoredConsole($"⚠️ Token 超额告警:{functionName} 消耗 {totalTokens} Token", ConsoleColor.Yellow); } } catch (Exception ex) { _stopwatch.Stop(); // ⭐ 记录失败指标 RecordMetrics(functionName, success: false, _stopwatch.ElapsedMilliseconds, 0); // ❌ 函数执行失败 - 彩色输出 _logger.LogError( ex, "❌ 函数执行失败:{FunctionName},耗时 {ElapsedMs}ms,错误:{Error}", functionName, _stopwatch.ElapsedMilliseconds, ex.Message); PrintColoredConsole($"❌ 函数执行失败:{functionName},耗时 {_stopwatch.ElapsedMilliseconds}ms,错误:{ex.Message}", ConsoleColor.Red); throw; } finally { // ⭐ 新增检测 4:每 10 次调用打印汇总统计 PrintMetricsSummary(); } } // ⭐ 记录指标 private void RecordMetrics(string functionName, bool success, long elapsedMs, int tokens) { lock (_lock) { if (!_metrics.ContainsKey(functionName)) { _metrics[functionName] = new FunctionMetrics(); } var m = _metrics[functionName]; m.TotalCalls++; m.SuccessCalls += success ? 1 : 0; m.TotalElapsedMs += elapsedMs; m.TotalTokens += tokens; m.LastUpdated = DateTime.Now; } } // ⭐ 打印汇总统计 private void PrintMetricsSummary() { lock (_lock) { var totalCalls = _metrics.Values.Sum(m => m.TotalCalls); // 每 3 次调用打印一次 if (totalCalls % 3 == 0 && totalCalls > 0) { Console.WriteLine("\n═══════════════════════════════════════════════════════════"); Console.WriteLine("📊 函数调用统计(累计 " + totalCalls + " 次)"); Console.WriteLine("═══════════════════════════════════════════════════════════"); foreach (var kvp in _metrics) { var m = kvp.Value; Console.WriteLine($" {kvp.Key}:"); Console.WriteLine($" 调用={m.TotalCalls}, 成功率={m.SuccessRate:P1}, " + $"平均耗时={m.AvgElapsedMs}ms, Token={m.TotalTokens}"); } Console.WriteLine("═══════════════════════════════════════════════════════════\n"); } } } // ⭐ 新增检测 5:辅助方法 - 彩色控制台输出 private void PrintColoredConsole(string message, ConsoleColor color) { Console.ForegroundColor = color; Console.WriteLine(message); Console.ResetColor(); } private string Truncate(string text, int maxLength) { if (string.IsNullOrEmpty(text) || text.Length <= maxLength) return text; return text[..maxLength] + "..."; } } // ⭐ 指标数据结构 public class FunctionMetrics { public int TotalCalls { get; set; } public int SuccessCalls { get; set; } public long TotalElapsedMs { get; set; } public int TotalTokens { get; set; } public DateTime LastUpdated { get; set; } public double SuccessRate => TotalCalls > 0 ? (double)SuccessCalls / TotalCalls : 0; public double AvgElapsedMs => TotalCalls > 0 ? (double)TotalElapsedMs / TotalCalls : 0; } }
项目引用
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <NoWarn>$(NoWarn);NU5104</NoWarn> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.SemanticKernel" Version="1.78.0" /> <PackageReference Include="OpenTelemetry" Version="1.17.0" /> <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.17.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> <PackageReference Include="Sdcb.DashScope" Version="2.0.0" /> </ItemGroup> </Project>

浙公网安备 33010602011771号