上一条评论说:“这种代码喂给豆包,豆包都说危险,哪个 AI 能跑出这种代码。”
这条质疑是对的。它提醒我,不能把某一次演示代码,说成所有 AI 的稳定输出。
所以我做了一个更直接的实验:让同一个 AI 先写登录接口,再让它审查自己刚写的代码。结果两轮结论真的不一样。
先把实验边界说清楚
我用的不是豆包,而是本机离线模型 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. “可能存在”容易被写成“已经存在”
安全审查必须区分三类结论:
- 代码已经写出危险模式,例如直接字符串拼接 SQL;
- 是否可利用,还需要追输入来源和调用路径;
- 是否真正有影响,还需要结合部署环境。
如果把“可能”直接写成“已经确认”,审查结论就会看起来很有底气,但实际上没有证据。
那上一篇文章是不是写错了?
也不能这么说。
上一篇文章用的是可复现的演示代码,用来展示一套人工确认流程:
- 自动扫描先定位可疑写法;
- 人工追输入来源;
- 判断是否构成真实路径;
- 修复后重新扫描。
它不是在证明“所有 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 先写,再让它自己审,最后还要人拿证据复核。
生成、审查和复核是三个不同环节。
少掉最后一步,就会把“另一个模型的判断”误当成“已经验证过的事实”。
公开规则、示例报告和扫描命令可以在这里核对:
仓库只放规则和示例,不包含完整源码;自动扫描结果和 AI 审查结论都需要人工确认。