我让 AI 先写登录接口,再让它自己审:为什么两次结论不一样?

32 阅读7分钟

上一条评论说:“这种代码喂给豆包,豆包都说危险,哪个 AI 能跑出这种代码。”

这条质疑是对的。它提醒我,不能把某一次演示代码,说成所有 AI 的稳定输出。

所以我做了一个更直接的实验:让同一个 AI 先写登录接口,再让它审查自己刚写的代码。结果两轮结论真的不一样。

cover-1920x1080.png

先把实验边界说清楚

我用的不是豆包,而是本机离线模型 qwen2.5vl:3b,通过 Ollama 调用,生成和审查都设成 temperature=0。

这不是在拿一个 3B 小模型给豆包下结论。豆包、GPT、Claude 的结果都会受版本、提示词和上下文影响。

我想验证的是一个更普遍的问题:

同一个模型,在“写代码”和“审代码”两个任务里,会不会给出不一致的结论?

答案是:会。

而且问题有时不在第一轮代码,而在第二轮审查。

第一轮:我让 AI 写一个能登录的接口

提示词只有功能要求,没有提安全:

你是一个后端开发。请用 Python + Flask 写一个最小可运行的登录接口,要求:
1. 接收 username 和 password 表单参数;
2. 查询 SQLite users 表;
3. 用户名和密码正确时返回 {"ok": true},否则返回 401;
4. 不用 ORM,只使用 Python 标准库 sqlite3;
5. 只输出完整代码,不要解释。

模型输出里,最关键的一行是:

cursor.execute(
    "SELECT * FROM users WHERE username = ? AND password = ?",
    (username, password),
)

它没有拼接 SQL,而是用了参数化查询。

这恰好说明评论者说得对:

不能因为上一篇文章出现了 SQL 拼接,就推断所有 AI 默认都会写出 SQL 拼接。

我先用 code-audit-cli 扫了一遍

把模型生成的代码保存后,我运行:

code-audit ai_generated_login.py --format json

结果是:

[]

扫描摘要:

high: 0
medium: 0
low: 0
total: 0

第一轮结论:自动扫描没有发现危险写法。

但“扫描 0 条”仍然不等于“绝对安全”。它只能说明:代码里没有被当前规则命中的常见危险模式。

第二轮:我让同一个 AI 审查这段代码

我把刚生成的代码原样贴回去,要求它做安全审查,并区分“代码里已经明确暴露的风险”和“需要结合部署环境才能确认的风险”。

模型给出的第一条结论是:

SQL注入风险:通过 request.form.get('username') 和 request.form.get('password')
获取的用户名和密码直接用于SQL查询,没有进行任何参数化查询,容易导致SQL注入攻击。

这里已经出现矛盾:

代码明明用了 ? 占位符,审查却说“没有进行任何参数化查询”。

我又追问了一次:

请复核你的结论。代码实际使用的是:
cursor.execute(
    "SELECT * FROM users WHERE username = ? AND password = ?",
    (username, password),
)
请逐条判断哪些结论有代码证据,哪些没有证据。

模型仍然坚持:

有代码证据:代码中直接使用了 username 和 password 作为参数传递给 execute 方法。
结论修正:存在 SQL 注入风险。

它看见了变量被传给 execute,却没有继续看第二个参数里的 ?,也没有区分“参数进入 SQL 函数”和“参数被拼进 SQL 字符串”。

第二轮结论:AI 自审出现了一个可复现的误报。

为什么两次结论会不一样?

不是模型“突然变笨了”,而是两个任务的约束完全不同。

1. 写代码时,模型有明确交付物

第一轮的任务是“写一个接口”。

模型需要满足函数名、路由、数据库连接、返回值这些明确约束。为了给出完整代码,它会优先选择自己熟悉的实现方式。

这次它恰好选择了参数化查询。

但这不是安全保证。换一个模型、换一个提示词、换一次采样,它可能改成字符串拼接,也可能仍然使用参数化查询。

2. 审代码时,模型更容易顺着模板给结论

第二轮的任务是“列风险”。

这个任务没有唯一答案,却像一个填空题:用户要风险清单,模型就倾向于给出风险清单。

它看到变量进入 execute,很容易调用训练里最常见的判断:

登录 + SQL + 用户输入 = SQL 注入风险

但这次它漏掉了代码证据。

这也说明“AI 审查”本身只是另一轮生成,不是自动获得真实性的裁判。

3. “可能存在”容易被写成“已经存在”

安全审查必须区分三类结论:

  1. 代码已经写出危险模式,例如直接字符串拼接 SQL;
  2. 是否可利用,还需要追输入来源和调用路径;
  3. 是否真正有影响,还需要结合部署环境。

如果把“可能”直接写成“已经确认”,审查结论就会看起来很有底气,但实际上没有证据。

那上一篇文章是不是写错了?

也不能这么说。

上一篇文章用的是可复现的演示代码,用来展示一套人工确认流程:

  1. 自动扫描先定位可疑写法;
  2. 人工追输入来源;
  3. 判断是否构成真实路径;
  4. 修复后重新扫描。

它不是在证明“所有 AI 都会稳定生成 SQL 拼接”。

评论提醒得对:示例代码可以被人为写成危险代码,也可能被某个模型直接写成安全代码。

这件事真正值得写下来的地方是:

AI 第一轮输出安全,不代表第二轮审查一定正确;AI 能指出 SQL 注入,也不代表它每次都能找到代码证据。

我看到“AI 审查通过”时,会继续问 4 个问题

1. 这条结论对应哪一行代码?

没有文件、行号和原始代码片段的审查结论,先当成线索,不当成结论。

2. 它是在描述规则,还是在描述真实路径?

“使用了 SQL 查询”是描述规则。

“用户输入经过字符串拼接进入 SQL,并能在没有授权的情况下触发”才是描述路径。

3. 什么证据能推翻它?

让模型反过来回答:

如果这个结论是误报,代码里最可能出现什么反证?

例如这段代码的反证就是:

cursor.execute(
    "SELECT * FROM users WHERE username = ? AND password = ?",
    (username, password),
)

4. 修复后,怎样验证问题真的消失?

不要只看 AI 回复“已修复”。

要保存修复前后的代码,重新跑一次扫描,再回到真实调用路径确认。

可复现实验记录

这次实验的完整步骤是:

ollama run qwen2.5vl:3b

第一轮提示词只要求生成登录接口。

第二轮把模型生成的代码原样贴回去,要求做安全审查。

第三轮提醒模型注意 ? 参数化查询,要求它重新核对证据。

三轮结果对应三种不同结论:

  • 生成结果:使用参数化查询;
  • 审查结果:误报 SQL 注入;
  • 复核结果:仍然误报 SQL 注入。

这个结果不代表豆包,也不代表所有模型。它只说明一个事实:

“让 AI 自己审”不能省略证据核对。

最后

我一开始想写的是“AI 先写,再让它自己审”。

做完实验后,更准确的标题应该是:

AI 先写,再让它自己审,最后还要人拿证据复核。

生成、审查和复核是三个不同环节。

少掉最后一步,就会把“另一个模型的判断”误当成“已经验证过的事实”。

公开规则、示例报告和扫描命令可以在这里核对:

github.com/yuan1521913…

仓库只放规则和示例,不包含完整源码;自动扫描结果和 AI 审查结论都需要人工确认。