# 多 Agent 对抗审查作为工程实践

> 把“找问题”与“核实问题”拆成两段对抗流程：N 个审计员按维度并行提出带 file:line 与失败场景的 finding，再由独立核验者以证伪为目标逐条复核，只留 CONFIRMED。实证：一次全项目审计 6 审计员产出 27 条确认问题、修复 24；一次会话级审查确认 7 个真问题、其余证伪。并讨论成本、适用边界与“双方都是 LLM”的固有盲区。

- Canonical (HTML): https://kaguc.com/blog/adversarial-review-zh/
- Date: 2026-07-29


*English version: [Multi-agent adversarial review as an engineering practice](/blog/adversarial-review/)。“LLM 应用工程”系列第 13 篇。*

## 问题：LLM 产出的 finding，典型失效是“像真的”

用 LLM 审查代码，产量从来不是瓶颈——让它对一个代码库列五十条“潜在问题”毫不费力。瓶颈在另一端：LLM finding 的典型失效模式是 **plausible-but-wrong**。行号具体、失败叙事完整、修复建议成套，但拿到代码里对质就散了：护栏其实已经存在、所谓的竞态路径根本走不到、“泄漏”的资源在另一处早被回收。

没有核验环节的审查会被假 finding 淹没，而假 finding 的代价是双向的：要么逐条人工证伪，审查省下的时间全数赔回；要么开始整体忽略报告，真问题跟着陪葬。后者更常见也更致命——一份真假混杂的报告，工程信息价值趋近于零。

另一个独立的问题是广度：单一视角的 review——无论人还是单个 agent——无法在安全、并发、计价、跨端一致性这些维度上同时保持警觉。

两个问题指向同一个结构性解法：把“找问题”和“核实问题”拆成两段目标相反的流程——找的一方求全，核的一方求真。本文以一个生产项目的两次实跑（一次全项目审计、一次会话级审查）加一组翻案数据为实证，拆解这套流程。

## 结构：按维度并行发现，逐条对抗核实

**第一段：N 个审计员并行，每人只守一个维度。**全项目审计用了 6 个（后端 core、API-DB-安全、LaTeX-verify-distill、前端、测试盲区、文档漂移）；会话级审查用了 4 个（并发/runs、安全、前端回归、数据完整性）。按维度切分的意义不在并行提速，而在**视角强制**：一个只看并发的审计员，不会被功能正确性分走注意力。

finding 有格式纪律：每条必须落到 `file:line` + 具体失败场景 + 修法。这一条是后面一切的前提——它把 finding 从“感觉不对”变成**可证伪命题**。

**第二段：每条 finding 交给独立核验者。**核验者不是产出这条 finding 的那个 agent，不带着它的推理过程进场；指令是**默认怀疑、专攻证伪**——去代码里找这条 finding 不成立的证据。裁决三值：CONFIRMED / uncertain / dismissed，只有 CONFIRMED 进入修复队列。

独立性是必需而非可选。同一个 agent 自查自纠时，自己的 finding 就是上下文里的沉没成本，它会为之辩护；把“翻案成功”设为核验者的成功判据，激励才对齐。这与系列第 9 篇讨论的[评委效度](/blog/judge-validity-zh/)是同一件事的两面：核验者本身就是一种评委，它的效度要靠“确实翻得了案”来证明——下文有数据。

```mermaid
flowchart TD
    A[代码库 / 改动集] --> B1[审计员：安全]
    A --> B2[审计员：并发]
    A --> B3[审计员：……共 N 个维度]
    B1 --> F[finding：file:line + 失败场景 + 修法]
    B2 --> F
    B3 --> F
    F --> V{独立核验者：默认怀疑，专攻证伪}
    V -->|CONFIRMED| T[按严重度分级]
    V -->|dismissed| X[丢弃]
    V -->|uncertain| U[存疑留档]
    T --> R[修复，或显式 defer 并留理由]
    R --> P[回归测试确认无回归]
```

## 两次实测

|  | 全项目审计 | 会话级审查 |
|---|---|---|
| 对象 | 整个代码库（后端+前端+文档+测试），重点是更广/更老的部分 | 单会话改动：44 文件 / 1467 行，含一次核心路径重构 |
| 审计员 | 6，按子系统/维度 | 4，按风险维度 |
| CONFIRMED | 27（5 高 / 14 中 / 8 低） | 7 |
| uncertain / dismissed | 0 / 2 | 其余全部证伪 |
| 处置 | 修 24，显式 defer 3 | 修 7 |
| 修复后验证 | pytest 243 passed（+6 新测）、vite build 通过 | pytest 18 passed + 定向实测 |

数字之外，更值得看的是**问题的类型**——确认清单里有相当一部分，靠单人通读 diff 很难抓到：

