九月底那个下午,我把一个两百来行的 diff 丢给三个 subagent 做评审。三个独立 context window,三份独立 prompt,跑完合并。
三份报告里有同一条意见:第 47 行那个 user 可能为 None,建议加判空。
行吧,加。两天后线上还是挂了,挂在我自己写的、被我当成“肯定没问题”的幂等判断上。三个 agent,没有一个提到它。
我第一反应是这三个东西水平不行。
后来看到一篇论文才反应过来,问题不在它们身上,在我身上。
10 月 7 号挂上 arXiv 的,编号 2610.09824,Prakhar Ganesh 领衔,一共七个人。标题里那个词用得挺准——Homogenization,同质化。讲的是多智能体系统里,agent 互相交互着交互着,会收敛到彼此相似的行为模式上去。
这事本身不新鲜,干过活的都见过。新鲜的是后面那几句。
论文把同质化拆成三个可度量的方向:对多数意见的从众、往极端位置的两极分化、以及交互轮次越多越不愿意改的惰性。前两个我一看就懂,第三个看了两遍。惰性——轮次增加,改变的成本在 agent 眼里越来越高。太真实了。你让三个 agent 来回讨论五轮,第五轮的时候它们不是在讨论问题,是在互相确认前三轮说过的话。谁先松口,谁就是把前面四轮的 token 白烧了。
然后是我真被戳到的地方。论文在代码生成任务上报告:同质化会掩盖相关误差,并且把它放大,最后形成系统性漏洞。
相关误差。不是随机误差。
随机误差是三个评审员各自看漏不同的地方,合并一下还能兜底。相关误差是三个评审员看漏的是同一个地方——你以为你在做集成,其实你是在把同一个盲区数了三遍,还多付了两倍的钱。
我那个幂等判断就长这样。它不复杂,甚至不难看,就是那种“大家都觉得没问题”的写法。三个 agent 都从同一个角度审它,都从同一个角度看过去了。
我原本以为换模型就行了
说个丢人的事。今年上半年,我一直坚信多智能体评审的价值在于“异构”:用不同的模型,就能拿到不同的偏见,不同的偏见一平均,误差就抵消了。
所以我那套配置长这样:
reviewers:
- name: r1
model: claude-sonnet
context: [shared/CLAUDE.md, diff.patch]
- name: r2
model: gpt-5
context: [shared/CLAUDE.md, diff.patch]
- name: r3
model: gemini-pro
context: [shared/CLAUDE.md, diff.patch]
三个不同的模型,看起来很像那么回事。
你多看一眼就发现问题了:三行 context 一模一样。同一个 CLAUDE.md,同一份 diff,同一个目录结构,同一条“你是一个资深代码评审员,请找出这段 diff 中的问题”的 prompt。
模型换了,输入没换。
论文测了两类“简单提升多样性”的做法——利用采样随机性,和用混合模型搭多智能体系统。结论是两者都没能降低同质化带来的风险。
我看到这一段的时候正在喝水,差点呛着。因为这正是我上半年交的那笔智商税 😅。
采样随机性是 token 分布层面的扰动。你把 temperature 调到 0.9,它会在同一条思路上换几个词,不会换一条思路。混合模型更微妙——权重不一样了,但三份完全相同的输入喂进去,那些权重差异会被 prompt 和上下文先压平一大截。模型之间的分歧,抵不过输入的一致。
画外音:真正决定一个评审员说什么的,是它看到了什么,不是它脑子里那点参数抖动。
而让我那套配置彻底变成摆设的,是我自己的编排代码。为了让“合并”这一步好写,我在给第二个 agent 的 prompt 里,顺手把第一个 agent 的结论带上了。
prompt = f"""
你是资深代码评审员,请独立审查以下 diff。
参考:上一位评审员认为主要风险在 {r1.summary}。
diff:
{diff}
"""
写完我还挺得意,觉得这叫“信息共享”。现在回头看,这不是信息共享,这是串供。独立 那两个字被我写在 prompt 里,然后被我自己下一行代码干掉了。
而且这不是“稍微有点影响”。第一个 agent 的结论一旦进了 prompt,第二个 agent 的活就从“找问题”变成了“评估这个结论对不对”——它的注意力被我亲手锚在了那个位置上。它后面报出来的东西,无论同意还是反对,都已经不是独立发现。
我后来量了一下
改之前我先做了件粗暴但有效的事:把三个评审员抽出来的问题标签拿出来,两两算重合度。
def jaccard(a, b):
a, b = set(a), set(b)
return len(a & b) / len(a | b)
pairs = [(0, 1), (0, 2), (1, 2)]
print([round(jaccard(findings[i], findings[j]), 2) for i, j in pairs])
# -> [0.63, 0.58, 0.67]
六成左右。剩下的四成不是不同见解,是同一个意思的不同措辞,r1 说“缺少空值保护”,r3 说“边界情况未覆盖”。我人工看了一遍,真正算独立发现的,一条。
三个评审员,一个意见,三份报告。这就是同质化在生产环境里的样子——不壮观,不报警,就是静静地什么都没多给你。
改法很土,谈不上什么方法论:
- 每个 agent 只拿到一片信息,不给全量 diff。
r1只看数据层改动,r2只看并发路径,r3只看对外接口的契约变化。 - 合并之前不共享任何结论,谁也别看谁的报告。
- 合并由我自己做,或者写个只做去重、不做判断的脚本。
这么干之后重合度掉到 0.2 附近,但覆盖率反而上去了——因为 r2 现在能看到并发路径里那些 r1 根本没被喂进去的东西。重合度低本身不證明质量高,但至少说明它们开始看不同的地方了。
所以真正的多样性轴不在模型,在信息切片。这句话我认得很不情愿,因为它意味着过去半年我在模型选型上花的那些时间,基本是白花的。
它不在场了,它写的东西还在
论文还有一个任务是在招聘场景下测的,结论是:带有偏见的 agent 的影响,在它被移除之后仍然长期存续。
代码场景里这句话翻译过来是这样的——那个 agent 提出、被人 review 通过、合进主干的那段写法,会变成后面所有人的“先例”。
它已经不在场了。但它写的东西还在场上,而且被当成规范在用。下一个 agent 读代码库的时候读到它,会认为“这个项目就是这么写的”,然后照着写。
我想起去年一次重构。当时让 agent 把一个老模块整理一下,它整理得挺漂亮,顺手删了三行处理历史脏数据的判断。删得有理有据——那三行确实没有注释,确实看起来是死代码。CI 全绿,合了。三个月后一次数据回灌,那三行没了,于是多了一轮通宵。
这个 bug 是在我毫无防备的情况下被上游数据发现的。而那个 agent 早就不在对话里了,它的判断还留在代码里,被后来每一个读这段代码的 agent 当成既定事实继承下去。
论文把同质化定性成多智能体系统的一种失效模式,我同意,但我觉得它在我这种场景下比“失效”更阴——它不漏痕迹。你的 eval 跑不出来,因为 eval 是我挑的用例;你的 CI 跑不出来,因为 CI 只测我说得出名字的东西。
作者还主张,评估多智能体系统不能只看总体性能,还要分析 agent 间的交互动态。我举双手同意,但想加一句更不客气的:在写代码这个场景,总体性能这个指标本身就带着欺骗性。它是一个平均值,而同质化造成的洞恰恰集中在分布的尾部——那些你没测、没想到、也不在 checklist 上的地方。平均值越好看,尾部越黑。
这也是为什么“我们 CI 全绿”这句话从来没让我安心过。
不过我得承认一件事,写到这里我犹豫了一下要不要展开。
我不太确定“多智能体”这个框架用在我日常写代码上,是不是从最开始就被我用歪了。论文说的失效模式,前提是多个 agent 反复交互、在交互中互相影响。而我大部分时候干的活,其实是一个 agent 干活加我自己审——这严格说不算它描述的那个东西。我把论文的结论往自己身上套,套得挺顺,但会不会套错了?
这个我没想明白。可能我只是在给自己上半年那套“三模型集成”找理论背书,好让这次翻车显得不那么蠢。
但有一条我确定:不管是三个 agent 还是我一个加一个 agent,只要参与者看到的是同一份信息、共享的是同一个先验,收敛就一定会发生,人多只是让收敛更快。七个作者用三个任务、三个指标测出来的东西,和我盯着三份几乎一样的报告发呆,是同一件事。
下次再想开三个 agent 投票之前,我先问自己一句:它们各自掌握着什么别人没有的信息?
答不上来,那就是花三倍的钱买一次自我确认。
