编辑
2026-04-21
C#
00

目录

1️⃣ 问题深度剖析:没有监控的 AI 应用有多危险
🚨 三个典型痛点场景
2️⃣ 核心要点提炼:日志监控的底层逻辑
📐 分层监控模型
🔑 关键设计原则
3️⃣ 解决方案设计
🛠️ 方案一:ILogger 基础集成(适合快速起步)
📊 方案二:Serilog 结构化日志(推荐生产环境)
🚀 方案三:Application Insights 遥测(企业级完整方案)
🔧 调试技巧与性能分析
🐛 断点调试关键场景
⚡ 性能分析要点
📋 完整监控系统集成示例
💎 三个核心收获总结
💬 开放讨论

生产环境某次 AI 调用莫名超时,排查了两小时,最终发现是 Temperature 参数配置异常导致响应时间暴增 300%。如果当时有完善的日志监控体系,这个问题五分钟就能定位。

在构建 Semantic Kernel 应用时,日志与监控往往是最容易被忽视的环节。代码跑通了、接口通了,然后就直接上线——这种操作在小项目里或许无伤大雅,但在生产环境中,一旦出现 AI 调用失败、Token 超支、响应延迟飙升等问题,没有监控就等于盲开飞机。

读完本文,你将掌握:

  • ILogger 与 Semantic Kernel 的深度集成方式
  • Serilog 结构化日志的实战配置
  • Application Insights 遥测接入
  • 性能分析工具的选用与使用技巧
  • 一套可直接复用的完整日志监控系统模板

1️⃣ 问题深度剖析:没有监控的 AI 应用有多危险

🚨 三个典型痛点场景

痛点一:AI 调用失败后无迹可查

很多项目的错误处理长这样:

csharp
// ❌ 典型的"吞掉异常"写法 try { var result = await kernel.InvokePromptAsync(prompt); return result.ToString(); } catch (Exception ex) { return "服务暂时不可用"; // 问题被掩盖,运维完全不知道 }

这种写法导致的后果是:用户反馈"AI 不好用",运维查不到任何错误信息,开发只能干瞪眼。

痛点二:Token 消耗失控

Semantic Kernel 每次调用都会消耗 Token,如果没有监控,以下情况很容易发生:

  • ChatHistory 无限增长,单次请求 Token 超 8000
  • 某个插件被频繁触发,一天消耗正常量的 10 倍
  • 生产环境 Temperature=0.9(本该是开发配置),导致回复质量下降

痛点三:性能劣化无法感知

没有追踪时,你根本不知道:响应从 800ms 变成 3000ms 是从哪天开始的?是模型接口变慢了,还是某个插件计算耗时暴增?


2️⃣ 核心要点提炼:日志监控的底层逻辑

📐 分层监控模型

一个完整的 Semantic Kernel 监控体系,需要覆盖三层:

层级监控内容工具选型
应用层业务逻辑、用户请求、对话历史ILogger + Serilog
框架层SK 内核事件、函数调用、插件触发SK 内置事件钩子
基础设施层HTTP 请求、延迟、Token 用量Application Insights

🔑 关键设计原则

  • 结构化优于文本logger.LogInformation("Token: {TokenCount}", count) 远比 logger.LogInformation($"Token: {count}") 好查询
  • 上下文随调用链传递:每次 AI 调用应携带 TraceId,便于端到端追踪
  • 性能数据必须量化:记录耗时、Token 数、重试次数,不能只记"成功/失败"

3️⃣ 解决方案设计

🛠️ 方案一:ILogger 基础集成(适合快速起步)

这是最轻量的方案,适合中小项目快速接入监控能力。

第一步:配置日志级别

json
// appsettings.json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.SemanticKernel": "Debug", "AppAiAgent": "Debug" } } }

💡 将 Microsoft.SemanticKernel 设为 Debug,SK 框架会自动输出函数调用、模型请求等详细信息。

第二步:封装带监控的 AI 服务

