在本地pre-commit阶段拦截AI生成的虚假import,比CI pipeline更快更便宜。给出了完整的.pre-commit-config.yaml配置示例,Python项目可直接复用。
CI 最终会捕获幻觉导入,但"最终"意味着在 push 之后、流水线排队之后、构建完成之后、有人查看结果之后。本地 pre-commit 钩子能在几秒内捕获同样问题,甚至在 commit 创建之前、开发者脑海中上下文还清晰时就已捕获。对于这种成本极低的检查,没有理由把它推迟到流水线中更慢、更昂贵的阶段。
pre-commit 框架是将此类检查接入 git 的标准工具,与语言无关。它管理钩子的安装、版本控制和团队执行,无需每位开发者手动配置 git 钩子。Python 项目的最小 .pre-commit-config.yaml 可以引用在每次 commit 时运行的一系列钩子,再添加一个用于导入验证的钩子完全符合同样的模式。
对于 Python,捕获真正缺失的包和幻觉导入的最简单的本地检查,是在项目的真实虚拟环境中尝试实际导入变更文件中引用的每个模块,如果任何导入抛出 ModuleNotFoundError 则使 commit 失败。仅此一项就能立即捕获包的幻觉,因为环境中不存在的包无法被导入,就是这么简单。
要捕获幻觉的方法调用而不是完全缺失的包,静态类型检查器能做更多工作。将 mypy 作为 pre-commit 钩子运行会标记对 typed 库实际未公开的方法或属性的调用,这正是幻觉 API 表面的特征:真实的包,发明的方法。
对于 JavaScript 和 TypeScript 项目启用导入解析规则的严格 ESLint 配置,同样能在 commit 时而非构建时捕获未解析的导入。结合 TypeScript 自身对类型对象成员访问的编译器检查,这两者能捕获两类问题:解析为空的导入,以及对实际从未定义的真实对象的方法调用。
任何 pre-commit 钩子最大的风险是变得足够慢,以至于开发者开始使用 --no-verify 来跳过它。将导入和类型检查的范围限定为 commit 中实际变更的文件,而不是整个代码库,并缓存工具允许缓存的内容。为 commit 增加两三秒的钩子会被使用。增加三十秒的钩子在一周内就会被绕过,这就完全违背了将检查放在工作流如此早期阶段的目的。
一个精简的 .pre-commit-config.yaml,对于做这件事的 Python 项目,可能将导入解析检查和类型检查作为单独的钩子链接在一起,两者都只限定在变更的文件上:
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--ignore-missing-imports]
- repo: local
hooks:
- id: verify-imports
name: verify imports resolve
entry: python -c "import importlib,sys; [importlib.import_module(m) for m in sys.argv[1:]]"
language: system
files: \.py$
这是有意精简的。真实配置通常会在其上叠加格式化和 lint 钩子,但上面两个钩子才是做实际幻觉捕获工作的:一个确认每个导入都解析为已安装的内容,另一个确认每个方法调用与库的真实类型定义相匹配。
同样的 pre-commit 框架通过其语言无关的钩子定义支持非 Python 钩子,因此 JavaScript 或 TypeScript 项目可以将 ESLint 的导入解析规则和 TypeScript 编译器的 --noEmit 检查接入同样的本地 pre-commit 流程,在 commit 存在之前而非 CI 构建失败之后捕获未解析的导入或无效的成员访问。
本地钩子不是 CI 中运行相同检查的替代品,它是更快的第一道关卡。开发者仍然可能有意或无意地跳过本地钩子,而 CI 是保证没有任何东西在检查实际运行且无法被绕过的情况下合并的后防线。在 GitHub 自身关于 required status checks 的文档中,将相同的导入和类型验证配置为必需的 CI 步骤,为跳过本地版本的人补上了这个缺口。
第一次这个钩子在真正的幻觉导入上触发时,值得将其视为有用的信号而不是快速打发的烦恼。检查相同的提示或类似的提示是否再次产生相同的幻觉名称,因为这种可重复性正是使一个名称值得在代码库其他地方关注的模式。如果队友独立地遇到相同的虚假建议,值得在团队的共享文档或提示库中做一个简短记录,因为下一个问助手类似问题的人很可能看到相同的发明名称。
所有这些都不需要作为一个大变更一次性落地。先从导入解析检查开始,因为它配置最简单,几乎没有误报,能捕获最 loud 的失败模式——包幻觉。然后一旦第一个稳定了,团队也习惯了 pre-commit 工作流,就添加类型检查钩子。试图同时引入两者,在从未有过任何一种的代码库上,往往会挖出一波与 AI 幻觉无关的现有类型错误,这会让整个努力看起来比必要的更具有破坏性。
所有这些检查都不是异类的——它们是大多数成熟代码库已经出于其他原因运行的相同静态分析工具。这里的特定价值是将"这个导入或方法是否真的存在"作为工具自动检查的又一项,而不是每次 AI 编码助手建议新依赖或不熟悉的方法调用时人类必须记住要验证的东西。一旦接好,它每次都以相同方式运行,不会因为那是下午审查的第五个建议而感到疲倦或跳过一步。
对于更大的代码库,即使限定在变更文件上,每次 commit 运行完整类型检查仍然可能累积起来(如果变更触碰到具有大型依赖图的文件的文件)。大多数类型检查器都支持增量或缓存模式专门为此设计,mypy 也不例外,从一开始就这样启用是值得的,而不是在钩子已经慢到让人开始抱怨之后。一个保持快速的钩子才是一个保持在启用的钩子。
如果你的团队正在建立 AI 编码助手工作流,并希望这种护栏从一开始就被内置进去,而不是在事件发生后才附加,137Foundry 的这个 AI 自动化团队已经为将助手集成到真实生产代码库的客户端完成了这种设置。