当前位置: 首页 > news >正文

[MAF预定义的IChatClient中间件-01]LoggingChatClient——在LLM调用前后输出日志

LoggingChatClient是一个预定义的IChatClient中间件,它在调用前后输出日志,帮助我们更好地了解Agent的执行过程。它会记录每次调用的输入和输出,以及调用的时间戳等信息。这对于调试和监控Agent的行为非常有用。

1. 利用LoggingChatClient中间件来记录针对LLM的调用

如果将LoggingChatClient这个中间件至于连接LLM的IChatClient之前,那么针对后者对LLM的调用情况会以日志的形式记录下来。我们可以通过设置不同的日志级别来控制输出的详细程度。在如下的演示程序中,我们利用创建了一个基于OpenAIClientIChatClient对象。在调用AsBuilder扩展方法将ChatClientBuilder构建出来后,通过调用UseLogging方法来注册LoggingChatClient中间件,并且传入一个ILoggerFactory对象来控制日志的输出。由于我们在创建ILoggerFactory对象的时候设置了日志级别为Debug

usingAzure;usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingOpenAI;DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varloggerFactory=newServiceCollection().AddLogging(logging=>logging.SetMinimumLevel(LogLevel.Trace).AddConsole()).BuildServiceProvider().GetRequiredService<ILoggerFactory>();varclient=newOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsBuilder().UseLogging(loggerFactory:loggerFactory).Build();awaitclient.GetResponseAsync("What is Azure OpenAI?");Console.ReadLine();

LoggingChatClientGetResponseAsync方法会在调用前输出一条日志,表示正在调用LLM,并且会记录调用的输入内容;在调用完成后会输出另一条日志,表示调用已经完成,并且会记录调用的输出内容。通过这些日志,我们可以清楚地看到每次调用的输入和输出,以及调用的时间戳等信息。

dbug: Microsoft.Extensions.AI.LoggingChatClient[1723383095] GetResponseAsync invoked. dbug: Microsoft.Extensions.AI.LoggingChatClient[1553703230] GetResponseAsync completed.

如果我们将日志等级设置为更低的Trace级别,那么LoggingChatClient还会输出更详细的日志信息,包括调用的输入内容和输出内容等。

varloggerFactory=newServiceCollection().AddLogging(logging=>logging.SetMinimumLevel(LogLevel.Trace).AddConsole()).BuildServiceProvider().GetRequiredService<ILoggerFactory>();

输出:

trce: Microsoft.Extensions.AI.LoggingChatClient[805843669] GetResponseAsync invoked: [ { "role": "user", "contents": [ { "$type": "text", "text": "What is Azure OpenAI?" } ] } ]. Options: null. Metadata: { "providerName": "openai", "providerUri": "https://eap2410.cognitiveservices.azure.com/openai/v1", "defaultModelId": "gpt-5.2-chat" }. trce: Microsoft.Extensions.AI.LoggingChatClient[384896670] GetResponseAsync completed: { "messages": [ { "createdAt": "2026-05-22T01:28:42+00:00", "role": "assistant", "contents": [ { "$type": "text", "text": "**Azure OpenAI** is Microsoft’s cloud-based service that provides access to advanced AI models (like OpenAI’s GPT, GPT‑4, and image generation models) through the **Microsoft Azure** platform.\n\nIn simple terms, it lets businesses and developers use powerful AI models within Microsoft’s secure cloud environment.\n\n### Key Features:\n- **Access to OpenAI models** (GPT‑4, GPT‑4o, embeddings, image generation, etc.)\n- **Enterprise-grade security and compliance**\n- **Data privacy** — your data isn’t used to train the base models\n- **Integration with Azure services** (Azure AI Search, Azure Functions, Power BI, etc.)\n- **Scalable infrastructure** for production workloads\n\n### What It’s Used For:\n- Chatbots and virtual assistants \n- Document summarization \n- Code generation \n- Data analysis \n- Image generation \n- Semantic search and embeddings \n\n### How It’s Different from OpenAI’s public API:\n- Runs within the **Azure ecosystem**\n- Offers enterprise security controls\n- Regional data hosting options\n- Integrated billing through Azure\n\nIn short: \n**Azure OpenAI = OpenAI models + Microsoft Azure’s enterprise cloud platform.**" } ], "messageId": "chatcmpl-Di8yoRfX62nycHbngYbn11qNFWvJk" } ], "responseId": "chatcmpl-Di8yoRfX62nycHbngYbn11qNFWvJk", "modelId": "gpt-5.2-chat-latest", "createdAt": "2026-05-22T01:28:42+00:00", "finishReason": "stop", "usage": { "inputTokenCount": 12, "outputTokenCount": 252, "totalTokenCount": 264, "cachedInputTokenCount": 0, "reasoningTokenCount": 0, "additionalCounts": { "InputTokenDetails.AudioTokenCount": 0, "OutputTokenDetails.AudioTokenCount": 0, "OutputTokenDetails.AcceptedPredictionTokenCount": 0, "OutputTokenDetails.RejectedPredictionTokenCount": 0 } } }.

