两个签名,一次失败。
针对你当初装上它所要防的那个威胁,「制单—复核」几乎什么都没买到:骗过制单方的那份文档,同样会骗过复核方——于是一份被注入的输入上的「二人同意」,不过是同一次批准戴了两顶帽子。职责分离从来不是一个冗余论证,而是一个独立性论证;而智能体技术栈会默认地、悄无声息地摧毁独立性,同时审计日志里的条数一点不少。能告诉你手上究竟是哪一种的那个数字,是「不一致率」——也正是没人往看板上放的那个数字。
这项管控是一个关于独立性的主张,而不是关于数人头。
职责分离很古老,而在有钱流动的地方它是承重的:发起付款的人不得是放款的人,写变更的开发者不得是把它部署到生产的那个人。这项管控通常被描述成「必须两个人同意」,而那是仪式,不是机制。真正让它管用的是:第二个人失败的原因,和第一个人不一样。
有三条性质在干这件事,而古典的职责分离白捡了这三条——因为两方是扮演不同角色的两个不同的人:
- 错误不相关。复核方的错误不是制单方的错误。一个疲惫的申请人与一个粗心的审批人,在不同的时刻、针对不同的发票、因为不同的原因失手。
- 信息不同。审批人握有申请人没有的东西——预算、供应商历史、「这家供应商上个季度已经终止合作」这件事。复核添加的是一个事实,而不只是又看了一眼。
- 动机不同。串通需要两个人都决定串通,而每个人都有东西可输。击穿这项管控的代价,是一场合谋,而不是一次失误。
现在把复核方换成一个智能体。仪式完好无损——有两个步骤、两行日志、回执里有两个身份。而上面三条性质现在都成了悬而未决的问题,且默认答案都很糟。这和引用监视器那套检验抓到的是同一种失败,只是换了个样子:被管控的东西能影响的管控不是管控,而一个读着同一份上下文的第二个智能体,正受着影响了第一个的那个东西的影响。
说难听一点:如果你的复核方是对同一个模型的第二次调用,外加一句「请仔细复核」的提示词,那你并没有加上一项管控。你加的是一个采样步骤——而集成(ensemble)那一支的文献说得很明白:相关的样本只能买到次线性的提升,见智能体辩论与集成,那里「多样性」是承重变量。一项行为像集成的管控,就该按集成来做预算。
四种耦合,而你大概带着其中三种。
独立性不是只有一种失败方式。它沿四条轴失败,每条单看都容易审,而合起来几乎从没被审过——因为每一条都归不同的团队。
- 共享模型。同一套权重,或同一个家族,或仅仅是训练数据有重叠。自我偏好是可测的:让一个 LLM 去评判候选输出,它会偏爱自己的生成——也就是说,制单方的输出被批准的难度,系统性地低于一个陌生者的输出。这与复核方存在的目的正好相反,见评判器校准与元评估。
- 共享上下文。复核方拿到的是制单方的对话记录、计划或摘要。制单方读过的一切,复核方现在也读了,包括不可信的那部分。这是最要紧的一种耦合,也是最像「尽职」的那一种。
- 共享权限。一份凭据、一条网络路径、一个浏览器配置文件。如果制单方不经复核方就能执行该动作,复核方只是顾问;如果复核方的批准只是制单方自己循环里置上的一个布尔值,那第二个签名是由第一方产出的。这件事藏在环境权限里。
- 共享目标。两个智能体都按「把任务做完」来打分。一个隐含目标是「让流程别卡住」的复核方会收敛到批准——与人类复核者在「另一个选项是周五弄坏点什么」时的表现一模一样。
# Score your own maker/checker pair. Any ✗ is a coupling you are carrying. model context authority objective same call, "review" ✗ ✗ ✗ ✗ 2nd call, same model ✗ ✗ ~ ✗ 2nd model, same ctx ✓ ✗ ~ ✗ 2nd model, task only ✓ ✓ ~ ✗ policy engine + rules ✓ ✓ ✓ ✓ human reviewer ✓ ~ ✓ ~ ✓ independent ~ partially ✗ coupled
把中间那几行读仔细些,因为多数生产系统就坐在那儿。换上另一家厂商的模型,修好了一列,留下三列。人们以为自己搭出来的是最后一行;实际搭出来的通常是第二或第三行。
算一算,以及随之而来的那个反转。
给它配上数字,因为直觉错在一个很具体的方向上。设制单方提出一个坏动作的概率为 p。设复核方在独立时抓住一个坏动作的概率为 q。再设 c 为复核方因为和制单方同样的原因而失手的概率——它读了同一张被投毒的页面、它共享那个盲区、它把自己的输出认成了正确答案。
# escape rate = p × (1 − (1 − c) × q) p = 0.05, q = 0.80
c = 0.0 escape 1.0% risk ÷ 5.0 the control you costed
c = 0.3 escape 1.8% risk ÷ 2.8
c = 0.5 escape 3.0% risk ÷ 1.7
c = 0.9 escape 4.6% risk ÷ 1.1 the control you have
c = 1.0 escape 5.0% risk ÷ 1.0 ceremony
由此有两件事。第一,c 压过 q:一个平庸但独立的复核方胜过一个优秀但相关的复核方,所以花在「把复核方做得更聪明」上的力气,通常不如花在「把它做得更不一样」上。第二是那个反转,而它正是这一页存在的全部理由。
c 不是常数。对制单方的随机失手——写错的账号、凭空捏出的字段名——它接近零;而恰恰对你真正害怕的那些失败,它接近一。一份被检索到的文档里的注入指令,两方都会读到,对两方都有说服力。一份错述了工具行为的工具描述,会同时误导两方。一个在长程运行中漂移掉的目标,已经漂在复核方正在读的那份记录里了。于是这项管控的有效性与威胁的严重程度呈负相关:它对错别字管用,对对抗性输入则烟消云散。
这就是为什么「我们有一个复核智能体」不是对提示注入的回答,也是为什么提示注入防御那一摞把重量压在隔离与出站上,而不是压在「再听一个意见」上。一个与制单方共享上下文的复核方,是一台「事故探测器」,你就该按这个来给它做预算。
不一致率就是那个指标,而它有一条可审计的下界。
你没法直接测 c。你能测的是复核方多久一次与制单方意见不合,而这个数字的下界,由你对这一对所声称的东西决定。如果制单方 5% 的时候是错的,复核方能抓住其中的 80%,而复核方还会对 2% 的好动作提出异议,那你应该观测到大约 0.05 × 0.8 + 0.95 × 0.02 ≈ 5.9% 的不一致。要是观测到的是 0.3%,那你那两条信念里恰好有一条是假的:要么制单方远比你的错误预算所说的更好,要么复核方并没有在复核。
# The inequality an auditor can run in one query. observed_disagreement ≥ claimed_error_rate × claimed_catch_rate # If it fails, one of the three numbers is wrong. Find out which # before you find out in an incident.
有三处细化能让这个数字可信。把复核方的判定记在制单方的动作落定之前,否则你测的是后见之明。把两个方向分开——「拦下了且确实是坏的」与「拦下了但其实没问题」——因为一个被调到几乎零误报的复核方,就是一个被调到几乎零命中的复核方。还要刻意给数据流下种:以低比率注入已知为坏的提案,确认复核方会拒绝它们,这是唯一能拿到「命中率」而不是「一致率」的办法。智能体产出的复核队列里那个「98% 批准率」的论证出于同一套推理,而给样本量定尺寸的统计学在评测方差与统计效力。
留意那个看起来最健康的退化情形。一个不一致率 0%、批准率 100% 的复核方,会产出一张干干净净的看板、一条完整的审计链,以及完全不存在的管控。记录里没有任何东西能把它和一个正常工作的复核方区分开——这正是上面那条下界该待在你的监控里、而不是待在一次季度复盘里的原因。
在三条轴上买独立性,从最便宜的开始。
独立性是可以买的,而便宜的那几笔恰恰不是团队会去伸手拿的。按「每单位力气换来的保障」排序:
- 给不同的信息,而不是更多的信息。把提议出的动作及其参数交给复核方——解析后的 SQL、收款方与金额、那段 diff、目标主机——而制单方的对话记录一概不给。读不到那份注入文档的复核方,就不可能被它注入。单这一项改动对
c的撬动超过任何换模型的做法,而且它让复核方变得更便宜而非更贵。 - 在不变量允许的地方换机制。你想听第二个意见的那些事,大多并不是意见。「这个收款方出现在已核准供应商表里」「这段 diff 没有碰
infra/下的任何文件」「这个合计与各行明细相符」都是规则,而面对一个改不动它的对手,一条规则的c = 0。把模型花在剩下的残差上——见面向智能体的策略即代码。 - 换权限,并在制单方之外强制。复核方的判定必须经由一条制单方控制不了的路径抵达副作用。如果批准是制单方自己状态里的一个字段,那么一次重试或一次恢复会把它再生产一遍——这正是批准被重放、而不是被重新挣得的方式。
- 换模型,放最后。有用、真实,而且是四者里最贵的。它处理的是 STEP 2 那张表里的一列,却往往被当成处理了全部四列。
每次都会碰到的两条设计笔记。其一,给复核方一个会终止的「拒绝」,而不是一条评论:一个会变成「供制单方权衡的建议」的判定,已经塌回成一方了。其二,让复核方的输出是结构化、枚举化的,而不是散文,这样一件事被拦下的原因就可被机器读取、也可跨运行比较——这是结构化拒绝与理由链里的论证,也是决策回执与审计那份回执的输入。
拿不到独立性的时候,就别再把这项管控算进去。
有些检查确实需要制单方的完整上下文才有意义——「这是不是针对工单里描述的那个 bug 的正确修法」没法只看 diff 来判断。在那里,独立性出任何价钱都买不到,而诚实的做法是把这件事写下来,而不是买第二个模型让自己好受些。
- 在管控说明里点出那个耦合。「由一个独立智能体复核」与「由读着同一份记录的第二个模型复核」是两项不同的管控,残余风险也不同。你的问责记录应当写清你手上是哪一项。
- 把残差推给一个不需要「看懂」的机制。如果没有任何东西能独立判断这个动作,那就改为约束损害:一个花费上限、一个范围限制、一条只走可逆路径的通道。爆炸半径是那项不依赖任何人判断正确的管控。
- 把人留给相关的那些情形,而不是频繁的那些。人类复核者昂贵且不完美,但面对一份被注入的文档,他们的
c确实很低——因为他们不会去执行自己读到的指令。按耦合而不是按金额来路由,这是人在环中里的分诊论证。 - 让批准被重新挣得,绝不重放。把判定绑定到那个确切的动作上——参数取哈希、短过期、一次性。一个能在重试、恢复或参数变更之后还活着的批准,从来就没有与制单方分离过。
今天就做这件事:挑出你那个后果最严重的智能体动作,诚实地把 STEP 2 那张表填上一行。如果「上下文」那一列是 ✗——它几乎肯定是——那就先试最便宜的修法,只把解析后的动作交给复核方、不给任何对话记录,再看它的不一致率有没有动。然后把 STEP 4 那条不等式放到和批准率同一张看板上,因为这两者一旦分叉,你手上就是一项「还在记日志、已经不工作」的管控。至于这样做之后仍然暴露着的那些情形,检测智能体被攻破讲的是那些不依赖「第二个意见」的信号。