跳到主要内容

你的模型认为你的技术栈已过时 2 年

· 阅读需 12 分钟
Tian Pan
Software Engineer

有一类 AI 生成的 Bug 几乎每次都能通过代码审查,它既不是幻觉函数,也不是捏造的包。它是完美的地道代码——地道到符合模型训练数据冻结时你的技术栈版本。模型为运行 Tailwind v4 的项目编写 tailwind.config.js,在 Hooks 代码库中调用类组件的生命周期方法,或者调用一个在三个小版本前就被弃用、并在你的 lockfile 实际锁定的版本中已被删除的 API。代码看起来毫无破绽。它看起来就像出自某个备受推崇的教程。只是那个教程来自 2024 年。

称之为“训练截止日期 Bug”:这类缺陷的存在并非因为模型推理能力差,而是因为模型对你依赖项的了解带有时间戳,而你的 lockfile 并不在意。一项 ICSE 2025 对 8 个常用 Python 库、7 个代码模型的研究发现,在看似合理的补全代码中,弃用 API 的使用率高达 25–38%——当周围代码已包含过时模式时,这一比例会攀升至 70–90%。这些并非罕见的边缘情况。它们是要求一个冻结的产物为不断变化的目标编写代码时的默认失败模式。

令人不安的是,代码审查无法捕捉到这些 Bug,因为审查是针对不同的威胁进行校准的。人类审查者扫描逻辑错误、遗漏的边缘情况和不熟悉的模式。而训练截止日期 Bug 恰恰相反——它们是熟悉的代码,是关于该库所有已知文档的统计中心。一个在 2022 年学习 React 的审查者会对 React 19 悄悄废弃的模式点头示意。代码没有“坏味道”。它只是无法运行——或者更糟,它在运行时语义发生了微妙的变化。

为什么这类 Bug 在结构上是不可见的

三个特性使得训练截止日期 Bug 难以通过大多数团队现有的直觉和工具来捕捉。

首先,代码局部正确。幻觉出的 API 会快速失败:导入报错、函数不存在、CI 在几秒钟内变红。而过时的 API 通常存在——它只是被弃用、起了别名或发生了微妙的变化。Pandas 在 concat 取代 DataFrame.append 后仍将其保留了多年。代码补全了,你没写的测试没有失败,而弃用警告在安装输出的信息墙中一闪而过。关于 API 演进的研究发现,对模型来说最令人困惑的变化不是剧烈的删除,而是微小的调整——包重构、重命名关键字参数、移动模块——正是这些变化产生了几近可用的代码。

其次,错误是分布式的,而非局部化的。逻辑 Bug 存在于函数中。而陈旧性 Bug 存在于每一行生成的代码与模型从未读取的文件中的版本号之间的关系中。当 Agent 一次性搭建组件、配置环境并编写迁移脚本时,2024 年的影子散布在整个 diff 中。代码审查中没有哪一行是可以单独指出的,这正是为什么没有人指出任何问题。

第三,模型很自信,而自信具有传染性。对版本限制生成(要求模型为特定库版本编写代码)的研究发现,前沿模型仅在约一半的时间里获得成功,但在成功和失败时听起来同样确信。更糟糕的是,同样的研究表明,即使正确的当前文档被直接放入上下文,模型也经常在生成过程中退回到其记忆的版本。陈旧的参数化知识不是一个需要填补的空白;它是一个会对抗你修正的活跃先验。

依赖选择的变体更为糟糕。当模型自行锁定版本时——在 requirements.txtpackage.json 或 Dockerfile 中——它们会重现其训练数据的版本分布。将 LLM 指定的版本与漏洞数据库进行比较的测量研究发现,模型经常锁定带有已知 CVE 的版本,因为模型不是在选择版本,而是在回忆版本。代码是正确的。但依赖项是你供应链两年前的快照。

停止将其视为模型问题

本能的思维定式——“模型过时了,等下一个版本吧”——错上加错。从经验上看它是错的:模型的知识落后于发布几个月到一年,而你的依赖项每周都在更新;这个窗口永远不会关闭。从架构上看它也是错的,因为它把解决方案放在了你无法控制的地方。

有效的思维方式是,知识陈旧是一个依赖管理问题,而依赖管理是工程团队已经擅长的事情。你不会指望 CI “知道”库的正确版本——你会锁定它、审计它,并在它发生漂移时收到警报。模型对你技术栈的了解也应该得到同样的对待:它是一个带有版本的隐式依赖(训练截止日期),这个版本永久落后且无法按你的计划升级。

一旦你这样构思,缓解措施就不再是小技巧,而变成了基础设施:

  • 模型锁定的知识需要与你的 lockfile 进行比对。 对于每个主要依赖项,你要么知道模型的知识早于你锁定的版本,要么就是在瞎猜。
  • 陈旧性需要一个在生成时触发的反馈通道,而不是在代码审查或生产阶段。
  • 修正需要存在于上下文中,因为权重不会改变,而且如研究所示,即使是上下文中的文档也需要表现得足够强势,才能战胜记忆中的先验知识。

文档注入:与 Lockfile 挂钩

第一个有效的缓解方案是将当前的文档注入到模型的上下文中 —— 但这种简陋的版本会以一种极具启发性的方式失败。仅当文档与你实际运行的版本匹配时,粘贴 “最新文档” 才有帮助。如果一个使用 Next.js 14 的团队注入了 Next.js 15 的文档,那就只是用一个过时 bug 替换了另一个 bug,而且这一次,模型还有一个看起来权威的来源来支持它的错误回答。

