Block 开源多 Agent 通信平台 buzz 的仓库结构导读

2026-08-05

本文基于 buzz 仓库 commit 8342dfc(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/block/buzz 最新代码与文档为准。

这个仓库最值得抄的地方不是它做了什么,而是它把”协议内核”压到了一个不允许碰 I/O 的包里,其余全部外推成独立 crate——职责边界不靠口头约定,靠 Cargo.toml 的依赖表强制。 你读完 crates/buzz-core/Cargo.toml 最后那行注释,基本就知道这个团队在怕什么、又在保什么了。

先做个消歧:这里说的 buzz 是 Block 开源的多 Agent 通信平台(仓库 https://github.com/block/buzz,Apache-2.0,Copyright 2026 Block, Inc.),不是”热度""蜂鸣”那个普通英文词。下文出现的 buzz 一律指这个项目。

一、先说清楚它的底座:Nostr 意味着什么

如果你只做 AI 工程,没碰过去中心化协议,下面四个词先各给一句话:

  • 事件(event):Nostr 里一切内容的统一载体。ARCHITECTURE.md 写得很直白——一条聊天消息、一个表情回应、一步 workflow、一次画布更新、一次语音房事件,全都是同一种 JSON 结构,六个字段:idpubkeykindtagscontentsig
  • 签名:每条事件由作者的私钥用 Schnorr 算法签,id 是规范序列化后的 SHA-256。任何人拿到事件都能独立验真伪,不需要问服务器。
  • 中继(relay):存事件、转发事件的服务端。客户端通过 WebSocket 连上去,提交事件、订阅事件。
  • 密钥自持:身份就是一对 secp256k1 密钥,不是账号密码。没有”找回密码”这一说,私钥丢了,那个身份就没了。

再澄清一件常被套错的事:它不是区块链,也没有八卦传播(gossip,即节点之间互相扩散消息的那种拓扑)ARCHITECTURE.md 第一节的原话立场很硬——中继是唯一真相源,所有读写都过它,没有点对点交换、没有 gossip、没有复制。这跟你脑子里”去中心化 = 全网同步”的默认印象是反的,读代码前先把这个预设关掉。

kind 这个无符号整数是唯一的分发开关。中继按 kind 路由、存储、扇出;客户端按 kind 过滤订阅。所以在这个仓库里加功能的标准姿势是:先去 crates/buzz-core/src/kind.rs 定一个新 kind 常量,再在 buzz-relay 里实现处理——老客户端看不见新 kind,什么也不会坏。

站内已经写过几篇同类的仓库导读,分工不同:opencode 仓库结构 拆的是终端编码 Agent 的分层,hermes 仓库结构 拆的是自托管 Agent 怎么按”跑在哪个运行时里”切目录,Pascal Editor 仓库结构 拆的是 3D 编辑器用”谁不许知道谁”的禁止清单定边界、以及 MCP 那侧 Agent 有权改什么;这一篇拆的是”多 Agent 共处一张消息网络”时,包边界该怎么划。

二、根目录分两层:28 个 crate 加三个客户端

先建立地图。Cargo.toml 是个 Rust workspace,members 数组里 28 条指向 crates/ 下的包,另有一条 examples/countdown-botls crates/ 数出来正好 28 个目录,两边对得上。还有一条容易漏看的:

[workspace]
members = [
    "crates/buzz-relay",
    "crates/buzz-core",
    # ... 省略
]
exclude = ["desktop/src-tauri"]

桌面端的 Rust 侧被显式排除在 workspace 之外。AGENTS.md 的 Common Gotchas 第 5 条专门为此写了一句:仓库根跑 cargo test 不会跑到桌面端测试,得用 cargo test --manifest-path desktop/src-tauri/Cargo.toml。这类”排除”决定,看根 Cargo.toml 三十秒就能省掉半小时的困惑。

前端侧有三个面向用户的客户端目录,体量差得很远:desktop/(Tauri 2 + React 19,2359 个受版本控制的文件)、mobile/(Flutter,467 个)、web/(浏览器端仓库浏览器,由中继直接服务,65 个)。另有 admin-web/ 14 个文件的运维前端。全仓 3704 个受版本控制的文件里,crates/ 只占 413 个——这个仓库的绝大部分体积在客户端,不在 Rust 后端。数法很简单,git ls-files <目录> | wc -l,你自己复现一遍就知道我没编。

组成部分它负责什么对应仓库位置你什么时候会碰到它
协议内核事件类型、签名校验、过滤器匹配、kind 注册表crates/buzz-core/src/kind.rsevent.rsverification.rsfilter.rs加新事件类型、排查签名校验失败
中继服务WebSocket 服务器、订阅注册表、HTTP 桥、语音房、git smart HTTPcrates/buzz-relay/src/subscription.rshandlers/audio/api/排查事件为什么没扇出、端口暴露面评估
存储与检索Postgres 事件库、全文检索、哈希链审计crates/buzz-db/src/event.rschannel.rsworkflow.rspartition.rs)、crates/buzz-search/crates/buzz-audit/查历史消息、做合规审计
Agent 接入面把 @提及 桥接到 AI Agent、最小 ACP Agent、开发工具 MCP、人格包crates/buzz-acp/src/relay.rsqueue.rspool.rs)、crates/buzz-agent/crates/buzz-dev-mcp/src/crates/buzz-persona/接自己的 Agent、限制 Agent 能动什么
命令行Agent 优先的 buzz CLI、运维用 buzz-admincrates/buzz-cli/src/commands/crates/buzz-admin/让 Agent 读写工作区、管成员名单
Git 互操作用 Nostr 密钥签 git 对象、git 凭据助手crates/git-sign-nostr/crates/git-credential-nostr/想让提交记录挂在同一个身份上
规范与验证自定义 NIP 草案、TLA+/Tamarin 模型、一致性回放检查器docs/nips/docs/spec/MultiTenantRelay.tlacrates/buzz-conformance/想搞懂某个行为是”设计如此”还是”实现如此”

三、内核为什么必须小:buzz-core 的零 I/O 约束

crates/buzz-core/Cargo.toml 的依赖段全部走 workspace 继承,最后一行是一条大写注释:

[dependencies]
nostr      = { workspace = true }
serde      = { workspace = true }
uuid       = { workspace = true }
sha2       = { workspace = true }
# ……(其余十来条同样走 workspace 继承,此处省略)

# NO tokio, NO sqlx, NO redis, NO axum — zero I/O dependencies

这条注释不是口号,是一条能被 review 卡住的硬边界:内核里没有异步运行时、没有数据库驱动、没有 Redis 客户端、没有 HTTP 框架。ARCHITECTURE.md 把这层的定位写成”Zero I/O,其他每个 crate 的地基”,并明确列出它不做什么:不存事件、不发网络请求、不 spawn 任务、不依赖任何异步运行时。

好处在实际写代码时立刻能感到。签名校验函数长这样:

/// Verifies the event ID hash and Schnorr signature.
///
/// CPU-bound — call via `tokio::task::spawn_blocking` in async contexts.
pub fn verify_event(event: &Event) -> Result<(), VerificationError>

它是个纯函数——先校验 id 哈希,再校验签名,返回 Result。因为不碰运行时,所以它同时可以被中继(异步,包在 spawn_blocking 里)、CLI(同步)、测试客户端复用,也可以在单元测试里不起任何服务直接跑。文档注释还把”这是 CPU 密集的,别直接在 async 任务上调”写在了函数头上——这种把调用约束写进签名旁边的做法,比写在某篇 wiki 里靠谱得多。

上一层的依赖关系是一棵扁平的树:buzz-dbbuzz-authbuzz-pubsubbuzz-searchbuzz-auditbuzz-workflow 都只依赖 buzz-core,彼此之间不互相调用;buzz-relay 在最上面,把它们全部 import 进来编排。ARCHITECTURE.md 把这条说成关键架构原则并给了反例约束:buzz-workflow 从不调用 buzz-pubsubbuzz-search 从不调用 buzz-db,跨子系统的协调只发生在中继一层。

