跳到主要内容

为什么 AI 生成的注释腐烂得比代码还快

· 阅读需 12 分钟
Tian Pan
Software Engineer

当智能体(agent)在同一个 diff 中编写函数和注释时,该注释并不是文档。它是代码在编写时的转述,由同一个模型从同一个上下文中生成。当代码第一次发生变动时,它就会悄然出错。函数被重构,参数类型改变,或者添加了提前返回(early-return),但注释却保持不变。到下个季度,注释所编码的规范已不再与代码匹配,而下一位读者会因为注释更易读而选择相信它。

这是一个古老的失效模式 —— 人类修改代码,注释保持陈旧 —— 但智能体从三个维度同时加速了这一进程。注释量增加了,因为智能体无论是否需要,都会给每个函数添加文档块(doc block)。注释的语法非常完美,所以审阅者不会将其标记为低质量。而且,注释用与代码实际执行不同的术语来转述代码,因此它们看起来像文档,但实际上编码了第二套规范,这套规范独立于第一套规范而漂移。

代价在后期显现,表现为一个不断累积的维护陷阱。未来的读者根据注释构建心理模型。当两者出现分歧时,未来的智能体 —— 或未来的工程师 —— 会“修复”代码以匹配注释。最终交付的 bug 并非幻觉,而是对错误规范的忠实执行。

第二规范问题

转述其上方函数的代码注释是第二套规范。函数在代码中表达一个意思;注释用英语表达一个与之相近的意思。只要两者是由同一位作者在同一时刻编写的,它们就是一致的。代码一旦变动,它们就会产生分歧,并且没有任何机制能让它们再次达成一致。

对于人类编写的注释,这个问题会自我限制。人类懒于写注释,所以存在的注释往往编码了作者认为值得表达的内容 —— 一个不变式(invariant)、一个变通方法(workaround)或一个非显而易见的原因。这些内容仍然容易发生漂移,但总量较小,且幸存的注释通常编码了关键信息,细心的审阅者在周围代码变动时会注意到这些信息。

智能体没有这种过滤器。一个智能体会很乐意写一段四行的 docstring 来描述一个三行的函数。docstring 重新表述了参数名、返回类型以及显而易见的事实。在第一天,这些都没有错。在第一天,这些也都没有用。但其中的每一行都是对未来的“人质”:当函数改变时,docstring 中的每个子句都有可能出错。在经过两次重构后,整个注释块仍然准确的概率趋近于零,而且由于注释读起来依然流畅,没人会注意到这一点。

这种不对称性正是其危险所在。崩溃的 AI 生成代码往往会产生响亮的动静 —— 类型错误、测试失败、预发环境(staging)出现 500。而发生漂移的 AI 生成注释则是悄然崩溃的。当 docstring 撒谎时,没有测试会失败。当正文说“缺失键时返回 null”而代码现在抛出异常时,没有编译器会发出警告。注释的腐烂是不可见的,直到读者据此采取行动。

为什么审阅者会放过这些注释

在 2025 年的一篇关于 Copilot 代码审查的 GitHub 博客文章中,团队注意到智能体现在每次审查平均产生约 5.1 条评论,并在 29% 的 PR 中完全保持沉默 —— 这是为了减少噪音,使高信号的反馈得以保留而进行的明确努力的结果。此时,审阅方的纪律已经得到了很好的理解:当 AI 审查评论的操作率低于 30–40% 时,你就是在产生噪音,配置需要收紧。

但同样的纪律很少应用于 diff 内部 AI 生成的 docstrings 和行内注释。原因有两个。

第一,注释不会破坏构建(build)。审阅者在扫描一个 400 行的 PR 以寻找问题时,关注的是签名、控制流、错误处理以及显而易见的出错点。辅助函数上一个语法整洁的 docstring 在视觉上是绿灯 —— 它看起来像是作者足够细心地进行了记录。审阅者的模式匹配器将其视为积极信号并继续前进。

第二,这些注释整齐划一地流畅。人类编写的注释风格多样,反映了作者与代码的关系:对自己编写并信任的代码言简意赅,对令其紧张的代码充满防御性,对令其惊讶的代码长篇大论。智能体编写的注释风格统一且考究,语法统一且完整,并且统一使用中性的文档语气。没有语气信号能提示这条注释是否比下一条更关键。因此,审阅者要么全部信任它们(常见情况),要么全部不信任(罕见且令人疲惫)。

流畅性差距就是陷阱。一个会拒绝 "ok now we add the user" 这种注释的审阅者,却不会拒绝 "Adds the user to the database after validating the email and normalizing the username for case-insensitive lookup."。第二句话并不比第一句话更经过验证。它只是格式更好。

复合维护陷阱

注释第一次与代码发生偏离时,成本很小 —— 一个困惑的读者,损失几分钟时间。复合成本则发生在更远的一个周期,即这种偏离开始影响未来的修改时。

有三种模式会反复出现。第一种是“相信注释”式的修改,即未来的工程师或智能体阅读了注释,没有仔细重新推导代码的行为,并编写了依赖于注释所言而非代码所为的代码。第二种是“修复代码”式的修改,即工程师或智能体注意到了差异,并根据注释“修复”了代码,假设注释代表了原始意图。第三种是“文档即 API”式的修改,下游消费者 —— 包括代码库其他部分的智能体 —— 像对待契约一样引用注释。

这些失败中的每一种都比简单的“注释过时”更为严重,因为每一种都会将错误的规范传播到新代码中。当 bug 浮出面时,注释、调用代码以及可能的一些测试都已经相互一致,却与系统实际需要执行的操作不一致。对其进行排查(Bisecting)非常昂贵。这里没有单一的坏提交;而是一种由于注释对附近的每一次修改产生“引力质量”而导致的缓慢偏离。

