前一阵子我给自己布置了一个挺枯燥的任务:把代码库里十几个长期没人认真审过的模块做一次“底朝天”式复查。复查的方式有点特别——我没有只靠肉眼去扫,而是把五个最有代表性的模块抽出来,同时丢给 Claude Code 和 Codex 去审计,然后我坐在电脑前,一条一条地把它们报出来的问题做人工复核。结果有点刺激:五份模块代码,两款主流 AI 助手,最终只在日志配置模块上给出了完全一致的结论。准确说,它们只在 1 个模块上达成共识,其余要么互相补充,要么完全对不上。这篇就记录这次“双 AI 对账式审计”的完整过程,包括实验设计、原始提示词、逐模块的结论对比,以及我从中总结出来的一套可用流程。
1. 为什么这次审计非得拉上两个 AI 当壮丁
1.1 代码审计的旧方案,已经不太够用了
大服务进入维护期之后,安全审计往往处于“重要但没人排期”的状态。靠人一行行读代码,一个模块几百行还凑合,一旦涉及跨模块调用链、历史配置和第三方 SDK,效率就会急剧下降。静态扫描工具能抓一部分固定模式,比如硬编码密码、明显的注入拼接,但误报率感人,真正要花时间的反而是去辨别噪音。
我需要的不是另一个“报错机器”,而是一个能快速理解业务上下文、还能用自然语言解释“为什么这里有问题”的辅助工具。AI 审计刚好能补上这个位置。Claude Code 和 Codex 都能直接面对整个文件分析,区别在于它们的报告风格和风险敏感度差异非常大。这次实验之前我本以为两个模型会给出八九成相似的结论,结果现实给了我一巴掌。
1.2 我选 Claude 和 Codex 的理由
选这两款工具没有太多玄学。Claude Code 和 Codex 都是可以直接对着代码库做多文件分析的对话式 AI,我的诉求有三个:
- 能让我把整个文件内容放进去,而不是只贴一个函数片段;
- 能结合上下文做数据流分析,而不是只看语法;
- 当我不认同某个结论时,能来回追问。
两个工具都满足这三点,但风格差异非常明显。Claude Code 更像一个先通读全文、再写总结报告的安全顾问;Codex 则更倾向沿着关键函数一条路追到底,遇到可疑控制流就停下来标记。这两种风格的差异,在后面的实验结果里被放得很大。
1.3 先摆明态度:这不是一场评测
提前说明:这不是对两款产品的横向评测,样本只有 5 个模块,且它们来自我加工过的脱敏代码,结论不能推广到所有场景。我的目标是回答一个问题:当两个 AI 对同一份代码各自给出结论,判断不一致时,差异通常出在哪个环节?如果这个答案能帮我把“人工复核”的工作量降下来,那实验就算值了。
2. 实验设计:模块选择、提示词与判定标准
2.1 五个模块怎么挑出来的
五个模块刻意选了不同类型:
| 模块 | 类型 | 行数 | 典型风险点 |
|---|---|---|---|
| A | 字符串工具库 | 约 120 行 | 动态执行、正则、类型转换 |
| B | 文件上传处理 | 约 200 行 | 用户输入进入文件系统 |
| C | 异步任务队列 | 约 280 行 | 并发共享状态 |
| D | 日志与配置模块 | 约 80 行 | 敏感数据落日志 |
| E | 三方 SDK 调用封装 | 约 150 行 | 外部接口契约 |
每个模块控制在 80 到 300 行,因为太大模型容易丢失早期上下文,太小又看不出分析能力。模块里的问题故意埋得深浅不均,不是为了考谁满分,而是为了观察两个模型对不同风险设计的敏感度。
2.2 同一份审计提示词原样贴出
我把提示词完全一致地发给两个 AI,不换说法,也不加额外暗示,只贴模块代码:
你是一名资深安全审计工程师。下面是一段代码模块,请按如下要求输出审计结果: 1. 只报告你确认存在且能给出证据链的问题,不要罗列代码风格建议; 2. 每条问题必须包含:所在文件/函数/行号、数据来源与流向、触发条件、可能影响; 3. 按严重程度打标签:严重 / 中等 / 轻微 / 无法确认; 4. 如果某个环节你认为整体合理,请直接写“未发现问题”,不要凑数; 5. 最后用三句话总结这个模块最值得优先处理的地方。设计这份提示词的几个细节我得解释一下。第一句角色设定是必要的,“资深安全审计工程师”这顶帽子会明显影响输出结构,去掉它报告会松散很多。第 2 条是最关键的一条,没有“证据链”这个硬约束,大多数时候你会收到一堆“看起来可能有风险”的废话。第 4 条是为了压制凑数行为,可即便这样,后面你依然会看到,两款 AI 凑数的欲望比我预想得强。
2.3 判定“问题成立”的复核标准
我没有直接拿 AI 的结论当最终答案,而是把两个模型报出的所有问题整理成清单,逐条人工复核。复核时只问三个问题:
- 问题是否真实存在:回到上下文里,能否找到确切的那行代码、那条数据流;
- 触发条件是否可控:需要什么样的输入、依赖、环境才能触发;
- 修复成本与危害描述是否匹配:AI 有没有把轻微问题夸大成严重漏洞,或者反过来。
我按四档给每条问题定性:严重、中等、轻微、误报。最终“达成共识”的定义也定得很严格:两个 AI 都报同一个问题点,且危害等级判断一致,才算共识;只提到同一类现象但等级不同,不算。
3. 逐模块对账:5 份代码、2 份报告、1 轮人工复核
3.1 模块 A:字符串工具库,一次高误报现场
模块 A 是一组字符串工具,包括安全过滤、数字转换、正则判断,共 120 行。里面确实藏了一个真问题:parse_condition函数用eval去执行配置项里读到的表达式字符串。
def parse_condition(expr: str) -> bool: if not isinstance(expr, str) or not expr: return False # 配置来源可能是后台管理接口,人工录入时可能被篡改 return bool(eval(expr, {"__builtins__": {}}, {}))Claude 只报了这一个,分析很短,但准确。Codex 报了 5 个,结果其中 4 个是误报。它把一个“如果对象存在就返回 True”的写法当成逻辑漏洞,又把字符串与字节串拼接的场景描述成“潜在敏感数据暴露”——实际那个函数是格式工具,根本碰不到敏感信息。
逐条复核后,真正需要修的确实只有eval这一个点。这一刻我意识到,AI 审计的高“召回”往往伴随更高的“噪音”,关键不在于模型报得多不多,而在于它给出的证据链能不能说服你。单纯的报告数量在审计场景里没有任何意义。
3.2 模块 B:文件上传处理,关键漏洞与擦肩而过
模块 B 处理文件上传,固定保存到本地上传目录,白名单校验扩展名。问题出在文件名处理:save_upload直接拼接用户传入的filename到目标路径,没有做任何净化。
def save_upload(file_obj, save_dir: str) -> str: filename = file_obj.filename target = os.path.join(save_dir, filename) # 如果 filename 包含路径分隔符或 , target 可能跳出 save_dir data = file_obj.read() with open(target, "wb") as f: f.write(data) return targetClaude 明确指出了这个路径逃逸风险,并且说明了数据流:filename来自用户请求,直接进入os.path.join,没有经过basename或随机名改写。Codex 反而报了一个“没有大小限制”——代码里明明有MAX_SIZE判断;又报“没有进行文件类型检查”,实属无中生有。
真正危险的文件路径覆盖问题,Codex 完全没提。为什么会漏?我猜它的注意力被“扩展名校验是否可靠”这种显眼话题拉着走,反而忽略了更基础的文件路径处理。修复方式其实很朴素:
import uuid filename = os.path.basename(file_obj.filename.replace("\\", "/")) target = os.path.join(save_dir, f"{uuid.uuid4().hex}_{filename}")两条审计路线对风险优先级的排序差异,在这个模块里体现得淋漓尽致。
3.3 模块 C:异步任务队列,两边各自抓了一半
模块 C 是异步队列消费逻辑,从数据库拉任务、统计计数、调用外部接口。里面有全局计数器自增,也有共享连接对象在协程间复用。
_counter = 0 async def process(record): global _counter _counter += 1 # 并发下不是原子操作 conn = await pool.acquire() ... await pool.release(conn) # 连接在多个协程间共享复用Claude 报的是“协程并发下共享连接对象可能产生交叉读写”,建议按任务独立创建或使用连接池;Codex 报的是“全局计数器自增不是原子操作”,建议换成原子计数器。
两个结论都成立,但都不完整。真正的统一诊断是:这个模块缺少对“共享可变状态”的统一隔离策略,计数器、连接、统计缓存全都裸奔在同一进程里。如果只跟一个 AI 聊,我很容易按它给的单一方向改完就收工,另一个问题会在生产环境里等着我。
双 AI 对账在这里第一次体现出“互相当第二双眼睛”的意义。这句话平时听得多了,但真踩一次才会信。
3.4 模块 D:日志与配置模块,唯一达成共识的 1 个
模块 D 负责加载配置并生成日志,只有 80 行。它的关键问题非常显眼:启动日志里直接打印了从配置读取的数据库密码明文。
logger.info( "connecting to db %s:%s user=%s password=%s", db.host, db.port, db.user, db.password, )Claude 和 Codex 都把这个点放到了最高优先级,给出的修复方向也完全一致:不要在日志里输出敏感字段,改成从环境变量读取配置,加载完成后立刻丢弃明文。这是全程唯一一次,两边意见完全对齐。
不过我不愿意把这种共识解读成“两个 AI 很可靠”。因为在同一个模块里,还有一个catch everything and log的吞异常问题,这两款 AI 不约而同地忽略了。被训练语料反复标注过的“经典安全问题”环节,AI 的表现会异常稳定;一旦问题不在经典模板里,它们照样集体失明。
3.5 模块 E:SDK 调用封装,AI 知识停滞后的一地鸡毛
模块 E 是一个支付 SDK 的封装,包含签名、回调验签、订单查询。Claude 一眼看出某个字段拼接顺序不符合官方文档推荐的规范,建议调整;Codex 则认为该字段拼接顺序在当前版本中已经被官方接受,不需要改。
我去查了一圈最新文档后发现:两边说的其实都对,只是对不同版本正确。这个 SDK 在 AI 训练数据截止之后更新过接口约定,新版本修正了签名算法,旧版本则保留旧格式。两个模型都被自己训练语料里的“当时版本”束缚住了。
结论很明确:涉及外部 SDK 时,AI 的知识截止日期是一件要命的事。你可以把它当“风险提示器”,但最终契约必须查当前版本文档,绝不能拿模型的知识当合同。你让一个大语言模型回忆某个 SDK 两三年前的用法,它可能头头是道;但你要它判断今天线上这个版本该怎么写,它和你看文档的速度一样慢。
4. 分歧根源拆解:从 20 个问题到 6 个真问题
4.1 阅读代码的方式:全局扫描与调用链追踪
从结果反推,两款 AI 在阅读代码时的策略确实不一样。Claude Code 的报告更像先做全局扫描,找出“哪些危险操作出现次数多”,再回头确认优先级;Codex 则更倾向沿着关键函数的调用链一路推演,从入口到出口,遇到可疑控制流就停下来报告。
这两种策略决定了它们误报的形状不同。全局扫描容易在“看似调用风险函数、其实数据根本不可达”的地方犯错;调用链追踪则容易在“入口链路很长”时丢掉一些不在主链上的分支。了解这一点对我后续使用很有帮助——当一款 AI 报的问题不合常识,我会尝试把它当作“另一种视角”,而不是单纯当成错误删掉。
4.2 知识截止日期:一款模型被“过期的正确答案”带偏
模块 E 的分歧是最典型的“知识裂缝”。训练语料里一个 API 的行为和参数如果已经过时,模型会非常自信地给出错误建议。Claude 对 SDK 新版本的解读停留在旧版规范,Codex 虽然表现出“新版本可能已经接受这种写法”的直觉,但说不出具体依据。
两者唯一的共同点:都无法访问最新版官方文档。所以凡是涉及版本敏感的外部接口,我会把 AI 结论的优先级调低,把官方文档的优先级调高。这不是某一个模型的缺陷,所有语言模型都会面临同样的知识保鲜问题。
4.3 置信度取向:宁可漏报还是宁可误报
我这次把两个模型的全部输出按条整理成了表,维度包括报告总条数、成立条数、证据链完整度。五个模块的小样本里,结论比较有意思:
| 模块 | Claude 报告(成立/总数) | Codex 报告(成立/总数) | 是否共识 |
|---|---|---|---|
| A 字符串工具库 | 1 / 1 | 1 / 5 | 否 |
| B 文件上传处理 | 2 / 4 | 0 / 2 | 否 |
| C 异步任务队列 | 1 / 1 | 1 / 1 | 否(问题点不同) |
| D 日志与配置模块 | 1 / 1 | 1 / 1 | 是 |
| E SDK 调用封装 | 0 / 3 | 0 / 1 | 否 |
| 合计 | 5 / 10 | 3 / 10 | 1 个模块共识 |
20 条原始问题,人工复核后真正成立的真问题只有 6 条。Claude 偏“少而准”,Codex 偏“多而杂”。这更像置信度校准策略不同,不是谁智商更高。放进真实流程里,“少而准”适合处理高风险模块,能帮你快速对齐注意力;“多而杂”适合做全面盘点,能防止漏网之鱼。理想方案永远不是二选一,而是把两者叠加。
4.4 双 AI 合并的意义:交集定风险,并集提召回
上面这张表就是“双 AI 对账”最直观的价值证明。20 条原始问题里,合并去重后需要人工逐条复核的只有 6 条。如果单看 Claude,会觉得结论挺靠谱,但会漏掉 Codex 独报的计数器问题;如果单看 Codex,会被一半多的误报淹没,同时还会漏掉文件上传模块里的路径逃逸。
两者叠加后,复杂问题被互补覆盖,而唯一达成共识的模块 D 让我可以放心地把大部分注意力集中在最严重的泄露风险上。这才是双 AI 审计的正确用法:交集用来定风险优先级,并集用来提高召回率,人工大脑单独负责最后一公里的判断。
5. 把 AI 审计接进日常流程的实操建议
5.1 提示词里必须写清的三件事
基于这次实验,我现在做类似审计时会固定让 AI 回答三件事:哪里有问题、为什么这是问题、怎么验证这是问题。角色设定与输出格式必须写明,不然报告就会变成散文。我目前最常用的模板是:
你是一名资深安全审计工程师。以下代码模块,请按严格顺序输出: 1. 可疑问题清单,按严重程度排序; 2. 每个问题的证据链:文件/函数/行号、数据来源与流向、触发条件、影响范围; 3. 每个问题的修复建议,精确到需要改哪一行; 4. 最后给出一句话总结:这个模块现在能不能上线。5.2 让 AI 给“证据链”,而不是给“意见”
我给自己定了一条铁律:凡是能给出文件、函数、行号、数据来源、触发条件的,先标记为高优先级;凡是只说“可能存在风险”“建议加强校验”这种话的,一律标记为待定。
AI 最大的迷惑性在于,它可以用非常专业的语气说出根本无法复现的风险描述。你顺着它找过去,发现那行代码压根不存在,或者数据流根本到不了它说的位置。逼它给证据链,是筛选掉这类噪音最有效的手段。如果它给不出,那就当它没说过。
5.3 双 AI 报告合并去重的表格法
实际操作中我不会只依赖聊天窗口,而是把两份报告复制到一张表里。按模块分 section,每条问题一行,列包括:来源、位置、问题描述、预期等级、我的初步判断。合并时先把两个 AI 报的同一位置问题打上“交集”标签,剩余条目全部进入“待复核”。
这个流程很笨,但很有效。它最大的价值是把“AI 说了什么”强制转换为“我认为什么”。在做完第三步之后,我对每个模块的真实风险状态已经心里有数,不再需要反复翻聊天记录。
5.4 人工复核的最小闭环
我的复核流程固定六步:
- 确认问题真实存在,回到源码找到对应位置;
- 分析触发条件,需要什么输入、依赖、权限;
- 判断影响范围,评估是线上阻断还是低频偶发;
- 写出最小复现用例,至少在本地跑一次;
- 实施修复,尽量做最小改动;
- 补一条回归测试,防止同样的问题再次出现。
这六步无法外包给 AI,至少现在不行。不过有一个小技巧很值:修复完成后,把改动后的 diff 重新发给任意一个 AI,让它检查“修复是否引入新问题”。这个第二轮检查在本次实验里帮我抓到过一次修复不完整——我当时只处理了文件路径逃逸,却漏掉了对file_obj.filename可能为None的判空,Codex 在复查时补上了这点。
5.5 什么样的模块适合交给 AI 当第一道过滤
从这 5 个模块的表现看,AI 适合处理单一职责、文件数少、逻辑独立、外部依赖不强的模块。这类代码它读得懂、上下文塞得下,报告质量也最高。反过来,涉及跨服务调用链、复杂状态机、大量历史配置的全局性审计,AI 的结论往往会含糊,因为它很难在有限上下文里维护完整全局模型。
所以我现在使用 AI 审计的原则很简单:它做第一道过滤器,专门负责把“看起来要重点盯”的名单缩小;最耗精力的真正判断,永远放在最后一道,留给人来做。
最后再分享一个这次实验里印象最深的小细节。那个唯一达成共识的日志模块,修复起来其实只有三行:去掉日志里的password字段,改成从环境变量读配置时直接脱敏。可就这么一个问题,如果我此前只跟着 Codex 报的“文件包含检查”去翻模块 A,或者只跟着 Claude 报的“eval 风险”去翻模块 B,可能要迟很久才会注意到它。把两个 AI 放在同一张桌子上,让它们的报告互相“对质”,已经成了我现在做模块审计的默认起手式。你也可以试试,但请记住:它们负责把痕迹找出来,拿主意这件事,还得我们自己来。