AutoGen .NET:使用 Protocol Buffers 定义跨进程 Agent 消息类型
AutoGen .NET 的 Agent 可以在进程内(InProcessRuntime)直接用普通 C# 类通信,但要接入 gRPC 等分布式运行时,所有消息类型就必须以 Protocol Buffers(proto3)定义。本文基于仓库文档 protobuf-message-types.md 讲解这一硬性要求的来源,给出完整的 .proto 集成步骤与 C# 代码写法,并结合 GettingStartedGrpc 示例源码剖析消息在跨进程运行时的完整流转过程。
为什么跨运行时消息必须是 Protocol Buffers 消息
文档 protobuf-message-types.md 开篇即给出约束:
For a message to be sent using a runtime other than the
InProcessRuntime, it must be defined as a Protocol Buffers message. This is because the message is serialized and deserialized using Protocol Buffers.
也就是说,只要消息要离开当前进程(例如通过 Microsoft.AutoGen.Core.Grpc 的 gRPC 运行时发送到远程 Worker),消息在传输前会被 Protocol Buffers 序列化、在接收端被反序列化,因此消息类型本身必须由 .proto 文件定义并在构建期生成 C# 类。文档同时说明:这一限制未来可能通过允许 converter、自定义序列化等机制放宽,但在当前版本中它是刚性要求。
可以对比仓库中两个 GettingStarted 示例来验证这一点:
-
进程内示例 GettingStarted/CountMessage.cs 直接用普通 C# POCO 定义消息:
namespace GettingStartedSample; public class CountMessage { public int Content { get; set; } }该示例通过
appBuilder.UseInProcessRuntime()(见 Program.cs)运行,全程不发生网络序列化,所以不需要 protobuf。 -
而 gRPC 示例 GettingStartedGrpc/message.proto 将同样的消息改为 proto 定义:
syntax = "proto3"; option csharp_namespace = "GettingStartedGrpcSample.Events"; message CountMessage { int32 content = 1; } message CountUpdate { int32 new_count = 1; }其宿主 Program.cs 使用
appBuilder.AddGrpcAgentWorker("http://localhost:50051")接入远程 Worker,此时消息类型必须来自 proto 生成代码。
在 .NET 项目中集成 Protocol Buffers 的四个步骤
以下是文档给出的完整操作路径,四步缺一不可。
第 1 步:在 .csproj 中引入 Grpc.Tools 包
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
Grpc.Tools 负责在构建期执行 protoc,把 .proto 文件编译为 C# 消息类(以及可选的 gRPC 客户端/服务端桩代码)。PrivateAssets="All" 表示这是一个纯构建期依赖:它只在当前项目编译时生效,不会传递给引用方,也不会出现在 NuGet 包的依赖列表中。仓库示例 GettingStartedGrpc.csproj 使用了完全相同的写法:
<ItemGroup>
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup>
第 2 步:在项目中声明 .proto 文件
<ItemGroup>
<Protobuf Include="messages.proto" GrpcServices="Client;Server" Link="messages.proto" />
</ItemGroup>
Include指定要编译的 proto 文件;Link仅影响解决方案资源管理器中的显示路径,不影响实际位置。GrpcServices控制是否生成 gRPC 服务代码(Client/Server)。如果你只定义消息类型、不需要服务桩,可以像 GettingStartedGrpc.csproj 那样只写GrpcServices="Client",甚至可省略该属性,仅生成消息类。
第 3 步:编写 proto3 消息定义
文档给出的最小示例:
syntax = "proto3";
package HelloAgents;
option csharp_namespace = "MyAgentsProtocol";
message TextMessage {
string Source = 1;
string Content = 2;
}
按文档指引,字段定义需遵循 Protocol Buffers Language Guide 中引用的 proto3 规范,几个关键点:
syntax = "proto3":必须声明 proto3 语法,这是当前 .NET gRPC 工具链支持的版本。option csharp_namespace:指定生成 C# 类的命名空间。文档示例中使用MyAgentsProtocol;真实示例 message.proto 使用option csharp_namespace = "GettingStartedGrpcSample.Events";,因此后续 C# 代码中可以直接写Events.CountMessage这样的限定名,与示例代码 Checker.cs 中的IHandle<Events.CountUpdate>一致。- 字段编号从 1 开始且不能重复:
Source = 1、Content = 2是 wire format 的标签,一经发布即不可更改,新增字段只能追加新编号。 - proto 类型映射:
string、int32等基础类型会映射为 C# 的string、int(例如 proto 中int32 content = 1;生成 C# 属性int Content { get; set; }),这保证了生成的类可以直接作为业务消息使用。
第 4 步:针对生成的类编写 Agent 代码
文档示例:
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
using MyAgentsProtocol;
[TypeSubscription("default")]
public class Checker(
AgentId id,
IAgentRuntime runtime,
) :
BaseAgent(id, runtime, "MyAgent", null),
IHandle<TextMessage>
{
public async ValueTask HandleAsync(TextMessage item, MessageContext messageContext)
{
Console.WriteLine($"Received message from {item.Source}: {item.Content}");
}
}
要点解析:
-
TextMessage不是手写的,而是构建期由messages.proto自动生成的 C# 类,位于option csharp_namespace指定的MyAgentsProtocol命名空间。 -
IHandle<T>是 AutoGen 的通用消息处理契约,定义在 IHandle.cs:public interface IHandle<in T> { ValueTask HandleAsync(T item, MessageContext messageContext); }Agent 只要实现该接口(
T为 proto 生成的消息类型),运行时就会把匹配的消息分发进来。由于T位于逆变位置(in T),handler 也可以接受更宽泛的基类型。 -
HandleAsync的第二个参数MessageContext携带消息元数据,定义见 MessageContext.cs,核心属性包括:MessageId(消息唯一标识)、CancellationToken(取消令牌)、Sender(发送方AgentId,未指定时为null)、Topic(消息所属话题)、IsRpc(是否为 RPC 调用)。这些属性在调试跨进程消息流向时非常有用。 -
[TypeSubscription("default")]声明该 Agent 订阅default话题下TextMessage类型的所有消息。
实战示例:GettingStartedGrpc 的完整消息流
仓库中的 GettingStartedGrpc 是这套机制的最小可运行示例,完整复现了“修改—校验”循环,消息全程经过 gRPC 序列化。
消息定义(message.proto):CountMessage(携带当前计数)与 CountUpdate(携带更新后的计数),均生成到 GettingStartedGrpcSample.Events 命名空间。
Modifier Agent(Modifier.cs):实现 IHandle<Events.CountMessage>,收到消息后对计数执行注入的 modifyFunc,再发布 CountUpdate 回 default 话题:
[TypeSubscription("default")]
public class Modifier(
AgentId id,
IAgentRuntime runtime,
ModifyF modifyFunc
) :
BaseAgent(id, runtime, "Modifier", null),
IHandle<Events.CountMessage>
{
public async ValueTask HandleAsync(Events.CountMessage item, MessageContext messageContext)
{
int newValue = modifyFunc(item.Content);
var updateMessage = new Events.CountUpdate { NewCount = newValue };
await this.PublishMessageAsync(updateMessage, topic: new TopicId("default"));
}
}
Checker Agent(Checker.cs):实现 IHandle<Events.CountUpdate>,若 new_count > 1 则把计数重新包装为 CountMessage 发布回去形成循环;否则调用 hostApplicationLifetime.StopApplication() 终止应用:
public async ValueTask HandleAsync(Events.CountUpdate item, MessageContext messageContext)
{
if (!runUntilFunc(item.NewCount))
{
await this.PublishMessageAsync(new Events.CountMessage { Content = item.NewCount }, new TopicId("default"));
}
else
{
hostApplicationLifetime.StopApplication();
}
}
宿主程序(Program.cs):
AgentsAppBuilder appBuilder = new AgentsAppBuilder();
appBuilder.AddGrpcAgentWorker("http://localhost:50051"); // 连接远程 gRPC Worker
appBuilder.AddAgent<Checker>("Checker");
appBuilder.AddAgent<Modifier>("Modifier");
var app = await appBuilder.BuildAsync();
await app.StartAsync();
await app.PublishMessageAsync(new GettingStartedGrpcSample.Events.CountMessage
{
Content = 10
}, new TopicId("default"));
await app.WaitForShutdownAsync();
与进程内版本 GettingStarted/Program.cs 相比,唯一本质差异是 UseInProcessRuntime() 换成了 AddGrpcAgentWorker(...)——Agent 代码(Checker/Modifier)几乎逐行相同,只是消息类型从 POCO 换成了 proto 生成类。这说明:只要消息类型按本文方式定义,同一套 Agent 逻辑可以在进程内与 gRPC 运行时之间平移,代价仅仅是把消息类改为 proto 定义。
项目依赖上,GettingStartedGrpc.csproj 额外引用了三个项目:Microsoft.AutoGen.Contracts(IHandle<T>、TypeSubscription、MessageContext 等契约)、Microsoft.AutoGen.Core(Agent 基础类型与进程内能力)、Microsoft.AutoGen.Core.Grpc(gRPC 运行时与 Worker 接入)。
关键限制与注意事项
- 适用范围:protobuf 消息要求仅针对
InProcessRuntime之外的运行时。纯进程内应用可继续使用普通 C# 类(参见 GettingStarted/CountUpdate.cs 等 POCO),不必引入 proto 工具链。 - 未来可能放宽:文档明确提示该要求“may be relaxed in future by allowing for converters, custom serialization, or other mechanisms”,即官方预留了 converter / 自定义序列化机制的扩展方向。当前版本请以 proto 定义为唯一合规做法。
csharp_namespace决定代码组织:所有 C# 端引用(using、IHandle<...>泛型参数)都依赖该选项;示例将其设为GettingStartedGrpcSample.Events后,Agent 代码得以用Events.CountMessage这类短限定名引用生成类型。- 字段编号是 wire 契约:proto 字段的数字标签参与二进制编码,修改或复用编号会导致跨版本反序列化错乱,扩展消息时只应追加新字段。
- GrpcServices 按需选择:仅定义消息类型时可只生成客户端代码或完全不生成服务桩(示例使用
GrpcServices="Client");文档示例中的GrpcServices="Client;Server"适用于 proto 文件中同时定义service的场景。
按以上方式,你就可以在 AutoGen .NET 项目中定义一套跨进程可传输的强类型消息:构建期由 Grpc.Tools 生成 C# 类,运行期由 gRPC 运行时完成序列化与分发,Agent 通过 IHandle<T> + [TypeSubscription] 以类型安全的方式订阅并处理消息。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00