跳到主要内容

编程智能体绕过而未使用的代码规范(Idiom)

· 阅读需 13 分钟
Tian Pan
Software Engineer

我合作的一个支付团队的高级工程师曾给我讲过一个故事,我认为每一个运行编程 Agent(AI 代理)的团队最终都会经历。他们的代码库有一个 Result<T, E> 封装器——这是自研的,位于单个 core/result.ts 文件中,在该服务的约两百处调用点被使用。新代码被要求在每一个可能失败的函数中传递 Result;而 throw 则保留给真正意料之外的状态。这并非由 lint 规则强制执行。这就是他们的“方言”。

在使用编程 Agent 交付六个月后,他们审计了 Agent 合并的 diff(差异)。大约三分之一的新函数完全忽略了 Result。Agent 选择了 try/catch,返回了 T | null,抛出了带有描述性消息的 Error 子类——在某些设想的代码库中,这些选择中的每一个都是正确的。但在当前这个代码库中,没有一个是正确的。代码通过了类型检查。测试通过了。审阅者批准了它,因为每一行看起来都没有错。但 Agent 修改的文件不再与它旁边的文件保持一致,团队在自己的服务内部悄然滋生出了第二种“方言”。

这就是我想谈论的故障模式:不是 Bug,不是幻觉,也不是违反了 lint 规则——而是惯用法漂移 (Idiomatic Drift)。Agent 交付的代码可以编译、运行并通过测试,但其风格并非你的代码库所使用的。随着合并次数的增加,代码库会分化为 Agent 风格区和人类风格区,而代价会体现在任何仪表盘都无法监控的地方。

惯用法一致性是你的工具无法衡量的质量维度

在编程 Agent 和你的 main 分支之间,存在一整套自动化的质量关卡——类型检查器、linter、格式化器、测试套件、安全扫描器、构建流水线。每一个关卡都在检查可以用规则表达的属性。而风格一致性(那种源于团队在各处使用 Result<T, E> 的一致性)并不是一个规则。它是一种存在于两百个示例中、却没有规格说明的模式。

linter 无法告诉你“在这个代码库中我们不抛出异常,我们返回 Result”。Prettier 无法强制执行“使用 validateOrThrow 助手函数,而不是内联的 if (!x) throw 语句”。类型检查器对这两种形式都完全满意。使代码库对自身团队易于理解的约定——例如提前返回的守卫语句、具有特定参数顺序的测试构建器 DSL、使用字符串区分符而非类继承体系标记错误的方式——都存在于“通过 CI”和“通过审阅”之间的缝隙中。

2026 年的行业调查显示,大型工程组织交付的新代码中,约有 41% 是由 AI 生成的,其中大部分代码在有限的人类审阅下就被合并了。一项 2026 年的研究对比了 AI 编写和人类编写的 Pull Request,发现 AI 的 PR 明显不太可能复用现有的助手函数,而更有可能编写内联的重复逻辑。这种累积效应正是支付团队所观察到的分化:代码库在增长,风格孤岛也随之增长,团队共享的思想模型被那些没人标记为错误的代码弄得支离破碎,因为确实没有什么可以被标记为错误的地方。

这些关卡无法捕获这种现象的原因是结构性的。Linter 在局部语法上运行。Agent 的漂移是它所编辑的文件与它未读的那十八个文件之间的关系。你买不到任何能知道你的团队比起内联守卫更偏好 validateOrThrow 的静态分析工具,因为表明这是你偏好的唯一信号是你的仓库中 validateOrThrow 的经验密度。信号存在于语料库(Corpus)中,而非配置文件中。

为什么 Agent 会向“教科书”风格漂移

编程 Agent 通过关注两个上下文来生成代码:它正在编辑的文件(以及它明确提取的任何文件)和它训练数据的先验知识(Prior)。从构造上讲,训练先验是互联网上公共代码的平均值。互联网上的公共代码不会使用你的 Result<T, E>。它会抛出异常。它会使用 null。它有自己的约定,这些约定汲取自成千上万个团队,但没有一个是你的。

