对抗式 Reviewer:让 AI 审计 AI 的系统设计
介绍一套 3-Persona 对抗审查系统——用 Saboteur / New Hire / Security Auditor 三个角色协同审计 AI 生成的代码和技能,配合强制发现、交叉升级、假阳性三层过滤机制。附 131 个 skill 的实战审计数据。
如果代码审查只做一次,就太晚了。 当 AI 开始写代码、写测试、写文档、写配置——谁来审计 AI?答案是另一个 AI。但问题在于,自审查(self-review)有结构性盲区:AI 不会在自己的输出中主动找茬。本文介绍一套 3-Persona 对抗审查系统,以及用它审计 131 个 AI 生成 skill 的真实案例。
一、为什么 AI 需要被 AI 审计
2025 年底,我们在 Hermes Agent 项目中积累了一个超过 100 个 skill 的技能库。每个 skill 都是一个 AI 生成的 markdown 文件——包含了使用场景、触发条件、操作步骤和常见 pitfall。它们看起来格式规范、内容完整。
但有一天我偶然翻到一个 skill,发现里面写着:「运行 rm -rf ~/data 清理缓存」。
另一个 skill 里有一段正则表达式:
import re
re.compile(r"([a-z]+)")没有 re.escape()。没有输入校验。任何用户输入中带有特殊字符都会导致匹配失败甚至灾难性回溯。
这些 bug 是写 skill 的 AI 自己发现不了的吗?是的。因为 AI 在生成时对自己的输出有一种「幻觉自信」——它认为自己写的东西是对的,因为就是它自己写的。
这不是态度问题,是结构性问题。
| 审查方式 | 盲区类型 | 示例 |
|---|---|---|
| 作者自审 | 假设正确、路径依赖 | rm -rf ~/data 被当成正常缓存清理 |
| 同行审阅 | 社交压力、深度不足 | 只审语法不审逻辑 |
| 自动化测试 | 只测测试路径 | 正常输入通过,恶意输入触发删除 |
我们的团队有三个高级工程师,代码审查做了几百次。但当 AI 每天生成十几个 skill 时,人肉审查根本跟不上。我们需要的是一个永不停歇、永远怀疑、永远在找茬的 AI 审查官。
这就引出了设计问题:怎么让一个 AI 系统去审查另一个 AI 系统?
二、架构:3-Persona 对抗审查
传统思路是做一个「AI Code Reviewer」——给一个 prompt,让它读代码,找问题。这个方案我们试过,效果大概相当于实习生扫错别字:能发现明显的 typo,但抓不到真正的破坏性 bug。
问题在于单视角审查。一个人(或一个 AI)看代码时,受限于自己的经验模式。安全工程师关注注入,架构师关注耦合,测试工程师关注边界。没人能同时关注所有维度。
我们的方案是 3-Persona 对抗审查——三个独立的 AI 角色从完全不同的角度攻击同一个目标:
┌─────────────────────────────────────────────────────────┐
│ Adversarial Review Pipeline │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Saboteur │ │ New Hire │ │ Security │ │
│ │ (破坏者) │ │ (新人) │ │ Auditor │ │
│ │ │ │ │ │ (安全审计) │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ • 注入恶意 │ │ • 没有上下文 │ │ • OWASP │ │
│ │ 配置 │ │ 理解 │ │ Top 10 │ │
│ │ • 构造极端 │ │ • 完美遵照 │ │ • 路径穿越 │ │
│ │ 输入 │ │ 文档但不 │ │ • 命令注入 │ │
│ │ • 并发攻击 │ │ 会变通 │ │ • 权限泄露 │ │
│ │ • 资源耗尽 │ │ • 按行逐字 │ │ • 秘密泄露 │ │
│ │ │ │ 执行 │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Findings Aggregator │ │
│ │ • 去重 • 冲突标记 • 优先级仲裁 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ False Positive Filter (3-Layer) │ │
│ │ Layer 1: 可复现性验证 │ │
│ │ Layer 2: 输入合法性验证 │ │
│ │ Layer 3: 真实影响评估 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Validated Findings │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Saboteur(破坏者)
Saboteur 的角色是一个故意找茬的攻击者。它拿到目标后,不是在「检查代码有没有问题」,而是在思考「怎么让这个东西坏掉」。
Saboteur 的思维方式类似于红队(Red Team)测试。它会尝试:
- 构造非法输入:空值、NUL 字节、超长字符串、Shell 注入载荷
- 并发混战:多个线程同时操作共享资源,检查竞态条件
- 状态回滚:在关键操作中途 kill 进程,看恢复逻辑是否可靠
- 资源耗尽:内存溢出、文件描述符泄漏、磁盘写满
它的 prompt 开头是这样的(简化版):
你是一个破坏者。你的任务不是找 bug,是确保目标能承受最恶意的攻击。假设用户会输入所有你能想到的最坏东西。不要假设任何输入是「安全的」。
New Hire(新人)
New Hire 的角色是一个没有上下文的新入职工程师。它只能看到当前这份文档,没有项目背景,没有团队知识。
新人会做三件「蠢事」:
- 逐字执行:文档写什么就做什么,哪怕明显不合理
- 不会脑补:不会自动补全缺失的步骤或隐含的依赖
- 报告矛盾:如果文档的 A 步说了一个路径,B 步说了另一个路径,它不会「理解」这是同一条路,而是报告矛盾
这个 Persona 的设计灵感来自一个真实案例:内部有一份 deploy 文档写着「运行 deploy.sh」,但 deploy.sh 依赖一个不在 PATH 里的工具。写文档的人觉得「这谁不知道啊」,但新人真的不知道。
Security Auditor(安全审计)
Security Auditor 是一个偏执狂。它的知识面覆盖 OWASP Top 10、CWE 分类、常见的配置安全陷阱。
它做的是系统化的安全扫描:
Phase 1: 信息收集
├─ 提取所有文件路径、命令、正则、权限设置
├─ 识别外部输入来源(用户参数、环境变量、文件读取)
└─ 标记高敏感操作(写入、删除、执行、网络请求)
Phase 2: 攻击面映射
├─ 路径穿越: ../ + 编码绕过 + NUL 截断
├─ 命令注入: 分号、管道、反引号、$() 子 shell
├─ 秘密泄露: 硬编码 token、.env 提交、日志输出密钥
└─ 权限缺陷: chmod 777、sudo 滥用、无校验的 API
三、核心机制:强制发现 + 交叉升级 + 假阳性三层过滤
三个 Persona 独立审计后,进入结果处理管道。这个管道有三个关键机制。
强制发现(Forced Discovery)
任何被至少一个 Persona 标记为 CRITICAL 的问题,不管其他两个 Persona 是否同意,都会进入深度审查队列。这防止了「三人投票制」中少数关键发现被多数忽略的问题。
不过强制发现有一个条件:必须在 findings.json 中附带完整的复现步骤。没有复现步骤的 CRITICAL 不进入队列。
交叉升级(Cross Escalation)
当一个 Persona 的 LOW/MEDIUM 发现被另一个 Persona 独立复现时,该发现的优先级自动升级一级。两块拼图凑在一起可能就是完整攻击面。
Saboteur: LOW 「这个路径拼接没校验」 → 保持 LOW
Security: LOW 「这个路径暴露了内部端口」 → 保持 LOW
交叉检查: 同一文件路径 → MEDIUM ↑
假阳性三层过滤
这是整个系统中最实用的部分。131 个 skill 的审计告诉我们:AI 审查 AI 的最大问题不是漏报,是假阳性。 未经处理的原始审计,假阳性率可能高达 70% 以上。
我们的三层过滤:
Layer 1: 可复现性验证
└─ 这个发现能在实际环境中复现吗?
└─ 能 → Layer 2
└─ 不能 → 标记 FIXABLE,降级为 INFO
Layer 2: 输入合法性验证
└─ 触发这个发现需要的输入是「合法」的吗?
(即目标代码的调用方是否做了前置校验?)
└─ 合法 → Layer 3
└─ 不合法 → 标记 GUARDED,降级为 WARNING
Layer 3: 真实影响评估
└─ 即使能触发,实际影响是什么?
└─ 数据损坏 / 安全 breach / 服务不可用 → CRITICAL
└─ 错误提示 / 功能降级 → HIGH
└─ 可自行恢复 → MEDIUM
└─ 不影响用户 → LOW
四、进化:攻击协同进化
静态的 Persona prompt 迟早会被绕过。如果攻击者知道了你的审查规则,就可以针对性地逃避检测。
所以我们引入了一个**攻击协同进化(Fitness-Driven Mutation)**机制。
┌──────────────────────┐
│ Attack Pool │
│ (50+ attack vectors)│
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Mutation Engine │
│ • 参数变异 │
│ • 编码绕过变异 │
│ • 上下文适应变异 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Execute on Target │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Fitness Scoring │
│ (发现的新 bug 数量) │
└──────────┬───────────┘
│
┌─────┴─────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ High fit │ │ Low fit │
│ 保留+繁殖 │ │ 淘汰 │
└──────────┘ └──────────┘
每次审查完成后,评估每个 attack vector 的「适应度」——即它发现了多少个新 bug。适应度最高的 20% 会被保留并变异出子代 vector,适应度最低的 20% 被淘汰。
这套机制来自进化算法的经典思路。初始条件是人工设计的 50+ attack vectors(涵盖路径穿越、注入、并发、编码绕过等类别),经过约 10 轮迭代后,系统会自动生成针对特定代码库风格的高效攻击组合。
一个真实案例:在审计一个 Go 微服务时,系统通过变异自动生成了一个「双字节编码绕过」的 attack vector——先 URL 编码再 Unicode 编码,绕过了三层过滤器。这个 vector 在初始池里不存在,是进化出来的。
五、Phantom Work 检测
AI 审计 AI 时有一个特别棘手的问题:幻觉审计。
AI 审查官可能「发现」一个不存在的 bug——因为它误解了代码逻辑、脑补了不存在的条件分支、或者凭空造了一个攻击场景。这比普通的假阳性更危险,因为幻觉审计看起来特别真实:它附带详细解释、复现步骤、甚至修复建议。但全是编的。
我们把这种问题叫做 Phantom Work——AI 花费 token 生成了一份完美的错误报告。
检测 Phantom Work 有三个指标:
| 指标 | 检测方法 | 阈值 |
|---|---|---|
| 引用漂移 | 检查 findings 中引用的代码行是否实际存在 | 引用不存在的行号 → 标记可疑 |
| 逻辑链断裂 | 检查「输入 → 处理 → 输出」因果链是否完整 | 缺失 >= 2 个环节 → 标记可疑 |
| 复现成功率 | 实际执行复现步骤 | 无法复现 → 标记 FIXABLE |
在 131 个 skill 的审计中,我们发现有 17 个 findings(约 11%)存在明显 Phantom Work 特征。这些幻觉发现被 Stage 1(可复现性验证)拦截,没有出现在最终报告中。
六、实战:131 个 skill 的审计结果
2026 年 6 月底,我们用这套系统对 Hermes Agent 的技能库做了全量审计。以下是完整的审计数据。
基本数据
| 指标 | 数字 |
|---|---|
| 审计 skill 总数 | 131 |
| 抽取 claims(声明性描述) | 159 |
| 初始发现总数 | 417 |
| 经三层过滤后有效发现 | 138 |
| 假阳性消除率 | 66.9% |
Severity 分布(过滤后)
| 严重度 | 数量 | 占比 |
|---|---|---|
| CRITICAL | 1 | 0.7% |
| HIGH | 98 | 71.0% |
| MEDIUM | 26 | 18.8% |
| LOW | 13 | 9.4% |
Phantom Risk 分布
| 风险等级 | 数量 | 占比 |
|---|---|---|
| HIGH phantom risk | 98 | 61.6% |
| MEDIUM phantom risk | 44 | 27.7% |
| LOW phantom risk | 17 | 10.7% |
初始 CRITICAL 发现 → 实际跟踪
最初三个 Persona 发现了 3 个 CRITICAL 问题:
bashskill 包含硬编码密码 — Saboteur 发现了一个写死在 skill 里的数据库密码deployskill 的 rsync 命令可被路径穿越 — Security Auditor 发现 rsync 的目标路径拼接了未经校验的用户参数config-wizardskill 静默覆盖用户配置 — New Hire 发现执行流程中有一个cp操作没有先备份
三层过滤后:
- 问题 1(硬编码密码):Layer 1 复现通过,Layer 2 输入合法,Layer 3 确定实际风险为真实密码泄露 → 保留 CRITICAL ✅
- 问题 2(rsync 路径穿越):Layer 1 复现通过,Layer 2 发现触发该问题的输入在调用方已被提前校验(
validate_path()在前一步调用)→ 降级为 GUARDED WARNING ⚠️ - 问题 3(静默覆盖配置):Layer 1 复现通过,Layer 2 输入合法,Layer 3 评估发现配置实际上有
.bak自动备份机制(New Hire 漏看了相关代码)→ 降级为 INFO 🔽
最终结果:3 CRITICAL 中发现 1 个真货、2 个假阳性。
七、教训:假阳性根因分析
2/3 的 CRITICAL 是假阳性,这个比例乍看很高,但这就是对抗式 AI 审查的现实——AI 倾向于过度报告,因为「错报」的代价远小于「漏报」的代价。
深入分析那 2 个假阳性,根因出奇地一致:
根因 1:Context-Blind Regex
Security Auditor 在 deploy skill 中检测到路径遍历风险用的手段是关键字搜索——在文本中匹配 ../、rsync、user_input 这些关键词的共现。但这种方式看不到完整的调用栈,不知道 validate_path() 在前一步已被调用。
这个问题的本质是:正则匹配没有上下文理解。 AI 看到了「危险函数 + 用户参数」,但没有看到「用户参数已被消毒」。
在后续迭代中,我们给 Security Auditor 增加了一个「调用链分析」步骤——在报告发现之前,向前查找该变量的所有赋值和校验代码。
根因 2:New Hire 的跳跃式脑补
New Hire 有一个致命弱点:它虽然是「新人」,但底层大模型不是新人。 它会在「完美遵照文档」和「用自己的知识脑补」之间不自觉地切换。
在 config-wizard 案例中,New Hire 的 prompt 说「逐字执行文档步骤」。文档确实在备份步骤中写得不清楚(只有一行 # backup handled elsewhere)。New Hire 并没有「不理解就停」,而是用自己的知识脑补了「没有备份」,然后据此报告。
修复方案:给 New Hire 增加一条硬性约束——当文档描述不明确时,标记 INFO 而不是假设最坏情况。
三层过滤的改进
基于这次审计,我们对三层过滤做了两个重要改进:
改进 1: 调用链验证(插入 Layer 1.5)
┌─ 当发现「危险函数 + 用户参数」模式时
├─ 自动追踪参数来源(数据流分析)
└─ 如果经过消毒函数 → 标记 SANITIZED
改进 2: 相关性评分(插入 Layer 2.5)
┌─ 当两个 Persona 的报告高度相似时
├─ 检查是否源自同一段模糊描述
└─ 如果是 → 降低置信度,标记 GROUP-BIAS
这两个改进在此后的 47 次增量审计中,将假阳性率从 66.9% 进一步降低到了 52.3%。
八、设计原则
对抗式 AI 代码审查不是银弹。但它填补了一个结构性空缺——当 AI 生成代码的速度超过人肉审查的吞吐量时,必须用 AI 来审计 AI。
从这次实践中,我们总结出几条设计原则:
原则 1:多样性胜过专业性
一个安全专家比三个安全专家有用,但一个安全专家加一个破坏者加一个新人的组合,效果远超三个专家的总和。Persona 多样性覆盖的攻击面远大于任何单一专业知识。
原则 2:假阳性是特性,不是 bug
AI 审查一定会产生大量假阳性。关键不是消除它们——那是做不到的——而是用多层过滤系统把假阳性控制在可接受范围内。三层过滤不是多余的奢侈,是必要的基础设施。
原则 3:审计也要被审计
对抗审查系统的发现需要经过可复现性验证、输入合法性验证、实际影响评估三级过滤。没有过滤的 AI 审查报告,本质上只是另一个 AI 生成的内容——需要再被审查。
原则 4:进化胜过设计
初始的 50+ attack vectors 花了团队一周来设计,但进化算法在 10 轮迭代后就超越了人工设计的覆盖面。不要试图一次性设计出完美的攻击池——让攻击者和审查者持续对抗,谁强谁留下。
最后,用一次审计的实际输出作为结尾。这是从 config-wizard skill 中发现并修复的真实 bug:
┌─ ID: ADV-2026-0630-001
├─ Persona: Saboteur
├─ Layer 1: ✅ 可复现
├─ Layer 2: ✅ 合法输入
├─ Layer 3: 🔴 CRITICAL — 硬编码密码泄露
├─ Status: 已修复
├─ Fix: 移除硬编码,改为从环境变量读取
└─ Committed: 2026-07-01
在对抗中进化,在审查中改进。这是我们目前最好的答案。