代码审查命中率虚高,怎样识破
作者基于生产 Django 后端构建开源 Python 检查工具 healthkit,并用人工审查的45项发现回测。初看命中29项,逐条核对后真正定位问题的只有9项,揭示了自动化评估中的偶然命中。
AI 深度解读
AI 写代码越来越快,审查却没有同步提速。healthkit 的作者在一个生产 Django 后端里发现,近期约一半提交都在修问题,登录、退出、改密码和 JSON 解析逻辑各有多个版本,甚至有权限漏洞修好一周后再次出现。
他们做了一套 Python 检查工具,但最值得读的不是功能列表,而是一次测量:自动规则看似覆盖了 45 个问题中的 29 个,人工逐项核对后,真正命中的只有 9 个。这个落差,决定了规则、AI 和人各自该负责什么。
为什么 29 个命中,最后只算 9 个
作者拿一个模块的人工审查结果做回放:人工发现了 45 个问题,他们取出每次修复前的代码,检查规则能否指向对应问题。
最初成绩是 29/45。逐条复核后,他们发现不少命中只是位置重合:规则报了一个未使用的 import,但那次提交修的是行为错误;还有些告警落在本来就要删除的路由里。
只统计指向实际问题的结果,剩下 9/45,约 20%。
告警出现在修复附近,不等于工具发现了修复针对的问题。
这也解释了他们为什么把能力边界划得很窄。healthkit 用 ruff、Opengrep 和 import-linter 检查代码模式与依赖结构,用项目自己的 .healthkit/ 小型守卫补充项目知识。业务行为是否正确,则交给测试、QA 和人。
规则可以把过去犯过的错记下来,下次再见到时报警。但它不能据此判断逻辑、安全性和设计都正确。检查通过,只表示没有发现它认识的问题。
怎样让检查真正成为可信的门禁
healthkit 的五个设计决定,都围绕同一件事:让通过和失败有明确含义。
第一,AI 不参与最终放行。门禁只依赖确定性的规则和工具,AI 审查只提供建议。AI 如果反复发现同类问题,再把它固化成规则执行。
第二,默认只查改动行。healthkit review --base main 检查当前分支相对 main 修改的行,避免旧代码的问题淹没本次反馈;需要检查整个模块时,再用 scan 和 baseline。
第三,写代码的 agent 不能悄悄修改裁判。修改 .healthkit/ 设置,或添加关闭检查的 # noqa,都会被标成需要人工批准。作者曾手工发现,默认排除整个 migrations/,会让放进去的真实业务代码逃过检查。后来默认规则改成只匹配 Django 自动生成的迁移文件命名。
第四,不够准的规则只提醒。某条规则在两个未参与调优的项目上,准确率分别只有 41% 和 55%,因此只以 worth a look: 提示,不使检查失败。规则必须在至少两个项目的未见代码上达到 70%,才能进入阻断级别。
第五,无法检查不能算通过。退出码分别是:0 表示未发现问题,1 表示发现问题,2 表示无法检查,3 表示只有需要批准的设置变更。这个区分来自一次工具超时:代码明明存在死代码,工具却报了零个未使用函数,因为验证步骤超时后隐去了结果。现在适配器必须同时暴露底层工具的错误。
规则看不懂的行为,用请求去验证
行为错误需要另一条路径。healthkit api 通过 Schemathesis 向 API 路由发送正常和异常输入,把服务器错误作为发现项。在测试项目里,它找到了 6 个真实崩溃,没有一个对应已有规则。
权限检查则需要提供不同角色的 token,例如管理员、普通用户和其他组织的用户。每条路由都会按不同身份测试,检查用户是否能读取别人的对象。团队还可以明确声明哪些访问必须被拒绝:
[[api.deny]]
user = "HK_TEACHER"
routes = ["GET /people/students", "GET /finances/**"]
reason = "a teacher sees only her own class"
要看的是 routes:它把应当拒绝的请求写成可检查的约束,工具才能把成功响应标为问题。工具不会自行理解学校业务里的完整权限边界。
报告还会区分 got 200 (469 records) 和 got 200 (empty),只展示数量,不展示数据。两者都是成功响应,但调查优先级显然不同。
默认只发 GET 请求,添加 --allow-writes 才允许写操作。作者曾在扫描中停用了某个用户,导致下一轮崩溃数从 6 个降到 4 个,实际一个都没修好。测试改变了数据,结果也跟着变了。
他们开发工具时使用 AI,也遇到了类似的证据问题。验证 agent 曾误读自己引用的代码,把真实漏洞判成不存在,而且没有执行要求运行的测试;一个两分钟脚本就证明了漏洞。另一轮验证中,agent 反复检查已经封堵的旧路径,漏掉新思路,作者手工尝试的第一个新思路就完全绕过了门禁。
因此,他们要求结论附上实际执行的输出,并在验证任务中列明必须尝试的案例,逐项核对报告。
我的判断
我赞同这套方法最核心的取舍:先测量,再决定哪些检查值得强制执行。
例如,作者原计划做代码相似度引擎,但测量发现,它只找到 4 个真实重复案例中的 1 个。两个合计约 110 行的简单检查反而更有效,还找到了限流漏洞,以及一份复制后行为相反的安全辅助代码。这说明复杂方案未必先值得投入,但仍是特定项目里的经验。
局限也很明确。45 个发现来自一个模块,6 个崩溃来自测试项目,不能直接当成一般团队的收益预期。原文也没有给出与其他 AI reviewer、人工审查或测试方案的完整对照数据。70% 是他们的阻断门槛,并不是经过证明的通用标准。
只查改动行能减少噪声,却也可能漏掉跨文件、跨调用链的影响;GET 默认扫描减少写入干扰,也覆盖不到大量修改状态的流程。权限策略还依赖团队先把预期写对。
healthkit 是 MIT 许可,约有 220 个测试。作者称它适用于任意 Python 项目,并为 Django 和 Web API 提供额外检查,支持文本、面向 agent 的 JSON、PR 评论用 Markdown 和 GitHub annotations。这些便于接入,但测试数量和输出格式都不能替代实际效果验证。
能拿来做什么
- 如果你维护 Python 后端,可以先在一个模块上回放已有修复,逐项确认规则是否指向真正的问题,再决定是否接入阻断。原文给出的安装和增量检查命令如下:
uv tool install git+https://github.com/paudelbaibhav/healthkit
healthkit review --base main
第二行的 --base main 决定比较基线,使用前要确认它符合团队的分支流程。
- 如果你让 AI agent 修改代码,可以把规则配置、排除目录和关闭检查的注释纳入人工审查,并要求验证结论附带实际运行输出。
- 如果你维护多角色 API,可以为管理员、普通用户和跨组织用户准备独立身份,明确写出必须拒绝的路由,并把写入扫描放进可恢复的测试环境。
- 如果你准备自研检查工具,可以先拿已知问题验证最小方案。准确率不足的规则先做提醒,工具超时、解析失败等情况则单独报错,别让零发现掩盖未执行。