Skip to content

mq9 协议设计问答录

设计思路

设计 mq9 之前,先问了自己一个问题:Agent 和人有什么不一样?

人会间歇在线。人有身份。人有记忆。人收到重复的消息,自己判断要不要处理。人发邮件,不需要确认对方收到了才继续干活。

现在的消息基础设施是为机器设计的,不是为 Agent 设计的。Kafka 假设消费者持续在线、高吞吐、批量处理。NATS JetStream 强大,但概念密度太高——stream、consumer、durable、ack policy——Agent 开发者要花大量时间学习这些和业务无关的概念。这些系统都没有把"Agent 可能离线、可能崩溃、可能有多个实例"当成一等公民来处理。

mq9 的出发点是:Agent 通信应该像发邮件一样简单。

邮件系统没有 ack。知道地址就能发。每个人有独立的收件箱。离线的时候邮件在那等着,上线了继续读。没有人觉得 Email 是"不可靠的"——它在全球生产环境跑了几十年。

从这个出发点,推导出几条设计原则:

1. 以 mailbox 为核心抽象。 mailbox 是唯一天然承载"身份 + 记忆 + 间歇在线"三个特征的抽象。mail_address 是 Agent 的身份,消息持久化是记忆,offline buffering + ordered replay 解决间歇在线。不需要额外发明概念。

2. 复杂性由协议承担,不转嫁给 Agent。 优先级调度、offset 管理、TTL 清理——这些本来就是消息中间件该干的事。Agent 看到的接口应该极简,一个 queue name 参数就能覆盖广播、共享订阅、独立 offset 三种语义。

3. 责任归属准确。 幂等是业务层的问题,不是消息层的问题。exactly-once 在分布式系统里本质不存在,所有声称做到的底层都是 at-least-once + 去重。与其在协议里造一个虚假的幂等保证,不如把能力提供出来(可选的 dedup_id),让 Agent 自己决定用不用。这不是偷懒,是正确的责任归属。

4. 能力渐进,不强制。 普通用法不需要知道任何高级特性。需要延时发送,加 send_at header;需要幂等,带 dedup_id;需要优先级,在 subject 后缀加 .critical.urgent。每一层能力都是可选的叠加,不影响基础用法。

5. 复用而不重造。 NATS 是底层,subject 后缀机制天然支持优先级扩展,认证体系直接复用,request-reply 不重新设计。把精力花在真正需要设计的地方——mailbox 语义、offset 管理、Agent 通信原语。

这五条原则贯穿了下面所有的设计决策。看完这份问答录,可以回头对照这里,检验每一个答案是否和原则一致。


mq9 不是消息队列

mq9 看起来很像消息队列:有消息、有发送、有订阅、有 offset。但它不是按消息队列的思维设计的。这个区别值得单独说清楚,因为它决定了很多具体设计选择背后的逻辑。

消息队列的心智模型是管道。 生产者往管道里塞数据,消费者从管道里取数据,管道是无名的、中性的,它不在乎数据属于谁、谁在等它。Kafka 的 topic 是数据流,不是任何人的收件箱。消费者组是消费进度的载体,不是身份。整个系统的设计核心是吞吐和顺序——数据从 A 流向 B,越快越好,不丢不乱。

mq9 的心智模型是收件箱。 mailbox 属于某个 Agent,mail_address 是这个 Agent 的身份标识。消息不是在管道里流动的数据,而是寄给某个人的信。发件人知道收件人是谁,收件人有自己的读取进度,离线的时候信还在,上线了继续读。整个系统的设计核心是身份和状态——谁给谁发了什么,各自读到哪了。

这两种心智模型导致了完全不同的设计判断:

管道思维关心"消息有没有被处理",所以需要 ack——消费者处理完了要告诉系统,系统才能推进 offset。没有 ack,消息可能被重复投递,这是管道里的"bug"。

收件箱思维关心"消息有没有送达",ack 不是必须的——信送到收件箱就完成了,收件人什么时候读、读了做什么,不是邮件系统的责任。重复投递不是 bug,是 Agent 需要自己处理的现实,就像你可能收到两封内容相同的邮件。

管道思维里 offset 是全局资源,由系统统一管理,消费者组共享同一个推进进度。多个消费者要协调,要么广播要么竞争消费。

收件箱思维里 offset 是个人状态,每个 Agent 有自己的读取进度,互不干扰。queue name 就是"我是谁"的声明——同一个 mailbox,不同的 Agent 用不同的 queue name,各自独立推进,不需要创建额外的 consumer 对象。

管道思维的幂等是系统承诺,exactly-once 是一个让系统负责去重的承诺,消费者可以不关心。

收件箱思维的幂等是 Agent 能力,mq9 提供可选的 dedup_id 字段作为去重 key,Agent 自己决定要不要用。不强制,因为 Agent 有判断能力——就像你看到两封相同的邮件,自己决定回复一次还是两次。

理解这个区别,就能理解为什么 mq9 没有 ack、为什么幂等是可选的、为什么 queue name 一个参数就够了。这些不是功能缺失,是不同心智模型下的不同答案。

