Raft 博客里没告诉你的事:多 Agent 群聊为什么不乱

2026-08-07
Raft(前身 Slock)的官方博客很好读。九篇文章讲了一套完整的理念:agent 是队友名字就是地址群聊混乱要怪房间,别怪 agent别给公司建一个共享大脑。读完你会被说服,但你不会知道其中任何一件事是怎么做到的:held draft 怎么实现?agent「自己决定要不要读消息」靠什么支撑?理念全部给出,做法一个不给。
我做的事,是把他们的客户端拆开看。raft-computer v1.0.15 是一个单文件程序,里面装着约 30MB 没有加密的源代码,内部代号还叫 slock。我通读了这份代码,又用一次真实双 agent 协作的后台记录做了对照,下面引用的所有数字、规则、提示词原文都能在代码里找到。
先说结论:博客里的理念背后,是一套很实在的做法——谁认领谁做、发言前先检查有没有读过最新讨论、提醒只报数量不给正文、忙的时候通知排队等空隙。这群人把分布式系统里用了几十年的老办法,搬进了一个聊天产品。

一、没有人指挥的群聊

提起多 agent 协作,常见做法都有一个「指挥」:AutoGen 的群聊有个主持人角色,读完全场对话后点名下一个发言人;Anthropic 的多 agent 研究系统由一个领头 agent 拆任务、派活、收结果;MetaGPT 则按写好的流程走,像流水线。
Raft 的代码里找不到任何这类角色。频道就是一本按顺序记事的日志,每条消息有编号,每个 agent 只被提醒「和你有关的部分」。没人指派谁来响应,三件事凑在一起,秩序就出来了:
  1. 发消息时就标好了意图。发在哪个频道、@了谁、挂在哪个话题下面,本身已经说明了「这事和谁有关」,不需要再有一个中心去分析;
  1. 先认领,再动手。任务面前人人可见,谁先认领成功谁做。认领由服务端裁决,同一时刻只会有一个赢家,两个人同时扑上去改同一份代码的情况从源头消失了;
  1. 每个 agent 的提示词里都写着群聊规矩:没被 @ 别插话、别替别人汇报、认领失败就退出、没实质内容别刷屏。
认领失败时,系统会明确告诉你「现在是谁在做」,同时补一句:认领只是一把防止撞车的锁,和「这件事归谁领导」无关。锁只管一类动作——真正动手的实现工作;看热闹、做评审、参与讨论都不受限。
我用一次真实协作验证了这套说法。人类在频道里广播了一句「各位 agent 协调一下,做个 hello world 页面,然后另外一位验收」,没有任何分工指派。Claude 认领成功开始写文件,Claude-2 认领失败后在自己的思考过程里推导出「我做的是验收,验收不受认领限制」,于是继续用无头浏览器渲染页面、截图举证,全程没抢话,也没越权去改任务状态。
Raft 界面里这个对话的真实样子:
notion image
人类广播任务,Claude 做完后主动 @Claude-2 验收。分工由做事的人指派。
notion image
验收通过、任务关闭、互相道谢收尾。全程没有任何主持人的角色。
 

二、发言前的版本检查

Agent 在群里说话前会检查看到的讨论是否是最新的。
我们人类在使用 IM 工具的时候,经常打好一大段回复,发出去的瞬间发现别人刚说了新结论,你这段话立刻变成废话。人类靠自觉撤回,Raft 给 agent 装了一道硬检查。
每个 agent 都记着自己在每个话题里读到了第几条。发言时把这个「已读位置」一并交给服务端;如果这个话题在你读完之后又有了新消息,这条发言发不出去——内容存成草稿,系统提示你先去看新消息。看完、把已读位置推进到最新,再重发,检查通过。
动态演示:agent 想发言时别人刚发了新结论,服务端拦下发言并存成草稿;读完新消息后再发,顺利通过。
这套检查的配套设计也很周到:
  • 草稿不会丢。被拦下的内容自动保存;重发草稿前还会先等一秒,确认没有正在输入的新内容,防止旧草稿盖掉新写的;
  • 盲审模式。承担评审角色的 agent 可以开启隔离:被拦时只告诉它「有 3 条新消息」,不展示内容和发送人,避免评审被最新讨论带偏;
  • 服务端记账。系统记着每个模型实际读到了第几条,既当审计记录,也用来做上面的检查。
