大廠實踐: LLM 加速大規模測試遷移
Airbnb最近完成了第一次由 LLM 驅動的大規模代碼遷移,將 3500 個測試文件從 Enzyme 更新為 React測試庫(RTL,React Testing Library)。最初我們估計這需要 1 年半的時間來手工完成,但通過使用前沿模型和強大的自動化組合,我們在 6 周內完成了整個遷移。
本文將重點介紹從 Enzyme 遷移到 RTL 所面臨的獨特挑戰,如何通過 LLM 解決這些挑戰,以及如何構建遷移工具來執行 LLM 驅動的大規模遷移。
一、背景
2020 年,Airbnb 采用 React 測試庫(RTL)進行所有新的 React 組件測試開發,標志著我們邁出了遠離 Enzyme 的第一步。盡管自 2015 年以來,Enzyme 一直為我們提供良好的服務,但它是為 React 的早期版本設計的,并且該框架對組件內部的深度訪問不再符合現代 React 測試實踐。
但由于框架之間存在根本性差異,我們無法輕易替換(閱讀 Introducing the React Testing Library[2] 獲取更多關于差異的信息)。而且分析發現如果僅僅刪除 Enzyme 文件,會在代碼覆蓋率中造成顯著缺口。為了完成遷移,需要一種自動化方法來將測試文件從 Enzyme 重構為 RTL,同時保留原始測試意圖以及代碼覆蓋率。
二、如何做到
2023 年中,Airbnb 的一個黑客馬拉松團隊展示了大語言模型可以在短短幾天內成功將數百個 Enzyme 文件轉換為 RTL。
在這個很有希望的結果基礎上,我們在 2024 年為 LLM 驅動的遷移開發了一個可擴展流水線。我們將遷移分解為離散的、可以并行化每個文件步驟和配置的重試循環,并通過額外的上下文顯著擴展了提示詞。最后,對復雜文件的長尾執行了寬度優先的提示詞調優。
1. 文件驗證和重構步驟
首先將遷移分解為一系列自動驗證和重構步驟。可以把它想象成一個生產流水線:每個文件都經過驗證階段,當檢查失敗時,就引入 LLM 來修復。
我們將此流程建模為狀態機,只有在前一個狀態通過驗證后才將文件移動到下一個狀態:
這種分步驟的方法為自動化流水線提供了堅實基礎,使我們能夠跟蹤進度,優化特定步驟的故障率,并在需要時重新運行文件或步驟。基于步驟的方法還使同時在數百個文件上運行遷移變得簡單,這對于快速遷移簡單文件和在遷移過程中逐漸消除長尾文件至關重要。
2. 重試循環和動態提示
在遷移早期,我們嘗試了不同的提示工程策略來提高每個文件遷移的成功率。然而,在分步驟方法的基礎上,我們發現改善結果的最有效途徑是簡單的蠻力:多次重試步驟,直到通過或達到極限。我們更新了步驟,為每次重試使用動態提示,將驗證錯誤和文件的最新版本提供給 LLM,并構建了循環執行器,可以配置每個步驟的嘗試次數。
通過簡單的重試循環,我們發現可以成功遷移大量簡單到中等復雜度的測試文件,其中有些需要重試幾次才能成功完成,大多數可以在 10 次嘗試后成功完成。
3. 擴展上下文
對于具有一定復雜度的測試文件,只需增加重試次數就可以了。然而,要處理具有復雜的測試狀態設置或過多間接文件,我們發現最好的方法是將盡可能多的相關上下文放入提示詞中。
在遷移結束時,我們的提示已經擴展到 40,000 到 100,000 個 token,涉及多達 50 個相關文件,大量手工編寫的示例,以及來自同一項目中現有的、編寫良好的、通過測試的文件示例。
每個提示詞包括:
- 被測組件的源代碼
- 正在遷移的測試文件
- 驗證失敗的步驟
- 來自同一目錄的相關測試(維護團隊特定模式)
- 一般性的遷移指南和通用解決方案
下面是實際應用中的樣子(為了可讀性做了部分修改):
// Code example shows a trimmed down version of a prompt
// including the raw source code from related files, imports,
// examples, the component source itself, and the test file to migrate.
const prompt = [
'Convert this Enzyme test to React Testing Library:',
`SIBLING TESTS:\n${siblingTestFilesSourceCode}`,
`RTL EXAMPLES:\n${reactTestingLibraryExamples}`,
`IMPORTS:\n${nearestImportSourceCode}`,
`COMPONENT SOURCE:\n${componentFileSourceCode}`,
`TEST TO MIGRATE:\n${testFileSourceCode}`,
].join('\n\n');
這種豐富的上下文方法被證明對更復雜的文件非常有效,LLM 可以更好的理解團隊的特定模式、通用測試方法和代碼庫的整體體系架構。
應該注意到,盡管我們在這個步驟中做了一些提示工程,但主要成功驅動因素是選擇正確的相關文件(查找附近的文件,來自同一個項目的好的示例文件,過濾與組件相關的文件的依賴項,等等),而不是依賴更完美的提示工程。
通過重試、構建豐富的上下文以及測試遷移移腳本之后,當我們進行第一次批量運行時,在短短 4 個小時內就成功遷移了 75% 的目標文件。
4. 從 75% 到 97%:系統化改進
75% 的成功率確實令人興奮,但仍然有近 900 個文件沒有達到驗證標準。為了解決這個長尾問題,我們需要一種系統化方法來了解剩余文件卡在哪里,并改進遷移腳本來解決這些問題。我們希望首先擴展廣度,積極減少剩余文件,而不要被最困難的遷移案例所困。
為此,我們在遷移工具中構建了兩個特性。
- 首先,我們構建了一個簡單的系統,通過在文件中添加自動生成的注釋來記錄每個遷移步驟的狀態,從而使我們能夠看到腳本所面臨的常見問題。下面是代碼注釋的樣子:
// MIGRATION STATUS: {"enyzme":"done","jest":{"passed":8,"failed":2,"total":10,"skipped":0,"successRate":80},"eslint":"pending","tsc":"pending",}
- 其次,我們添加了輕松重新運行單個文件或路徑模式的能力,根據它們所攜帶的特定步驟進行過濾:
$ llm-bulk-migration --step=fix-jest --match=project-abc/**
基于這兩個功能,我們可以快速運行反饋循環來改進提示和工具:
- 運行所有剩余的失敗文件,以找到 LLM 卡住的常見問題
- 選擇文件樣本(5 到 10 個)來說明某個常見問題
- 更新提示詞和腳本來解決這個問題
- 重新運行失敗文件樣本以驗證修復
- 再次對所有剩余的文件執行上述操作
在運行這個“采樣、調整、掃描”循環 4 天后,我們已經將完成的文件從所有文件的 75% 推到了 97%,只剩下了不到 100 個文件。到目前為止,我們已經對許多長尾文件進行了 50 到 100 次重試,似乎已經達到了通過自動化修復的極限。我們沒有投入更多調優,而是選擇手動修復剩余文件,從基線(失?。╅_始重構,從而減少修復這些文件的工作量。
三、結果及影響
有了驗證和重構流水線、重試循環和擴展上下文,我們能夠在4小時內自動遷移 75% 的目標文件。
經過四天的“采樣、調優和掃描”策略實現的提示詞和腳本優化,我們完成了 3500 個原始 Enzyme 文件的97%。
對于剩余的 3% 沒有通過自動化完成的文件,腳本為手動干預提供了一個很好的基線,幫助我們在一周之內完成了剩余文件的遷移。
最重要的是,我們能夠在保持原始測試意圖和代碼覆蓋率的同時替換 Enzyme。即使在遷移的長尾上有很高的重試次數,總成本(包括 LLM API 的使用和 6 周的工程時間)被證明比最初手動遷移的估算要高效得多。
四、下一步
這種遷移突出了 LLM 對大規模代碼轉換的能力。我們計劃擴展這種方法,開發更復雜的遷移工具,并探索 LLM 驅動的自動化的新應用,以提高開發人員的生產力。
參考資料
- [1] Accelerating Large-Scale Test Migration with LLMs: https://medium.com/airbnb-engineering/accelerating-large-scale-test-migration-with-llms-9565c208023b
- [2] Introducing the React Testing Library: https://kentcdodds.com/blog/introducing-the-react-testing-library