生产环境某次 AI 调用莫名超时,排查了两小时,最终发现是 Temperature 参数配置异常导致响应时间暴增 300%。如果当时有完善的日志监控体系,这个问题五分钟就能定位。
在构建 Semantic Kernel 应用时,日志与监控往往是最容易被忽视的环节。代码跑通了、接口通了,然后就直接上线——这种操作在小项目里或许无伤大雅,但在生产环境中,一旦出现 AI 调用失败、Token 超支、响应延迟飙升等问题,没有监控就等于盲开飞机。
读完本文,你将掌握:
痛点一:AI 调用失败后无迹可查
很多项目的错误处理长这样:
csharp// ❌ 典型的"吞掉异常"写法
try
{
var result = await kernel.InvokePromptAsync(prompt);
return result.ToString();
}
catch (Exception ex)
{
return "服务暂时不可用"; // 问题被掩盖,运维完全不知道
}
这种写法导致的后果是:用户反馈"AI 不好用",运维查不到任何错误信息,开发只能干瞪眼。
痛点二:Token 消耗失控
Semantic Kernel 每次调用都会消耗 Token,如果没有监控,以下情况很容易发生:
痛点三:性能劣化无法感知
没有追踪时,你根本不知道:响应从 800ms 变成 3000ms 是从哪天开始的?是模型接口变慢了,还是某个插件计算耗时暴增?
一个完整的 Semantic Kernel 监控体系,需要覆盖三层:
| 层级 | 监控内容 | 工具选型 |
|---|---|---|
| 应用层 | 业务逻辑、用户请求、对话历史 | ILogger + Serilog |
| 框架层 | SK 内核事件、函数调用、插件触发 | SK 内置事件钩子 |
| 基础设施层 | HTTP 请求、延迟、Token 用量 | Application Insights |
logger.LogInformation("Token: {TokenCount}", count) 远比 logger.LogInformation($"Token: {count}") 好查询这是最轻量的方案,适合中小项目快速接入监控能力。
第一步:配置日志级别
json// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.SemanticKernel": "Debug",
"AppAiAgent": "Debug"
}
}
}
💡 将
Microsoft.SemanticKernel设为Debug,SK 框架会自动输出函数调用、模型请求等详细信息。
第二步:封装带监控的 AI 服务
csharpusing 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;
}
}
}

踩坑预警: 不要在日志里记录完整的 prompt 内容,特别是包含用户输入的场景——一方面是隐私风险,另一方面会让日志文件迅速膨胀。记录长度和关键标识即可。
Serilog 是目前 .NET 生态里结构化日志的事实标准,配合 Seq 或 Elasticsearch 可以做到强大的查询与可视化。
安装依赖包:
bashdotnet 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):
csharpusing 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);
封装带上下文传播的日志服务:
csharpusing 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");
}
}
}
}

结构化日志的查询优势: 使用上面的写法,你可以在 Seq 或 Kibana 里直接查 SessionId = "abc123" 的所有日志,或者按 ElapsedMs > 3000 筛选所有慢请求——这是纯文本日志做不到的。
Application Insights 提供了分布式追踪、异常聚合、性能分析一体化的能力,特别适合云端部署的 SK 应用。

安装依赖:
bashdotnet add package Microsoft.ApplicationInsights.AspNetCore dotnet add package Microsoft.ApplicationInsights.WorkerService
配置与集成:
csharpusing 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),在用户感知之前就发现问题。
你在生产环境的 Semantic Kernel 应用中,遇到过哪些因为缺乏监控而难以排查的问题?是 Token 超支、函数调用错乱,还是其他情况?
对于 AI 应用的日志,你认为哪些字段是必须记录的?有没有在隐私合规和调试需求之间权衡的实践经验?
相关标签: #C#开发 #SemanticKernel #日志监控 #Serilog #性能优化 #ApplicationInsights #AI应用开发 #调试技巧
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!