加上第一节的认领机制,Raft 其实有两道闸:认领决定「谁来做」,发言检查决定「这句话现在能不能说」。一个管所有权,一个管时效。
在学术界也有类似研究。CMU 的 Christopher Meiklejohn 在 Multi-Agent Systems Have a Distributed Systems Problem 里检查了几个主流多 agent 框架,发现它们共享状态却没有任何防撞车机制,但他只指了方向,没给做法。6 月的 CoAgent 论文进一步说明为什么数据库里的老办法直接搬过来太重:agent 一次推理要几分钟,锁住等不起,失败重来也亏不起。Raft 的做法轻得多:不锁定、不回滚,只是拦一下,让你读完新消息再说。在我看到的范围里,这是第一个把这套检查真正做进多 agent 群聊的产品。

三、怎么叫醒一个 agent

Raft 从来不把消息正文直接推给 agent。agent 收到的提醒只有:哪里有几条未读、有没有被 @。读不读、什么时候读,由 agent 自己判断——提示词里甚至专门叮嘱:提醒没给正文不代表没有内容,选择不读就是选择搁置,要如实汇报,不许偷懒说「没事干」。
叫醒的节奏由两个设计控制。
其一,3 秒合并窗口。 频道里连续来消息时,后台服务先等 3 秒,把这段时间到达的消息合并成一次提醒。没有这个窗口,每条消息都会单独叫醒 agent 一次。
动态演示:三条消息陆续到达,合并成一条「只有数量、没有正文」的提醒;agent 一次读完再回。(循环播放)
其二,忙时排队,只在安全缝隙送入。 agent 正在生成回答、等工具返回、整理上下文的时候,新提醒一律进等候队列;只有到了「说完一段话」「工具刚返回」「完全空闲」这类自然停顿点,排队的提醒才批量送进去。Claude 被照顾得更小心:它思考过程的数据是签了名的,中途塞内容会弄坏数据流,所以给 Claude 的提醒只在回合的完整边界上投递。而那些不支持边跑边收消息的 agent,处理方式更简单——把手头的事做完就退出,有新消息时再启动一个新的。
动态演示:agent 忙的时候,提醒在队列里等候;到达「工具边界」和「空闲」两个缝隙时才批量送入。(循环播放)
这些细节官方博客一个字没提。它们是「房间不混乱」真正的地基,群聊规矩只是最上面的一层软约束。

四、把各家的 agent 收进同一套规矩

市面上的 coding agent 各说各话:Claude Code、Codex、Cursor、Kimi、Pi……Raft 给每家套了一层适配:有的作为子进程启动、统一数据格式,有的直接嵌进后台进程,有的通过插件桥接。殊途同归,最后所有 agent 都用同一条 raft 命令行和频道里的世界说话。
不过有两点 raft 官方没有提:
  1. Claude 启动时被关掉了所有权限确认。无人值守运行必须如此,但使用者应当知情;
  1. Claude 自己定计划、自己设闹钟的工具被禁用了。什么时候干活、什么时候醒来,由后台服务 raft daemon 统一安排。

五、把团队规矩写进提示词

还有一类设计,是把人类团队的惯例翻译成 agent 能执行的规则。三个例子:

@ 人永不无声失败

@ 了不在频道里的人,消息照常发出,但系统会单独告诉发送方「这条 @ 没送达」,并给出补救命令;被 @ 的圈外人则会收到「你无法在该频道回复,可以私信提及你的人」的指引。要么送到,要么明确告知,没有第三种结局。
动态演示:@ 送不到时,消息照样发出,但发送方一定会收到明确告知和补救入口。(循环播放)

路过发言自动降噪

系统能认出「只为了发一条消息而临时加入频道」的行为,主动提示「可以屏蔽本频道的日常更新,@ 你的消息仍然能收到」。
动态演示:路过的 agent 屏蔽日常更新后,频道噪声消失,@ 它的消息仍然畅通。(循环播放)

冻结和合并的规矩写死

任何「先别动」的冻结都必须登记来源、范围、失效条件,冻结解除时要通知到所有还在按旧前提干活的人;合并代码的条件是一个封闭清单——检查通过、独立评审同意、没有未解除的冻结,就必须合并,不允许因为「这是某人提交的」就额外加一道审批。
动态演示:冻结要登记三件事,合并只认三项硬条件,凭身份加码会被规则驳回。(循环播放)
官方的说法是团队结构「自然生长」。读完代码才知道,自然生长的底下垫着一整套管道:频道做隔离、提醒做裁剪、认领和版本检查做防撞、提示词做约束、统计信号做提醒。五层各管一段,合力替代了那个「指挥」。
动态演示:五层机制自下而上各管一段,合起来接管了「中央指挥」的全部工作。(循环播放)

六、群聊之外:daemon 怎样照顾一个长期同事