用管道的眼光看 mq9,会觉得它"不可靠"、"功能不全"。用收件箱的眼光看,它刚好够用,没有多余的复杂度。


为什么从消息队列的出发点思考 Agent 通信

这个问题值得正面回答,因为它不是偶然的。

我们从消息队列出发,不是因为惯性,而是因为 Agent 的通信需求和消息队列解决的问题,在结构上高度重叠。持久化、异步、解耦、多消费者——这些是消息队列的核心能力,也是 Agent 通信的核心需求。从消息队列出发,是一个合理的起点。

但 Agent 有几个特点,是消息队列从来没有认真对待过的。正是这些特点,让我们最终走向了不同的设计。

Agent 是短生命周期的。 传统消息队列假设消费者是长驻服务——Kafka consumer 一旦启动就持续消费,掉线是异常,需要 rebalance。但 Agent 天然短命:一个任务启动,完成,退出。下次启动可能是同一个 Agent,也可能是另一个实例,也可能是几小时后。消息队列没有为这种"随时可能消失,随时可能回来"的消费者设计过。mailbox 的 offline buffering 正是为此——Agent 不在的时候,信在那等着,回来了继续读,不需要系统感知 Agent 的存活状态。

Agent 天然是一对多的。 一条指令广播给所有相关 Agent,一个事件通知多个下游——这在 Agent 系统里是常态,不是特例。消息队列处理一对多很笨重:要么创建多个 consumer,要么用 topic 扇出,要么每个下游单独一个 queue。mq9 用 queue name 一个参数就区分了广播和共享订阅,Agent 开发者不需要理解背后的机制。

Agent 像人,不像服务。 这是最根本的差异。传统消息队列的消费者是无状态的处理单元,它没有身份,没有记忆,只要把消息处理完就行。Agent 有身份(mail_address),有记忆(mailbox 里的历史消息),有自主判断(收到重复消息自己决定怎么办)。这要求通信层不能只是一个数据管道,还要承载身份和状态。

Agent 需要简化通信,不是增加复杂度。 Agent 本身已经足够复杂——它要理解上下文、做决策、调用工具。通信层如果再引入 consumer group、ack policy、durable name 这些概念,认知负担是叠加的。mq9 的目标是让 Agent 用最少的概念完成通信:知道地址就能发,有 queue name 就能订阅,其他的协议来处理。

所以我们从消息队列出发,又走出了消息队列。不是否定消息队列,而是承认:为机器设计的管道,不适合直接用于为 Agent 设计的通信层。 需要的是同样扎实的基础能力,但完全不同的抽象模型。


这是一次关于 mq9 协议设计的完整讨论记录。每个问题背后都有原因,每个答案背后都有取舍。整理在这里,作为后续持续打磨协议的基础文档。


设计决策速览

话题决策核心理由
写入幂等性可选:header 中携带字符串 dedup_id 实现幂等去重;不带则允许重复写入能力内置不强制,幂等是业务层责任
ack 语义无 ack,不保证不重复投递push 是通知而非投递保证,可靠性由 mailbox 持久化 + pull/list 兜底;Agent 类比人的判断力
消费幂等header 带 dedup_id 发送侧去重;服务端透传 dedup_id,消费侧自主决策是否用于去重能力内置,不强制,两侧各自独立
多订阅者 offset以 queue name 为 key,各自独立一个参数解决 durable consumer 问题
优先级三级枚举 high / normal / low,优先级内严格有序枚举比数字直觉,跨优先级不乱序
优先级传递subject 后缀:.critical / .urgent,无后缀为 normal零协议改动,向下兼容
延时 / TTLheader:send_at + expire_at零额外概念,不填即普通消息
mail_address 生命周期mq9 只管分配,Agent 自决定固定还是变职责分离,mq9 不感知 Agent 状态
消息保留策略mailbox 级 TTL + 消息级 expire_at,两层独立灵活够用,粒度不同
消息大小上限可配置(如 10MB),超限直接拒绝,不分片分片复杂,大文件应走存储层
访问控制无 ACL,mail_address 即凭证,不可猜类比 Email 地址,安全靠不可猜
消息级 tag发送时 header 携带可选 tags 字段;list 时支持按 tag 过滤,服务端返回匹配子集公告板场景无法控制发件方;mailbox 消息积累后全量拉取成瓶颈
消息列表查询支持 tag 过滤参数;建议加 max_count 上限服务端过滤,向下兼容,不带参数行为不变
消息删除单条删除 by message_id,后续可扩展批量够用优先
mailbox 删除立即不可访问,消息跟随 TTL 自然回收不做级联删除,实现简单
订阅断开恢复有 queue name → offset 持久化;无 → 断连清除durable / ephemeral 自然区分
共享订阅无 queue name → 广播;同 queue name → 轮流分发一个参数覆盖两种语义
reply_to直接用 mail_address,mq9 不做额外处理不越权,地址变了是 Agent 的事
mail_address 格式{name},name 限小写字母+数字+点号命名空间统一,不可乱发明后缀
message_id 格式u64,mailbox 内唯一递增,服务端生成简单高效,mailbox 内唯一即满足需求
消息顺序三个优先级各自独立有序队列模型清晰,无需全局排序
mailbox 数量上限无上限,存储层扩展协议不承担存储问题
多租户隔离连接层实现,连接时识别租户,协议层零感知协议不带租户标识,Agent 无感知
消息广播给所有 mailbox当前不支持,属 admin 功能运维能力,不污染客户端协议
连接认证对齐 NATS:username/password、token、JWT、nkey复用 NATS 生态,学习成本低
流量控制无应用层控制,依赖 TCP 背压当前够用,高吞吐场景可扩展
public mailbox 发现创建时指定 public=true,服务端自动写入 public;大规模场景预留 tag 过滤扩展创建即注册,服务端自动维护,客户端只读
request-reply直接复用 NATS 原生语义不重复造轮子

