Uber 发布了一篇文章,讲他们怎么把 Java monorepo 里 75,000+ 个测试类、125 万行测试代码从 JUnit 4 迁到 JUnit 5。对于任何正在权衡 AI 辅助重构的工程团队来说,最关键的细节是:Uber 明确选择不使用生成式 AI 做这个转换。文章里他们的原话是:在这种规模下,确定性的转换工具对一致性是关键的,而基于 LLM 的方法在自定义测试模式上产生不一致结果。Uber 团队反而选择基于 OpenRewrite 来做。OpenRewrite 是一个开源的语义代码转换框架,操作的是无损语义树而不是原始文本,他们写了针对 Uber 内部基类和测试运行器的自定义 recipe。他们配套做了:一个统一执行的兼容层(JUnit Platform 同时跑 Vintage 和 Jupiter 引擎,让部分迁移的仓库继续工作)、防止部分迁移的前置检查、一个内部叫 Shepherd 的编排系统,把转换并行扩展到几千个 Bazel target 上,每个都通过 CI 验证。

这个选择背后的技术现实比"LLM 还是不用 LLM"这个框架更有意思。在 Uber 这种规模下,最致命的失败模式是静默的不一致:一个在 99.5% 的文件上能工作的转换,悄悄把 0.5% 搞坏了,就会产生 375 个坏掉的测试类,每一个都得诊断、手动修。OpenRewrite recipe 是确定性的;同样的输入 AST、同样的 recipe,每次跑出来的输出都一样,转换可以表达成类型化语义树上可组合的访问者。基于 LLM 的代码转换则在 token 层面不确定,特别是在训练数据里没怎么见过的稀有模式上挣扎,而 Uber 的自定义测试运行器和基类层级正好是这种稀有模式扎堆的地方。InfoQ 文章注意到 Shepherd 早期跑出了一些构建和测试失败,这些失败反过来反馈给转换逻辑去更新;这个迭代循环你真的能用确定性工具跑,因为失败是可复现的。换成 LLM,你重跑同样的 prompt 会得到一个稍微不一样的错误,这在大规模下诊断起来困难得多。

对 AI 编码工具叙事的更广含义,需要说清楚。Uber 不是说 LLM 不能做代码转换;他们是说对这一类特定问题(高量、机械但模式丰富、对正确性要求严格),确定性工具赢了。这跟前沿实验室自己内部做的事是一致的:Google、Meta、微软的大规模代码库重写多年来都用确定性重构工具(重写引擎、jscodeshift、gofmt 风格的变换、Comby、OpenRewrite)做,LLM 只在确定性 recipe 表达不出来的长尾模式上选择性使用。科技媒体里"AI 取代代码重构"这个框架是反过来的:在规模上,AI 辅助是用在写 recipe 和处理边缘情况上,不是在批量转换那一遍。一次性迁移的经济学也偏向确定性:写一个 recipe 是固定成本,能在 75,000 个文件上摊薄;跑一个 LLM 过 75,000 个文件是线性增长的可变成本,而且产出你还得验证。

对工程团队来说,可以行动的要点是把重构任务分三个桶来想。第一,机械的、有限的、规则定义清楚的模式转换:API 重命名、import 更新、注解替换、JUnit 版本迁移。这归确定性 AST 工具,没得商量,Uber 这篇文章是迄今最清楚的规模化案例研究。第二,带判断的语义重构:抽取抽象、为可读性重命名、重构控制流。这里是 AI 辅助编码工具发挥作用的地方,因为修改是局部的、可审查的,LLM 的灵活性在僵硬 recipe 顶不住的地方有用。第三,带嵌入式重构的 bug 修复或 feature 工作:这是 agentic 编码工具的甜点,模型能读上下文并适应。要避免的错误是用一个桶里的工具去做另一个桶的工作。Uber 选择用 OpenRewrite 加上确定性 CI 循环和并行编排来交付 125 万行机械迁移,对第一个桶来说是正确答案。下一次有人提议把 Claude 或 GPT 扔到一百万行的重构上,记住这件事是值得的。