Block 开源多 Agent 通信平台 buzz 的仓库结构导读
本文基于 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 结构,六个字段:id、pubkey、kind、tags、content、sig。 - 签名:每条事件由作者的私钥用 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-bot;ls 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.rs、event.rs、verification.rs、filter.rs) | 加新事件类型、排查签名校验失败 |
| 中继服务 | WebSocket 服务器、订阅注册表、HTTP 桥、语音房、git smart HTTP | crates/buzz-relay/src/(subscription.rs、handlers/、audio/、api/) | 排查事件为什么没扇出、端口暴露面评估 |
| 存储与检索 | Postgres 事件库、全文检索、哈希链审计 | crates/buzz-db/src/(event.rs、channel.rs、workflow.rs、partition.rs)、crates/buzz-search/、crates/buzz-audit/ | 查历史消息、做合规审计 |
| Agent 接入面 | 把 @提及 桥接到 AI Agent、最小 ACP Agent、开发工具 MCP、人格包 | crates/buzz-acp/src/(relay.rs、queue.rs、pool.rs)、crates/buzz-agent/、crates/buzz-dev-mcp/src/、crates/buzz-persona/ | 接自己的 Agent、限制 Agent 能动什么 |
| 命令行 | Agent 优先的 buzz CLI、运维用 buzz-admin | crates/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.tla、crates/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-db、buzz-auth、buzz-pubsub、buzz-search、buzz-audit、buzz-workflow 都只依赖 buzz-core,彼此之间不互相调用;buzz-relay 在最上面,把它们全部 import 进来编排。ARCHITECTURE.md 把这条说成关键架构原则并给了反例约束:buzz-workflow 从不调用 buzz-pubsub,buzz-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.rs、str_replace.rs、read_file.rs、rg.rs、tree.rs、view_image.rs、todo.rs——shell 加文件编辑,一眼能看出 Agent 的能力边界在哪。buzz-persona:人格包解析器,处理.persona.md文件。sprig:把上面几个打成一个二进制。它的main.rs靠 argv0 分发——软链接名叫buzz-acp就跑 ACP harness,叫buzz-agent就跑 Agent,buzz-dev-mcp自己还额外认rg、tree、buzz、git-credential-nostr、git-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.tla 和 GitOnObjectStore.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.rs、membership.rs、wire.rs,包描述写的是跨中继 QUIC mesh 的传输、成员管理与围栏化线路契约。ARCHITECTURE.md 的 crate 参考章节没有覆盖它。两边不一致时以代码为准,别拿文档当结论。
放弃了”文档即全貌”。 AGENTS.md 的 Repo Structure 列了 23 个 crate,实际目录里是 28 个——buzz-conformance、buzz-push-gateway、buzz-voice、buzz-relay-mesh、buzz-backend-kubernetes 这 5 个没进列表。同一份 ARCHITECTURE.md 里,ALL_KINDS 的数量在第 2 节写成 127、在第 6 节写成 80。这不是黑它——一个高频迭代的仓库,文档滞后是常态,只是你得知道以 crates/buzz-core/src/kind.rs 为准。
明确不管的事,仓库自己列了一张表。 ARCHITECTURE.md 第 9 节”Known Limitations”写的是已核实的实现缺口,不是设计愿景:限流没有实现(buzz-auth 里 RateLimiter trait 只有测试桩 AlwaysAllowRateLimiter,RateLimitConfig 定义的四档只是设计目标);workflow 的审批门没打通端到端,跑到审批步骤的 run 会被标成失败;send_dm 和 set_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_URL、BUZZ_PRIVATE_KEY、BUZZ_AUTH_TAG 由 ACP harness 自动注入被管理的 Agent 子进程——意思是那个子进程的环境变量里有一把能以该身份签名发消息的 Nostr 私钥。加上 buzz-dev-mcp 提供的 shell 与文件编辑工具,以及中继侧 /git/{owner}/{repo}/git-receive-pack 这个推送端点,一个接进来的 Agent 在最宽松配置下能发消息、能改本机文件、能推代码。密钥自持在这里是双刃的:Agent 有独立身份和独立审计线索,这很好;但私钥泄漏就等于身份被完整接管,也没有找回通道。怎么收紧,参考 最小权限设计 那套思路先把边界画出来再接。
六、上手与避坑清单
每条都写清楚为什么会踩,以及怎么避。
-
先激活 hermit 工具链再动手。 为什么会踩:仓库自带工具链管理,
bin/下那 41 个文件是 hermit 的软链接,不激活的话你的全局 Rust/Node 版本可能对不上,而 git hook 会在你不知情的情况下失败。怎么避:按AGENTS.md的顺序来——. ./bin/activate-hermit,cp .env.example .env,just setup,just relay。AGENTS.md还专门警告:别为了绕开 PATH 没配好而去改写 hook 命令。 -
查询一定带
kinds。 为什么会踩:不带 kinds 的开放式查询会撞上中继的 p-gate 返回 403,而这个错看起来像权限问题,你会往认证方向查,方向就偏了。怎么避:任何 REQ 或messages search都显式带 kinds;AGENTS.md给的最小集是--kinds 9,45001,45003。 -
频道范围用
h标签,不是e标签。 为什么会踩:Nostr 通用习惯里e标签指向事件,容易顺手就用;但 buzz 用的是 NIP-29 的群组标签h,用错了过滤器一条也匹配不上,还不报错。怎么避:频道内部的事件用h标签定位;描述频道本身的可寻址事件(kind:39000 元数据、kind:39001、kind:39002 成员)把频道 id 放在d标签里——get_channels是从 kind:39002 的d标签解析用户频道的,不是从h。 -
--format compact是全局 flag,位置错了就不生效。 为什么会踩:绝大多数 CLI 把格式选项放子命令后面,肌肉记忆会写成buzz channels list --format compact。怎么避:它必须写在子命令前面:buzz --format compact channels list。 -
别从文档抄 crate 清单和常量数字。 为什么会踩:上一节说过的两处不一致就是活例子。怎么避:crate 清单以
ls crates/和根Cargo.toml的members为准,kind 常量以crates/buzz-core/src/kind.rs为准,HTTP 端点以crates/buzz-relay/src/router.rs为准。 -
提交要带
-s。 为什么会踩:仓库有 DCO 检查,缺Signed-off-by尾注的提交会让 PR 直接红。just hooks装的 commit-msg 钩子会给你本地新建的提交补上,但git rebase和git cherry-pick不走那条路。怎么避:git commit -s写进你的脚本;已经有未签名提交的分支用git rebase --signoff main修。 -
别按”这是 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 上。