协议问答

写入幂等性

为什么问: 分布式系统写入重复是常见问题,at-least-once 还是 exactly-once 是协议设计的根本分歧。

答案: 可选支持。发送时不带 dedup_id,mq9 服务端自己生成 message_id,多次发就是多条消息,语义和 Email 一样。发送时 header 带上 dedup_id,服务端做幂等去重——同一个 dedup_id 只存一条,后续重发直接返回原来的 message_id。调用方自己决定要不要幂等,不强制。

建议: 合理。exactly-once 在分布式系统里本质上不存在,所有声称做到的系统底层都是 at-least-once + 去重,去重的 key 还是业务自己提供。mq9 把能力内置进协议但不强制——不需要幂等的 Agent 直接发,需要的带上 dedup_id,两种用法各自干净,没有心智负担。


ack 语义

为什么问: ack 决定了可靠投递承诺。有 ack 才能做"消息不会丢"的保证,这通常被认为是进入生产的门槛。

答案: 没有 ack,没有 ack 的语义。不保证不重复投递。如果指定 queue name,那么一条消息只会投递一次,根据 offset 消费推进。如果指定新的 name,就会全量消费。也可以一次性获取所有消息。既有邮箱的语义,又有消息队列的语义。区别是不承诺不重复投递。

建议: 合理。"没有 ack 就不能进生产"这个结论是错的。

核心原因是 push 在 mq9 里是通知,不是投递保证。消息写入成功,就已经持久化在 mailbox 里了。push 只是告诉 Agent "有新消息",push 丢了顶多通知晚了,消息本身不会丢——Agent 随时可以通过 list 或 pull 拿到未读消息。可靠性由 mailbox 持久化 + pull/list 兜底来保证,不依赖 push 的可靠性。这和 JetStream 没有 ack 消息就可能真丢的场景根本不同。

引入 ack 反而有害:broker 要等 ack 超时重投,Agent 可能重复收到消息,制造了本来不存在的问题,还增加了双方的状态机复杂度。

邮件系统没有 ack,Email 是最成功的异步通信协议之一,在生产跑了几十年。Agent 代表人在行动,它应该有人一样的判断能力——收到重复消息,自己决定要不要处理。把幂等推给 Agent 不是偷懒,是正确的责任归属。


消费幂等

为什么问: mq9 不保证不重复投递,但 Agent 可能需要保证消费幂等。这个复杂度由谁承担,如何在协议层提供支持而不强制。

答案: 发送时可以在 header 里携带可选的 dedup_id 字段:

  • 不带:mq9 服务端自己生成 message_id,多次发就是多条消息,语义和 Email 一样
  • 带了:mq9 服务端做幂等去重,同一个 dedup_id 只存一条,后续重发直接返回原来的 message_id

消费侧收到消息时,服务端将 dedup_id 透传到消息 header,Agent 自己决定要不要用它做消费侧去重。两侧都是可选的,Agent 完全自主决定用不用,不强制。

建议: 这个设计非常干净。把幂等能力做进协议,但不强制使用——需要幂等的 Agent 在发送时 header 带上 dedup_id,服务端去重后透传给消费侧,消费侧用同一个 dedup_id 做去重判断,语义一致。dedup_id 建议用调用方自己生成的有意义 ID(如业务流水号),这样才能跨重试保持幂等语义。

dedup_id 的生命周期与消息绑定——消息过期或被删除,对应的 dedup_id 记录跟着失效。不需要单独管理 dedup_id 的 TTL,实现简单,语义一致。


多订阅者 offset 独立

为什么问: 多 Agent 订阅同一个 mailbox 时,各自进度是否独立,决定了 mq9 是不是真正的 Agent 原语,还是 NATS 的封装。

答案: queue name 作为 offset key,各自独立。不同 Agent 用不同的 queue name,各自维护自己的消费进度。

建议: 这是 mq9 最大的亮点,没有之一。NATS JetStream 里实现多消费者独立 offset 需要创建多个 durable consumer,每个 consumer 单独 track。mq9 用一个参数解决了这个问题,天然对应"收件箱"的心智模型:每个人订阅同一个公告栏,各自看各自的进度。


优先级设计

为什么问: 数字还是枚举,严格优先级还是加权调度,决定了 Agent 的使用体验和实现复杂度。

答案: 三级枚举:high / normal / low。优先级内严格按 offset 顺序,跨优先级 high 先于 normal 先于 low。