- **409 并发护栏的 TOCTOU 竞态**（会话级）：“检查是否已有 running run”与“创建 run”非原子，并发请求可以双开。修法是把判断、创建、登记句柄整体移进 `runs._LOCK`；修复以实测收口——并发两请求返回 `[200, 409]`。
- **chat 计价用了未解析的 provider**（会话级）：`DEFAULT_PROVIDER=anthropic` 且请求省略 provider 时，按 deepseek 的单价计费，约 60× 低估。provider 解析在一处、计价在另一处，单模块 review 两边各自“看起来都对”。
- **Anthropic 流式 `max_tokens=32000` 而 DeepSeek 是 64000**（全项目，中）：长报告在其中一家静默截断，靠跨 provider 一致性对照才现形；修为统一 `_STREAM_MAX_TOKENS=64000`。
- **出图沙箱环境 POSIX 硬编码**（全项目，高）：Windows 分发包（主平台）上 CPython 子进程因缺 SystemRoot/TEMP 起不来，`make_figure` 必崩。
- **`EXPOSE_RESET_TOKEN` 默认 true**（全项目，高）：任意匿名者可拿重置令牌改密，构成账号接管；改为安全默认 false。

这些问题分别需要并发时序推理、跨模块数据流追踪、跨 provider 对照、跨平台运行时知识——正是“每维一个专职视角”覆盖得到、单人一遍通读容易漏的类别。

处置侧还有一个值得抄的细节：3 条 defer 不是沉默的遗漏，而是**带理由的显式决策**——均为 SaaS-only 问题，在产品出货的 STANDALONE 形态里被 `_no_cloud`/强制 local 中和，硬修反而要动一批非 STANDALONE 的测试语义，故留档待重启 SaaS 时处理。审查的产出不只是修复，还包括这类可追溯的“为什么不修”。

## 核验不是橡皮图章：翻案数据

对抗核验环节是否真的在工作，需要它自己的证据。同一套“发现 → 对抗复核”结构曾用于另一类对象：审计[蒸馏实验](/blog/distill-judgment-experiment-zh/)（系列第 12 篇）产出的写作规则是否真在学“判断”、而非结构模板换皮。共 11 个 agent：每对规则先逐条初审分类，再做对抗式复核，指令同样是默认怀疑，专攻“because 是同义反复 / 结构动作只贴了层薄理由”的条目。

| 对 | 初审判断占比 | 对抗复核后 | 整对结论 |
|---|---|---|---|
| 1 | 87% | 80% | 维持 |
| 2 | 82% | 73% | 维持 |
| 3 | 73% | 67% | 维持 |
| 4 | 82% | 64% | 维持 |
| 5 | 100% | 90% | 维持 |

五对的初审数字**全部被下调**，对抗后均值 0.746；被翻案的条目模式高度一致——动作与结构模板重叠、理由近乎同义反复。整对结论 5/5 维持，但没有一对毫发无损。原报告还主动提示 pair 5 的 0.9 偏乐观（初审 1.0 本身可疑）。

这组数据说明两点。第一，核验者确实在逐条翻案，不是流程装饰。第二，对抗的价值是双向的：在代码审查里它杀假阳性（2 条 dismissed、会话级“其余全部证伪”），在评估任务里它压乐观自评（100%→90%）。

## 成本与适用

成本结构很直白：每条 finding 都要多付一次专门的核验过程，外加编排开销。什么时候值得：

- **大改动面。**会话级那次覆盖 44 文件/1467 行、含一次核心路径重构（“对话并入 run”），改动面超出单人一遍能守住的范围。
- **发布前 / 安全面。**`EXPOSE_RESET_TOKEN`、Stripe webhook 未配密钥即不校验（修为失败关闭：未配密钥返回 503）这类问题，漏过的代价与核验成本完全不成比例。
- **从未系统审过的存量。**全项目审计的目标本来就是“不止新代码，重点是更广/更老的部分”——这些代码不在任何近期 review 的视野里。

什么时候不值得，源记录里有现成判断：单功能实现是“顺序+交织、无 fan-out”，solo 完成即可——审查/审计（广度+独立验证）才是多 agent 工作流的强项。推论到小改动：几十行的 diff 上，人工证伪三条 finding 比编排一轮六审计员流程快得多，核验开销直接超过收益。

## 局限

1. **同源盲区。**核验者与被审代码的作者是同一个（或同族）LLM。对抗结构消除的是立场偏差——不再为自己的产出辩护——但不扩展能力边界：双方共同看不见的问题类别，核验环节同样看不见。它替代不了异源信号——真实运行、并发实测、人的领域知识。
2. **“0 uncertain”不等于“0 漏网”。**27 条 CONFIRMED、0 uncertain 度量的是精度侧：提出来的问题几乎都是真的；它对召回侧——没被任何审计员提出的问题——没有任何陈述。这次审计本身就确认了三条测试盲区：审计能找到测试的洞，却没有工具度量审计自己的洞。
3. **核验深度本身是抽样的。**翻案实验的结论页诚实注明：只实证抽查了 pair 4 的源文件，其余依赖子 agent 的标注。核验也可能是浅的，浅核验盖出的 CONFIRMED 应打折扣——把“核验做了”当成“核验做实了”，是这套流程最诱人的自我误导。
4. **结构依赖 finding 纪律。**`file:line` + 失败场景 + 修法把 finding 变成可证伪命题；放松成“某模块可能有问题”，核验者无从对质，整个流程退化为两轮意见交换。