2. LoggingChatClient

LoggingChatClient直接继承自DelegatingChatClient,是一个非常简单的中间件实现,它直接利用构造函数传入的ILogger对象来输出日志信息。DelegatingChatClient在没有出错的情况下只会输出等级分别为DebugTrace的日志信息,如果最低日志等级设置为Debug,那么就只会输出调用前和调用后的日志;如果最低日志等级设置为Trace,那么就会输出更详细的日志信息,包括调用的输入内容和输出内容等。Trace等级的日志的内容以JSON形式输出,所以它提供了一个JsonSerializerOptions属性来控制日志中输入输出内容的序列化方式。我们可以通过设置这个属性来控制日志中输入输出内容的格式,比如是否使用驼峰命名、是否忽略空值等。

publicpartialclassLoggingChatClient:DelegatingChatClient{publicLoggingChatClient(IChatClientinnerClient,ILoggerlogger);publicJsonSerializerOptionsJsonSerializerOptions{get;set;}publicoverrideasyncTask<ChatResponse>GetResponseAsync(IEnumerable<ChatMessage>messages,ChatOptions?options=null,CancellationTokencancellationToken=default);publicoverrideasyncIAsyncEnumerable<ChatResponseUpdate>GetStreamingResponseAsync(IEnumerable<ChatMessage>messages,ChatOptions?options=null,CancellationTokencancellationToken=default);}

针对GetResponseAsync的日志输出采用如下的逻辑:

  • 在调用innerClientGetResponseAsync方法之前,输出一条Debug/Trace等级的日志,表示正在调用LLM,并且会记录调用的输入内容;
  • 在成功调用并得到响应之后,输出另一条Debug/Trace等级的日志,表示调用已经完成,并且会记录调用的输出内容;
  • 如果调用过程中发生了异常,那么会输出一条Error等级的日志,表示调用失败,并且会记录异常信息;

针对GetStreamingResponseAsync的日志输出采用如下的逻辑:

  • 在调用innerClientGetStreamingResponseAsync方法之前,输出一条Debug/Trace等级的日志,表示正在调用LLM,并且会记录调用的输入内容;
  • 如果调用失败,那么会输出一条Error等级的日志,表示调用失败,并且会记录异常信息;
  • GetStreamingResponseAsync会对返回的IAsyncEnumerable<ChatResponseUpdate>进行迭代,对于每一次迭代:
    • 如果成功获取到一个ChatResponseUpdate,并且最低日志等级设置为Trace,那么会输出一条Trace等级的日志,表示获取到了一个更新,并且会记录这个更新的内容;
    • 如果在迭代过程中发生了异常,那么会输出一条Error等级的日志,表示迭代失败,并且会记录异常信息;
  • 在迭代完成之后,输出一条Debug等级的日志,表示调用已经完成;

对于我们前面演示的例子,如果我们将日志等级设置为Trace,那么在调用GetStreamingResponseAsync方法时,我们就可以看到每一次迭代获取到的ChatResponseUpdate的内容都被记录在日志中了,这对于调试和监控Agent的行为非常有用。由于这种情况下输出内容容量可能会非常大,所以当我们将日志等级设置为Trace时,得评估一下日志对性能带来得影响。

usingAzure;usingdotenv.net;usingMicrosoft.Agents.AI;usingMicrosoft.Extensions.AI;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingOpenAI;DotEnv.Load();varmodel=Environment.GetEnvironmentVariable("MODEL")!;varapiKey=Environment.GetEnvironmentVariable("API_KEY")!;varendpoint=Environment.GetEnvironmentVariable("OPENAI_URL")!;varloggerFactory=newServiceCollection().AddLogging(logging=>logging.SetMinimumLevel(LogLevel.Trace).AddConsole()).BuildServiceProvider().GetRequiredService<ILoggerFactory>();varclient=newOpenAIClient(credential:newAzureKeyCredential(apiKey),options:newOpenAIClientOptions{Endpoint=newUri(endpoint)}).GetChatClient(model:model).AsIChatClient().AsBuilder().UseLogging(loggerFactory:loggerFactory).Build();awaitforeach(varupdateinclient.GetStreamingResponseAsync("世界上最深的淡水湖是哪个?在10字内作答!")){}

