上周拆了 DeepSeek Harness,写了篇作业分析。拆完手痒,把自己一直在写的 Agent 框架整理出来开源了:0xAgent(包名 agent-harness),MIT,TypeScript 严格模式,src 六千行出头。昨天推了第一个 commit。

这篇不讲功能清单,README 里有。只讲我最花脑子的一件事:多个 agent 凑在一起干活,到底靠什么协作。

先看清死因

把几个 agent 拉进一个聊天室自由讨论,场面会很热闹,结果通常没用。我观察到的死因翻来覆去就那几种:

  • 两个 agent 互相客气,你回我一句我回你一句,死循环;
  • 同一个 agent 把同一句话复读三遍;
  • 发言基于半小时前的旧上下文,别人早就改主意了;
  • 一个 agent 在干正事,另外几个排队找它闲聊,把它的时间片和上下文窗口耗光。

盯着这个列表看一会儿会得出一个不太舒服的结论:这些病换再聪明的模型也治不好。死循环、复读、脏读、打断风暴,全是分布式系统里的经典故障,只不过参与者从服务器换成了 LLM。所以多 agent 协作的本质是并发与一致性问题,不是智能问题。0xAgent 协作层的所有设计都从这一句推出来,协调机制移植自 cumora 的工程实践,总原则写在 README 里:

并发与一致性用代码硬闸,判断与表达用模型。

下面是这条总原则展开成的五条具体原理。

原理一:规则要长在协议层,不能长在 prompt 里

让 agent”不要刷屏、不要复读”,写进 system prompt 是劝告,模型心情一坏就当没听见。劝告不如物理:0xAgent 的 Registry(中继)在协议层直接拒收,返回 409 或 429,消息根本没发出去。

挂在 Registry 上的五道闸全是这个思路:

  • lapping 判据:没有人类在场时,agent 消息数一旦超过不同发言者数就拒收。你比在场所有人加起来都能说,那一定是出毛病了。这个判据不用调参数,人越多它自动越宽松;
  • 逐字重复拦截:同一发送者在同一渠道发逐字重复的消息,拒收,override 也绕不过;
  • 速率地板:每个 agent 每分钟 30 条,不看内容,纯成本兜底;
  • 新鲜度闸:发言前有未读消息就拒收,详见原理二;
  • 两档地板:人类 10 分钟内发过言,lapping 放松成有界 cap(自适应 max(6, μ+2σ));人一走立刻回到严格模式。人类流量永不节流,人类一句话重置全部循环计数。

最后一条藏着协作系统的站位:群聊是给人开的,agent 是来干活的。人在场,说明讨论有价值,放开一点;人不在场,agent 之间的热闹大概率是空转,往死里掐。

原理二:发言权必须基于最新状态

agent 拿着半小时前的上下文慷慨陈词,是多 agent 系统最常见的无效输出。解决办法和数据库的乐观并发控制是同一个思路:发言前校验你看到的版本还是不是最新版。

实现上,Registry 给每个 agent 维护一个独立于读取路径的 seen-cursor。你要广播时游标落后了,直接 409,并把未读消息内联在拒收响应里,agent 读完重算再说。一定要说也行,系统发一个 hold-token:绑定你当时看到的 seq、120 秒有效、一次性消费。用过期 seq 的 token 会被拒。

这个设计把”我知道最新情况”从口头声明变成了可校验的凭据。覆盖闸门等于签字:我看过这个版本,我为自己的发言负责。

原理三:协作要落进状态机,不能落进对话里

“这个任务谁做来着""上次说好用方案 A 还是 B”,对话里的承诺是可抵赖的、易失的。0xAgent 的答案是三张状态机板子,协作的每一步都留下不可抵赖的记录:

任务板:ready → in_progress → review → done。开工必须先填验收标准;验收人不能是干活的人(approver ≠ owner);租约 30 分钟,逾期触发评分重派,公式里没有主观分,全是数据库事实:在场 − 负载×10 + 历史成功率×10;done 之后想返工,必须带新证据 diff;取消任务强制写 ADR,不许悄悄埋掉。

决策板:发起表决,全体在场 agent 投票(选项加理由),quorum 达成即定案。timebox 到点没决出来就升级给人,升级封套固定四行:要决定什么、各选项票数、默认项、超时后果。人也超时,就采用默认项。总之不允许事情停在”等等看”。

承诺账本:agent 说”我来搞”只产生承诺候选,它确认后才入依赖图。依赖边登记时防环。催办只点名关键路径上逾期的阻塞者,同一条边 45 分钟冷却。

三张板子共同的思想是:把协作从”聊天记录里翻旧账”变成”查数据库里的当前状态”。对话负责产生意图,状态机负责固定事实。

原理四:打断要收费

一个 agent 持有 in_progress 租约时,它在深度工作。这时候房间消息如果条条直送,它的上下文窗口和工具预算会被闲聊活活耗光。

0xAgent 给这个场景装了一个 focus window:租约持有期间,房间消息不直送,进个人摘要队列(上限 50 条),等任务流转或者 30 秒巡检发现窗口结束,一次性补发一份 digest。永远放行的只有两类消息:@ 它的直聊,和关键路径上的催办。

本质是给打断定价。打断一个干活的 agent 要花它的稀缺资源,那就只允许付得起这个成本的消息通过:点名找它的,和它正在阻塞别人的。

原理五:经验变原则,也要过闸

协作系统跑久了会积累经验,但 agent 的”我觉得”直接变成下次的行为准则,相当于自言自语有了立法权。0xAgent 把记忆分两层:干完活登记 episode 经验;候选原则想晋升进 semantic 层,要两个以上独立来源背书,或者人类手动 pin。晋升后的原则注入 agent 上下文的头部,计入 token 预算。

学习速率被故意压慢,换来的是原则库的干净。

底座:消息比进程活得久

上面所有机制都建立在一个前提上:消息不能丢。Registry 用邮箱模型,状态 1 秒防抖落盘、原子替换,在途消息重启不丢,落盘是唯一事实源。bus agent 走两秒轮询的存储转发拓扑,任何机器都能加入,不需要入站端口。我的 Registry 就跑在树莓派上,Mac 的 agent 和 Web 聊天室都挂它。

收尾用 README 里那句话:

我不负责让场面热闹。我负责让事情变清楚。