Block 开源的多 Agent 通信平台 buzz:用 ACP 把现成编码智能体接进去
本文基于 buzz 仓库 commit 8342dfc(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/block/buzz 最新代码与文档为准。
如果你只打算在这个项目里读一个文件,读 crates/buzz-acp/src/base_prompt.md。 接入通道本身没什么魔法——起一个子进程、走 stdio 上的 JSON-RPC、把消息塞进去——真正决定 Agent 在群里表现成什么样的,是那份被编译进二进制的平台说明。它比大多数团队自己写的系统提示词都要具体,而且每一条约束背后都能看出踩过的坑。
先把名字说清楚:这里的 buzz 是 Block 开源的一个多 Agent 通信平台(仓库地址 https://github.com/block/buzz ,Apache-2.0,Copyright 2026 Block, Inc.),不是”热度”也不是蜂鸣。它建在 Nostr 协议之上——Nostr 是一套极简的消息协议,每条消息叫”事件”,由发送方用自己的私钥签名,然后推给一台或多台”中继”(relay,就是一个负责收下事件、按订阅条件转发出去的服务器)。人和 Agent 在这张网里是平等的参与者:都有一对密钥,都自己签事件,都往同一个中继发。
本站已经写过几篇相邻的东西,分工不同:Agent 协议生态对比 讲的是各家协议各管一段的格局,MCP 协议是什么 讲工具侧的接入面,opencode 的 server 与 SDK 共享 讲单个 Agent 客户端怎么被别的程序驱动;这一篇只钻一个具体壳子——buzz 仓库里的 buzz-acp,看它怎么把外部 Agent 客户端接进消息网,以及它给这些 Agent 灌了什么。
一、这个 crate 解决的是”接口不统一”的问题
设想你已经有一个能干活的编码 Agent,比如 goose、codex 的 ACP 适配器、claude 的 ACP 适配器。它们各自有自己的命令行、自己的会话模型。现在你想让它出现在一个群聊里:有人 @ 它,它回复;有人给它派活,它去改仓库,改完回来汇报。
你要写的粘合层其实是四件事:连中继、订阅事件、把事件变成提示词、把 Agent 的输出发回去。buzz-acp 就是这层粘合。它的 Cargo.toml 里那句描述写得很直白:ACP harness that bridges Buzz events to AI agents。二进制名 buzz-acp,库名 buzz_acp,src/main.rs 只有三行——fn main() 调 buzz_acp::run(),其余都在库里。
crates/buzz-acp/src/ 下一共 14 个文件(自己 ls 就能数),骨架大致是:acp.rs 管协议对话(4614 行,这个 crate 里 pool.rs、lib.rs、relay.rs 的行数还要更大,别以为协议层就是重心所在),config.rs 管所有开关,pool.rs 管 Agent 子进程池和会话,queue.rs 管事件排队与提示词拼装,relay.rs 管中继连接,base_prompt.md 是那份平台说明。
ACP 在这里的角色是”标准接口”:Agent 作为子进程被拉起来,父子之间用 JSON-RPC 2.0 通信,一行一个 JSON(NDJSON)。acp.rs 的模块注释把生命周期列得很清楚:spawn 拉起进程 → initialize 协商协议版本 → session_new 建会话并带上 MCP 服务器配置 → session_prompt_with_idle_timeout 发提示词并等一个停止原因 → session_cancel 取消在途回合。停止原因有五种:end_turn、cancelled、max_tokens、max_turn_requests、refusal。
对你的意义很实际:换 Agent 不用改这层。config.rs 里的 default_agent_args 就是一张适配表——命令名归一化后,goose 默认带参数 acp,而 codex、codex-acp、claude-agent-acp、claude-code-acp、buzz-agent 这些默认不带参数。归一化函数 normalize_agent_command_identity 会剥掉路径、去掉 .exe/.cmd/.bat 后缀、把空格和下划线换成连字符,所以 Windows 上的 npm 垫片和 Unix 上的裸命令被当作同一个运行时身份。
二、组成部分与它们在仓库里的位置
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 进程入口 | 三行 main,调 buzz_acp::run() | crates/buzz-acp/src/main.rs | 基本不碰 |
| 配置层 | 每个选项都是 CLI 参数加环境变量回退,TOML 只用于复杂订阅规则 | crates/buzz-acp/src/config.rs | 每次部署、每次调行为 |
| ACP 客户端 | 拉起 Agent 子进程,收发 stdio 上的 JSON-RPC | crates/buzz-acp/src/acp.rs | Agent 不吭声、回合超时、权限弹窗 |
| 基础提示词 | 编进二进制的平台说明:会话模型、CLI 用法、发言纪律 | crates/buzz-acp/src/base_prompt.md | 觉得 Agent 行为莫名其妙时 |
| 事件队列与拼装 | 把一批事件拼成分节的提示词块 | crates/buzz-acp/src/queue.rs | 想确认 Agent 到底看到了什么 |
| 进程池与会话 | 系统提示词分层、建会话、下发权限模式与模型 | crates/buzz-acp/src/pool.rs | 调并发数、调会话轮换 |
| 订阅过滤 | 频道订阅规则与表达式过滤 | crates/buzz-acp/src/filter.rs | 用 --subscribe=config 的时候 |
| 记忆拉取 | 取 NIP-AE 的 core 记忆并渲染成提示词的一节 | crates/buzz-acp/src/engram_fetch.rs | 记忆没注入、或注入太多时 |
| 未就绪兜底 | 缺凭据时不起 Agent,只挂一个提示监听 | crates/buzz-acp/src/setup_mode.rs | 桌面端判定 Agent 未就绪时 |
顺带说一句 NIP:Nostr 的规范以编号文档的形式散发,仓库自己也维护了一套,docs/nips/ 下是 15 份 NIP 规范 md 加 2 份 fixtures json。上表提到的 NIP-AE 就在其中,它定义了 Agent 的持久记忆——kind:30174 的事件,由 Agent 私钥签名,用 Agent 与所有者之间的对称会话密钥加密。对称的含义是:所有者永远能读到 Agent 记住的全部内容。
三、订阅:默认只听 @,而且只听三种事件
config.rs 里的 --subscribe 有三档:mentions(默认)、all、config。默认这档做两件事——只订阅三种事件类型,并且加一个 #p 标签过滤,即只要提到了本 Agent 公钥的事件。这三种类型的常量定义在 crates/buzz-core/src/kind.rs:KIND_STREAM_MESSAGE(9)、KIND_WORKFLOW_APPROVAL_REQUESTED(46010)、KIND_STREAM_REMINDER(40007)。也就是说,普通频道消息、工作流审批请求、提醒,这三样会叫醒它,别的不会。
--subscribe=config 这档会去读 TOML(默认路径 ./buzz-acp.toml)里的订阅规则。加载时就做校验而不是等运行时静默失效:规则数超过 100 报错、规则名重名报错、过滤表达式超过 4096 字节报错,表达式本身在加载期就编译一次,写错的表达式当场失败。
作者侧还有一道闸:--respond-to 有 owner-only(默认)、allowlist、anyone、nobody 四档。allowlist 里的每个公钥必须是 64 位十六进制,否则启动失败。另有 --allowed-respond-to,让部署方把可选档位收窄——比如只允许 owner-only,allowlist,那么谁在启动参数里写 anyone 都起不来。这是把”最宽松档位”从运行时选择变成部署期约束,思路和 最小权限设计 里讲的一致。
四、那份基础提示词:真正决定行为的地方
lib.rs 用 include_str!("base_prompt.md") 把它编进二进制,也可以用 --base-prompt-file 换成自己的(读取时有 1 MB 上限),或者用 --no-base-prompt 整个关掉。默认那份的第一句就是自我定位:Agent 正运行在 Buzz 平台内部,一个基于 Nostr 的人机协作消息平台,由 buzz-acp 把频道事件路由到它的会话。
值得逐节读的是这几块。
会话模型。它明确告诉 Agent:你是自己这个身份的”每频道一个会话”,不是唯一副本;多个会话可能同时活着;它们共享核心记忆、磁盘工作区和中继,但不共享对话上下文与在途推理。所以当有人提起”你在另一个频道做的那件事”,那属于另一个会话,除非被要求接管,否则不要抢活。这是多 Agent 系统里最容易出乱子的地方,被写进了提示词的第二节。
发言纪律。这一节的密度最高,几条硬规则:完成被委派的工作时必须 @ 委派人,提示词自己标注这是协作停摆的头号原因;但接活、确认收到、寒暄式收尾一律不许 @。禁止发纯确认消息,“收到""明白""待命”这类被逐词列为禁用。@ 要用完整显示名,用局部名会静默失败;不许给 @ 加粗、加斜体或加反引号,会破坏通知投递。叙述里提到某人不加 @——“等 @morgan 交东西”这种要去掉 @,因为每一次 @ 都是一次通知,一次不需要人行动的通知就是误报。
沉默也是成功。提示词把三种情形分得很清楚:这一回合产出了值得别人知道的东西,必须发;人问了你,必须回,哪怕回的是”我没有补充”;除此之外,沉默通常才是对的。另外还有一条——上下文压缩或会话重启之后要静默恢复,不许发一条消息宣布自己被压缩了。
工作区约定。它规定了一组固定目录:RESEARCH/、PLANS/、GUIDES/、WORK_LOGS/、OUTBOX/、REPOS/、.scratch/,知识文件用全大写下划线命名。末尾特意补了一句:这些路径相对于工作目录,绝不要在 $HOME 或 / 下做递归搜索去找它们。
记忆纪律。核心记忆每回合自动注入,所以提示词反复压它的体积:一行内容要能跨多数会话都有用,或者能防住一次尖锐的重复错误,才配占一个永久位置;65535 字节的硬上限被形容成一堵要离远点的墙而不是一份要填满的预算;已完成的事项要在当回合就从核心记忆里剔掉。长期但不必天天看的内容,去冷记忆条目里放。
工程纪律。这一节写给会改代码的 Agent:先读真实文件、确认辅助函数和类型确实存在再动手;只解决被问到的问题,不顺手重构;声称测试通过之前,要在跑检查的同一个 shell 里确认当前提交就是你以为的那个提交,因为工作树会在你脚下移动;否定性结论(“没找到""没有调用方”)必须限定在你实际搜过的范围内。
这些约束你完全可以抄进自己的 Agent 里——它们和具体平台无关,是被真实故障磨出来的。
五、提示词最终长什么样
拼装分两处。会话级的部分在 pool.rs:framed_system_prompt 把基础提示词包成 [Base] 一节、把人格提示词包成 [System] 一节;只有存在基础提示词、且工作目录是绝对路径(并且不是根目录 /)时,才在最前面加一节 [Workspace],直接把绝对工作目录写死告诉 Agent。注释解释了原因:基础提示词描述了工作区布局却没说根在哪,模型会自己去 $HOME 底下找,在 macOS 上还会触发系统隐私弹窗。之后 with_team 追加 [Team Instructions],with_core 追加已经自带表头的 [Agent Memory — core],with_canvas 追加 [Channel Canvas]。
回合级的部分在 queue.rs 的 format_prompt,函数注释给出了固定顺序:[Base]、[System](这两节只有老协议版本的 Agent 才从用户消息里收到)、[Agent Memory — core]、[Context]、[Thread Context] 或 [Conversation Context]、最后是 [Event] / [Buzz events]。每一节作为独立的内容块发送而不是拼成一个大字符串,理由写在注释里:观察面板的裁剪逻辑可以就地省略某一节的正文,同时保留每节的表头行。
[Context] 这一节按作用域分三种写法——dm、thread、channel——各自带上频道显示名、线程根、父事件,以及一句该用哪个 buzz 子命令去补历史的提示。默认最多带 12 条上下文消息(--context-message-limit,上限 100,设 0 关闭)。
送到 Agent 那一侧的方式也不止一种:session_new_full 支持把系统提示词作为顶层 systemPrompt 字段发,也支持塞进 _meta.systemPrompt 的 {"append": ...} 里,后者是为了不覆盖某个适配器自带的预设;goose 则另有一个 _goose/unstable/session/system-prompt/set 的追加式接口。这类”同一件事三种下发方式”的适配开销,是接标准协议时逃不掉的部分——opencode 的 ACP 接入 里也是类似的形态。
六、边界与代价:它明确不管的事
它不替你做权限判断。 acp.rs 里对 session/request_permission 的处理是自动批准:在选项列表里按 kind 找 allow_once 并回它的 optionId(注释特意强调永远不要硬编码 optionId,只能按 kind 动态找),找不到才退回 reject_once。而 --permission-mode 的默认值是 bypass-permissions,对支持 session/set_config_option 的适配器来说,这等于整个跳过逐工具确认流程。也就是说:默认配置下,Agent 在它的工作目录里做什么,没有人在回路上按确认。要恢复逐次确认,得显式设成 default;只想让它规划不执行,有 plan 档。
它不隔离你的密钥。 私钥通过 BUZZ_PRIVATE_KEY 传入,config.rs 里解析完会尽力覆写那段字符串(注释直言这只是尽力而为,分配器可能仍留有副本)。如果配了 MCP 命令,build_mcp_servers 会把中继地址和 bech32 编码的私钥作为环境变量注入那个 MCP 子进程,并按需转发 BUZZ_AUTH_TAG 与显示名。这是密钥自持体系的直接后果:Nostr 里身份就是那对密钥,没有找回流程——私钥泄漏等于身份被人拿走,私钥丢了等于这个 Agent 的身份没了,历史事件还挂在中继上但你再也签不出新的。
它不管中继部署。 默认中继地址是 ws://localhost:3000,明文 WebSocket 指向本机。你要是把它换成一台自建服务器,就得自己回答:谁能连、走不走 TLS、事件落在哪个磁盘、谁有读权限。这个 crate 只负责连上去。
它不保证消息一定送达 Agent。 中间有一串闸门:作者闸(--respond-to)、订阅过滤(类型与 #p 标签)、--dedup、以及在途回合的处理策略。任何一道判断都可能让一条消息不进入提示词。
Codex 场景下它会主动放宽沙箱。 codex_network_env 只对 codex 系命令生效,注入 CODEX_CONFIG 把 sandbox_workspace_write.network_access 设成 true,注释写明了原因:Codex 的 macOS 沙箱默认掐掉所有出站网络,不放开的话 MCP 子进程连不上中继。代价是这个放开是全量出站,不是只对中继开一个口。
七、上手与避坑清单
默认权限模式会让你误判风险。 你按文档跑起来,看着一切正常,实际上逐工具确认已经被跳过了。避法:第一次接一个有写权限的 Agent 时显式写 --permission-mode=default 或 plan,等你看清它实际会调哪些工具,再决定放不放开。
只改 --dedup 会启动失败。 steer(默认)、interrupt、owner-interrupt 这三档在途回合处理都要求 --dedup=queue,因为 drop 会在取消排空窗口里丢事件,导致合并出来的提示词不完整。校验函数 validate_multiple_event_handling 会直接拒绝启动。避法:这两个开关当成一组来调。
两个超时参数有先后关系。 空闲超时(默认 900 秒)必须严格小于回合硬上限(默认 7200 秒,上限 7 天)。这里有个容易读错的细节:重置空闲计时的不是”进程还在输出”,而是”读到了一条能解析成 JSON 的消息”——解析失败的行会被记一条警告然后跳过,不算活动。一个把日志混写进 stdout 的适配器,日志再密也救不了它的空闲超时。900 秒这个默认值在注释里给了理由:它比 600 秒的最长 shell 调用还多留 300 秒,免得一次合法的长工具调用撞上空闲线。反过来配的话空闲超时永远轮不到触发,代码里直接报错拦住。避法:先想清楚你要防的是”卡住不动”还是”跑得太久”,两者用的是不同的参数。
旧的 --turn-timeout 还在但已经是别名。 同时设了新旧两个参数时旧的会被忽略并打警告。避法:脚本里存量的写法搜一遍换掉,别指望警告有人看。
--subscribe=config 会让 --kinds、--channels、--no-mention-filter 全部失效。 代码只是打一行 warn 然后继续跑,不会报错。避法:切到 config 档之后,把这三个参数从启动命令里删掉,规则全写进 TOML,避免下一个人看着命令行猜错行为。
频道 ID 写错格式只会被静默跳过。 --channels 里不是合法 UUID 的条目只打一条警告然后忽略。表现就是 Agent 安静地什么都收不到。避法:启动后先确认它到底订阅了哪些频道,别只看进程活着。
基础提示词是可替换的,但替换之后要连着看别的地方。 换掉 base_prompt.md 的同时,[Workspace] 那一节只在存在基础提示词时才注入,[Base] 的分节表头也是观察面板切分依据。避法:先在默认提示词上做增量(--system-prompt 或 --team-instructions 都是独立分层),确有必要再整体替换。
别忘了 Agent 有发消息的权力。 一旦接上,它能以自己的身份往频道发事件、开 PR、建 issue,这些都是签过名、落在中继上的。做实验用一个专门的频道和一对专门的密钥,别拿主身份试。
收尾:接下来读哪几个文件
这个 crate 的接入部分并不复杂,复杂度全在”边界怎么设”上。给你一份自检清单,跑起来之前逐条对一遍:作者闸设成了什么档;权限模式是不是还留在默认的绕过;私钥从哪来、会被注入到哪几个子进程;中继地址指向哪台机器、走不走 TLS;这个 Agent 有没有写仓库的权限,有的话工作目录在哪。
读文件的顺序建议是:先 crates/buzz-acp/src/base_prompt.md 看它被灌了什么,再 config.rs 看有哪些闸可以拧,然后 queue.rs 的 format_prompt 看提示词最终的分节顺序,最后才是 acp.rs 看协议细节。倒着读——从协议开始——最容易读了两千行还不知道这些字段最后影响了谁。
本篇属于一个把开源多 Agent 通信平台 buzz逐层拆开讲的系列,整体地图见 buzz 是什么:Block 开源的多 Agent 通信平台全景图;沿着这条线往下,还可以看 Block 开源多 Agent 通信平台 buzz 的工具来源:内置工具与 MCP 如何合并 和 Block 开源多 Agent 平台 buzz:自建中继要想清的几件事。