csharp
using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using System.Diagnostics; public class MonitoredAIService { private readonly Kernel _kernel; private readonly ILogger<MonitoredAIService> _logger; public MonitoredAIService(Kernel kernel, ILogger<MonitoredAIService> logger) { _kernel = kernel; _logger = logger; } public async Task<string> InvokeWithLoggingAsync(string prompt, string traceId = null) { // 生成追踪 ID,便于日志关联 traceId ??= Guid.NewGuid().ToString("N")[..8]; var sw = Stopwatch.StartNew(); _logger.LogInformation( "[{TraceId}] AI 调用开始 | Prompt 长度: {PromptLength}", traceId, prompt.Length); try { var result = await _kernel.InvokePromptAsync(prompt, new KernelArguments( new OpenAIPromptExecutionSettings { MaxTokens = 2000, Temperature = 0.7 })); sw.Stop(); var response = result.ToString(); // 记录成功调用的关键指标 _logger.LogInformation( "[{TraceId}] AI 调用成功 | 耗时: {ElapsedMs}ms | 响应长度: {ResponseLength}", traceId, sw.ElapsedMilliseconds, response?.Length ?? 0); return response ?? string.Empty; } catch (HttpRequestException ex) { sw.Stop(); _logger.LogError(ex, "[{TraceId}] 网络请求失败 | 耗时: {ElapsedMs}ms | 错误: {ErrorMessage}", traceId, sw.ElapsedMilliseconds, ex.Message); throw; } catch (Exception ex) { sw.Stop(); _logger.LogError(ex, "[{TraceId}] AI 调用异常 | 耗时: {ElapsedMs}ms | 类型: {ExceptionType}", traceId, sw.ElapsedMilliseconds, ex.GetType().Name); throw; } } }

image.png

踩坑预警: 不要在日志里记录完整的 prompt 内容,特别是包含用户输入的场景——一方面是隐私风险,另一方面会让日志文件迅速膨胀。记录长度和关键标识即可。


📊 方案二:Serilog 结构化日志(推荐生产环境)

Serilog 是目前 .NET 生态里结构化日志的事实标准,配合 Seq 或 Elasticsearch 可以做到强大的查询与可视化。

安装依赖包:

bash
dotnet add package Serilog.AspNetCore dotnet add package Serilog.Sinks.Console dotnet add package Serilog.Sinks.File dotnet add package Serilog.Enrichers.Environment dotnet add package Serilog.Enrichers.Thread

配置 Serilog(Program.cs):

csharp
using Serilog; using Serilog.Events; using Microsoft.SemanticKernel; // 在 Host 启动前配置 Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.SemanticKernel", LogEventLevel.Debug) .Enrich.FromLogContext() .Enrich.WithEnvironmentName() .Enrich.WithThreadId() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") .WriteTo.File( path: "logs/sk-app-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] " + "{SourceContext} {Message:lj}{NewLine}{Exception}") .CreateLogger(); var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(Log.Logger);

封装带上下文传播的日志服务:

csharp
using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Serilog.Context; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppSemanticKernel06 { public class SKLoggingService { private readonly Kernel _kernel; private readonly ILogger<SKLoggingService> _logger; // 用于统计 Token 消耗的计数器 private long _totalTokensUsed = 0; private long _totalCallCount = 0; public SKLoggingService(Kernel kernel, ILogger<SKLoggingService> logger) { _kernel = kernel; _logger = logger; _kernel.FunctionInvocationFilters.Add(new LoggingFunctionInvocationFilter(_logger)); } /// <summary> /// 带完整监控的流式对话 /// </summary> public async Task<string> StreamChatWithMonitoringAsync( ChatHistory chatHistory, string sessionId) { var callId = Guid.NewGuid().ToString("N")[..8]; var sw = Stopwatch.StartNew(); var fullResponse = new StringBuilder(); // 使用 LogContext 为本次调用的所有日志添加上下文字段 using (LogContext.PushProperty("SessionId", sessionId)) using (LogContext.PushProperty("CallId", callId)) { _logger.LogDebug("流式对话开始 | 历史消息数: {HistoryCount}", chatHistory.Count); try { var chatService = _kernel.GetRequiredService<IChatCompletionService>(); var settings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, MaxTokens = 2000, Temperature = 0.7 }; var streamResponse = chatService.GetStreamingChatMessageContentsAsync( chatHistory, settings, _kernel); await foreach (var content in streamResponse) { if (!string.IsNullOrEmpty(content.Content)) { fullResponse.Append(content.Content); Console.Write(content.Content); } } sw.Stop(); Interlocked.Increment(ref _totalCallCount); // 记录结构化的成功指标 _logger.LogInformation( "对话完成 | 耗时: {ElapsedMs}ms | 响应Token估算: {TokenEstimate} | 累计调用: {TotalCalls}", sw.ElapsedMilliseconds, fullResponse.Length / 4, // 粗略估算 _totalCallCount); // 性能告警:超过 3 秒触发警告 if (sw.ElapsedMilliseconds > 3000) { _logger.LogWarning( "响应时间过长告警 | 耗时: {ElapsedMs}ms | 阈值: 3000ms", sw.ElapsedMilliseconds); } return fullResponse.ToString(); } catch (Exception ex) { sw.Stop(); _logger.LogError(ex, "对话异常 | 耗时: {ElapsedMs}ms | 异常类型: {ExceptionType}", sw.ElapsedMilliseconds, ex.GetType().Name); throw; } } } private sealed class LoggingFunctionInvocationFilter : IFunctionInvocationFilter { private readonly ILogger _logger; public LoggingFunctionInvocationFilter(ILogger logger) { _logger = logger; } public async Task OnFunctionInvocationAsync( FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) { _logger.LogDebug( "插件函数触发 | 插件: {PluginName} | 函数: {FunctionName}", context.Function.PluginName, context.Function.Name); await next(context); _logger.LogDebug( "插件函数完成 | 插件: {PluginName} | 函数: {FunctionName} | 状态: {Status}", context.Function.PluginName, context.Function.Name, context.Result.ValueType?.Name ?? "void"); } } } }

image.png

结构化日志的查询优势: 使用上面的写法,你可以在 Seq 或 Kibana 里直接查 SessionId = "abc123" 的所有日志,或者按 ElapsedMs > 3000 筛选所有慢请求——这是纯文本日志做不到的。


🚀 方案三:Application Insights 遥测(企业级完整方案)

Application Insights 提供了分布式追踪、异常聚合、性能分析一体化的能力,特别适合云端部署的 SK 应用。

image.png

安装依赖:

bash
dotnet add package Microsoft.ApplicationInsights.AspNetCore dotnet add package Microsoft.ApplicationInsights.WorkerService

配置与集成:

csharp
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppSemanticKernel07 { public class AppInsightsSKService { private readonly Kernel _kernel; private readonly TelemetryClient _telemetry; private readonly ILogger<AppInsightsSKService> _logger; public AppInsightsSKService( Kernel kernel, TelemetryClient telemetry, ILogger<AppInsightsSKService> logger) { _kernel = kernel; _telemetry = telemetry; _logger = logger; } public async Task<string> InvokeWithTelemetryAsync( string prompt, string userId = "anonymous") { // 创建自定义追踪操作,可在 Azure 门户看到完整调用链 using var operation = _telemetry.StartOperation<DependencyTelemetry>("SK.InvokePrompt"); operation.Telemetry.Type = "SemanticKernel"; operation.Telemetry.Target = "DeepSeek-API"; var sw = Stopwatch.StartNew(); try { var result = await _kernel.InvokePromptAsync(prompt, new KernelArguments( new OpenAIPromptExecutionSettings { MaxTokens = 2000, Temperature = 0.7 })); sw.Stop(); var response = result.ToString(); // 上报自定义指标到 Application Insights _telemetry.TrackMetric("SK.ResponseTime", sw.ElapsedMilliseconds); _telemetry.TrackMetric("SK.ResponseLength", response?.Length ?? 0); // 记录自定义事件,便于统计分析 _telemetry.TrackEvent("SK.InvokeSuccess", new Dictionary<string, string> { ["UserId"] = userId, ["PromptLength"] = prompt.Length.ToString(), ["ElapsedMs"] = sw.ElapsedMilliseconds.ToString() }); operation.Telemetry.Success = true; operation.Telemetry.Duration = sw.Elapsed; return response ?? string.Empty; } catch (Exception ex) { sw.Stop(); // 异常上报到 Application Insights _telemetry.TrackException(ex, new Dictionary<string, string> { ["UserId"] = userId, ["PromptLength"] = prompt.Length.ToString(), ["ElapsedMs"] = sw.ElapsedMilliseconds.ToString() }); operation.Telemetry.Success = false; _logger.LogError(ex, "AI 调用失败 | UserId: {UserId}", userId); throw; } } } }

🔧 调试技巧与性能分析

🐛 断点调试关键场景

在 Semantic Kernel 开发中,有几个地方特别值得打断点:

csharp
// 在函数调用事件上断点,观察 AI 到底调用了哪个插件 _kernel.FunctionInvoking += (sender, e) => { // 在这里打断点,检查 e.Function.Name 和传入的参数 var args = e.Arguments; Console.WriteLine($"断点观察点: 插件={e.Function.PluginName}, 函数={e.Function.Name}"); };

条件断点技巧: 当某个特定插件被调用时才中断,可以在 Visual Studio 里设置条件 e.Function.Name == "GetWeather",避免每次都停下来。

⚡ 性能分析要点

使用 dotnet-trace 采集 CPU 热点:

bash
# 采集 30 秒的性能追踪 dotnet-trace collect --process-id <PID> --duration 00:00:30 # 使用 PerfView 或 SpeedScope 分析生成的 .nettrace 文件

内置性能计时模板:

csharp
// 可复用的性能测量工具类 public static class PerfTracker { private static readonly ILogger _logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger("PerfTracker"); public static async Task<(T Result, long ElapsedMs)> MeasureAsync<T>( string operationName, Func<Task<T>> operation) { var sw = Stopwatch.StartNew(); try { var result = await operation(); sw.Stop(); _logger.LogInformation( "性能测量 | 操作: {Operation} | 耗时: {ElapsedMs}ms", operationName, sw.ElapsedMilliseconds); return (result, sw.ElapsedMilliseconds); } finally { if (!sw.IsRunning) return; // 已在 try 中 stop sw.Stop(); } } } // 使用示例 var (response, elapsed) = await PerfTracker.MeasureAsync( "SK.InvokePrompt", () => kernel.InvokePromptAsync(prompt).ContinueWith(t => t.Result.ToString()) );

📋 完整监控系统集成示例

把上面所有部分整合到一起,这是一个可以直接落地的入口程序:

csharp
#pragma warning disable SKEXP0010 using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Serilog; using Serilog.Events; using System.Text; // 初始化 Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft.SemanticKernel", LogEventLevel.Debug) .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day) .CreateLogger(); var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddSerilog(); // 构建 Kernel 并注入日志 var kernelBuilder = Kernel.CreateBuilder(); kernelBuilder.AddOpenAIChatCompletion( modelId: "deepseek-chat", apiKey: Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? "sk-xxx", endpoint: new Uri("https://api.deepseek.com/v1") ); kernelBuilder.Services.AddLogging(b => b.AddSerilog()); var kernel = kernelBuilder.Build(); builder.Services.AddSingleton(kernel); builder.Services.AddSingleton<SKLoggingService>(); var host = builder.Build(); var aiService = host.Services.GetRequiredService<SKLoggingService>(); // 主对话循环 Console.OutputEncoding = Encoding.UTF8; var chatHistory = new ChatHistory(); chatHistory.AddSystemMessage("你是一个专业的技术助手,用中文回答问题。"); var sessionId = Guid.NewGuid().ToString("N")[..8]; Console.WriteLine($"会话 ID: {sessionId} | 输入 exit 退出"); while (true) { Console.Write("\n你: "); var input = Console.ReadLine()?.Trim(); if (string.IsNullOrWhiteSpace(input)) continue; if (input.Equals("exit", StringComparison.OrdinalIgnoreCase)) break; chatHistory.AddUserMessage(input); Console.Write("\nAI: "); try { var response = await aiService.StreamChatWithMonitoringAsync(chatHistory, sessionId); chatHistory.AddAssistantMessage(response); Console.WriteLine("\n"); } catch (Exception ex) { Console.WriteLine($"\n[错误] {ex.Message}"); } } Log.CloseAndFlush();

💎 三个核心收获总结

1. 结构化日志是可观测性的基础
{参数名} 占位符代替字符串拼接,让日志变成可查询的数据,而不是堆砌的文本——这一步的成本极低,收益却是长期的。

2. 监控要贴近业务语义
不只是记录"成功/失败",而是记录"哪个插件被调用了、消耗了多少 Token、响应时间在哪个区间"——这才是真正有价值的运维数据。

3. 性能告警要主动,不要被动
设置合理的阈值(如响应 > 3s 触发 Warning,> 8s 触发 Error),在用户感知之前就发现问题。


💬 开放讨论

  1. 你在生产环境的 Semantic Kernel 应用中,遇到过哪些因为缺乏监控而难以排查的问题?是 Token 超支、函数调用错乱,还是其他情况?

  2. 对于 AI 应用的日志,你认为哪些字段是必须记录的?有没有在隐私合规和调试需求之间权衡的实践经验?


相关标签: #C#开发 #SemanticKernel #日志监控 #Serilog #性能优化 #ApplicationInsights #AI应用开发 #调试技巧

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!