[MAF预定义Agent中间件-01]LoggingAgent——在Agent调用前后输出日志

ChatClientAgent管道由三部分组成,面向LLM的ChatClient管道中预定义了一个LoggingChatClient中间件,Agent中间件管道中预定义了一个LoggingAgent中间件,除了所处位置决定的调用时机不同外,它们的设计和实现方法几乎一摸一样。所以我们以LoggingChatClient------在LLM调用前后输出日志这篇文章一样的结构来介绍LoggingAgent中间件。该中间件它在调用Agent前后输出日志,帮助我们更好地了解Agent的执行过程。它会记录每次调用的输入和输出,以及调用的时间戳等信息。这对于调试和监控Agent的行为非常有用。

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

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

csharp 复制代码
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenAI;
using System.ClientModel;

DotEnv.Load();
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var loggerFactory = new ServiceCollection()
    .AddLogging(logging => logging
        .SetMinimumLevel(LogLevel.Debug)
        .AddConsole())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>();

var agent = new OpenAIClient(
    new ApiKeyCredential(apiKey),
    new OpenAIClientOptions {  Endpoint = new Uri(openAIUrl)})
    .GetChatClient(model: model)
    .AsAIAgent()
    .AsBuilder()
    .UseLogging(loggerFactory)
    .Build()
    ;
await agent.RunAsync("评价一下林则徐的楷书,200字以内");

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

复制代码
dbug: LoggingAgent[1723383095]
      RunAsync invoked.
dbug: LoggingAgent[1553703230]
      RunAsync completed.

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

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

输出:

复制代码
trce: LoggingAgent[805843669]
      RunAsync invoked: [{"role":"user","contents":[{"$type":"text","text":"评价一下林则徐的楷书,200字以内"}]}]. Options: null. Metadata: {"providerName":"openai"}.
trce: LoggingAgent[384896670]
      RunAsync completed: {"messages":[{"createdAt":"2026-05-29T01:51:43+00:00","role":"assistant","contents":[{"$type":"text","text":"林则徐楷书以唐楷为基,融合欧体之严谨方峻与颜体之雄浑端重,形成刚柔并济的独特风貌。其用笔一丝不苟,点画精到,竖笔挺立如松柏,捺画含蓄而力沉;结字匀称疏朗,于工整中见灵动气象。尤为可贵的是,其书风与人格高度统一,字里行间透出"无欲则刚"的清正骨气,既显传统士大夫的深厚学养,又饱含"苟利国家生死以"的凛然节操。这种将个人修养、时代精神注入笔墨的境界,使其超越了纯粹技法层面,成为晚清文人书法的典范。"}],"messageId":"d9ea27a1f0c7436097d5ac6892100c2b"}],"agentId":"2f904288a3c24dd39d80ca5bfde40fec","responseId":"d9ea27a1f0c7436097d5ac6892100c2b","createdAt":"2026-05-29T01:51:43+00:00","finishReason":"stop","usage":{"inputTokenCount":16,"outputTokenCount":158,"totalTokenCount":174,"additionalCounts":{}}}.

2. LoggingAgent

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

csharp 复制代码
public sealed class LoggingAgent : DelegatingAIAgent
{
	public JsonSerializerOptions JsonSerializerOptions {get;set;}
	public LoggingAgent(AIAgent innerAgent, ILogger logger);
	protected override async Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default(CancellationToken));
}

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

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

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

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

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

csharp 复制代码
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenAI;
using System.ClientModel;

DotEnv.Load();
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var openAIUrl = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var loggerFactory = new ServiceCollection()
    .AddLogging(logging => logging
        .SetMinimumLevel(LogLevel.Trace)
        .AddConsole())
    .BuildServiceProvider()
    .GetRequiredService<ILoggerFactory>();

var agent = new OpenAIClient(
    new ApiKeyCredential(apiKey),
    new OpenAIClientOptions {  Endpoint = new Uri(openAIUrl)})
    .GetChatClient(model: model)
    .AsAIAgent()
    .AsBuilder()
    .UseLogging(loggerFactory)
    .Build()
    ;
await foreach (var update in agent.RunStreamingAsync("世界上最深的淡水湖是哪个?在10字内作答!"))
{
}

输出:

