大模型写的代码过了功能测试,但静态检查一跑全是坑——AI 生成代码的安全与规范漏洞该怎么补
很多团队对 AI 代码的真实体验是:功能测试绿了,CI 里的静态检查红一片。问题不在于“AI 写不出能跑的代码”,而在于它默认优化的是“看起来对”,不是“工程上安全、可维护、可审计”。要补这个缺口,静态检查的配置和审查流程都得跟着变——不能再用审人写代码的那套默认规则去审模型输出。
模型生成的代码为什么特别容易踩静态检查的坑
核心结论:大模型是从“统计上最像正确代码”的分布里采样,而不是从“通过工程规范”的分布里采样,所以它天然倾向于生成训练语料中最常见的写法,而这些写法往往就是静态检查规则要抓的反面教材。
我拿几个真实场景说。某团队让模型生成一个 Python 的 FastAPI 接口,功能测试全部通过,但 ruff 一跑报了 14 个问题,其中 6 个是 subprocess 使用了 shell=True、3 个是异常裸捕获 except:、剩下的是硬编码密钥和过宽的异常抛出。这些代码在功能上完全正常,甚至比很多初级工程师写的还干净,但放到生产环境的检查门槛下就是不合格。
这背后的原因很具体:
-
训练数据里“坏代码”占比极高。GitHub 上公开仓库的代码,大量是教程、demo、个人项目,安全规范远低于企业标准。模型学到的是“常见写法”,不是“合规写法”。SQL 拼接、
eval()、明文密码、*导入,这些在公开代码里到处都是。 -
模型没有“组织规范”的概念。你公司规定所有数据库访问必须走 ORM,禁止手写 SQL;规定所有外部 API 调用必须带超时和重试;规定日志必须用结构化格式。这些约束模型不知道,除非你写进 prompt,而大部分人写 prompt 时不会覆盖这么细。
-
模型对“上下文缺失”的处理是猜。静态检查工具能拿到完整的项目配置、依赖版本、类型标注,但模型只能拿到你贴给它的那点上下文。它猜着写,猜错的地方就是检查报错的地方。比如它不知道你项目里
requests已经封装成了带超时的http_client,直接裸调requests.get(url),功能没问题,但违反了团队规范。
功能测试为什么兜不住这些问题
核心结论:功能测试验证的是“行为对不对”,静态检查验证的是“实现方式合不合规”,这两者的覆盖面重叠度很低,AI 生成的代码恰恰在“行为对但实现方式违规”这个区间里问题最多。
我见过最典型的一个案例:模型生成了一段处理用户上传文件的代码,功能测试覆盖了正常文件、空文件、超大文件,全过了。但代码里用的是 os.path.join(user_input, filename),没有做路径穿越校验。功能测试不会测 ../../etc/passwd 这种恶意输入,因为测试用例是人写的,人的思维默认“输入是正常的”。而静态检查工具(比如 Bandit、Semgrep 的规则库)恰恰有专门的路径穿越检测规则。
还有一个更隐蔽的:模型生成的代码里用了 pickle.loads() 来反序列化用户传入的数据。功能测试用正常数据跑,完全没问题。但这个函数在安全规则里是硬性禁止的,因为它可以执行任意代码。这种问题靠功能测试永远测不出来,因为测试数据是良性的。
所以结论很直接:AI 生成代码的审查,静态检查的优先级要提到和功能测试同等甚至更高的位置。因为模型写代码的行为模式决定了,它最容易在“静态可检查的规范问题”上翻车,而不是在“功能逻辑”上翻车。
怎么调整静态检查策略来适配 AI 代码
核心结论:审 AI 代码要用“从严模式”——把规则集调得比审人写代码更严格,同时把检查前移到代码合入之前,并且针对模型的高频错误模式做定向规则配置。
具体做法分三层:
第一层:把安全规则从“警告”升级为“阻断”
大多数团队在用 ESLint、ruff、Pylint、SonarQube 的时候,安全相关的规则默认是 warning 级别,不阻断 CI。审人写代码时这个配置合理,因为人写出 shell=True 的概率低,偶尔出现一次 review 时提醒一下就行。但 AI 生成代码时,这类问题出现的频率高一个数量级,warning 等于没有拦截。
我建议直接把这几类规则设成 error,CI 不过:
- 命令注入类:
subprocess的shell=True、os.system、exec()、eval() - 路径穿越类:用户输入直接拼接到文件路径
- 序列化类:
pickle、yaml.load(无 SafeLoader) - 硬编码密钥类:任何看起来像密码、token、API key 的字符串字面量
- SQL 注入类:字符串拼接 SQL、
execute()带格式化参数 - 资源泄露类:文件句柄、连接未用 context manager
拿 Semgrep 举例,直接上它的 p/security-audit 和 p/owasp-top-ten 规则集,CI 里配 --error。ruff 的话在 pyproject.toml 里把 S(bandit 规则)和 B(bugbear)全开,设成 error:
[tool.ruff.lint]
select = ["E", "F", "B", "S", "I", "UP"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
第二层:给模型输出加一道“专项扫描”
光靠通用规则不够,因为模型有一些特有的坏习惯,通用规则库不一定全覆盖。比如:
- 过度宽泛的异常捕获:
except Exception然后pass,或者捕获了但只print。模型特别爱这么写,因为它想让代码“看起来不会崩”。Pylint 的W0703能抓,但很多团队关了这条。 - 注释与代码不一致:模型生成的注释经常是“看起来合理”但和实际逻辑对不上。这个静态检查抓不到,但可以配
docstring覆盖率检查,强制模型输出的函数都有文档,然后 review 时人工比对。 - 重复代码块:模型在长文件里会反复生成相似逻辑,SonarQube 的 duplication 检测要开,阈值调到 3% 以上就报。
- 未使用的导入和变量:模型生成代码时经常引入一堆“可能用到的”导入,ruff 的
F401、F841要开 error。
第三层:把检查结果喂回给模型
这一步很多人忽略了。静态检查报错不是给人看的,是可以直接作为反馈让模型自己修的。现在的 LLM 对“根据报错信息修复代码”这个任务的完成度很高,尤其是 lint 错误这种有明确行号和规则编号的。
一个实用的工作流是:模型生成代码 → 跑静态检查 → 把检查结果(过滤掉格式类噪音)作为上下文发回给模型 → 让它自己修 → 再跑检查。通常两轮以内能把 80% 以上的问题清掉。剩下的深层问题(比如架构层面的、需要业务上下文判断的)再转人工。
不能光靠工具:审查者的注意力要转移到工具覆盖不到的地方
核心结论:静态检查能兜住“可规则化”的问题,但 AI 代码里还有一类“语义层面”的错误是工具抓不到的,这些才是审查者真正该花时间的地方。
我见过模型生成过一段“看起来合理但逻辑完全错误”的代码:它把两个不同用户的订单合并逻辑写反了,功能测试用的是单用户数据所以过了,静态检查也全绿,但上线就会串数据。这类问题靠规则匹配抓不到,得靠懂业务的人通读代码。
所以审查 AI 代码时,人的精力分配要变:
- 规范类问题:交给工具,人不用细看。
- 业务逻辑正确性:这是重点,要一行行读,尤其注意边界条件、状态流转、数据归属。
- 依赖选择的合理性:模型喜欢引入新库,比如该用标准库的地方拉了个第三方包,或者用了过时版本。审查者要盯住
requirements.txt/package.json的 diff。 - 模型“编造”的 API:这是最坑的。模型有时候会调用一个不存在的函数或参数,如果项目用了动态类型语言,静态检查不一定能全抓到。审查时发现可疑的 API 调用要顺手跑一下或者查文档确认。
常见问题
问:AI 生成的代码过了功能测试,为什么还要花时间跑静态检查?直接上生产不行吗?
不行。功能测试只能证明代码在“你预料到的输入”下行为正确,但生产环境的输入是不可预料的。静态检查抓的是实现方式的安全隐患——SQL 注入、命令注入、路径穿越、反序列化漏洞,这些在正常功能测试数据下永远不会触发,但攻击者专门构造输入就能打进去。AI 生成代码时这类问题出现的概率远高于人类工程师,所以静态检查不是可选项,是必须项。
问:用 AI 写代码就是为了快,加这么多检查流程不是把效率优势抵消了吗?
不是。静态检查是自动化的,跑一次几秒钟到几分钟,成本极低。真正耗时的是“人工 review 发现规范问题再打回修改”这个循环。如果你把检查前置,让模型根据报错自己修,这个循环就变成了“模型生成→检查→模型自修”,全程不需要人介入。反而是不做检查、靠人肉 review 抓问题,才是真正的效率黑洞,因为人的 review 速度远跟不上模型生成代码的速度。
问:静态检查规则那么多,针对 AI 代码应该优先开哪些?
优先级从高到低:安全类(注入、路径穿越、危险函数、硬编码密钥)> 资源管理类(泄露、未关闭的句柄)> 异常处理类(裸捕获、吞异常)> 代码质量类(未使用变量、重复代码)。安全类是底线,必须 error 级阻断。资源管理和异常处理是 AI 的高频翻车点,建议开 warning 以上。代码质量类可以放宽松一点,避免噪音太多把真正的问题淹没了。
问:模型自己根据报错修代码,会不会越修越差?
有这个风险,但可以通过约束来控制。具体做法:只把静态检查的报错信息(行号、规则号、错误描述)喂给模型,不要让它自由发挥重写整个文件;要求它“只修改报错行相关的最小范围”;修复后再跑一次完整测试套件确认功能没有回归。实践下来,lint 类错误的自修成功率很高,真正需要担心的是让它修“架构级”问题——那种问题不要丢给模型,直接人工处理。