建议: 合理。枚举比数字直觉友好,Agent 用起来不需要理解调度算法。"优先级内有序"这个约束很重要,避免了同优先级消息乱序的问题。通过 subject 后缀传递:$mq9.AI.MAILBOX.MSG.{mail_address}.critical / .urgent,无后缀为 normal,零协议改动,向下兼容。


延时发送 / 消息 TTL

为什么问: Agent 场景有时效性需求——有些消息需要未来才生效,有些消息超时就没有意义了。

答案: 通过 header 传递两个字段:

  • send_at:消息可见时间,不填则立即可见
  • expire_at:消息失效时间,不填则永不失效

两个字段组合就是消息的有效窗口。

实现语义: send_at 由服务端暂存实现——消息发到服务端后立即持久化,但在 send_at 到达之前对任何人不可见(list 查询和消费侧 push/pull 均不返回),到时间后自动变为可投递状态。Agent 发完即可,不需要自己维护定时器,也不需要感知消息当前是否可见。

注意: 当前不支持取消暂存中的消息。消息一旦发出,在 send_at 到达前无法通过 list 查询到,因此也无法获取 message_id 来删除。如需取消延时消息,目前只能等消息可见后再删除,或通过 expire_at 设置一个合适的失效时间让其自动过期。

建议: 这是一个非常干净的设计。通过 header 传递,零额外概念,向下兼容。不填就是普通消息,填了就有额外能力。send_atexpire_at 一对,表达"消息在 T1 到 T2 之间有效",Agent 使用零负担。服务端暂存的实现让延时语义对 Agent 完全透明,发送和投递的时间解耦由协议承担。


mail_address 生命周期

为什么问: Agent 崩溃重启后地址是否固定,决定了"Agent 崩溃重启后继续工作"这个核心场景能否成立。

答案: mq9 只负责创建和分配地址,全局唯一。Agent 自己决定重启后用哪个地址——要固定就固定,要变就变。

建议: 合理。职责分离清晰。mq9 不管 Agent 的生命周期,只管地址存在期间的消息。这个设计让 mq9 不需要感知 Agent 的状态。


mailbox 消息保留策略

为什么问: 消息什么时候删,决定了存储成本和 Agent 离线多久还能收到消息。

答案: 消息跟随 mailbox 的生命周期。mailbox 创建时可以设置 TTL,可以永久,也可以设置某个时长。单条消息也支持独立的 expire_at(通过 header 传)。

建议: 合理。mailbox 级 TTL 控制整体,消息级 TTL 控制单条,两层独立,灵活够用。


消息大小限制

为什么问: Agent 场景可能传大文件、图片、长上下文,协议层需要明确边界。

答案: 支持大消息,上限可配置(如 10MB、30MB)。超过上限直接拒绝,返回错误。不支持分片传输。

建议: 合理。不分片是正确的选择——分片实现复杂,出错难以排查,而且 Agent 通信的消息不应该是超大 payload,真的需要传大文件应该用专门的存储层,mailbox 里只放引用。这一点可以在文档里说清楚。


访问控制

为什么问: mailbox 的安全性依赖什么——ACL,还是地址本身?

答案: 没有 ACL。知道 mail_address 就能发,就能订阅。mail_address 相当于密码,持有即授权。系统自动生成的地址({uuid})全局唯一不可猜;用户自定义的地址({name})是可读的,安全性依赖 name 本身不被泄露。

建议: 合理。类比 Email——知道地址就能发,协议极简,没有权限层。需要注意的是:自定义地址语义清晰但也意味着可预测,对安全敏感的 mailbox 应使用系统生成地址,避免被猜到。mail_address 泄露等于权限泄露,Agent 应妥善保管。


消息列表查询

为什么问: Agent 如何浏览 mailbox,需不需要服务端过滤,影响协议复杂度。

答案: 支持查看 mailbox 内的消息列表,当前不支持条件过滤,无分页。

建议: 建议加 max_count 上限(默认返回最新 N 条),避免 mailbox 消息积累多了全量拉取的性能问题。过滤条件由 Agent 自己做,协议不需要承担。这是"协议极简,能力够用"原则的体现。


消息删除

为什么问: 删除操作的粒度和原子性,影响 Agent 的使用体验。

答案: 当前支持单条删除,by message_id。后续可以扩展批量删除。

建议: 合理。单条够用,批量按需加。


mailbox 删除行为

为什么问: mailbox 删除后里面的消息是立即清除还是自然过期,影响实现复杂度。

答案: mailbox 删除后立即不可访问,消息跟随 mailbox TTL 自然过期清除,不需要立即回收。

建议: 合理。不做级联删除,实现简单,存储层自然回收。


订阅断开恢复

为什么问: Agent 重连后 offset 是否保留,决定了"Agent 离线后继续工作"这个场景能否成立。

答案: 指定 queue name 的订阅,offset 持久化保留,TTL 可配置(如 7 天)。重连后自动恢复投递进度。不指定 queue name 的订阅,断连就清除,重连后全量消费。

建议: 合理。durable 语义和 ephemeral 语义通过有没有 queue name 自然区分,Agent 不需要理解额外概念。


共享订阅

为什么问: 多 Agent 订阅同一个 mailbox 时,是广播还是负载均衡,两种语义都有用,需要明确区分。