写到这里,标题里的问题其实已经回答完了。但我沿着 daemon 继续往下读,发现 Raft 还在解决另一个更麻烦的问题:如果 agent 不是一次性的聊天窗口,而是一个会长期待命、会换电脑、会卡死、还要接触公司内部工具的「同事」,怎么让它一直活着,又不把钥匙散得到处都是?

先确认它真的看见了

Raft 不把「服务端已经发出」当成「agent 已经读到」。一条消息从 server 到模型,中间有四张逐级变硬的回执:
服务端交给通信核心,只算第一步;harness 接受、提醒成功塞进某个具体 runtime,分别再记一步;直到模型的已读位置真的推进,才叫 model_seen。中间任何一层出问题,也不是笼统地报「失败」,而是区分没有 session、runtime 正忙、注入失败、协议版本不对、认证被撤销。上一节说的「服务端记账」,原来只是这条证明链的最后一格。

卡死了怎么办

「忙的时候先排队」有一个隐含前提:agent 最后总会忙完。要是它永远忙不完呢?
Raft 给 runtime 放了三层保险。第一层是慢下来:进程启动失败后按 1 秒、2 秒、4 秒这样退避,最高等 30 秒,不让连续到来的消息把一次故障放大成启动风暴。第二层是承认重试没用:相同错误连续出现 3 次,daemon 会熔断,不再把同一个坏配置一遍遍拉起来。
第三层才是重启。一个 agent 显示 working 或 thinking,整整 15 分钟没有任何进展,daemon 会先看它的进程是不是真的还活着。PID 还在就不误杀;进程确实死了、同时 inbox 里又有消息在等,才先发 SIGTERM,10 秒还不退出再 SIGKILL。旧进程走后,排队的消息没有被当成处理过,新进程接着从 inbox 醒来。
动态演示:启动失败先退避,确定性错误三次后熔断;真正卡死且有消息等待时,才结束旧进程并保留 inbox 重启。(循环播放)
Raft 甚至限制了同一台机器同时起跑的 agent:默认最多 5 个,相邻两次启动至少错开 500 毫秒。这里没有什么高深的 AI 理论,就是后台服务最朴素的背压。

不把钥匙直接交给 agent

前面提到 Claude 被关掉了权限确认,听起来多少让人不安。继续看下去会发现,Raft 对操作系统权限很大胆,对自己的服务端密钥却谨慎得多。
真正的 agent API key 留在 daemon 里。runtime 启动时拿到的是一个 sap_ 开头、只属于这次运行的随机通行证;它调用 raft 命令时,请求先到 127.0.0.1 上的本地代理,由代理核对这是哪个 agent、哪次启动、允许做什么,再替它带上真密钥访问服务端。agent 停止,旧通行证跟着撤销。
动态演示:原始 agent key 留在 daemon;runtime 只拿本次启动的随机通行证,请求经本机代理转发,停止后旧票失效。(循环播放)
Managed MCP 也走同样的路。daemon 不把第三方工具的连接和密钥一股脑塞进每个 agent,而是给这次启动生成一个随机的本地 MCP 地址。agent 能看见哪些工具,由服务端下发目录;真正调用时还要再核对「这份配置是不是最新版」「这个工具是不是仍然分配给你」。所以这里的代理不是网络优化,它更像公司的门卫室:钥匙留在里面,谁来、办什么事、通行证有没有过期,每次都重新看。
当然,这不等于 sandbox。agent 仍然可以拿通行证做它被授权做的事;Raft 防的是原始 bearer key 被子进程、工具输出和环境变量顺手带走,不是把 agent 关进笼子。

一个 agent 怎样搬家

Raft 的 agent 被描述成会积累经验的长期同事。那换一台电脑时,总不能让这位同事失忆重来。
代码里有一套完整的跨机器迁移协议。它会打包 workspace、MEMORY、skills 和 session 状态,但跳过 node_modules.venv、缓存、编译产物这些随时能重建的东西,也不搬 .envcredentials.json 里的密钥。大包默认切成 8 MiB 一块;每块算一次 SHA-256,整包再算一次。传到一半断线,目标机器先问「我已经有哪几块」,只补缺的,不从头再来。
动态演示:长期状态分块搬到新电脑,断线后只补缺块;路径、摘要、磁盘和目标冲突全部检查通过,最后才原子接管。(循环播放)
到达新电脑也不会马上覆盖现有目录。包先落进 staging,检查摘要、路径、软链接、磁盘余量和目标 workspace 有没有冲突;可用空间还得比内容的两倍再多 512 MiB。所有检查通过,最后写下 commit marker、原子接管。这个流程和数据库迁移很像:宁可留下一个可以清理的半成品,也不让半套 agent 状态开始工作。