输出:

trce: Microsoft.Extensions.AI.LoggingChatClient[805843669] GetStreamingResponseAsync invoked: [ { "role": "user", "contents": [ { "$type": "text", "text": "世界上最深的淡水湖是哪个?在10字内作答!" } ] } ]. Options: null. Metadata: { "providerName": "openai", "providerUri": "https://eap2410.cognitiveservices.azure.com/openai/v1", "defaultModelId": "gpt-5.2-chat" }. trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "contents": [], "responseId": "", "messageId": "", "createdAt": "1970-01-01T00:00:00+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "text", "text": "" } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "text", "text": "贝" } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "text", "text": "加" } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "text", "text": "尔" } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "text", "text": "湖" } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "finishReason": "stop", "modelId": "" } trce: Microsoft.Extensions.AI.LoggingChatClient[1513570378] GetStreamingResponseAsync received update: { "role": "assistant", "contents": [ { "$type": "usage", "details": { "inputTokenCount": 24, "outputTokenCount": 78, "totalTokenCount": 102, "cachedInputTokenCount": 0, "reasoningTokenCount": 64, "additionalCounts": { "InputTokenDetails.AudioTokenCount": 0, "OutputTokenDetails.AudioTokenCount": 0, "OutputTokenDetails.AcceptedPredictionTokenCount": 0, "OutputTokenDetails.RejectedPredictionTokenCount": 0 } } } ], "responseId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "messageId": "chatcmpl-Di9WdprY6ZgpHqiLY25t8Y23kmsRa", "createdAt": "2026-05-22T02:03:39+00:00", "finishReason": "stop", "modelId": "" } dbug: Microsoft.Extensions.AI.LoggingChatClient[1553703230] GetStreamingResponseAsync completed.

3. 利用Source Generator生成日志输出代码

日志是典型得高频操作,尤其是当我们将日志等级设置得很低得时候更是如此,所以针对日志输出的每一个微小的细节都会高倍放大,比如字符串拼接和值类型转换成引用类型导致的装箱等。在此方面,Source Generator就能派上用场了。我们可以利用Source Generator来生成日志输出的代码,从而避免手写日志输出代码可能带来的性能问题。Microsoft.Extensions.Logging库已经提供了一个名为LoggerMessageAttribute的Source Generator,我们可以利用它来生成日志输出的代码。

LoggingChatClient涉及的日志输出被定义成对应的方法,并在这些方法上使用LoggerMessageAttribute特性来标记日志的级别和消息模板。LoggerMessageAttribute特性会告诉Source Generator生成对应的日志输出代码,从而避免了手写日志输出代码可能带来的性能问题。这也是LoggingChatClient被定义成partial类的原因。

publicpartialclassLoggingChatClient:DelegatingChatClient{[LoggerMessage(LogLevel.Debug,"{MethodName} invoked.")]privatepartialvoidLogInvoked(stringmethodName);[LoggerMessage(LogLevel.Trace,"{MethodName} invoked: {Messages}. Options: {ChatOptions}. Metadata: {ChatClientMetadata}.")]privatepartialvoidLogInvokedSensitive(stringmethodName,stringmessages,stringchatOptions,stringchatClientMetadata);[LoggerMessage(LogLevel.Debug,"{MethodName} completed.")]privatepartialvoidLogCompleted(stringmethodName);[LoggerMessage(LogLevel.Trace,"{MethodName} completed: {ChatResponse}.")]privatepartialvoidLogCompletedSensitive(stringmethodName,stringchatResponse);[LoggerMessage(LogLevel.Trace,"GetStreamingResponseAsync received update: {ChatResponseUpdate}")]privatepartialvoidLogStreamingUpdateSensitive(stringchatResponseUpdate);[LoggerMessage(LogLevel.Debug,"{MethodName} canceled.")]privatepartialvoidLogInvocationCanceled(stringmethodName);[LoggerMessage(LogLevel.Error,"{MethodName} failed.")]privatepartialvoidLogInvocationFailed(stringmethodName,Exceptionerror);}