答案: 不指定 queue name → 广播,每个 Agent 都收一份。指定相同 queue name → 共享订阅,轮流分发,同一条消息只投递一次。

建议: 这个设计极其优雅。一个参数覆盖了两种完全不同的语义,Agent 用起来直觉就懂。

优先级与共享订阅的交互: 多个 Agent 用同一个 queue name 共享订阅时,每次投递从当前最高优先级队列取一条,再轮转到下一个 Agent。即:优先级决定取哪条消息,轮转决定投给哪个 Agent,两者独立。结果是所有 Agent 都优先处理 high 消息,high 处理完再处理 normal,normal 处理完再处理 low,优先级语义在共享订阅下完整保留。


reply_to

为什么问: Agent 崩溃重启后 mail_address 可能变了,reply_to 的语义是否需要特殊处理。

答案: 直接用 mail_address,reply_to 复用 NATS 原有字段,mq9 不做额外处理。Agent 地址变了是 Agent 自己的问题。

建议: 合理。mq9 不越权,职责边界清晰。


mail_address 格式

为什么问: 地址格式决定了可读性、身份语义和扩展路径。

答案: 统一采用 {name} 格式,`` 是固定后缀,用户只需关心 @ 前面的部分。三种形态:

  • 用户自定义:{name},name 只能是小写字母 + 数字 + 点号,如 lobo.robustmqpayment.agent
  • 系统自动生成:{uuid},随机生成,不可猜
  • 公共邮箱:public,系统保留地址,用户创建会被拒绝

地址里不携带租户信息。租户是 broker 层的概念,mq9 协议不感知。Agent 连接时通过 header 或 account 确定租户,地址本身保持纯粹。public 是租户级的,每个租户各自独立,Agent 只能看到自己租户内的公开 mailbox。

建议: `` 固定后缀是正确的决策。命名空间统一在一个域下,不会出现用户各自发明后缀的混乱。用户只需关心前缀,认知负担最小。public 比之前的 mail@public 更直觉——主体在前,域在后,和 Email 完全一致。系统保留地址是固定的白名单,不靠命名规则区分,用户创建时服务端直接拦截。后续新增保留地址时同步更新白名单和文档即可。


message_id 格式

为什么问: 唯一性、可读性、可追溯性,三者如何平衡。

答案: 服务端生成,类型为 u64,mailbox 内唯一递增。同一 mailbox 内每条消息有唯一的 message_id,不同 mailbox 之间不保证唯一。简单高效,满足消息标识需求。

建议: 合理。u64 递增在 mailbox 粒度内唯一即够用,不需要全局唯一。实现简单,存储和比较成本低。


消息顺序保证

为什么问: 优先级插队和严格顺序之间有冲突,需要明确语义。

答案: 优先级内严格按 offset 顺序,跨优先级 high 先于 normal 先于 low。相当于三个独立的有序队列,投递时按优先级选队列。

建议: 合理。这个模型清晰,实现也直接——三个优先级各自一个队列,各自维护 offset,不需要全局排序。


mailbox 消息数量上限

为什么问: 消息无限积累会不会撑爆存储。

答案: 无上限,存储可以扩。

建议: 合理。存储层解决存储问题,协议层不需要承担。但建议在运维文档里说清楚存储扩容的方式,避免用户踩坑。


多租户隔离

为什么问: 多团队共用一个 mq9 集群时,mailbox 之间如何隔离。

答案: 已实现,在连接层解决。连接时从 header、account 等地方尝试获取租户信息,有则归属该租户,没有则归属默认租户。协议层不感知租户,每条消息不需要携带租户标识。同一租户内 mail_address 唯一,不同租户可以有同名地址,互不干扰。

建议: 这是正确的实现方式。连接层解决隔离,协议层零负担,Agent 使用时完全不需要感知自己属于哪个租户。默认租户和普通租户没有任何区别,只是系统初始化时预建好的一个租户,省去了单租户场景下手动建租户的步骤。其他租户需要在后台手动创建。


消息广播给所有 mailbox

为什么问: 系统通知场景,管理员需要通知所有 Agent。

答案: 当前没有,属于 broker 内部的管理功能,客户端协议不暴露。

建议: 合理。这是运维能力,不是协议原语。放在 admin API 里,不污染客户端协议。


连接认证

为什么问: 安全接入的方式影响部署复杂度和安全性。

答案: 对齐 NATS,支持 username/password、token、JWT、nkey,按需扩展。

建议: 合理。复用 NATS 生态,用户学习成本低。


流量控制

为什么问: Agent 处理慢时,mq9 无限推送可能压垮 Agent。

答案: 当前没有应用层流量控制,依赖 NATS TCP 背压自然限速。

建议: 当前够用。TCP 背压能处理大多数场景。如果后续有高吞吐场景,可以加应用层的 credits/窗口机制,按需扩展。


public mailbox 发现机制

为什么问: Agent 如何发现其他 Agent 提供的能力。

答案: public 是系统保留的发现地址。创建 mailbox 时指定 public=true,服务端自动将该 mailbox 写入 public,无需客户端额外操作。Agent 订阅或拉取 public 发现公开 mailbox。mailbox 删除后服务端自动清理对应记录,客户端无需感知。

