跳到主要内容

大规模 AI 辅助代码库迁移:自动化处理那些没人想碰的升级

· 阅读需 13 分钟
Tian Pan
Software Engineer

当 Airbnb 需要将 3,500 个 React 测试文件从 Enzyme 迁移到 React Testing Library 时,他们估计该项目需要 1.5 年的人力。通过使用 LLM 驱动的流水线,他们仅用 6 周就完成了交付。当 Google 研究了一个由 3 名开发人员在 12 个月内执行的 39 次不同代码迁移(595 次代码更改,93,574 次编辑)时,他们发现 74% 的编辑是由 AI 生成的,其中 87% 的编辑在没有人工修改的情况下就被提交了,整体迁移时间缩短了 50%。

这些数字是真实的。但这也是事实:在这些迁移过程中,工程师花费了大约 50% 的时间来验证 AI 的输出——修复上下文窗口故障、清理幻觉生成的导入,以及理顺测试未能捕捉到的业务逻辑错误。效率的提升是真实的,痛点也是真实的。问题不在于 AI 是否属于代码迁移;而在于准确了解它在何处提供帮助,以及在何处创造的清理工作超过了它所节省的时间。

你需要的两种截然不同的工具

大多数团队犯的第一个错误是将“AI 辅助迁移”视为单一类别。实际上有两个不同的工具家族,它们各自擅长不同的事情。

基于 AST 的 codemods (ast-grep, jscodeshift, GritQL, OpenRewrite, Comby) 在语法树级别工作。它们是确定性的——相同的输入始终产生相同的输出——并且可以完美扩展。ast-grep 可以在几秒钟内搜索并转换多语言代码库中的数百万行代码。OpenRewrite 拥有 5,000 多个用于 Java、Python、YAML、Terraform 和 Kubernetes 迁移的配方。这些工具不会产生幻觉。它们也不理解你的代码在做什么——它们转换的是结构,而不是语义。

基于 LLM 的智能体 (Claude Code, Copilot, 基于前沿模型构建的自定义流水线) 理解语义。它们可以将回调风格的 API 迁移到基于 promise 的 API,同时保留周围业务逻辑的意图。它们可以阅读内联注释和编码风格,并生成符合代码库惯例的代码。它们有时也会发明不存在的导入,对相同的提示词产生非确定性的输出,并在代码库超过其上下文窗口时静默失败。

实际结论:使用 AST 工具进行检测和结构转换,使用 LLM 进行需要理解含义的语义转换。混合方法——使用确定性引擎进行模式匹配,使用 LLM 重写匹配的部分——结合了前者的可靠性和后者的上下文智能。

AI 迁移在哪些方面确实快了 10 倍

并非所有迁移都是平等的。某些任务类别的特征使得 AI 辅助显然是正确的选择:

具有明确定义转换规则的机械式 API 替换。 React 已弃用的 componentWillMountcomponentDidMount 转换。React Testing Library 对 Enzyme 的 .find().simulate() 的模拟。Next.js 用于 Pages Router → App Router 迁移的 codemods。这些都有记录在案的转换模式,新的 API 表面已被充分理解,并且测试能立即告诉你转换是否正确。AI 在这些方面能取得成功,是因为映射关系清晰且测试信号紧密。

测试框架迁移。 Airbnb 涉及 3,500 个文件的 Enzyme 迁移就是典型的例子。测试文件很少包含 LLM 需要仔细推理的业务逻辑——它们包含遵循可预测模式的测试设置、断言和 mock。输出质量很高,失败模式是隔离的(损坏的测试不会破坏生产环境),并且测试套件本身就是验证机制。

清洁业务逻辑上的语言版本演进。 Python 2 → 3,Java 8 → 17,TypeScript 严格模式采用。当代码是算法性的——主要是数据转换、实用函数、没有复杂基础设施的领域逻辑时——在 80–90% 的文件上实现无需人工干预的正确性是现实的。剩下的 10–20% 是需要人工审查的边缘情况,而不是会传播的结构性故障。

批量机械重构。 在 4,000 个文件中重命名已弃用的符号。移动导入。在启用 ESLint 之前规范化遗留代码库中的空格和格式。这些任务具有足够的确定性,即使是纯粹的 codemods 也能很好地处理——LLM 的作用较小,相应的幻觉风险也较低。

AI 迁移在哪些方面创造的工作量超过了节省的工作量

失败模式集中在四个类别:

复杂的架构重构。 从单体服务转向模块化架构。将一个上帝类 (God class) 拆分为适当的领域对象。重组多模块依赖。LLM 在跨类推理方面表现极差。它们缺乏理解模块之间隐含依赖关系所需的全局代码库上下文,并且往往生成的代码在局部是一致的,但在全局是不一致的。这里的清理工作很容易超过规范的人工方法所花费的成本。

具有特定领域限制的迁移。 舍入行为具有监管影响的财务计算。业务规则嵌入在代码中而非文档中的保险承保逻辑。行为语义由规范驱动的医疗设备软件。AI 可以正确转换表面语法,同时微妙地改变语义,而单元测试无法捕捉到这些变化——并且这些问题可能直到客户投诉或审计时才会浮出水面。

不进行重新设计的大规模架构重写。 将一个 20 万行的 Delphi 应用程序逐行翻译成 C# 并不能产生一个 C# 应用程序——它产生的是一个在错误的运行时上编译的 Delphi 应用程序。AI 智能体可以准确地翻译代码语法和模式,但它们无法修复结构性问题。如果你在迁移时不重新思考架构,你就是在自动化地延续原始的设计错误,而清理成本会在以后出现。

缺少类型信息的代码库。 无类型的 JavaScript、没有注解的 Python、没有文档的遗留 C 语言。当代码没有明确类型和关系时,LLM 会幻觉出这些信息。一个幻觉出的类型会产生级联效应:一个虚构的接口定义导致调用方传递错误的形状,从而导致生产环境中的序列化错误,最终导致在一个看似与迁移完全无关的系统中发生故障。

确保安全性的验证策略

无论你使用哪种工具,有一个原则是不可逾越的:在人工审阅 Diff 之前,每个迁移的文件都必须通过其现有的测试套件。

Google 的生产流水线强制执行这一点。变更在开发者审阅之前通过 CI/CD 进行验证——开发者只会看到已经通过构建和测试的变更。这把审阅的工作量从“这正确吗?”转变为“是否有测试未覆盖到的地方?”。这是一个更小的认知任务,也是“人机回环”模式的正确用法。

测试关卡对你如何安排迁移顺序有明确的影响:你无法在测试覆盖率低的代​​码库上安全地运行大规模 AI 迁移。 在尝试 3,000 个文件的迁移之前,你需要知道哪些文件具有实质性的测试覆盖,哪些没有。有覆盖的文件进入自动化流水线。没有覆盖的文件则需要手动迁移,或采用测试先行(test-first)的手动迁移——即先编写测试,再迁移,最后验证。如果不加区分地混合处理,会产生虚假信号:流水线报告 3,000 个文件已迁移且测试通过,但你却在 400 个没有测试的文件中悄无声息地引入了行为退化。

对于生产系统,单元测试之外的验证层也非常重要。契约测试(Contract testing)可以验证重构后的代码在其他服务依赖其 API 行为时是否符合接口预期。跨家族 LLM 裁判(使用不同的大模型家族来审阅 AI 生成的 Diff)可以捕捉到原始模型无法识别出的幻觉。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates