对抗审查 + Property-Based Testing:让 AI 帮你找代码里的逻辑 bug
把 Anthropic 的 agentic-pbt 研究整合进我们的对抗性代码审查体系:用 Hypothesis 自动推导 invariant 并搜索反例,补上安全/设计/运行时审查看不到的纯逻辑 bug。
把 Anthropic 的 agentic-pbt 研究整合进我们的对抗性代码审查体系:用 Hypothesis 自动推导 invariant 并搜索反例,补上安全/设计/运行时审查看不到的纯逻辑 bug。
一、209 个测试全绿,但配置全丢了
上个月给 headroom 的 opencode registrar 做 code review。209 个 pytest 全绿,30 项 E2E 测试全过。看起来可以合并了。
然后我用攻击性输入测了一下——构造了一个包含 JSONC 注释的配置文件,调一下 registrar API。
整个配置被清空了。
原因是这样的:_read_json 用了 json.loads(),JSONC 文件里只要有 // 注释行,解析就失败。失败不抛异常,而是静默返回 {}。然后 _write_json 拿到这个空 dict,开开心心地覆盖了整个 config 文件。
provider 配置、model 配置、MCP 配置——全没了。
三个 bug 都是同一个套路发现的:正常测试用例只测正常输入,对抗性输入才能暴露真正的漏洞。 这件事后来被写进了我们的 code review skill 的第 28 号 pitfall:100% 测试通过 ≠ 没有 bug。
从那以后,我们形成了一套对抗审查的工作流——不读代码、不看 diff,直接用并发压力、恶意输入、崩溃恢复去攻击代码。这套方法确实有效,从 config parser 的静默损坏到 flock+os.replace 的锁丢失,都是这么抓出来的。
但它有一个盲区。
二、对抗审查抓不到的东西
我们的对抗审查大概长这样:
Phase 1: 基线测试(确认现有测试通过)
Phase 2: 攻击 —
• 并发混战(8 线程 × 20 次读写)
• 恶意输入(空值、NUL 字节、路径穿越、Shell 注入)
• 崩溃恢复(kill -9 mid-write)
• 配置变异(JSONC 注释各变体、截断、空文件)
• E2E 真跑(fork+exec,不 mock)
Phase 3: 修复 + 验证
Phase 4: 死代码清理 + 模式一致性检查
这套组合拳能抓到四类 bug:并发竞态、崩溃恢复失败、恶意输入导致的路径穿越/注入、设计缺陷(比如配置传播不一致)。实际上最近所有的高价值 bug——config 静默损坏、flock 锁丢失、空 command 导致的 IndexError——都是这套方法找出来的。
但它有一个结构性盲区:纯逻辑 bug。
我说的不是 crash,不是注入,不是数据损坏——而是代码逻辑本身就是错的。比如:
numpy.random.wald有时候返回负数,但 Wald 分布的定义域是正数slice_dictionary函数重复返回第一块数据,因为迭代器没有前进item_hash对所有 list 输入返回相同的值,因为.sort()返回的是 None 而不是排序后的列表calculate_label_colors少了一个右括号,返回的 HSL 颜色代码是无效的
这些 bug 的共同特征是:不会 crash,不会损坏数据,不会触发安全漏洞——但就是错的。 并发测试测不出来(它们是确定性的错误),恶意输入测不出来(正常输入就错),崩溃恢复测不出来(不是 IO 操作)。
它们只会在一种情况下暴露:有人明确知道"这个函数应该满足什么性质",然后输入一批数据去验证这个性质。
这就是 property-based testing 做的事。
三、Anthropic 的 agentic-pbt
今年一月,Anthropic 发了篇论文:用 Claude 驱动的 agent 自动给 Python 包写 property-based test,然后用 Hypothesis 框架搜索反例。他们在 100 多个 PyPI 包上跑了 984 个目标,56% 的报告是真 bug,32% 值得提 issue。
这件事跟我们的对抗审查在理念上完全一致——不靠看代码,靠跑测试来找 bug。 但做法不同:我们是人设计攻击场景,他们是 AI 推导数学性质。
他们的 agent 工作流是这样的:
1. 读取目标代码(introspection + 追踪 import 链)
2. 推导"代码声称的 property"
— 从 docstring、类型标注、函数名、调用方用法中找证据
— 只看代码明确承诺的性质,不凭空假设
3. 用 Hypothesis 写 property-based test
4. 跑 pytest(500 个随机示例)
5. 三阶段 triage:
a) 可复现吗?
b) 是合法输入吗?(查调用方有没有做前置校验)
c) 影响真实用户吗?
6. 自省循环:如果测试全过了,检查是不是 property 太弱
(有一次 agent 发现 test 通过了但外面包了 try-catch)
7. 输出标准化 bug report(含复现脚本 + git diff 修复)
这套流程的设计有几个细节我很欣赏。
第一,只看代码"声称"的性质。 不是让 AI 猜这个函数应该满足什么数学关系,而是从 docstring、类型标注里提取代码自己承诺的 contract。"这个函数应该返回排序后的列表"——如果 docstring 没写这句话,agent 就不会去测。这大幅降低了假阳性率——论文里 44% 的假阳性已经不算低,但你要让 AI 自由发挥"这个函数应该满足……"那假阳性率可能翻倍。
第二,自省循环。 这是把 PBT agent 和普通 fuzzer 区分开的关键。传统 fuzzer 只关心 crash,PBT agent 还关心"我的测试是不是太弱了"。论文里举了个例子——agent 写了个 test 通过了,但自省时发现整个 test 被 try-catch 包住了,去掉 try-catch 后测试失败,找到了真 bug。
第三,调用方验证。 找到一个"输入合法但输出错误"的 case 后,agent 还会检查调用方是否对输入做了前置校验。如果所有调用方都保证了输入在某个范围内,那这个"bug"在实际上不会触发——这不是假阳性,而是需要标注"理论 bug,实际被调用方保护"。
他们用这套方法找到了几个很有代表性的 bug:
| Bug | 包 | 状态 |
|---|---|---|
numpy.random.wald 返回负数 | NumPy | ✅ merged |
slice_dictionary 重复返回第一块 | AWS Lambda Powertools | ✅ merged |
item_hash 对所有 list 返回相同值 | AWS CloudFormation | ✅ submitted |
calculate_label_colors 输出无效 CSS | HuggingFace tokenizers | ✅ merged |
全是那种"代码看起来没问题、测试全绿、但就是错的"的 bug。恰恰是我们对抗审查的盲区。
四、怎么整合
发现 agentic-pbt 之后,我想的是:能不能直接拿来用?
好消息是它是 MIT 协议开源的,核心就是一个 Claude Code 命令(hypo.md,200 行 prompt)+ 一个并行 runner(run.py)。坏消息是它是为 Claude Code 写的,我们用的是 Hermes,不是同一个体系。
但它的核心逻辑——"读代码 → 推导 property → 写 Hypothesis test → triage"——是一个纯 AI agent 工作流,跟工具无关。只要把 prompt 适配到 Hermes 的 delegate_task 系统就行。
然后就是"放哪"的问题。三个选项:
A:作为对抗审查的新 Phase 嵌入。 在基线测试之后、并发测试之前跑一轮 PBT。问题是 PBT 贵——每次跑要用 Claude 读代码写 test,一个模块可能烧 $0.5-2。嵌入常规流程的话,每次 review 都跑一遍,性价比太低。而且 PBT 最适合的是有明确 invariant 的库代码(数学函数、序列化、数据结构),不是所有 PR 都有这类代码。
B:作为并行审查的第四个 lens。 我们已有"用尽全力找问题"模式,dispatch 三个并行子代理分别做安全、设计、运行时审查。PBT 天然适合做第四个——Logic Lens。只在深度审查时跑,不浪费 token,而且并行跑不增加总时间。
C:做成独立 skill。 可以单独对第三方依赖跑 PBT,不需要完整的对抗审查。
我选了 B + C。
B 是主要用法——当你跟我说"用尽全力找这个问题"时,四个 lens 同时启动:
├── Security → 路径穿越、注入、env var 攻击
├── Design → 向后兼容、配置传播、调用方同步
├── Runtime → import 链、真实 binary 验证、fallback 正确性
└── Logic → 【新增】PBT invariant 测试
四个子代理跑完回来,合并结果去重,安全 lens 发现的问题如果跟逻辑 lens 的 invariant 违反有关联,自动升级优先级。
C 是灵活用法——想临时审查某个第三方库的函数,直接说"对 requests.get 做 PBT 审查",就 dispatch 一个子代理去干活。
具体实现上,Logic Lens 的 prompt 从 agentic-pbt 的 hypo.md 精简而来,去掉了 Claude Code 特定指令,保留了核心 6 步流程。独立 skill 也差不多,加了目标格式说明和高级选项(调整测试强度、批量审查)。
整个过程改了四个文件,建了两个新文件,花了一下午。
五、这个东西值不值
理性上讲,PBT 有明确的数据支撑——56% 的 bug report 是真 bug,32% 值得提 issue。对值得做深度审查的 PR,加上这一层,逻辑 bug 的漏网率会大幅下降。
但感性地讲,我觉得这件事更大的价值不在数据上。
做对抗审查这一年多,我们积累了一套经验——测试值矩阵(空字符串、.、..、/、NUL 字节等)、并发压力模型(8 线程 × 20 次、register+unregister+force 混战)、崩溃恢复验证(kill -9 + os.replace 原子性检查)。这套经验是硬啃出来的,从 30 个 pitfall 里一个一个踩出来的。
但说到底,这些都是人想出来的攻击面。你只能攻击你想得到的方向。
PBT 不一样。AI 读代码,发现代码自己承诺的性质,然后用随机搜索找反例——这不是"人攻击代码",而是**"代码攻击自己"**。AI 不需要理解并发,不需要理解路径穿越,它只需要理解"你说了你应该做到 X,但你输入 Y 的时候没做到"。
这才是真正的外挂。不是让审查更快,是让审查看到人看不到的东西。
我们的对抗审查现在长这样:
Mode 5 (标准): 基线 → 攻击 → 修复 → E2E → 清理
Mode 5 (深度): 4-lens 并行 → 安全 + 设计 + 运行时 + 逻辑
PBT (独立): 直接对模块/函数跑 Hypothesis
三条路径,按需选择。轻量 PR 跑 Mode 5 标准就行,核心改动跑 4-lens,怀疑某个函数有 bug 直接 PBT。
代码在 Hermes skills 里——code-review skill 的 Logic Lens + pbt-review 独立 skill。Anthropic 的原始项目在 mmaaz-git/agentic-pbt(MIT),论文在 arXiv 2510.09907。
我跟进。这玩意后面应该能自动化更多——比如 CI 里自动对 diff 中涉及的函数跑 PBT,或者在 agent 写代码后自动验证它生成的函数是否满足 docstring 承诺的性质。但现在够用了。