Command Palette

Search for a command to run...

0

对抗式 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 的角色是一个没有上下文的新入职工程师。它只能看到当前这份文档,没有项目背景,没有团队知识。

新人会做三件「蠢事」:

  1. 逐字执行:文档写什么就做什么,哪怕明显不合理
  2. 不会脑补:不会自动补全缺失的步骤或隐含的依赖
  3. 报告矛盾:如果文档的 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 分布(过滤后)

严重度数量占比
CRITICAL10.7%
HIGH9871.0%
MEDIUM2618.8%
LOW139.4%

Phantom Risk 分布

风险等级数量占比
HIGH phantom risk9861.6%
MEDIUM phantom risk4427.7%
LOW phantom risk1710.7%

初始 CRITICAL 发现 → 实际跟踪

最初三个 Persona 发现了 3 个 CRITICAL 问题

  1. bash skill 包含硬编码密码 — Saboteur 发现了一个写死在 skill 里的数据库密码
  2. deploy skill 的 rsync 命令可被路径穿越 — Security Auditor 发现 rsync 的目标路径拼接了未经校验的用户参数
  3. config-wizard skill 静默覆盖用户配置 — 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 中检测到路径遍历风险用的手段是关键字搜索——在文本中匹配 ../rsyncuser_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

在对抗中进化,在审查中改进。这是我们目前最好的答案。