建议: 极简设计,零额外概念。创建时指定 public=true 即注册,服务端自动写入并在 mailbox 删除后同步清理,客户端无需感知。public 是系统保留地址,每个租户各自独立,Agent 只能看到自己租户内的公开 mailbox。

大规模场景的预留扩展点:tag 过滤。 当一个租户里有几百个公开 mailbox,全量返回会成为性能瓶颈。预留的解法是 tag 机制:

  • 创建 mailbox 时可以带多个 tag,比如 payment-agent tags: [finance, payment, v2]
  • 订阅 public 时支持按 tag 过滤,服务端过滤后只返回匹配的子集
  • 不带 tag 的订阅行为和现在完全一样,向下兼容,渐进升级

tag 作为 header 字段传递,零协议改动。延伸方向:发消息时也可以指定 tag,只有订阅了该 tag 的 Agent 才收到,public 由此演变为一个轻量的 pub/sub 路由层。这个能力现阶段不实现,等发现机制在大规模场景下跑起来后再按需扩展。


消息级 tag 过滤

为什么问: 发消息时 header 携带 tag,list 时按 tag 过滤,是否值得支持?

答案: 支持。两个场景决定了这是必要的:

场景一:公告板类 mailbox。 public 或共享通知 mailbox,多个发件方往里写,收件方只关心特定类型。这种场景天然是"一个 mailbox 混收多类消息"——收件方无法控制别人往哪个 mailbox 发,多 mailbox 解决不了这个问题,服务端 tag 过滤是唯一干净的解法。

场景二:mailbox 消息积累多了之后的 list 查询。 全量拉取再客户端过滤,随着消息量增长会成为性能瓶颈,服务端过滤是必要的。

协议设计:

  • 发送时 header 携带可选的 tags 字段,值为字符串列表,如 tags: [finance, payment, v2]
  • list 时支持 tag 过滤参数,服务端返回包含该 tag 的消息子集
  • 不带 tag 的消息和不带过滤的 list 行为完全不变,向下兼容
  • 服务端写入时对 tag 建索引,list 时按 tag 扫描

建议: 隔离的首选粒度仍是 mailbox——能用多 mailbox 隔离的场景优先用多 mailbox,天然干净。tag 过滤用于无法控制发件方、或同一 mailbox 内消息量大需要精确查询的场景。两者互补,不互斥。


request-reply

为什么问: Agent 同步问答场景是否需要特殊支持。

答案: 直接复用 NATS 原生 request-reply 语义。

建议: 合理。不重复造轮子。


mailbox 之间如何通信

为什么问: 通信的基本单位是消息还是 mailbox,决定了协议有没有特例路径。

答案: 没有"直接发消息"的模式。要通信,只能发到对方的 mailbox,对方去读。对方如果在线实时读,就是同步通信;对方离线,消息在 mailbox 等着,就是异步通信。通信模式由对方是否在线决定,不由协议决定。

建议: 这个约束是对的。所有通信都经过 mailbox,协议行为统一,没有特例路径。"在线等于同步,离线等于异步"是自然涌现的,不是额外设计的。


一个 Agent 能有几个 mailbox

为什么问: Agent 是否可以按任务粒度创建 mailbox,决定了隔离粒度和生命周期管理的灵活度。

答案: 没有限制,Agent 可以按需创建。比如每次分发一个任务创建一个临时 mailbox,任务结束不再使用,系统按 TTL 自动回收。也可以有一个永久的"身份 mailbox",加上多个临时的"任务 mailbox",各自独立。

建议: 这个设计非常灵活,也非常符合 Agent 的短生命周期特点。每个任务一个 mailbox,天然隔离,不需要在消息里区分任务上下文。任务结束 mailbox 自动消失,没有遗留状态。


mailbox 创建方式

为什么问: 是否需要注册中心,决定了 Agent 的自主程度和部署复杂度。

答案: Agent 自己调用 create 接口创建,可以选择永久或临时(带 TTL)。没有"注册"的概念,没有中心化的地址分配,Agent 按需创建,按需使用。

建议: 合理。去中心化的创建方式让 Agent 完全自主,不依赖任何注册中心。永久和临时的区分刚好对应两种用法:身份用永久,任务用临时。


消息 payload 格式

为什么问: 协议层是否规定格式,决定了 mq9 和上层应用生态的耦合程度。

答案: 协议层不规定。但 A2A(Agent-to-Agent 协议)规定了消息格式,Agent 可以通过 mq9 发送 A2A 消息,生态自然闭环。mq9 是传输层,A2A 是应用层,两层各司其职。

建议: 这个分层非常清晰。mq9 不越权去规定应用层格式,A2A 负责语义约定,Agent 只要遵守 A2A 就能互通。这也意味着 mq9 不绑定任何特定的 Agent 框架,任何实现了 A2A 的 Agent 都可以用 mq9 通信。


消息状态查询

为什么问: 发件人能否知道消息被处理了,是 Agent 工作流里的常见需求,是否在协议层支持影响很大。

