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 扔到一百萬行的重構上,記住這件事是值得的。
