Block 开源多 Agent 通信平台 buzz 的四层排障顺序
本文基于 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.rs、docs/linux-rendering-troubleshooting.md | Linux 上启动后白屏、透明窗口、直接 abort |
| 客户端到中继 | 签名事件的提交与查询 | crates/buzz-relay/,冒烟步骤见 TESTING.md | 发消息报错、端口占用、鉴权失败 |
| 中继到 Agent | 事件筛选、作者门禁、进程调度 | crates/buzz-acp/(README 与 src/base_prompt.md) | Agent 完全不回、启动即闲置、回复刷屏 |
| 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=1 和 WEBKIT_DISABLE_COMPOSITING_MODE=1,且只对本次启动生效,不会被记住:
./Buzz_*.AppImage --safe-rendering
# or for a native install:
buzz-desktop --safe-rendering
这里有个容易踩的设计细节:如果你自己在环境里设了 WebKit 相关变量,又同时传了 --safe-rendering,buzz 会拒绝启动并打印究竟是哪个变量冲突。源码里把这个策略写成了不变式——模块只会写 WEBKIT_DISABLE_DMABUF_RENDERER 和 WEBKIT_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-acp 要 ws:// 而不是 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_URL、REDIS_URL 指定)。本地模式下要求鉴权令牌的开关默认是关的,启动日志会为此打一条 WARN——那是给本地测试用的,别原样搬上公网。还有一个容易忽略的破坏性细节:开发栈和桌面端共用同名容器与默认端口,仓库明确警告 just reset 会连桌面端的数据一起抹掉。
Agent 的权限边界是身份,不是开关。 README 把这一点当卖点讲:Agent 有自己的密钥、自己的频道成员资格、自己的审计轨迹,按身份限定范围,就像你限定一个同事一样;同时它能做的事包括打开仓库、提交补丁、跑工作流。这两句话要合起来读——你给一副私钥,就是给一个队友级别的权限面,收回的唯一手段是移除成员资格或换掉这副密钥。
根目录那八份以 VISION 开头的文档写的是愿景,不是已实现功能。 排障时不要拿它们当行为规范去对照,会得出错误结论。
七、上手与避坑清单
- 别从 Agent 日志开始查。 会踩是因为症状(没回复)看起来最像 Agent 的问题。避法是按第一节那张表自上而下:先证明窗口渲染正常,再跑一遍冒烟发收,最后才看 Agent。
- 别用界面上的计数判断消息到没到。 会踩是因为徽标和面板走的是两条独立的路,徽标是中继的统计。避法是回到
buzz messages get或buzz messages thread去看真实事件。 - 开工前先
unset上一轮的环境变量。 会踩是因为BUZZ_AUTH_TAG、BUZZ_RELAY_URL、BUZZ_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:从拉仓库到跑通中继。