答案: 不引入。不定义"消息被读"的协议原语。Agent 如果需要确认对方处理完了,靠对方主动回消息,或者用 request-reply。

为什么不引入: "被读"在 Agent 场景里没有清晰的定义。拉取了但没处理、处理了一半崩了、处理完了没有后续动作——这三种在协议层无法区分。"读到"和"处理完"是两件事,"处理完"和"处理成功"又是两件事。要在协议层定义哪一个,每一个都会引入新的状态机和新的交互回合,mailbox 要跟踪每条消息对每个订阅者的状态,存储和实现复杂度都会上一个量级,而且一旦引入很难退回来。处理完的定义是业务语义,不是协议语义。Email 里这个问题的答案是回信,mq9 沿用同样的模式。

推荐模式: 需要确认处理完成,用 request-reply 或 B 主动往 A 的 mailbox 回一条消息。需要去重,Agent 自己用 dedup_id 做。


mailbox 权限粒度

为什么问: mail_address 即完全权限,有没有场景需要区分读权限和写权限,还是永远不需要?

答案: 当前没有细粒度权限。只要有 mail_address 就有完全权限——可读、可写、可删。设计语义是:消息是自然长出来的,天然信任持有地址的人。mail_address 可以认为是密码,持有即授权。引入权限系统会让协议变复杂,从一个通信原语变成一个应用平台,不是 mq9 想走的方向。

建议: 合理。这和 mail_address 即凭证的整体设计一脉相承。需要访问控制的场景,由上层应用自己管理 mail_address 的分发和保密,协议层不介入。


mailbox 地址能改名吗

为什么问: mail_address 作为对外公布的身份,能否随 Agent 职责变化而更新。

答案: 没有改名能力。mail_address 一旦创建就不可变更,无论是自定义地址还是系统生成地址。地址的语义由 PUBLIC.LIST 里注册时写的描述来承载,描述可以随时更新,地址本身保持稳定。

建议: 合理。地址不可变,才能作为稳定的身份锚点。需要调整语义,改描述就行,不需要改地址,也不影响已经知道这个地址的其他 Agent。


消息转发

为什么问: Agent 收到消息后转发给另一个 Agent,是否需要协议层支持,还是 Agent 自己处理。

答案: 协议层没有转发原语。这是 Agent 的活——Agent 收到消息,读取 payload,自己决定要不要转、转给谁、是否保留原始 sender 信息(写进新消息的 payload 或 header)。在当前语义下,Agent 完全可以实现这个能力。

建议: 合理。转发是业务语义,不是传输语义。协议层提供转发原语会引入"谁是真正的 sender"的歧义,让 mq9 承担不必要的复杂度。


mq9 和 A2A 的关系

为什么问: 两层协议是否真的单向依赖,A2A 会不会反过来对 mq9 提出新的协议要求。

答案: 当前是单向的——mq9 是传输层,A2A 是应用层,A2A 跑在 mq9 上面,mq9 不感知 A2A 的语义。未来随着两者结合加深,A2A 作为标准协议可能会对传输层提出新的要求,届时 mq9 可能需要针对性地演进。但现阶段边界清晰,不提前设计。

建议: 当前的单向分层是对的,不要提前耦合。等 A2A 结合场景真正跑起来,再看有哪些真实需求反推到传输层,届时有具体场景再设计,比现在猜测要准确得多。


多对一拓扑(聚合 mailbox)

为什么问: 任务收集、投票、结果聚合等场景,多个 Agent 往同一个 mailbox 写,一个消费者汇总处理,是否天然支持。

答案: 天然支持。多个 Agent 知道同一个 mail_address 就能往里发,消费者知道这个地址就能订阅读取。mailbox 本身没有"谁能写"的限制,知道地址即可操作,多对一拓扑不需要任何额外设计。

建议: 这是 mail_address 即凭证设计的自然红利。广播(一对多)、点对点(一对一)、聚合(多对一)三种拓扑全部由地址的分发范围决定,协议零感知。


聚合 mailbox 的消费顺序

为什么问: 多个 Agent 并发写同一个 mailbox,消费者看到的顺序是否确定。

答案: 按写入先后顺序,内核以递增 offset 记录,消费者看到的就是这个顺序。先到先得,确定性的。值得注意的是,offset 是内核实现的内部概念,不暴露在 mq9 协议层——Agent 看不到 offset,只看到消息的投递顺序。

建议: 这个设计干净。内部用 offset 保证顺序,外部不暴露 offset,Agent 不需要理解这个概念。顺序由写入时序自然决定,不需要协议层额外约定。


PUBLIC.LIST 的写入权限

为什么问: 任何人能不能往 PUBLIC.LIST 随意注册,决定了公开发现机制的可信度。

答案: 客户端不能直接往 PUBLIC.LIST 写。正确的方式是创建 mailbox 时指定 public=true,服务端自动将该 mailbox 写入 PUBLIC.LIST。Agent 对 PUBLIC.LIST 只有消费权限,没有写权限。省去了"先创建再注册"两步,创建即注册。

public 是租户级别的,每个租户各自有独立的 public,租户之间完全隔离。Agent 订阅 public 只能看到自己租户内的公开 mailbox,看不到其他租户的。这个行为由连接层的租户隔离天然保证,不需要协议层额外处理。

