很多代理,一个架构:当其中两个改动同一个系统时会发生什么

三个代理,三个 pull request,关于重试逻辑该放在哪里的三个合理意见。

一个把重试放进 HTTP 客户端。一个包住 handler。一个加了一个队列并把它消费掉。这三个里随便挑一个单独读,你都会 approve。把它们放在同一个下午读完,你会发现系统现在在三个地方重试,用着三套不同的 backoff 策略,而且没有人做过这个决定。

只要有不止一个代理同时在一个代码库上干活,问题就是这个形状。每个代理在局部都是对的。不一致是全局的,而且只有最后审阅的那个人才看得见。

为什么规则文件仲裁不了这件事

标准答案是一份规则文件:CLAUDE.mdAGENTS.md.cursor/rules。我们写过这些文件为什么会过期,而过期在这里是小问题。大问题是规则文件没法仲裁。

它是散文。把同一段话交给两个代理,它们会给出两种不同的读法,两种都站得住脚,而这两种读法没有任何相遇的地方。它按仓库存在,所以一条关于服务边界的规则待在一个仓库里,而边界另一侧的服务在另一个仓库里。它也没有状态:它无法知道四十分钟前另一个代理提过什么,因为它是一个文件,而文件不知道任何事。

仲裁需要的不是更好的散文。需要的是一个共享的东西,两个代理都能读能写,能承载一个决定,也能注意到一次分歧。

模型能给你而文档给不了的东西

架构模型是可以查询的元素和关系。系统、container、component、它们之间的边,以及挂在这些边上的、让一个设计成其为设计的那些东西:让某条边界成为有意为之的那个决定、需要通知的 owner、某个 consumer 依赖的 contract。

由此得到三件事,每一件都是机制,而不是意图。

每个代理都能读到同样的字节。generate-context action 从模型里写出一份 archyl.txt,默认是 markdown,也可以选择自动 commit 到仓库。九个代理读同一份生成出来的文件,跟九个代理各自去转述一份散文文档,是两种不同的情况。这不聪明。它只是共享的。

分歧可以在入口处被拦住。conformance-check action 会针对 pull request 改动过的文件运行架构规则,把违规内联标注出来,并按你选择的严重级别让检查失败。fail-on 接受 errorwarningnone。如果"重试属于客户端"是一条规则而不是一句话,那两个把重试放到别处的代理会在 CI 里知道这件事,而不是在评审里。

**决定有地方可以待。**ADR 会挂在它所约束的 C4 元素上。那个队列存在的原因就在队列上,而不是在三月份某条没有任何代理见过的 Slack 讨论串里。

我们做错的那部分

从这里开始,这就不再是一篇讲漂亮想法的博客了。

代理不会直接改动模型。它们开一个变更请求:一个由人审阅并合并的提案。变更请求被创建时,archyl 会记下它是基于哪个模型版本构建的。它被合并时,版本号递增。这正是你会想为这个问题准备的机制。

我们从来没有把这两者接起来。

基线版本在创建时被写下,然后哪里都没有读回来。这意味着下面这个顺序会安静地、完整地跑通:

  1. 代理 A 和代理 B 都读了模型。两个都看到版本 7。
  2. A 开了一个变更请求。B 开了一个变更请求。两个都基于版本 7。
  3. A 的变更请求合并了。模型现在是版本 8。
  4. B 的变更请求合并了。它是基于一个已经不存在的模型写出来的。

没有警告,没有冲突,历史里也没有任何记录。第二批改动落在第一批上面,如果两者互相矛盾,这个矛盾现在就是被记录下来的架构。这是一次把冲突检测拆掉的合并,而它恰好就是整个"很多代理"的说法本该防住的那种失败。

所以我们修了它。合并一个基线版本已经和项目对不上的变更请求,现在会以 409 Conflict 失败,并带上一条把两个版本都点出来的消息:

architecture request is based on version 7 but the model is now at version 9;
rebase the request and merge again

不过,让它变安全的并不是那次比较。两个在同一瞬间到达的合并,会双双通过比较、双双继续下去。真正堵上这个缺口的,是把版本递增本身变成有条件的:只有当模型仍然停在这个变更请求所基于的版本上时,合并才会把模型往前推。如果它已经动过了,合并就找不到可以往前推的东西,整件事回滚,一处改动都不会被应用。前面那次比较之所以存在,只是为了让错误消息能告诉你落后了多少。

409 而不是 400,比看上去更重要。一个在 400 上重试的代理会永远循环下去,因为格式错误的请求还是格式错误。409 说的是反过来的意思:你发过来的东西没问题,只是不再适用了。取一次当前模型,再试一次。

这仍然做不到的事

四个限制,每一个你都可以自己核对。

**没有任何代理会合并任何东西。**没有哪个 MCP tool 可以合并一个变更请求。代理提出建议;由人来审阅和合并。这是一条刻意的边界,我们也没有打算拿掉它,但这意味着这个闭环不是全自动的,你不该按照全自动去设计。

**冲突检测是粗粒度的。**版本是按项目算的,不是按元素算的。两个代理去动同一个项目里真正毫不相干的角落,仍然会在版本上撞车。这是出错时比较安全的那个方向,但它就是错的。

上下文检索是词法的。find_relevant_context 按名称、描述、标签和路径上的词语重叠来给元素打分。没有 embedding,也没有同义词扩展,所以一个关于"checkout"的任务不会把一个叫 OrderProcessor 的 component 捞上来。好处是实打实的(确定性、没有 token 成本、不会把代码发到任何地方),但这是匹配,不是理解。

**规则不会自己写出来。**上面所有这些,都假定有人把"重试属于客户端"表达成了一条合规规则。空的规则集什么都拦不住,不管你跑着多少个代理。

这周可以做点什么

想搞清楚自己处在什么位置,不需要买任何东西。

挑出你团队最近一次在一周内合并了不止一个由代理写的 pull request 的那一周。把它们放在一起读,而不是一个接一个地读。问一问其中有没有任意两个用不同的方式做了同一个决定,然后再问,在你现在的配置下,有什么会把这件事告诉你。

如果答案是"审阅的人注意到了",那它能管用到审阅的人要一次读九个的那一天。


变更请求、合规规则和 C4 模型都是 archyl 的一部分。GitHub Actionsagent skills 是开源的。相关阅读:为什么你的代理有一份规则文件而没有模型变更请求是怎么工作的,以及模型是怎么被保持诚实的