这种现象在 AI 辅助开发中已被记录为“文档偏离(documentation drift)” —— 智能体工作流的无形杀手,即智能体的配置引用了过时的架构文档,而每一个建议都建立在谎言的基础之上。同样的动态也发生在源文件内部的函数级别。一旦智能体写了一个错误的注释,下一个智能体就会将这个错误的注释视为事实真值(ground truth)。

将智能体注释视为一类独立的产物

解决办法不是“停止使用 AI 编写代码”,而是在代码评审中将 AI 生成的注释视为一类独立的产物,并执行比 AI 生成的代码更严格的规则,因为其失败模式更隐蔽,且验证手段更弱。

以下三条规则涵盖了大部分情况。

除非你自己原本也会写,否则不要合并该注释。 这是最简单的过滤器,它从源头上消除了大部分腐烂。如果函数是自解释的,而且你本来不会写 docstring,那就删掉智能体生成的。如果注释只是在复述代码,删掉它。如果注释编码了一个非显式的不变量 —— 即未来读者无法从代码中推导出的那种 —— 请保留它,但用你自己的话重写,从而为你所声明的内容负责。智能体只是草稿提供者,而不是共同作者。

将关键的“承重”注释提升为测试。 一句写着“如果输入列表为空则失败”的注释是在编码一个前置条件。与其相信文字能保持准确,不如编写一个当前置条件被违反时会失败的测试。测试是不会偏离的注释,因为破坏契约的重构会使测试失败。这并不是什么新鲜观点 —— 这是十年前的“可执行规约”论点 —— 但在 AI 辅助的代码库中,它的意义有所不同,因为另一种选择是让模型生成的文字充当契约。

针对“六个月后”的版本评审注释,而不是“第一天”的版本。 在第一天,注释在构造上是准确的。评审者的工作是问:“如果函数形状在两次重构中发生了变化,这个注释还会准确吗,还是会悄悄变错?”如果答案是“悄悄变错”,那么该注释就是一种负担,应该被移除,或者重写为编码一些更具韧性的东西 —— 一个意图、一个原因、一个约束 —— 而不是对当前实现的复述。

这些规则可能会让人感到不适,因为它们要求评审者删除某人(或某个智能体)编写的代码。辩护理由是:一个空的函数比一个带有错误 docstring 的函数更接近正确。沉默胜过自信的谎言。

Lint 规则、启发式方法与团队策略

除了人工评审,还有一层值得构建的工具层,尽管它并不完美。注释是由智能体编写的信号是统计性的,而非确定性的 —— 统一的语法结构、小函数上的文档样式格式、词汇与函数名及参数名过于相似。这些在单一注释上都不是决定性的,但综合起来可以为你提供有用的先验判断。

对于 N 行以下的函数,如果其 docstring 语法高度统一,则使用 lint 规则进行标记,这虽然烦人但很有用。它能识别出最容易成为“伪装成复述的腐烂”的那类注释。一个 pre-commit 钩子,要求在添加 docstring 而没有伴随行为变更时提供理由,虽然手段较硬,但能堵住最常见的漏洞。一个 CI 检查,根据函数签名重新验证 docstring —— 至少确认参数和返回类型仍然匹配 —— 可以捕捉到最机械的偏离。

这些工具都无法捕捉语义偏离。一个写着“此函数通过主键去重”的注释,在去重逻辑切换为整行哈希时,lint 检查不会报错。防止语义偏离的唯一防线是评审者,他需要同时阅读注释和代码,并询问两者是否表达了同样的意思。智能体可以辅助这种评审,但要谨慎:LLM 作为评委(LLM-as-judge)在检查“此注释是否匹配此代码”时,会继承产生该注释的同种“复述倾向”,它更有可能确认一个流畅的注释,而不是标记它。

团队策略层面的做法是使标准显式化。评审注释是看它们在六个月后会说什么,而不是看它们今天说什么。AI 生成的 docstring 默认应该是可删除的,除非它们编码了文字能表达而代码不能表达的东西 —— 通常是“为什么”(why),偶尔是变通方案,极少数是“为什么不”(why-not)。除此之外的一切都是复述,而复述就是等待发生的腐烂。

前行的道路

务实的立场是,AI 生成的注释在默认情况下是负收益的,只有在通过明确的过滤机制时才具有正收益。这种过滤机制与人类本应应用于自身注释的准则是一致的 —— 即这段注释是否编码了代码本身无法提供的信息 —— 但现在这一点变得更为重要,因为注释的数量增加了 5 倍,而流畅度提升了 10 倍。

处理得当的代码库与 AI 辅助的平均水平相比,看起来会显得注释略少。函数将通过签名、类型和测试来完成以前需要散文式文字尝试表达的工作。少数幸存下来的注释将编码那些未来的读者确实无法从代码中重构出的原因、约束和警告。这些注释值得阅读,因为它们是那些没人舍得删掉的内容。

处理不当的代码库看起来会有着完美无瑕的文档,但会悄然积累一套用英文编写的并行规范,随着每一次重构,这些规范会与代码渐行渐远,并在引力作用下诱导未来的修改走向错误的答案。最终上线的 Bug 看起来会像是智能体的幻觉。但实际上,这是智能体在忠实地执行一份团队在六个月前未经仔细阅读就批准的规范。修复方法不是换一个更好的模型,而是删除键 —— 尽早使用,且更频繁地使用。

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