对你意味着什么:当你要判断”改这里会炸到哪”时,依赖方向就是答案。改 buzz-core 里的类型,全仓都要重编;改 buzz-search 的查询逻辑,影响面止步于中继的调用点。这也是为什么”内核尽量小”值得付出拆包的成本——小内核意味着高频变更区被挤到了叶子上。

四、外围各自成包:Agent 面、互操作面、规范面

Agent 相关的包切得特别细,值得单看:

  • buzz-acp:独立二进制,把中继事件桥接到 AI Agent,走 ACP(Agent Client Protocol,基于 stdio 的 JSON-RPC)。它拉起 1 到 32 个 Agent 子进程,用 NIP-42 认证连上中继,按频道排队 @提及 事件,每个频道同时最多一个 prompt 在飞。
  • buzz-agent:一个最小 ACP Agent,它的 README 自述是”非流式、不持久化、不耍聪明”。
  • buzz-dev-mcp:给 Agent 用的开发者 MCP 服务器,src/ 下摆着 shell.rsstr_replace.rsread_file.rsrg.rstree.rsview_image.rstodo.rs——shell 加文件编辑,一眼能看出 Agent 的能力边界在哪。
  • buzz-persona:人格包解析器,处理 .persona.md 文件。
  • sprig:把上面几个打成一个二进制。它的 main.rs 靠 argv0 分发——软链接名叫 buzz-acp 就跑 ACP harness,叫 buzz-agent 就跑 Agent,buzz-dev-mcp 自己还额外认 rgtreebuzzgit-credential-nostrgit-sign-nostr 这几个名字。这是发行体积与部署复杂度的一种取舍:装一个文件,摆几个软链接。

这种把 Agent 运行时、Agent 本体、Agent 工具三者拆开的做法,跟各家框架的取向差别不小,可以对照 Agent 协议生态对比 一起看。

互操作面是两个小工具 crate:git-sign-nostr 用 Nostr 密钥签 git 对象,git-credential-nostr 是 git 凭据助手。配套的规范草案在 docs/nips/NIP-GS.md,标题写的是 Git Object Signing with Nostr Keys。也就是说,同一把密钥既是聊天身份,也是提交签名身份。

规范面是这个仓库里比较少见的一块:docs/nips/ 下 15 份 NIP 草案 md 加 2 份 fixtures json,另有第 16 份藏在 crates/buzz-core/src/pairing/NIP-AB.md(设备配对)。挑几个看名字就懂的:NIP-RS.md 是跨设备已读状态同步,NIP-AA.md 是 Agent 认证,NIP-MP.md 是多仓库项目,NIP-PL.md 是推送租约。这里的 NIP 沿用的是 Nostr 生态写协议提案的那套文档格式:一份 md 把”某个行为该怎么约定”写成可被别的实现照着做的规范,buzz 把自己扩出来的部分也照这个格式落到 docs/nips/。文件头上带 draft optional 这类标记的都还是草案,不是既成事实。AGENTS.md 的 Key Patterns 一节也直接把上游 NIP 索引的地址列了出来,方便你对照哪些是通用约定、哪些是这个仓库自加的。

再往上是形式化验证——用数学模型把系统状态和迁移写出来,让工具穷举检查性质是否被违反,而不是靠测试用例撞运气。docs/spec/ 下有 MultiTenantRelay.tlaGitOnObjectStore.tla(TLA+)以及 MultiTenantAuth.spthy(Tamarin,做协议安全性证明),crates/buzz-core/src/pairing/NIP-AB.spthy 是配对协议那份。crates/buzz-conformance 的包描述写的是”MultiTenantRelay.tla 的运行时 trace schema 加独立回放检查器”——模型和实现之间有回放校验这一环。

值得学的是它的诚实度。docs/formal/STATEFUL_GATEWAY.md 在列完模型检查的八条性质之后,专门写了一段模型声称什么:不声称对推送服务商的恰好一次投递、不建模 PostgreSQL 实现细节、不覆盖尚未发布的中继匹配器与 worker。一份写清楚自己边界的验证文档,比一份只报”已验证”的可信得多。