文档注入的单位应该是 lockfile 条目,而不是库名称。这就是 Context7 类工具做得正确的地方:从项目中解析出库 及其版本,然后在查询时将该特定版本的文档提取到上下文中。如果你在内部构建这个系统,流水线是机械化的 —— 解析 lockfile,将每个关键依赖映射到文档源(例如 llms.txt、更新日志或迁移指南),并注入与当前任务相关的切片。难点不在于检索,而在于将检索与已解析的版本(而非 “最新” 版本)挂钩的自律性。

在上下文预算中,迁移指南值得被赋予特殊优先级。生成 pre-v4 Tailwind 代码的模型不需要完整的 v4 参考资料 —— 它需要的是差异部分:“JavaScript 配置已移除,主题现在位于 CSS 的 @theme 块中,init 命令不再存在。” 更新日志和升级指南是模型所缺失知识的最稠密编码,因为它们是维护者专门为了纠正那些习惯于旧方式的人(以及现在的模型)而编写的。几百个 token 的 “2024 年以来的变化” 比几千个 token 的通用文档效果更好,因为它直接针对先验知识进行修正,而不是指望在权重上压倒它。

Lint 规则:你已经拥有的反馈通道

文档注入是预防性和概率性的。第二种缓解方案是纠正性和确定性的:让你的工具链在生成的瞬间大声拒绝过时的模式。

这之所以奏效,是因为一种值得内化的不对称性:模型不擅长识别什么是最新的,但智能体擅长响应结构化错误。 一个凭借记忆编写 tailwind.config.js 的智能体会乐此不疲地写下去;而同一个智能体,如果看到一条 lint 错误信息说 “v4 中不再使用 tailwind.config.js —— 主题配置属于 CSS”,它会立即正确地修复它。知识不需要存在于权重中,甚至不需要存在于提示词中。它只需要存在于失败消息中。

具体而言,这意味着要将大多数团队视为可选规范的内容提升到智能体的内层循环中:

  • 在生成代码首次运行的环境中,将弃用警告(deprecation warnings)转为错误。警告在智能体面前掠过,就像在人类面前掠过一样;而错误则变成了智能体必须处理的工具执行结果。
  • 采用编码了 API 演进的规则集 —— 标记已移除 React 模式的 ESLint 插件,捕获过时 Python 惯用法的 Ruff 和 pyupgrade 规则,跟踪 Rust API 变动的 cargo clippy lint。每一条规则都是一个在需要时精准交付的 “后断点知识” 单元,且在触发前零上下文窗口消耗。
  • 为你的内部库编写自定义规则。你内部框架的 API 在上个季度发生了变化;没有任何模型的权重会包含这一点。一条写着 “请使用 createClient 代替 Client() —— 已在 v3.2 中更改” 的 lint 规则,就是你通过唯一能确保触达模型的通道,在对模型的知识进行补丁。

类型检查器在签名发生变化的地方免费完成了这项工作,这是智能体时代支持强类型代码库的最强有力实践论据之一。但类型检查会漏掉签名相同但语义变化的情况,且完全覆盖不到配置文件 —— 而 lint 层正好弥补了这一空白。核心逻辑是:每一个你能从 “审阅者记忆” 转移到 “机器可检查规则” 的修正,都会将一类隐形 bug 转化为一个自愈循环。

审计你的过时面(Staleness Surface)

预防和纠正都假设你知道暴露点在哪里。大多数团队并不清楚。这个审计非常简单,本周就可以运行:

  1. 列出你的核心依赖 —— 即大多数生成代码会涉及 API 的那十到二十个库。不是 lockfile 中的所有内容,而是智能体每天实际编写代码所针对的库。
  2. 针对每一个依赖,找到上一次破坏性更新或惯用法变更的发布日期 —— 不是版本号,而是 日期。包括大版本更新、配置格式变化、“新推荐方案” 的公告。
  3. 与你模型的训练截止日期(training cutoffs)进行对比。每一个其最后一次惯用法变更晚于截止日期的依赖都是过时隐患;而那些变更发生在近期 旧惯用法已流行多年的依赖则是严重隐患。2025 年 1 月发布的 Tailwind v4 面对的是长达数年的 v3 训练数据,这就是一个典型的严重案例:极强的先验知识,极大的偏离。
  4. 针对每个隐患,决定缓解方案的层级:在项目的智能体指令中加入一行(“我们使用 Tailwind v4 —— 永远不要创建 tailwind.config.js”),为高频使用的库注入与版本挂钩的文档,或者针对频繁且机械化的违规编写 lint 规则。

然后将此审计视为一份动态文档,因为它会随着两个时钟衰减:每当主依赖升级时重新运行增量审计,以及每当你更换或升级模型时 —— 新的截止日期会消除一些隐患,而你的下一次重大升级又会创造新的隐患。

对于任何 维护 库或平台的人来说,这里还有一个隐含的战略意义:模型现在是你旧 API 的一个分发渠道。你的过时模式在教程中存在的每一年,都会在训练数据中累积一年,而团队会越来越多地将 “难以与 AI 助手配合使用” 直接体验为 “难用”。在发布破坏性版本的同时提供 llms.txt、智能体可读的迁移指南和 lint 规则,不再仅仅是文档的润色 —— 而是你对数百万个你不受控的模型进行补丁的方式。

训练截止日期这类 bug 不会消失;固化的权重和不断演进的生态系统注定了这一点。但它也并不神秘。它的表现就像任何其他依赖漂移(dependency-drift)问题一样:如果你依赖记忆和人工审阅,它是隐形的;一旦你锁定版本、使用 lint 并进行审计,它就是可控的。你的模型认为你的技术栈是两年前的。这没关系 —— 只要你的工具链知道现在到底是哪一年。

References:Let's stay in touch and Follow me for more thoughts and updates