复制代码
trce: LoggingAgent[805843669]
      RunStreamingAsync invoked: [{"role":"user","contents":[{"$type":"text","text":"世界上最深的淡水湖是哪个?在10字内作答!"}]}]. Options: null. Metadata: {"providerName":"openai"}.
trce: LoggingAgent[1513570378]
      RunStreamingAsync received update: {"role":"assistant","contents":[{"$type":"text","text":""}],"agentId":"80b29c13d58344928730074396dd37fc","responseId":"462019f99bad4e9f868f8f166085bb46","messageId":"462019f99bad4e9f868f8f166085bb46","createdAt":"2026-05-29T02:04:02+00:00"}
trce: LoggingAgent[1513570378]
      RunStreamingAsync received update: {"role":"assistant","contents":[{"$type":"text","text":"贝"}],"agentId":"80b29c13d58344928730074396dd37fc","responseId":"462019f99bad4e9f868f8f166085bb46","messageId":"462019f99bad4e9f868f8f166085bb46","createdAt":"2026-05-29T02:04:02+00:00"}
trce: LoggingAgent[1513570378]
      RunStreamingAsync received update: {"role":"assistant","contents":[{"$type":"text","text":"加尔湖"}],"agentId":"80b29c13d58344928730074396dd37fc","responseId":"462019f99bad4e9f868f8f166085bb46","messageId":"462019f99bad4e9f868f8f166085bb46","createdAt":"2026-05-29T02:04:02+00:00"}
trce: LoggingAgent[1513570378]
      RunStreamingAsync received update: {"role":"assistant","contents":[],"agentId":"80b29c13d58344928730074396dd37fc","responseId":"462019f99bad4e9f868f8f166085bb46","messageId":"462019f99bad4e9f868f8f166085bb46","createdAt":"2026-05-29T02:04:02+00:00","finishReason":"stop"}
trce: LoggingAgent[1513570378]
      RunStreamingAsync received update: {"role":"assistant","contents":[{"$type":"usage","details":{"inputTokenCount":17,"outputTokenCount":4,"totalTokenCount":21,"additionalCounts":{}}}],"agentId":"80b29c13d58344928730074396dd37fc","responseId":"462019f99bad4e9f868f8f166085bb46","messageId":"462019f99bad4e9f868f8f166085bb46","createdAt":"2026-05-29T02:04:02+00:00","finishReason":"stop"}
dbug: LoggingAgent[1553703230]
      RunStreamingAsync completed.

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

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

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

csharp 复制代码
public partial class LoggingAgent : DelegatingAIAgent
{ 
    [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")]
    private partial void LogInvoked(string methodName);

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

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

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

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

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

csharp 复制代码
public static class LoggingAgentBuilderExtensions
{
    public static AIAgentBuilder UseLogging(
        this AIAgentBuilder builder,
        ILoggerFactory? loggerFactory = null,
        Action<LoggingAgent>? configure = null);
}
相关推荐
用户31346721435436 分钟前
LangChain 入门:LCEL 链式开发、LangSmith 追踪、RAG 检索与自定义 Agent 工具实战
agent
垚森3 小时前
我用 GLM-5.2 造了个炸裂主题后台:16 套主题随心切,可在线体验
ai·react
武子康4 小时前
调查研究-198 Agent 到底该记住什么?读懂《What Must Generalist Agents Remember?》
人工智能·openai·agent
武子康6 小时前
调查研究-197 FAISS vs Elasticsearch 全面对比:从向量检索、全文搜索到 RAG 选型指南
人工智能·elasticsearch·agent
leeyi6 小时前
Checkpoint 机制:Agent 怎么在断电后接着跑
redis·aigc·agent
米小虾7 小时前
Context Engineering —— 知识与记忆的窗口
人工智能·agent
doiito7 小时前
【Agent Harness】Gliding Horse 工具结果压缩体系:如何用“指针”驯服上下文膨胀
ai·rust·架构设计·系统设计·ai agent
葫芦和十三17 小时前
图解 MongoDB 13|WiredTiger 存储引擎:B-tree、页和 checkpoint 三件套
后端·mongodb·agent
To_OC18 小时前
数据集划分不是随便切:手把手切分大众点评情感数据集
人工智能·llm·agent
冬奇Lab19 小时前
Skill 系列(06):Skill 工程化与治理——路由准确率 38%、压缩节省 76%
人工智能·开源·agent