五、边界与代价:这套切法放弃了什么

放弃了多写者拓扑。 中继是单一真相源这条约束换来的是简单——不用处理冲突合并、不用做因果序、不用担心分区脑裂。代价是中继本身是可用性瓶颈,也是审查点。这里有一处文档与代码的张力要如实说:ARCHITECTURE.md 明确写”没有 gossip、没有复制”,而 crates/buzz-relay-mesh 这个包确实存在,src/ 下有 gossip.rsmembership.rswire.rs,包描述写的是跨中继 QUIC mesh 的传输、成员管理与围栏化线路契约。ARCHITECTURE.md 的 crate 参考章节没有覆盖它。两边不一致时以代码为准,别拿文档当结论。

放弃了”文档即全貌”。 AGENTS.md 的 Repo Structure 列了 23 个 crate,实际目录里是 28 个——buzz-conformancebuzz-push-gatewaybuzz-voicebuzz-relay-meshbuzz-backend-kubernetes 这 5 个没进列表。同一份 ARCHITECTURE.md 里,ALL_KINDS 的数量在第 2 节写成 127、在第 6 节写成 80。这不是黑它——一个高频迭代的仓库,文档滞后是常态,只是你得知道以 crates/buzz-core/src/kind.rs 为准。

明确不管的事,仓库自己列了一张表。 ARCHITECTURE.md 第 9 节”Known Limitations”写的是已核实的实现缺口,不是设计愿景:限流没有实现(buzz-authRateLimiter trait 只有测试桩 AlwaysAllowRateLimiterRateLimitConfig 定义的四档只是设计目标);workflow 的审批门没打通端到端,跑到审批步骤的 run 会被标成失败;send_dmset_channel_topic 两个 workflow 动作在 schema 里但返回 NotImplemented;语音房的录制与分轨发布只留了 kind,没有生产者;没有 sqlx 离线查询缓存,SQL 不在编译期校验。把这类清单当成选型输入,别当成待办——它们是你今天会撞上的事实。

不适用的场景也清楚。 如果你要的是一个不需要自己运维的 SaaS 工作区、或者一个不需要密钥管理的轻量聊天层,这套东西的运维面会显著大于收益。

自建中继的暴露面要算清楚。 docker-compose.yml 起的本地栈里,Postgres(5432)存全部事件、频道、令牌、workflow 与审计链,events 表按月做范围分区;Redis(6379)承载 pub/sub 扇出、在线状态与正在输入指示;MinIO(9000 API、9001 控制台)作为 S3 兼容对象存储放媒体;Prometheus 在 9090;Adminer 在 8082,仓库标注仅供开发。也就是说,你自建之后,全量聊天内容与审计链落在你自己的 Postgres 里,媒体落在你自己的对象存储里,任何一个端口暴露到公网都等于把这些直接递出去。临时事件(kind 20000–29999)是例外,它们不落库也不进审计链。

Agent 的权限要单独算。 AGENTS.md 写明 BUZZ_RELAY_URLBUZZ_PRIVATE_KEYBUZZ_AUTH_TAG 由 ACP harness 自动注入被管理的 Agent 子进程——意思是那个子进程的环境变量里有一把能以该身份签名发消息的 Nostr 私钥。加上 buzz-dev-mcp 提供的 shell 与文件编辑工具,以及中继侧 /git/{owner}/{repo}/git-receive-pack 这个推送端点,一个接进来的 Agent 在最宽松配置下能发消息、能改本机文件、能推代码。密钥自持在这里是双刃的:Agent 有独立身份和独立审计线索,这很好;但私钥泄漏就等于身份被完整接管,也没有找回通道。怎么收紧,参考 最小权限设计 那套思路先把边界画出来再接。

六、上手与避坑清单