还有一件应该透明的事

我拿来验证前面结论的 daemon trace,不只是用户主动「导出诊断」时才有。标准的 Raft Computer 启动 daemon 时会打开本地 trace:文件到 5 MiB 或写满 5 分钟就轮转,最多留 8 份。默认情况下,至少一分钟没再写的旧文件会被 gzip 压缩,通过一张服务端签发的上传凭证送到 slock-trace-upload.botiverse.dev;大约每 5 分钟跑一轮,一次最多传 4 份。设置 SLOCK_DAEMON_TRACE_UPLOAD_DISABLED=1 才会关闭。
动态演示:本地 trace 先按字段名过滤,再压缩上传。常见正文和密钥字段被丢弃,但 usage、cost、诊断 ID 与有限错误摘要仍会保留。(循环播放)
上传前有一层字段过滤:prompt、content、stdout、tool input/output,以及名字里明显带 token、secret、password、cookie、credential 的字段会被丢掉;数组通常只留数量,对象只留「存在」。所以这不是在上传聊天全文。
但也不能说「什么都没传」。我抓到的真实记录里能看到模型名、输入输出 token、缓存命中、上下文窗口、耗时、service tier、totalCostUsd,还有 server、machine、agent、launch 等诊断 ID 和有限长度的错误摘要。过滤器主要看字段名,新出现但没撞上黑名单的字符串也可能留下来。对一个长期在公司电脑上运行的 agent 来说,这类遥测应该在产品里讲清楚,而不是等别人拆代码才知道。

把这几层放在一起

到这里,Raft 对「长期同事」的定义才完整:四级回执保证消息不是假送达,退避和熔断防止坏进程无限复活,stall recovery 负责真正卡死后的接班,本地代理把密钥留在控制面,迁移协议让 workspace 换机器不失忆,trace 则让整套系统可以被诊断——同时也带来需要透明说明的数据边界。
这些机制不负责让 agent 更聪明。它们负责的是:当聪明本身不可靠时,系统还能收拾残局。

七、结语

多 agent 系统的失败研究这两年很热:Berkeley 的 MAST 分类法标注了 1600 多条运行记录,归纳出 14 种失败模式;Cognition 的 Don't Build Multi-Agents 劝大家回到单个 agent;Google 的 A2A 协议 在统一「agent 之间怎么说话」的格式。但「几个 agent 同时往一个共享对话里写东西时怎么不乱」这个问题,公开讨论里大多停留在指出问题,给出工程做法的很少。
Raft 悄悄把这个空填上了:频道当账本,认领当锁,发言检查当版本校验,提示词当规章制度。它不预测谁该说话,让每个 agent 自己判断,再由服务端保证争抢的结果唯一。
也要如实说代价:客户端代码没有加密(本文的存在就是证据);Claude 默认跳过权限确认;规矩层终究依赖模型自觉遵守。更根本的前提是,这套设计要求 agent 足够聪明,读得懂规则也愿意照做。这大概也是官方博客不谈做法的原因:真正难复制的不是某个机制,是这些机制和「足够好的模型」之间刚刚好的配合。早两年,模型太弱带不动这套规则;晚两年,也许一个全能 agent 就够了。就在现在这个窗口里,他们选择把分布式系统二十年的旧经验,悄悄搬进了一个聊天产品。

八、番外:几段原始提示词

前面是转述,这里贴几段从代码里逐字抽出的原文(模板变量已按默认值展开)。英文好的读者可以直接感受这套「规章制度」的写法。
每个 agent 的提示词开头
开工流程第 3 步——防止 agent 把「只有数量的提醒」误读成「没事干」,"unobserved is not the same as nonexistent" 是全套提示词里写得最好的一句:
群聊规矩——「别抢话」的完整原文:
发言被拦下后 agent 看到的输出——注意「不发了」也是被并列摆出的合法选项:
盲审模式的说明
冻结规矩——什么情况下可以扣留一个本来已授权的动作:
这些文案有一个共同点:每条规则都附带理由,没有裸命令——"not a ruling on lane ownership","a deferral to report honestly, not a conclusion"。它们要说服的对象是会推理的模型,光下命令不够,得把道理讲通。官方博客发明的 AX(Agent Experience)这个词,落到提示词层大概就是这个样子。

本文基于对 raft-computer v1.0.15 的静态分析(单文件程序内含明文源代码)与一次真实群聊的后台记录,所有数字、规则、提示词原文均可在代码中复核。分析仅为技术研究与学习目的。
Loading...