4. UseLogging扩展方法

UseLogging是一个ChatClientBuilder的扩展方法,它提供了一种简便的方式来注册LoggingChatClient中间件。我们只需要在构建IChatClient对象的时候调用UseLogging方法,并传入一个ILoggerFactory对象来控制日志的输出,就可以轻松地将LoggingChatClient中间件添加到我们的IChatClient对象中了。除此之外,UseLogging方法还提供了一个可选的configure参数,它允许我们在注册LoggingChatClient中间件的时候对其进行一些额外的配置,比如设置JsonSerializerOptions属性来控制日志中输入输出内容的序列化方式等。

publicstaticclassLoggingChatClientBuilderExtensions{publicstaticChatClientBuilderUseLogging(thisChatClientBuilderbuilder,ILoggerFactory?loggerFactory=null,Action<LoggingChatClient>?configure=null);}
http://www.jsqmd.com/news/889547/

相关文章:

  • 番茄小说下载器:5分钟打造你的个人数字图书馆,实现真正的阅读自由
  • Beyond Compare 5密钥生成器:从评估到期到永久授权的技术解密方案
  • 3种高效保存完整网页的终极方案:SingleFile工具完全指南
  • Windows Cleaner架构解析:基于Python的现代化Windows系统优化工具
  • 汕头市贵金属全品类回收同城靠谱回收门店权威:黄金+白银+铂金+钯金当场检测当面结算及联系方式推荐 - 亦辰小黄鸭
  • 温州黄金回收怎么选?福正美免费上门透明报价 - 上门黄金回收
  • 发膜功效对比:2026年修复力最强的5款 - 速递信息
  • OpenOOD开放集识别:3种方法如何应对未知类别识别挑战
  • MusicFree插件终极指南:如何打造你的专属音乐宇宙
  • 深圳昆仑腕表保养收费全公开:金桥线性机芯异响、海军上将杯自动陀螺丝松动怎么修?资深技师为你拆解工时费与原厂配件更换账单,守护你的独立制表品牌 “腕间艺术品” - 亨得利官方维修中心
  • 石家庄黄金回收哪家强?福正美免费上门堪称满分首选 - 上门黄金回收
  • 汕尾市贵金属全品类回收同城靠谱回收门店权威:黄金+白银+铂金+钯金当场检测当面结算及联系方式推荐 - 亦辰小黄鸭
  • GTA5线上小助手:完全免费的终极游戏体验增强工具
  • 基于AI跨资产联动模型的黄金市场分析:油价暴跌与美元降温背景下的金价重获支撑逻辑解析
  • Level数据分析集成:Heap Analytics与Fathom Analytics配置
  • 修复洗发水推荐:高级修复的洗发水品牌产品 - 速递信息
  • 免费AI视频补帧终极指南:Squirrel-RIFE让老旧视频秒变流畅大片
  • 如何用3个步骤将单张图片转换为专业PSD分层文件:Layerdivider完全指南
  • 太原黄金回收怎么避坑?福正美透明公道值得选 - 上门黄金回收
  • 大模型自主智能体记忆与反思机制设计如何落地企业?一篇深度解构与提效实战
  • 韶山市贵金属全品类回收同城靠谱回收门店权威:黄金+白银+铂金+钯金当场检测当面结算及联系方式推荐 - 亦辰小黄鸭
  • 开源英雄联盟回放分析工具:ROFLPlayer一站式解决方案
  • 2026湖北云仓代发平台权威推荐榜单 | 智能仓配优选,仓配之家领衔 - 品牌评测官
  • 使用Qwen3-Coder-30B-A3B-Instruct-FP8进行企业级代码审查与重构:提升代码质量的终极指南
  • 如何彻底解决Windows C盘爆红问题:Windows Cleaner智能清理工具完全指南
  • 3分钟彻底解决Windows窗口尺寸限制:WindowResizer让你的桌面随心所欲
  • 绍兴市贵金属全品类回收同城靠谱回收门店权威:黄金+白银+铂金+钯金当场检测当面结算及联系方式推荐 - 亦辰小黄鸭
  • 老Mac升级macOS终极指南:五步解决硬件兼容性问题
  • PUBG-Logitech终极压枪脚本指南:从零配置到实战优化
  • 2026年B站视频怎么下载?B站视频下载实用方法汇总 - 博客万