当 Agent 编辑你代码库中的一个函数时,它看到的是直接的上下文——该文件,或许还有通过检索发现的几个邻近文件。除非有机制将整个语料库放入其上下文,否则它无法看到你仓库更广泛的语料。因此,模型进行了插值:一小部分“这个文件看起来像什么”和大部分“做 X 的代码通常看起来像什么”。对于微小的修改(如拼写错误或重命名),局部上下文占据主导地位。而对于任何新颖的内容(如新函数、新的错误路径、新测试),教科书式的先验知识则会胜出。

这与 2026 年从业者报告中不断指出的动态一致:AI 建议会自信地引入忽略架构约定的教科书模式 (Textbook Patterns)。模型在公共代码上的训练量远大于任何团队的私有代码,在缺乏对你自己仓库进行强力检索的情况下,公共先验会压倒团队先验。Agent 并没有犯错。它只是在根据一个不包含你团队词汇表的分布进行生成,而输出忠实地反映了那个分布。

一种脆弱的权宜之计是把所有东西都写下来。AGENTS.md、CLAUDE.md、Cursor 规则——到 2026 年中期,这些文件出现在数以万计的公共仓库中,它们确实有帮助。但它们只能帮你解决你记得写下的规则。那些你无法清晰表达的约定——那些高级工程师即便说不清原因也会重构初级工程师 PR 的理由——正是 Agent 会跳过的约定,因为你没有告诉它,因为你甚至不知道自己知道这些约定。

风格孤岛的复利成本

如果 Agent 的 diff 合并了,代码库增加了一千行不太契合的代码,直接成本为零。功能发布了,测试通过了,On-call 轮值也没有受到影响。这就是为什么这种失败模式如此隐蔽——账单会在以后到期,而买单的团队往往并不是欠债的那个。

这里有三项具体的成本在累积。首先是以后每次阅读时的认知负荷。当开发者打开一个文件并看到 Result<T, E> 返回类型时,他们知道如何处理这个值。当他们打开另一个文件并看到 T | null 时,他们需要切换到另一种心智模型。一个拥有两种错误处理“方言”的代码库,会迫使未来的每一位读者在推理代码正确性之前,先搞清楚自己处于哪种语境中。随着 Agent 处理过的文件越来越多,团队集体的理解时间会缓慢上升。

其次,重构摩擦力成倍增加。当你最终决定迁移、在 Result 类型中添加一个字段,或者为每个可能出错的调用增加一个新的可观测性钩子(observability hook)时,迁移会触及使用 Result 的那两百个调用点——但会悄无声息地遗漏 Agent 编写的那些没有使用该类型的代码。现在,你那个“更新了错误处理路径”的变更就产生了漏洞,如果不审计仓库中的每一个函数,你就无法发现这些漏洞。编写迁移方案的团队以为他们完工了,而凌晨 3 点被报警叫醒的团队则发现事实并非如此。

第三,代码审查疲劳不断加剧。审查者在扫描一个 400 行的 Agent PR 以确保其正确性时,对于“等等,为什么这里没用 validateOrThrow?”这种问题的精力预算是有限的。前几次审查能捕捉到这种偏移,第一百次审查捕捉到的就少了。半年后,团队已经习惯于批准那些能编译并通过测试的代码,因为这是处理庞大代码量时唯一能给出的信号。从业者报告称,生成与审查的比例现在大约是 18 倍——Agent 在 10 分钟内生成的内容,一个细心的审查者需要一个小时才能看完——而这种比例最终达到的平衡状态就是“只要 CI 变绿就发布”。

这种复利效应是让团队感到意外的地方。惯用法偏移(Idiomatic drift)不是由一个糟糕的 PR 造成的。它是代码库重心缓慢重塑的过程。每一个通过审查并存留下来的 Agent 编写的文件,都会成为下一次 Agent 编辑的训练上下文,因为检索(retrieval)会抓取相邻文件,而相邻文件越来越多地正是 Agent 自己编写的。Agent 的“教科书式先验”在你的仓库内部得到了强化,而团队的惯用法则被悄无声息地“边缘化”了。

