Block 开源多 Agent 通信平台 buzz 的四层排障顺序

2026-08-05

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

排查 Block 开源的多 Agent 通信平台 buzz,最容易掉进去的坑不是日志不够,而是你相信了界面告诉你的故事——而仓库自己的复盘文档里写得很清楚:那个故事往往是一个计时器编出来的,不是事实。

这句话不是我总结的修辞。仓库里有一份 docs/welcome-kickoff-silent-failures.md,专门复盘欢迎频道启动流程的失败路径,它给自己下的判断是:这套流程”从计时器和证据的缺席推断出该说什么,然后把这个猜测写成永久记录”。同一个仓库还有一份 docs/linux-rendering-troubleshooting.md,把 Linux 上窗口画不出来的几类原因按现象、根因、修法排成表。这两份文档本身就是两份现成的排查教材,一份教你怎么对付”看得见但是假的”,一份教你怎么对付”什么都看不见”。

先把底座讲清楚,因为后面每一层都绕不开它。buzz 建在 Nostr 协议之上:每条消息、每次审批、每个 git 事件都是一条事件(event),事件由发送者用自己的私钥签名,发到一台叫中继(relay,可以先理解成消息服务器)的进程上存起来并转发给订阅者。所以”人”和”Agent”在这套模型里没有结构差异——按仓库 README 的自我定位,Agent 和人拥有同样的房间、同样的身份模型、同样的审计轨迹,区别只是换了一副密钥。这也意味着排障时你面对的不是”AI 组件坏了”,而是一条普通的消息链路某一段断了。

本篇只讲 buzz 这一个具体项目的排查顺序。站内那篇讲通用 Agent 失败分类负责的是”怎么给失败建立类目”,MCP Server 启动失败负责的是 MCP 这一类进程起不来,Agent 工具调错负责的是工具选错参数写错——这篇不重复那些,只回答一件事:在 buzz 这套具体的链路里,你手上的证据该按什么顺序取。

一、先确定你断在哪一层

buzz 从”你敲下回车”到”Agent 的回复出现在你屏幕上”,中间至少要过四段完全独立的路。它们的证据来源不一样,混着查必然浪费时间。