public 是系统保留地址,用户尝试创建同名 mailbox 会被直接拒绝。系统保留地址需要在协议文档里列出清单,让用户知道哪些地址不能用。

建议: 这个设计非常好。由服务端控制写入,客户端无法伪造注册,PUBLIC.LIST 里的记录天然可信——每一条都对应一个真实存在的 mailbox。mailbox 删除后服务端同步清理,失效记录问题一并解决。public 租户级隔离是必须的,这个语义必须在文档里明确说清楚。


mailbox 身份语义与地址命名

为什么问: 一个 Agent 可以有多个 mailbox,外部 Agent 如何结构化地找到它的主身份,而不是靠描述文字猜。

答案: 创建 mailbox 时允许自己指定地址,不强制系统自动生成。Agent 可以用有意义的名字作为自己的主地址(比如 payment-agent),其他临时 mailbox 用系统生成的 {uuid}。身份语义由地址命名约定来承载,协议不引入 type 或 role 字段,保持语义简单。

建议: 这个方向是对的。自定义地址让 Agent 的主身份一目了然,不需要额外的结构化字段。命名约定可以在 A2A 层或使用文档里说清楚,mq9 协议不需要感知。唯一性保证在租户内强制——同一租户内同名地址创建会报错,不同租户可以有同名地址,隔离边界清晰。


故障场景下的一致性模型

为什么问: 底层 NATS 集群出现网络分区时,mq9 的行为是优先可用性还是优先一致性,Agent 能依赖什么。

答案: 优先一致性,类似 Kafka 的数据一致性语义。写入成功即表示数据已持久化,不存在分区写入后数据分叉的情况。网络分区时写入会失败,不会在两个分区里各自积累造成 offset 冲突。Agent 可以信任"写入成功"这个确认。

建议: 这是正确的选择。CP 模型让 Agent 的行为可预期——要么写成功,要么明确失败,没有"可能成功可能没成功"的模糊状态。对于 Agent 通信这个场景,可预期比高可用更重要:Agent 收到失败可以重试,但收到一个虚假的成功却不知道,后果更难处理。


整体评价

定位

mq9 的定位非常清晰:Agent 异步通信的完整基础设施。协议极简,能力完整,复杂性由 mq9 自己承担,不转嫁给 Agent。

这个定位填的是一个真实的空白。现在的 Agent 框架要么用 MQ(太重、不是为 Agent 设计的),要么裸写异步逻辑(每个项目重造轮子),要么根本没有持久化通信能力。mq9 是第一个把"Agent 通信"作为一等公民来设计的基础设施。

优点

1. 抽象选得好。 mailbox 是唯一一个天然承载"身份 + 记忆 + 间歇在线"三个特征的抽象。mail_address 是身份,消息持久化是记忆,offline buffering + ordered replay 是间歇在线的处理方式。

2. queue name 的设计是点睛之笔。 一个参数,覆盖了广播、共享订阅、独立 offset 三种语义。没有额外概念,Agent 直觉就懂。

3. subject 后缀扩展优先级,header 扩展延时和 TTL,零协议改动。 普通消息直接发,需要优先级加 subject 后缀,需要延时加 header,向下兼容,渐进升级。

4. 协议极简,复杂性内置。 优先级调度、offset 管理、TTL 清理,这些复杂性全部在 mq9 内部消化,Agent 看到的接口极简。

5. Email 类比准确。 mail_address 即凭证,无 ACL,无幂等,可重发可删。这个心智模型对 Agent 开发者友好,没有消息队列的心理负担。

需要继续打磨的地方

1. 消息列表查询缺少 max_count。 当前全量返回,mailbox 消息多了是性能隐患。建议加一个默认上限(如最新 100 条),协议不需要分页,但要有上限保护。

2. 消息级 header 查询未支持。 大消息(10MB-30MB)场景下,Agent 无法先看 header 再决定要不要拉 payload。后续值得支持。

3. 协议文档缺少"这是设计选择,不是缺陷"的说明。 无 ack、无幂等、不保证不重复投递——这些如果不在文档里正面说清楚,用户会以为是没做完,而不是有意为之。这是影响 mq9 被采用的关键。

4. message_id 类型已定为 u64、mailbox 内唯一递增,需要在协议文档里正式写明。 写入幂等通过 header 中携带字符串类型 dedup_id 实现,两者是不同概念,需在文档里区分清楚,避免混淆。

5. public 的租户隔离语义需要在文档里显式说明。 每个租户各自有独立的 public,Agent 只能看到自己租户内的公开 mailbox,这个行为对用户来说不直觉,必须在文档里正面写清楚。

6. 系统保留地址清单需要随协议演进维护。 目前已知 public 是保留地址,后续会有更多固定的系统保留地址。每新增一个,协议文档里的保留地址列表同步更新。


这份文档作为协议设计的基准,后续每次打磨都可以回来对照。哪些问题解决了,哪些答案变了,都值得在这里更新。

🎉 既然都登录了 GitHub,不如顺手给我们点个 Star 吧!⭐ 你的支持是我们最大的动力 🚀