弥合差距的实际行动

有效的模式是将惯用法一致性视为头等工程问题,而不是等待更智能的模型来解决。在那些面对 Agent 生成的大量代码仍能保持风格一致的团队中,我观察到了四种做法。

惯用法索引,作为上下文加载。 维护一个简短的文件——姑且称之为 IDIOMS.md,把它放在 AGENTS.md 旁边,并在每次操作时都让 Agent 指向它。该文件命名了你的规范并指向了代表性的调用点。“错误处理:使用 core/result.ts 中的 Result<T, E>。参考 services/billing/charge.ts:42 作为一个典型的例子。”这是最低限度的要求:把你能量化表达的惯用法写下来,放在 Agent 能读到的地方。它可能无法捕捉到你无法言传的规范,但能捕捉到大部分显而易见的规范,而且编写成本仅需几小时而非几周。

生成前的检索。 那些交付最连贯的 Agent diff 的团队会运行一个预生成检索步骤:在 Agent 编写新函数之前,它会查询代码库“我们通常如何处理 X”,并阅读三到五个现有示例。这实现起来很直接——对仓库进行语义搜索,加上一些人工微调的查询——它将 Agent 的有效上下文从“此文件加上公共先验”转变为“此文件加上你团队的惯用法”。这是最接近结构化修复的方法。

生成后的惯用法审计。 在 Agent 编写完 diff 后,运行第二次处理——使用相同的模型,但提示词不同——搜索代码库中与新代码最接近的模式,并询问:“这个 diff 使用了相同的惯用法吗?”如果没有,第二次处理会将其重构为现有模式,或者提出一个具体的问题(“这引入了一种新的错误处理风格,是故意的吗?”)并将 diff 标记出来交由人工处理。生成后的审计成本很低,它能捕捉到很大一部分检索阶段遗漏的偏移。

定期的风格孤岛审计。 一季一次,运行一个工具来识别那些风格在统计上与代码库其余部分存在异常的函数——即那些错误处理、导入模式、测试结构偏离中位数的文件。将结果视为重构待办列表(backlog),而不是 PR 的反馈。其重点不是责备 Agent 造成的偏移,而是让偏移变得足够可见,以便团队可以有计划地解决它,而不是在下次迁移过程中才发现。

这些都不是万能灵药。每一种做法都有维护成本,而且成本并不为零。但另一种选择就是我在这篇文章开头提到的分叉(bifurcation),而这种分叉带来的成本要高得多,体现在从 On-call 告警到重构速度,再到新员工上手时间的方方面面。

风格是共享的认知,而非审美偏好

这一观点最难向领导层传达的部分在于其定性方式。风格一致性听起来像是资深工程师出于非理性原因——比如“我们就是喜欢这样”——才关心的事。事实并非如此。风格是团队已经辩论并解决的每一项决策的压缩编码。当一个代码库随处可见 Result<T, E> 时,那是团队的简写,意味着“我们已经决定错误处理太重要了,不能放在 catch 块中,每一个可能失败的调用都应该在类型签名中清晰可见”。一个忽略这种惯用语的新贡献者——无论是人类还是智能体(agent)——不仅仅是背离了一种偏好。他们在强迫团队的其他成员在每一次代码审查(code review)中重新审议一个已经解决的问题。

一个不学习这种惯用语的智能体,从严谨意义上讲,正在瓦解团队共享的认知基础设施。代码库不再是一个统一的产物,而是变成了一堆方言的联邦,每种方言都有自己的假设,且由最后触碰它的人来维护。团队对系统整体进行推理的能力正在侵蚀——并不是因为任何单一的更改是错误的,而是因为团队共同思考的媒介被悄然稀释了。

能够正确处理这一点的团队,并不是那些拥有最聪明智能体的团队。而是那些将代码库的惯用语视为智能体必须遵守的契约、为智能体提供遵守契约所需的上下文,并对结果进行审计的团队。其他所有人都是在预支现在的速度,并在未来的数年里为此偿还利息。

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