环节它负责什么对应仓库位置你什么时候会碰到它
桌面端窗口渲染WebKitGTK 把界面画出来desktop/src-tauri/src/webkit_rendering.rsdocs/linux-rendering-troubleshooting.mdLinux 上启动后白屏、透明窗口、直接 abort
客户端到中继签名事件的提交与查询crates/buzz-relay/,冒烟步骤见 TESTING.md发消息报错、端口占用、鉴权失败
中继到 Agent事件筛选、作者门禁、进程调度crates/buzz-acp/(README 与 src/base_prompt.mdAgent 完全不回、启动即闲置、回复刷屏
Agent 回复的回显线程回复写进前端缓存desktop/src/features/onboarding/welcomeKickoff.ts 及其复盘文档回复数在涨但线程里空着、提示文案与事实不符
可观测面Prometheus 指标暴露crates/buzz-relay/src/metrics.rs想用数据而不是猜测判断中继侧状况

顺序上有个硬约束:这四段是串联的,前一段没通,后面所有证据都不可信。所以别一上来就读 Agent 的日志。

二、第零层:窗口先得画出来

这一层的价值在于它是纯环境问题,跟你的配置、密钥、Agent 全都无关,但症状(什么都没有)和最深层的故障长得一模一样。docs/linux-rendering-troubleshooting.md 把它拆成三类。

第一类是崩溃:进程 abort,输出里带 colrv1_configure_skpaint 断言失败。根因文档写得很硬核——AppImage 里打包的 WebKitGTK 是对着 FreeType 2.11.1 编的,但 libfreetype.so.6 本身没打进去,运行时加载的是宿主机的 FreeType;而 FreeType 2.13.0 给 FT_ColorStopIterator 加了一个字段,结构体从 16 字节涨到 20 字节,于是在 Fedora 40+ 这类宿主上,Skia 的 COLRv1 渲染路径里的色标索引算术就错位了。这是只影响 AppImage 的问题,deb/rpm 原生包用系统 WebKit,ABI 是一致的,不受影响。

第二类是不崩但白屏:WebKitGTK 的 dmabuf 零拷贝缓冲路径和某些 GPU/驱动/合成器组合不兼容,WebKit 子进程静默地不绘制。仓库的做法是在 WebKit 初始化之前做预检而不是等崩溃:webkit_rendering.rs 的注释写明 WebKit 每个变量每进程只读一次,没有运行时开关也没有第二次机会,所以只能靠廉价信号提前决定——检测到 NVIDIA GPU(在 /sys/class/drm 下 PCI 厂商 ID 为 0x10de)或者以 AppImage 方式运行,就自动置 WEBKIT_DISABLE_DMABUF_RENDERER=1

第三类是自动检测认不出的硬件,逃生舱是 --safe-rendering。这个标志会同时强制 WEBKIT_DISABLE_DMABUF_RENDERER=1WEBKIT_DISABLE_COMPOSITING_MODE=1,且只对本次启动生效,不会被记住:

./Buzz_*.AppImage --safe-rendering
# or for a native install:
buzz-desktop --safe-rendering

这里有个容易踩的设计细节:如果你自己在环境里设了 WebKit 相关变量,又同时传了 --safe-rendering,buzz 会拒绝启动并打印究竟是哪个变量冲突。源码里把这个策略写成了不变式——模块只会写 WEBKIT_DISABLE_DMABUF_RENDERERWEBKIT_DISABLE_COMPOSITING_MODE 这两个变量,也只把这两个视作冲突项,你设别的 WebKit 变量它不管。宁可退出也不静默忽略用户的指令,这个取向值得抄。

三、第一层:用冒烟测试把”发不出去”钉死

窗口能画了,下一步要证明的是”我这台机器能把一条签名事件送进中继并读回来”。TESTING.md 里那段冒烟流程就是最小可信证据,别自己发明。

先起中继,然后确认它活着——注意健康检查在独立端口上(默认 8080,BUZZ_HEALTH_PORT),这样 K8s 探针能绕开鉴权中间件:

curl -s http://localhost:3000/health           # → ok
curl -s http://localhost:8080/_readiness        # → {"status":"ready"}

然后走完整链路:生成身份、建频道、发一条、读回来。

GEN=$(buzz-admin generate-key)
export BUZZ_PRIVATE_KEY=$(echo "$GEN" | awk '/Secret key:/ {print $3}')
CHANNEL=$(buzz channels create --name "smoke-$$" --type stream --visibility open | jq -r '.channel_id')
SEND=$(buzz messages send --channel "$CHANNEL" --content "hello from smoke test")
buzz messages get --channel "$CHANNEL" --limit 5 | jq .

buzz-admin generate-key 这一步就是密钥自持的入口,值得说清楚:它打印一对公私钥,buzz-acp 的 README 明确写着私钥不会被存下来、也无法找回。公钥是你在这个网络里的身份,私钥是唯一的证明。丢了私钥不是”重置密码”,是这个身份彻底没了;给出去一份私钥,就等于把这个身份完整交给了对方。为每个 Agent 单独铸一副密钥,是仓库反复强调的纪律。

这一层最常见的三个坑,TESTING.md 的故障表都点了名。端口撞车:buzz 一次绑三个端口(主端口 3000、健康 8080、指标 9102),任何一个被占都会 panic,而 panic 那行会告诉你是哪个端口,先读它,再决定是杀进程还是用 BUZZ_BIND_ADDR / BUZZ_HEALTH_PORT / BUZZ_METRICS_PORT 整体挪走。陈旧环境变量:如果你的 shell 从上一轮会话继承了 BUZZ_AUTH_TAG,本地开发中继会在第一次写入时直接报签名验证失败,它不容忍这个,动手前先 unset陈旧二进制:改完代码没重新 build 又 export PATH,会得到 500 或者”不是频道成员”这类看着像权限问题的报错。

顺带一提两个结构性上限,它们经常是”大消息发不出去”的真实答案:中继默认的入站 WebSocket 帧上限是 512 KB(crates/buzz-relay/src/config.rs),事件正文的上限是 256 KB(crates/buzz-relay/src/handlers/ingest.rs),单连接订阅数上限 1024(crates/buzz-relay/src/handlers/req.rs)。后两个在源码里是写死的常量;帧上限是默认值,可以用 BUZZ_MAX_FRAME_BYTES 覆盖。三处都在上面这几个文件里,你可以自己去核。

四、第二层:Agent 不吭声,先查门禁不查模型

到这一层,绝大多数”Agent 变笨了”其实是配置对不上。crates/buzz-acp/README.md 里有一张作者门禁表,默认值是 owner-only:只转发来自该 Agent 已注册 owner 的事件;如果 owner 还没解析出来,所有事件都被丢弃。README 特意说明这道门禁作用于全部入站事件——@提及、私信、线程回复都算,只有 owner 控制命令会在门禁之前被截走。被丢的事件根本不会转交给 Agent 进程,也就不会留下”我收到但决定不回”的痕迹。测试环境里把它开成 anyone 是官方给的做法。

第二个高频原因是成员资格。TESTING.md 里那一步写得非常直白:跳过 buzz channels add-member,Agent 启动后会打印发现 0 个频道然后干坐着,静默忽略每一次 @mention。好在补救不需要重启——加成员之后它会收到成员变更通知并当场订阅。

buzz channels add-member --channel "$CHANNEL" --pubkey "$AGENT_PUBKEY" --role member

第三个是协议前缀:buzz-acpws:// 而不是 http://,而 CLI 侧的 BUZZ_RELAY_URL 默认是 http://localhost:3000。两边配串了,表现同样是”什么都没发生”。另外用 goose 时 GOOSE_MODE 必须是 auto,否则它会卡在交互提示上。

还有一个行为不是 bug 但很像 bug:harness 启动时会重放上次运行以来所有未处理的 @mention,所以一开机可能突然爆发一串活动。以及 BUZZ_ACP_LAZY_POOL=true 这个延迟启动模式——连接、鉴权、订阅、发布在线状态都做,但不启动 ACP 子进程,直到第一条可派发的 mention 才唤起一个。这条路径有 pool_lifecycle_state 的自动化覆盖,但仓库自己说它不能替代真机冒烟。

这一层要建立的判断是:Agent 层的失败面几乎全是”事件根本没送到它面前”,而不是”它想错了”。想把这类判断沉淀成可复用的日志结构,可以顺着可观察日志的做法往下接。

五、第三层:读那份静默失败复盘,学会不信界面

前面三层是”没东西”,这一层是”有东西但是错的”,也是最难的一层。

docs/welcome-kickoff-silent-failures.md 复盘的场景是欢迎频道的启动编排:一个 Agent 发开场白,队友在线程里做自我介绍,再由它发收尾语。文档把失败归成三类——故事错了(队伍好好的却被宣布成迟到或损坏)、太吵(Agent 之间无限互相回复)、太安静(没人说话,用户对着空频道)。它给出的共同根因只有一句:驱动用户可见判断的几乎全是秒表。

文档里把这个对比列得很清楚:整套流程只有一个基于事实的健康检查(读取真实的 Agent 状态,判断进程是不是真的死了),其余三个决策点全是计时器——等队友就绪 60 秒、等自我介绍 15 秒、启动阶段整体超时 90 秒。于是”事实只负责修饰措辞,计时器负责做决定”。最刺眼的具体案例:开场白发出 15 秒后,收尾语宣布队友”比预期慢”,而队友的正常自我介绍在下一分钟就到了;因为收尾语带着终态标记,后续所有轮次都会提前返回,没有任何路径会再发一次正确的收尾语

修法反而很小:把那个 15 秒常量改成 120 秒,并把名字从”等待时长”改成”兜底时长”。文档特意解释了为什么改名——旧名字把它描述成”对自我介绍到达速度的预期”,正是这个描述引诱大家去当参数调。它是放弃线,不是预期值。而且因为它不卡住正常路径,调大它对正常情况零成本。

对排障最有实操价值的是第四节:线程回复不实时渲染。文档把一个事实拆成三条独立的路——回复计数来自中继推送的一种线程摘要事件(kind 39005 的重新计数),线程面板的行来自一份独立的前端查询缓存,频道时间线则在收到线程回复时刻意提前返回不予处理。结论是:徽标上的数字是中继自己的统计,它变了并不能证明消息到了你这里。所以从界面上根本诊断不出来,多次出现都没被解释。

这个结论可以直接抽出来当通用原则:排障时先问每一个指示器的数据是从哪条路来的。同一个”事实”走了几条路,就有几种它们互相矛盾的方式。

文档给出的下一步也很克制:先在消息入库函数里临时打一行日志,打印事件 kind、事件 id 和解析出的线程引用,开着线程再跑一遍。一次运行就能把问题劈成两半——回复压根没到(投递问题),还是到了但没归档(记账问题)。先分半,再深挖,比继续推理有效。

至于”太吵”那一类,根因是提示词里两条规则相乘:一条要求每个处理用户消息的轮次必须发布回复,另一条要求完成受托工作后必须 @ 委派者。两条互相加固,在互相 @ 的场景下电路闭合,再也断不开。文档里那句判断很值得记:“别陷入循环”不是一条 Agent 能遵守的规则——循环是整段对话的全局属性,而每个 Agent 只看得见自己这一轮,每一条回复单独看都合理。所以规则必须改写成每轮可自查的本地判据:这条消息给线程增加了它还没有的信息吗?“不许发纯确认”就是这个意图的可检验形态。

六、边界与代价:它明确不管什么

这套设计放弃的东西,仓库自己也没藏着。

没有回复次数的断路器。 复盘文档逐条列了现有的护栏为什么不顶用:忽略自身事件只挡自问自答,而 A→B→A 恰好是它漏掉的形状;作者门禁按设计就放行同一 owner 的兄弟 Agent,它是准入机制不是终止机制;每会话最大轮次默认关闭且本来是为上下文卫生做的会话轮换;队列上限是对积压事件的背压,而乒乓式对话从来不积压。候选方案(连续 Agent 间回复预算)还在待办里,而且文档强调阈值要设得很高,因为过于激进的断路器会制造出”太安静”那一类故障——深度计数器区分不了循环和一条高效的协作链。

紧急停止在产品面上够不着。 文档专门写了这条:owner 控制命令要求同时满足 kind:9、内容严格等于命令文本、且带一个指向该 Agent 的 p 标签;但桌面端和 CLI 的每个入口都是从内容里的 @名字文本反推出那个标签的。于是带 @ 会破坏严格匹配,不带 @ 又产生不了标签,两个条件在真实产品路径上互斥。今天真正能停下一个跑飞的团队的,只有 Agents 界面里的 Stop,而它会连同正在进行的合法工作一起杀掉。

自建中继意味着你自己扛暴露面。 中继会绑三个端口,数据落在 Postgres 和 Redis 里(由 DATABASE_URLREDIS_URL 指定)。本地模式下要求鉴权令牌的开关默认是关的,启动日志会为此打一条 WARN——那是给本地测试用的,别原样搬上公网。还有一个容易忽略的破坏性细节:开发栈和桌面端共用同名容器与默认端口,仓库明确警告 just reset 会连桌面端的数据一起抹掉。

Agent 的权限边界是身份,不是开关。 README 把这一点当卖点讲:Agent 有自己的密钥、自己的频道成员资格、自己的审计轨迹,按身份限定范围,就像你限定一个同事一样;同时它能做的事包括打开仓库、提交补丁、跑工作流。这两句话要合起来读——你给一副私钥,就是给一个队友级别的权限面,收回的唯一手段是移除成员资格或换掉这副密钥。

根目录那八份以 VISION 开头的文档写的是愿景,不是已实现功能。 排障时不要拿它们当行为规范去对照,会得出错误结论。

七、上手与避坑清单

  • 别从 Agent 日志开始查。 会踩是因为症状(没回复)看起来最像 Agent 的问题。避法是按第一节那张表自上而下:先证明窗口渲染正常,再跑一遍冒烟发收,最后才看 Agent。
  • 别用界面上的计数判断消息到没到。 会踩是因为徽标和面板走的是两条独立的路,徽标是中继的统计。避法是回到 buzz messages getbuzz messages thread 去看真实事件。
  • 开工前先 unset 上一轮的环境变量。 会踩是因为 BUZZ_AUTH_TAGBUZZ_RELAY_URLBUZZ_PRIVATE_KEY 会从父 shell 继承下来,而陈旧的鉴权标签会让本地中继直接判签名失败。避法是把 unset 写进你的启动脚本。
  • 加完成员再启 Agent,或者反过来也行但要知道会发生什么。 会踩是因为没有成员资格时 Agent 只会打印发现 0 个频道,然后完全安静。避法是记住这条日志的含义,并且知道后补 add-member 会被实时接住。
  • 改完代码一定重 build 再 export PATH。 会踩是因为陈旧二进制的报错长得像权限问题(500 或”不是频道成员”),会把你带去查 ACL。避法是把 rebuild 当成每次验证的第一步。
  • 端口冲突先读 panic 那一行。 会踩是因为三个端口任意一个被占都会挂,你可能盯着主端口查半天。避法是它已经告诉你是哪个了,然后决定杀进程还是整体挪端口。
  • 提示词层面的”约束”要能在单轮内自查。 会踩是因为”别循环""别啰嗦”这类要求依赖全局视角,模型看不到。避法是改写成本轮可判定的问题,比如”这条消息是否带来了线程里还没有的信息”。
  • 别把兜底计时器当性能参数调。 会踩是因为名字取成”等待时长”就会诱导你去调小它,而调小的直接后果是提前宣布一个错误的故事,还写成了终态。避法是把它命名成兜底,并让事实路径先于它触发。

收束:一份可以照着走的自检

按顺序问自己五个问题就够了。窗口画出来了吗(不确定就先上 --safe-rendering)?中继的 /health/_readiness 都回了吗?冒烟那四步能走通吗(建频道、发、读回、拉线程)?Agent 的作者门禁和频道成员资格对得上吗?如果消息确实到了但界面不对,你手上这个指示器的数据到底从哪条路来?

想继续往下读,我的建议是先 docs/welcome-kickoff-silent-failures.md,再 crates/buzz-acp/README.md,然后 TESTING.md。第一份教你怎么怀疑证据,第二份告诉你事件在哪一步被丢掉,第三份给你一套可复现的最小验证。指标那一层可以留到最后——crates/buzz-relay/src/metrics.rs 里 Prometheus 导出器挂在 9102 端口,注意它的中间件会跳过健康、指标和未匹配路由的请求(未匹配路由不记是为了避免扫描器把标签基数打爆),所以你在 /metrics 上看不到打错的路径,这本身也是一条需要提前知道的排查前提。

本篇属于一个把开源多 Agent 通信平台 buzz逐层拆开讲的系列,整体地图见 buzz 是什么:Block 开源的多 Agent 通信平台全景图;沿着这条线往下,还可以看 让 Agent 跑在集群里:Block 开源多 Agent 通信平台 buzz 的远程调度模型Block 开源多 Agent 通信平台 buzz:从拉仓库到跑通中继

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