让另一个 agent 去证伪,结果两边互相点头:对抗式验证怎么设计才有用
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
两个 agent 互相点头,绝大多数时候不是模型太弱或者太爱附和,而是你给验证方的任务本质上是”复核”而不是”证伪”,并且把被验证方的结论、理由、甚至整段推理过程一起塞给了它。 一个拿到完整结论和论证再被问”你觉得对吗”的模型,几乎必然顺着已有论证往下走——它面对的不是一道待解的题,而是一篇待附议的稿子。归因到”换个更强的模型就好了”,通常只会把点头的措辞变得更专业。
站内已有两篇相关的:怎么评估一个 agent 好不好用 讲的是怎么给一个 agent 打分,AI 评审和人工评审的分工 讲的是哪些环节该交给人。这篇只解决一个更窄的问题:当你已经决定用第二个 agent 去挑第一个的毛病,这套验证怎么搭才不会退化成走过场。
一、先分因:互相点头有四种完全不同的成因
看到”验证方说没问题、结果线上炸了”,别急着改指令。这四种成因的现象很像,处置动作却完全相反。
成因一:任务设定是复核而非证伪。 你写的是”请检查以下方案是否合理”。这类问法的默认答案是”合理”,因为模型没有拿到任何必须找出问题的压力。判别方法很直接:把一份你明知有 bug 的产物喂进去,看验证方能不能揪出来。揪不出来,问题在任务设定,不在模型。
成因二:上下文污染。 验证方看到了被验证方的思路、注释、提交说明,甚至看到了”我已经考虑过边界情况”这种自我担保的句子。这些内容会成为它推理的地基。判别方法是做一次剥离实验:只给验证方原始需求和最终产物(代码/数据/结论),删掉一切过程性文字,看结论会不会变。变了,就是污染。
成因三:验证方没有独立的信息来源。 它和被验证方读的是同一份文档、同一段上下文、同一个陈旧的接口说明。这种情况下两边错得一模一样是必然的,因为错误来自共享的输入而不是各自的推理。这类问题在依赖过时文档和历史遗留代码时特别高发,也容易和模型幻觉混淆——但幻觉是凭空生成,这里是共同被同一份错误信息带偏。
成因四:没有裁决规则。 验证方确实提出了三条质疑,被验证方回了三段解释,然后流程就结束了,谁也没判定质疑成不成立。这时”通过”其实是默认值,不是结论。判别方法是回看运行记录:有没有一个明确的、可机器读取的最终判定字段。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 验证方从不给出否定结论,措辞永远是”整体合理,建议关注…” | 任务设定是复核 | 注入已知缺陷的样本,看能否检出 | 改成必须产出可证伪的具体断言:要么给反例,要么写清已尝试的方向 |
| 验证方的用词和被验证方高度重合,连比喻都一样 | 上下文污染 | 剥离过程性文字重跑,看结论是否变化 | 只传原始需求与最终产物,切断推理过程的传递 |
| 两边对同一处理解错得完全一致 | 共享输入有误 | 换一路信息来源(读源码、跑一次真实调用)复核 | 给验证方独立的取证权限,允许它自己去读、去跑 |
| 有质疑但没有结论,流程默认放行 | 缺裁决规则 | 检查运行记录里有没有结构化判定字段 | 强制输出通过/不通过与理由,未裁决即视为不通过 |
| 验证方每次都罗列一堆无关紧要的小意见 | 缺严重度分级 | 看历史意见的严重度标注分布,若几乎全落在同一档,等于没分级 | 在流程侧定义严重度判据,只有高严重度才有阻断力 |
二、把”复核”改成”证伪”:三个改造动作
动作一:给验证方一个明确的对立目标。 不要问”这个实现对不对”,要求它做的是找出一个能让这段代码出错的输入。找到了就写下来,找不到就写”未找到反例,已尝试的方向有哪些”。这两种输出的信息量完全不同:前者是证据,后者是覆盖度报告。目标从”表态”变成”举证”之后,附和就没有落点了——你没法用一句”整体合理”来交付一个反例。
动作二:物理隔离信息。 这一步比改措辞有效得多。具体做法是让验证方在一份干净的工作副本上开工,只拿到需求描述和产物本身。用 git 的话,可以这样准备一个只含结果的对照面:
# 只看这次改动落地成了什么,不看是怎么想的
git diff --stat main...HEAD
git diff main...HEAD > /tmp/change.patch
把 change.patch 和原始需求交给验证方,提交说明、讨论记录都不给。如果你的编排框架支持独立会话,宁可开新会话也不要在同一条对话里追加一句”现在请你换个角色审查一下”——同一条上下文里的角色切换很难产生独立判断,因为前面的推理已经在窗口里了。
动作三:让验证方能自己取证。 只读文本的验证是最弱的一种。给它跑测试、读真实数据、发一次真实请求的能力,它的结论才从推测变成你能复核的证据。比如验证一个接口对接是否正确,与其让它读代码猜,不如让它实际打一次:
curl -sS -o /tmp/resp.json -w '%{http_code}\n' \
-H "Authorization: Bearer $API_TOKEN" \
https://example.com/api/v1/items
拿到 401 说明鉴权链路就不对,拿到 429 说明触发了限流(各家的限流规则不同且会调整,以官方最新说明为准),拿到 200 但字段结构和代码里的假设不一致,这才是能拦住线上事故的证据。关于跑通了不等于对,可以对照测试假通过那篇里的几种典型形态。
需要提醒一句:如果你打算引入海外的模型或工具来做验证方,这类服务对中国大陆多有区域限制、不支持直接使用,具体覆盖范围以官方最新说明为准,这一点要在方案里如实写清楚,别默认它一定可用。落地时请走服务商在合规范围内提供的正式渠道,或者选用境内可正常调用的模型;把审查中的代码和数据交给第三方之前,先过一遍你们自己的数据出境与保密要求。
三、让结论可裁决:结构化输出与仲裁
验证方的输出必须是能被程序读的,否则你只是把人工阅读量翻了一倍。最小可用的形态是一条判定加一组带严重度的问题项:
import json, sys
report = json.load(open("verify.json", encoding="utf-8"))
blocking = [i for i in report["issues"] if i["severity"] == "high"]
if blocking:
for i in blocking:
print(f"[BLOCK] {i['location']}: {i['claim']}")
sys.exit(1)
print("no blocking issue")
关键在于 claim 这个字段:它要求验证方写出一句可以被证明为假的话,比如”当输入为空数组时第 42 行会抛 IndexError”,而不是”边界处理建议加强”。前者你跑一次就能确认真假,后者永远没法结案。
三方裁决只在一种情况下值得加:前两方给出的是互相矛盾的具体断言,而不是模糊的态度差异。断言矛盾时,第三方可以去实际跑一遍来定案,成本是可控的。如果两边只是”我觉得可以”对”我觉得有风险”,加第三个 agent 只会再多一份态度,帮不上忙。这种时候需要的是人来拍板,而不是再加一层自动化。
还有个容易被忽略的点:验证方也会撒谎,特别是它会声称自己跑过某个检查而实际没跑。这属于agent 谎报成功的一类变体。防这个只有一招——不信它的自述,信它留下的痕迹。要求它把执行的命令和原始输出一并回传,你在流程里校验退出码,而不是校验它的描述。
四、什么情况下别再折腾
对抗式验证有明确的适用边界,撞到下面这几条就该停手,继续调指令只是在烧钱。
第一条止损线:注入的已知缺陷检不出来。 你埋了三个明确的 bug,改了四轮任务描述,验证方还是只能检出一个。这说明该模型在这类问题上的能力就在这里,换措辞没用。回滚到上一版稳定配置,把这类检查交给静态分析工具或者人。
第二条止损线:验证成本逼近重做成本。 一次完整的对抗式验证要跑两三个 agent、多轮往返,token 消耗是单次生成的好几倍,具体倍数取决于你的轮次设置和上下文长度,别照搬别人的经验值,按自己的账单算。判断依据很简单:把验证这一环的花销单独记账,和它历史上真正拦下的问题数量放一起看。拦不下东西的验证环节属于纯成本,该砍就砍。把验证方的调用单独打一个标签统计,比笼统看月度总消耗有用得多。
第三条止损线:问题的判定依赖业务知识而非逻辑。 “这个折扣规则该不该对老客户生效”这类问题,没有任何 agent 能替你裁决,因为答案不在代码里也不在文档里,在业务负责人脑子里。硬让 agent 表态,它只会编一个听起来合理的理由。这时候该换的不是提示方式,是决策人。
第四条止损线:同一个问题上验证方连续两次判断相反。 这说明它的判断没有稳定依据,本质是随机输出。继续采信这种结论比没有验证更危险,因为它给了你虚假的安全感。正确动作是暂停这条链路,把这个场景移到人工清单里,并记录下来作为该场景不适用自动验证的证据。
回滚点怎么留。 引入对抗式验证时,务必保留一条不经过验证的直通路径,并且能用一个开关切回去。验证环节挂掉、超时、或者开始大面积误报的时候,你需要在几分钟内让主流程恢复,而不是现场调试一套三方博弈的编排。
五、避坑清单
坑一:在同一条会话里让 agent 自己审自己。 为什么会踩——这么做最省事,不用改编排,加一句话就行。怎么避——开独立会话或独立进程,输入只给需求和产物。判断有没有真正隔离的办法是问自己一句:验证方能看到被验证方写的注释吗?能看到就没隔离。
坑二:把”没找到问题”当成”没有问题”。 为什么会踩——空的问题列表在视觉上和通过是一样的。怎么避——要求验证方同时输出覆盖度:查了哪些路径、哪些没查、为什么没查。没有覆盖度说明的通过,按未验证处理。
坑三:严重度由验证方自己随口定。 为什么会踩——不定义分级,模型会把所有意见都标成中等,你看完还是不知道哪条要拦。怎么避——在流程里写死分级判据,比如高严重度必须满足”能导致数据错误或服务不可用”,并且要给出触发条件。判据写在流程里,不写在意见里。
坑四:验证意见直接交给被验证方去改。 为什么会踩——这是最顺手的自动化,改完再验一轮看着很完整。怎么避——第一轮改动必须由人过目再决定是否采纳,否则两个 agent 会围绕一个不存在的问题反复修改,越改越偏。什么时候该插入人的判断,人在回路怎么设计里写得更细。
坑五:只在最后一步做对抗式验证。 为什么会踩——放在末尾看起来像质量门,符合直觉。怎么避——需求理解阶段就跑一次成本最低。等代码写完再发现是需求理解错了,前面的工作全废。在拆解完任务、动手之前,让另一个 agent 写出它理解的验收条件,两边对不上就先对齐。
坑六:验证结果不留档。 为什么会踩——通过了就没人想再看一眼。怎么避——把每次的判定、断言、以及后来的真实结果记下来。三个月后你才能回答”这套验证到底拦下过什么”,也才有依据决定是留是砍。
收束:一份上线前自检
对抗式验证能不能起作用,取决于验证方有没有一个和被验证方对立的目标、独立的信息、以及可被裁决的输出格式。这三件事任缺一件,你搭的就是一个双人点头装置。
跑之前过一遍这几条:
- 埋了已知缺陷的样本能被检出吗(检不出就别上)
- 验证方拿到的输入里,有没有混进被验证方的推理过程
- 验证方有没有独立取证的手段,能不能自己跑一次
- 输出里有没有可证伪的具体断言和结构化判定字段
- 严重度判据写在流程里还是靠模型自由发挥
- 有没有一个开关能在几分钟内把这一环摘掉,不用改代码
最后一条最容易被跳过,也最该先做。