每条都写清楚为什么会踩,以及怎么避。

  1. 先激活 hermit 工具链再动手。 为什么会踩:仓库自带工具链管理,bin/ 下那 41 个文件是 hermit 的软链接,不激活的话你的全局 Rust/Node 版本可能对不上,而 git hook 会在你不知情的情况下失败。怎么避:按 AGENTS.md 的顺序来——. ./bin/activate-hermitcp .env.example .envjust setupjust relayAGENTS.md 还专门警告:别为了绕开 PATH 没配好而去改写 hook 命令。

  2. 查询一定带 kinds 为什么会踩:不带 kinds 的开放式查询会撞上中继的 p-gate 返回 403,而这个错看起来像权限问题,你会往认证方向查,方向就偏了。怎么避:任何 REQ 或 messages search 都显式带 kinds;AGENTS.md 给的最小集是 --kinds 9,45001,45003

  3. 频道范围用 h 标签,不是 e 标签。 为什么会踩:Nostr 通用习惯里 e 标签指向事件,容易顺手就用;但 buzz 用的是 NIP-29 的群组标签 h,用错了过滤器一条也匹配不上,还不报错。怎么避:频道内部的事件用 h 标签定位;描述频道本身的可寻址事件(kind:39000 元数据、kind:39001、kind:39002 成员)把频道 id 放在 d 标签里——get_channels 是从 kind:39002 的 d 标签解析用户频道的,不是从 h

  4. --format compact 是全局 flag,位置错了就不生效。 为什么会踩:绝大多数 CLI 把格式选项放子命令后面,肌肉记忆会写成 buzz channels list --format compact。怎么避:它必须写在子命令前面buzz --format compact channels list

  5. 别从文档抄 crate 清单和常量数字。 为什么会踩:上一节说过的两处不一致就是活例子。怎么避:crate 清单以 ls crates/ 和根 Cargo.tomlmembers 为准,kind 常量以 crates/buzz-core/src/kind.rs 为准,HTTP 端点以 crates/buzz-relay/src/router.rs 为准。

  6. 提交要带 -s 为什么会踩:仓库有 DCO 检查,缺 Signed-off-by 尾注的提交会让 PR 直接红。just hooks 装的 commit-msg 钩子会给你本地新建的提交补上,但 git rebasegit cherry-pick 不走那条路。怎么避:git commit -s 写进你的脚本;已经有未签名提交的分支用 git rebase --signoff main 修。

  7. 别按”这是 Rust 后端项目”的直觉安排时间。 为什么会踩:3704 个文件里 crates/ 只占 413,desktop/ 占 2359;你以为改一个功能是改 Rust,实际大头常在桌面端。怎么避:动手前先 git ls-files <目录> | wc -l 看一眼重量分布,再决定从哪头切入。


收个尾。这个仓库值得带走的不是它的功能清单,而是三条可迁移的判断:内核的约束要写进构建系统buzz-core 那行 NO tokio 的注释是能被 review 执行的,不是文档里的一句期望);同层子系统之间不许互相调用,编排只发生在顶层边界文档要写清楚自己不管什么ARCHITECTURE.md 每个 crate 章节都有 Does NOT 段,形式化验证文档也写了不声称什么)。

真要动手,建议的阅读顺序是:Cargo.toml 看 workspace 切法 → crates/buzz-core/Cargo.toml 看内核约束 → crates/buzz-core/src/kind.rs 看事件类型全集 → ARCHITECTURE.md 第 4 节看事件管线 → 第 9 节看已知缺口 → 最后回 AGENTS.md 的 Common Gotchas 对一遍你打算踩的坑。至于 README.md 和根目录那 8 份 VISION*.md,它们是项目对自己的定位与愿景表述,读的时候记住那是愿景不是现状,别拿它当已实现功能的清单。

本篇属于一个把开源多 Agent 通信平台 buzz逐层拆开讲的系列,整体地图见 buzz 是什么:Block 开源的多 Agent 通信平台全景图;沿着这条线往下,还可以看 Block 开源多 Agent 通信平台 buzz:人机共处一张消息网Block 开源 buzz:多 Agent 平台为何建在 Nostr 上

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。