惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
T
Tenable Blog
D
DataBreaches.Net
GbyAI
GbyAI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
V
Visual Studio Blog
B
Blog
雷峰网
雷峰网
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
I
InfoQ
M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
The Cloudflare Blog
L
LangChain Blog
MongoDB | Blog
MongoDB | Blog
Vercel News
Vercel News
Cloudbric
Cloudbric
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
T
The Exploit Database - CXSecurity.com
Google DeepMind News
Google DeepMind News
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Attack and Defense Labs
Attack and Defense Labs
Martin Fowler
Martin Fowler
SecWiki News
SecWiki News
T
Threat Research - Cisco Blogs
J
Java Code Geeks
Cyberwarzone
Cyberwarzone
Forbes - Security
Forbes - Security
Spread Privacy
Spread Privacy
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
O
OpenAI News
D
Darknet – Hacking Tools, Hacker News & Cyber Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Schneier on Security
Schneier on Security
S
Security @ Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
H
Help Net Security
Y
Y Combinator Blog
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tor Project blog
量子位
U
Unit 42
S
SegmentFault 最新的问题
V
V2EX
D
Docker

博客园 - Artech

[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案 [MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF [MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现 [MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力 [MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控 [MAF预定义ChatClient中间件-07]PerServiceCallChatHistoryPersistingChatClient——基于ReAct循环的一步一存档 [MAF预定义ChatClient中间件-06]利用ImageGeneratingChatClient开发专业图片生成Agent [MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息 [MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义 [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间 [MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣 [MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 - Artech [MAF的Agent管道详解-05]对话历史的持久化和输入输出的增强 [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据? [MAF的Agent管道详解-03]连接LLM的IChatClient对象 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 [对比学习LangChain和MAF-04]针对消息的设计 [对比学习LangChain和MAF-03]完全不同的Agent设计哲学 [对比学习LangChain和MAF-02]基本编程模式的差异(下篇) [对比学习LangChain和MAF-01]基本编程模式的差异(上篇) 这是一篇测试文章 我所理解的Python元模型 除了按值和引用,方法参数的第三种传递方式 方法的三种调用形式 可以调用Null的实例方法吗? 自定义Key类型的字典无法序列化的N种解决方案 为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate 深入解析ASP.NET Core MVC的模块化设计[下篇] 深入解析ASP.NET Core MVC的模块化设计[上篇] ASP.NET Core MVC应用模型的构建[4]: Action的选择 ASP.NET Core MVC应用模型的构建[3]: Controller的收集 ASP.NET Core MVC应用模型的构建[2]: 定制应用模型 ASP.NET Core MVC应用模型的构建[1]: 应用的蓝图 基于HTTP2/3的流模式消息交换如何实现? 编写高效的代码,你应该了解Array、Memory、ReadOnlySequence . . . WebAssembly核心编程[4]: Memory WebAssembly核心编程[3]: Module 与 Instance WebAssembly核心编程[2]:类型系统 WebAssembly核心编程[1]:wasm模块实例化的N种方式 WebAssembly入门笔记[4]:利用Global传递全局变量 WebAssembly入门笔记[3]:利用Table传递引用
[MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志
Artech · 2026-06-03 · via 博客园 - Artech

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

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

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

using Azure;
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenAI;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var loggerFactory = new ServiceCollection()
    .AddLogging(logging=>logging
        .SetMinimumLevel(LogLevel.Trace)
        .AddConsole())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>();

var client = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model:model)
    .AsIChatClient()
    .AsBuilder()
    .UseLogging(loggerFactory: loggerFactory)
    .Build();

await client.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还会输出更详细的日志信息,包括调用的输入内容和输出内容等。

var loggerFactory = new ServiceCollection()
    .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属性来控制日志中输入输出内容的序列化方式。我们可以通过设置这个属性来控制日志中输入输出内容的格式,比如是否使用驼峰命名、是否忽略空值等。

public partial class LoggingChatClient : DelegatingChatClient
{   
    public LoggingChatClient(IChatClient innerClient, ILogger logger);
    public JsonSerializerOptions JsonSerializerOptions { get; set; }

    public override async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = default);
    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null, 
        CancellationToken cancellationToken = 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时,得评估一下日志对性能带来得影响。

using Azure;
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenAI;

DotEnv.Load();

var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var loggerFactory = new ServiceCollection()
    .AddLogging(logging=>logging
        .SetMinimumLevel(LogLevel.Trace)
        .AddConsole())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>();

var client = new OpenAIClient(
        credential: new AzureKeyCredential(apiKey),
        options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
    .GetChatClient(model:model)
    .AsIChatClient()
    .AsBuilder()
    .UseLogging(loggerFactory: loggerFactory)
    .Build();


await foreach(var update in client.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类的原因。

public partial class LoggingChatClient : DelegatingChatClient
{ 
    [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")]
    private partial void LogInvoked(string methodName);

    [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: {Messages}. Options: {ChatOptions}. Metadata: {ChatClientMetadata}.")]
    private partial void LogInvokedSensitive(string methodName, string messages, string chatOptions, string chatClientMetadata);

    [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")]
    private partial void LogCompleted(string methodName);

    [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {ChatResponse}.")]
    private partial void LogCompletedSensitive(string methodName, string chatResponse);

    [LoggerMessage(LogLevel.Trace, "GetStreamingResponseAsync received update: {ChatResponseUpdate}")]
    private partial void LogStreamingUpdateSensitive(string chatResponseUpdate);

    [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")]
    private partial void LogInvocationCanceled(string methodName);

    [LoggerMessage(LogLevel.Error, "{MethodName} failed.")]
    private partial void LogInvocationFailed(string methodName, Exception error);
}

4. UseLogging扩展方法

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

public static class LoggingChatClientBuilderExtensions
{
    public static ChatClientBuilder UseLogging(
        this ChatClientBuilder builder,
        ILoggerFactory? loggerFactory = null,
        Action<LoggingChatClient>? configure = null);
}