AutoGen.NET 中使用 PrintMessageMiddleware 实现 Agent 消息的格式化控制台打印
本文基于 AutoGen 的 .NET 实现,讲解内置中间件 PrintMessageMiddleware 的用法与原理。它可以将 Agent 的回复(包括流式更新)格式化为人类可读的文本并输出到控制台,是调试多 Agent 对话时最常用的观测手段。读完本文,你可以掌握:如何把打印中间件注册到任意 Agent 或流式 Agent 上、它支持哪些消息类型,以及从源码层面理解它对普通回复与流式回复分别走了哪条处理路径。
PrintMessageMiddleware 是什么
PrintMessageMiddleware 是 AutoGen.Core 中内置的中间件,定义在 PrintMessageMiddleware.cs。它的作用是"将 Agent 的回复漂亮地打印(pretty print)到控制台"。从源码看,该类直接实现的是 IStreamingMiddleware 接口(该接口位于 IStreamingMiddleware.cs),也就是说它同时覆盖了普通 Agent 与流式 Agent 两种场景。
根据官方文档说明(见 Print-message-middleware.md),它支持格式化输出的 IMessage 类型包括:
- 普通消息:
TextMessage、MultiModalMessage、ToolCallMessage、ToolCallResultMessage、Message - 流式消息:
TextMessageUpdate(流式文本更新)、ToolCallMessageUpdate(流式工具调用更新)
具体的格式化逻辑由扩展方法 IMessage.FormatMessage() 完成,该扩展方法定义在 MessageExtension.cs,内部按消息的具体类型(Message、TextMessage、ImageMessage、ToolCallMessage 等)分派到各自的格式化实现。
在 Agent 上注册打印中间件
注册打印中间件不需要手动 new PrintMessageMiddleware(),仓库提供了便捷扩展方法 RegisterPrintMessage,定义在 PrintMessageMiddlewareExtension.cs。仓库示例代码(PrintMessageMiddlewareCodeSnippet.cs)展示了典型用法:
var agent = new OpenAIChatAgent(gpt4o, "assistant", config.DeploymentName)
.RegisterMessageConnector();
// 注册打印中间件,返回一个包装了中间件的 MiddlewareAgent
var agentWithPrintMessageMiddleware = agent
.RegisterPrintMessage();
await agentWithPrintMessageMiddleware.SendAsync("write a long poem");
RegisterPrintMessage 提供了三个重载(对应源码中 L34、L47、L60 三处签名):
- 接收任意
TAgent : IAgent,返回MiddlewareAgent<TAgent>; - 接收已经套了中间件的
MiddlewareAgent<TAgent>,可以再叠一层,返回新的MiddlewareAgent<TAgent>; - 接收
MiddlewareStreamingAgent<TAgent>(TAgent : IStreamingAgent),内部改调UseStreaming(middleware),返回MiddlewareStreamingAgent<TAgent>。
三个重载的实现模式一致:new PrintMessageMiddleware() 实例化中间件 → 用 MiddlewareAgent<TAgent>(agent) 包装原 Agent → 调 Use(middleware)(流式场景为 UseStreaming)→ 返回新的中间件 Agent。注意注册不会修改原 Agent,而是返回一个新的包装实例,原 Agent 保持"纯净"可继续复用,这与 MiddlewareExtension.cs 中 RegisterMiddleware 系列方法的"返回新 Agent"设计一致。
注册后调用 SendAsync 发起对话,Agent 的回复就会被格式化并打印到控制台:
提示:源码中还存在
RegisterPrintFormatMessageHook系列旧 API,均被标注为[Obsolete("This API will be removed in v0.1.0, Use RegisterPrintMessage instead.")],请直接使用RegisterPrintMessage。
源码解析:非流式与流式两条处理路径
PrintMessageMiddleware 实现了两个 InvokeAsync 方法,分别对应普通 Agent 与流式 Agent,这是理解其行为的关键。
非流式路径(普通 IAgent)
public async Task<IMessage> InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default)
{
if (agent is IStreamingAgent streamingAgent)
{
// 若底层 Agent 实际支持流式,转交给流式版本处理
...
}
else
{
var reply = await agent.GenerateReplyAsync(context.Messages, context.Options, cancellationToken);
var formattedMessages = reply.FormatMessage();
Console.WriteLine(formattedMessages);
return reply;
}
}
逻辑非常直接:先调用底层 Agent 的 GenerateReplyAsync 拿到完整回复,再调用 reply.FormatMessage() 得到人类可读的格式化文本,Console.WriteLine 输出后原样把 reply 返回给上层——中间件只"旁观"打印,不修改消息内容,因此不会改变对话语义。
一个值得注意的细节:非流式入口里先判断 agent is IStreamingAgent,如果是,就把处理委托给流式版本的 InvokeAsync。这意味着即使你按普通 Agent 方式调用,只要底层 Agent 支持流式,打印行为也会走流式路径,最终仍会打印格式化后的完整消息。
流式路径(IStreamingAgent)
流式版本是一个 IAsyncEnumerable<IMessage> 迭代器(见 PrintMessageMiddleware.cs),它逐段消费 agent.GenerateStreamingReplyAsync 产出的增量消息,按类型分三种处理:
TextMessageUpdate(流式文本增量):- 首次收到时,先打印
from: {agentName}头(标识消息来源),随后用Console.Write(不换行)追加文本内容,让文字"打字机式"逐字出现;内部同时把增量累积进一个TextMessage; - 后续增量继续
Console.Write追加,并通过recentTextMessage.Update(textMessageUpdate)更新累积消息。
- 首次收到时,先打印
ToolCallMessageUpdate(流式工具调用增量):不打印任何内容,只把增量累积到ToolCallMessage中。- 完整
IMessage:直接记录为recentUpdate并透传。
流结束后统一做收尾:打印一个换行,若最终消息不是 TextMessage(例如 ToolCallMessage,因为流式期间没打印过),就补一行 FormatMessage() 的完整格式化输出;最后 yield return 最终的完整消息。这样下游拿到的始终是一个完整的 IMessage,而控制台看到的是增量式体验。
流式消息支持的实际效果
把 PrintMessageMiddleware 注册到实现了 IStreamingAgent 的 Agent 上,流式回复就会边生成边打印。仓库示例(PrintMessageMiddlewareCodeSnippet.cs)中,OpenAIChatAgent 经 RegisterMessageConnector() 转换后即为流式 Agent,直接链式追加 RegisterPrintMessage():
var streamingAgent = new OpenAIChatAgent(gpt4o, "assistant")
.RegisterMessageConnector()
.RegisterPrintMessage();
await streamingAgent.SendAsync("write a long poem");
运行时控制台先出现 from: assistant,随后文本增量逐字刷新;对于 ToolCallMessageUpdate,流式期间控制台保持安静,流结束后会打印格式化后的完整工具调用消息。
小结与使用建议
PrintMessageMiddleware是AutoGen.Core内置的观测型中间件,通过扩展方法RegisterPrintMessage一行代码即可注册,返回的MiddlewareAgent<TAgent>/MiddlewareStreamingAgent<TAgent>可继续链式叠加其他中间件;- 它同时支持普通消息(
TextMessage、MultiModalMessage、ToolCallMessage、ToolCallResultMessage、Message)与流式消息(TextMessageUpdate、ToolCallMessageUpdate),格式化能力由IMessage.FormatMessage()扩展方法提供; - 该中间件只读不改消息,适合在调试 Agent 对话、观察多 Agent 消息流转时直接挂到任意 Agent 上使用;生产环境若不希望控制台输出,取消注册即可,不影响 Agent 本身的对话行为。
参考文件:
- 中间件实现:dotnet/src/AutoGen.Core/Middleware/PrintMessageMiddleware.cs
- 注册扩展:dotnet/src/AutoGen.Core/Extension/PrintMessageMiddlewareExtension.cs
- 通用中间件机制:dotnet/src/AutoGen.Core/Extension/MiddlewareExtension.cs
- 示例代码:dotnet/samples/AgentChat/AutoGen.Basic.Sample/CodeSnippet/PrintMessageMiddlewareCodeSnippet.cs
- 原始文档:dotnet/website/articles/Print-message-middleware.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

