AI News HubLIVE
站內改寫5 分鐘閱讀

接手了一個擁有15萬用户的前端應用,大部分代碼是AI生成的

作者接手了一個內部AI應用的前端,代碼質量堪憂,大部分由AI生成。面對15萬用户,他決定逐步重構,改善代碼質量,同時保持業務運行。

來源Hacker News AI作者: koolcodez

不久前,我在倫敦與一位老同事喝咖啡。我們曾一起參與過一個期權交易項目,我至今仍引以為豪——我們將一個過時、臃腫的Redux Saga重寫成了真正優秀的代碼庫,儘管領域複雜。

他告訴我那邊的項目依然進展順利,而我隨即向他傾訴當前團隊遇到的困難。他打斷了我對代碼質量、AI垃圾等問題的抱怨,直接問道:

你現在有多少用户?

15萬。

我們只有少數交易員。你不能説團隊這樣做不對。

我知道背景很重要,因為我們處於不同的領域,但這仍然讓我思考界限在哪裏。何時接受或拒絕變更?什麼對業務真正重要?

老實説,我不確定。但那次對話時常縈繞在我心頭,所以我想分享當前項目的狀況以及我如何應對。

起點

大約六個月前,我加入了構建公司內部AI應用的團隊。這個產品是公司業務運行的基石,自然備受關注,尤其是來自高層的關注。

代碼質量堪憂。我並非刻薄,而且後面會承認自己也貢獻了一些問題。這是小團隊快速構建、嚴重依賴AI、且從未有安靜的一週來清理的必然結果。

該項目始於2025年初,大量使用AI生成代碼。我聽説後端工程師經常端到端實現功能,這意味着也要接觸前端。有一句關於AI的話讓我覺得很有趣:

AI生成的代碼看起來非常棒,直到它涉及到你的專業領域。

總之,以下是我發現的大致情況。

Web應用是一個TypeScript React應用,仍在使用react-app-rewired。如果你不瞭解,那感覺就像身處中世紀。構建耗時約十分鐘。

檢查工具是從隨機模板設置的,從未集成到git工作流或CI/CD管道中。所以實際上沒有檢查,也沒有編輯器檢查。

只有少數幾個單元測試。沒有可用的端到端測試套件。

TypeScript,但很多地方用了any,手寫的客户端類型與真實響應產生了偏差。

沒有服務端狀態管理器。每個獲取數據的組件都重新實現了自己的加載和錯誤處理。

任何時候都有超過40個打開的拉取請求。花幾天處理一個功能,回來時就會落後數百個提交,因此解決衝突成為常態。

一個手寫的事件總線,任何組件都可以觸發事件,任何其他組件都可以監聽(重新實現Redux?!)。當然,事件名稱在每個註冊處硬編碼,導致“my-event”變成“mY-event”。

localStorage被用來緩存不應該緩存的內容,包括不斷增長的base64頭像圖像。有一次我們因達到localStorage大小限制而開始出現生產錯誤。

一個巨大的上下文包裹所有東西。工件、上傳的文件、預設、集成,全都在同一個地方。

四種不同的文件上傳方式,每種都略有不同(聞起來像是AI生成代碼?哈哈)。

核心組件幾千行,幾乎全是回調、副作用、狀態和標誌,只在最後有一層薄薄的UI。

設計系統糟糕。到處是自定義Tailwind標記,大小、顏色、間距不一致。

有時我震驚於這些東西竟然能運行。

最明顯的做法是什麼都不做。它運行着,人們用着,沒人願意承擔根本性改變帶來回歸的風險。

開玩笑,我不在乎。我無法每天在一個感覺像地獄的代碼庫中工作。這個領域很前沿,項目也令人興奮,但那個倉庫並不有趣,而我希望在工作中享受樂趣,並快速、自信地發佈。

關於我的工作方式,有一點很重要:我認為在作業中使用AI生成代碼是不負責任的。將團隊中無人理解的代碼交付給這麼多人依賴的產品,最終會失敗,而且通常由別人買單。我經常使用AI,但作為工具,有規則,來回修改,並在發佈前閲讀每一行代碼。

我不會將我不理解的代碼交付給這麼多人依賴的產品。我需要能夠將其掌握在腦中。這並非為了原則本身,而是我唯一能快速前進而不讓事情變得更糟的方法。

所以,冒險開始了

我做的第一件事是遷移到Vite。抱歉Webpack,你的時代已經過去了。這花了我大約一週半的時間,將構建時間從大約十分鐘縮短到不到兩分鐘,而且模塊熱重載不會因簡單的className更改而觸發整個頁面重新加載。這類改進在演示中沒人注意,但六個月後,它給整個團隊節省了大量時間。

我確實感到壓力,因為我並沒有立即帶來可量化的成果,但幸好我之前與這個管理團隊合作過,處於被信任的甜蜜點,所以我擁有並且仍然擁有很多自由。

然後是認證。邏輯分散在幾個不相關的組件中,這正讓每個人都不敢觸碰任何相關部分。我將它集中到一個地方,將整個應用置於一個單一入口之後,並刪除了一堆處理訪問、令牌和權限的過時代碼。過程中出現了一些迴歸問題,隨着出現而修復,之後一直穩定。

然後是類型。後端已經發布OpenAPI模式(謝天謝地),所以我們開始從這些模式生成類型,而不是在客户端手寫並期望正確。訣竅是逐步採用,一次幾個端點,而不是停止一切進行大規模重寫。這部分值得單獨寫一篇文章,所以先保留。

然後是一堆小東西。檢查工具已啓用,儘管我們仍有積壓代碼需要清理才能將其集成到CI/CD中。更多測試,更重要的是,就新代碼的外觀達成一致,這樣我們就不會繼續增加混亂。更小的拉取請求,將邏輯從組件移到鈎子中,不再使用any,不使用無根據的useEffect。要點是在我們慢慢修復其餘部分的同時,阻止混亂繼續增長。

當一些優秀的新員工加入,並且他們認同許多相同的原則時,我們開始扭轉局面。當你不是孤軍奮戰,並且得到支持時,事情變得簡單得多了。

移動端噩夢

我加入後不久,一個新需求出現了。我們需要一個移動應用,而且需要儘快完成(CEO對在公司AI應用在手機上的想法感到興奮,以便在去見總統的路上使用)。

我們選擇了Capacitor,它很棒,沒有任何抱怨。但為了進入生產環境,我們不得不實施一些頂級的hack,每次想起都讓我忍俊不禁。

事情從演示開始。我們需要快速展示些什麼,所以在移動端重寫了瀏覽器fetch以返回一些模擬數據,但至少顯示了UI功能,並告訴自己之後會清理。

我們從未清理。代碼堆積起來,變得太大了。所以今天移動應用發出的每一個HTTP請求都被我們自己的fetch版本攔截並轉換為RPC調用,因為通過公司防火牆的唯一途徑是一個端點,所有東西都必須通過它。

而流式傳輸,聊天依賴的部分,是最瘋狂的部分。在移動端,SSE被Swift層轉換為WebSocket以通過那個只支持套接字的端點,然後在無狀態網關節點上轉換回SSE,最後才到達最終實例。我們來回轉換,以便它能夠完成旅程。

想象一下,在這一切之上,添加重連邏輯和斷線消息處理,事情會變得多麼複雜。一點也不有趣。

我們正在制定具體計劃,主要轉向使用套接字,以避免這種儀式,並且也為了只有一個流,而不是同時有多個SSE連接(這是一個設計問題,但你能怎麼辦)。我們將使用react-socket作為編排器。它是開源的。如果你在處理流,可以看看它。

還有很長的路要走

我想誠實地説明這一點。我們開啓了很多事情,完成得很少,還遠未到一半。以下是在進行中的工作,而不是勝利宣言。

設計系統是其中之一。一個並行的CSS文件,加上遷移到Tailwind 4,繼承現有樣式表的優點並增加新內容。目標是逐個遷移每個組件,我們從低影響的組件開始,比如徽章、切換開關、模態框皮膚。

我們使用Storybook作為文檔界面,每個組件都有完整説明和變體。還有很多組件待處理。

我們剛剛開始的另一件事是核心部分的重構。主要驅動者。AI聊天。

我們已經到了一個無法更改它而不引起迴歸的地步。並不是説你需要小心處理,而是觸碰這個文件本身就是風險。在一處做小改動,就會在其他地方微妙地破壞某些東西,可能兩週後你的代碼上線後才發現。

最明顯的症狀是一個bug:開始新聊天時,如果之前的聊天還在運行,內容會泄露到新聊天中。你會打開一個全新的聊天,最後一條聊天的一部分會顯示出來。真噁心。如果存儲有一個頂層的chatId鍵,其他所有內容嵌套在它下面,這個bug本來可以很容易地避免。

那麼計劃是什麼?我們不確定。首先將其分解成更小的部分而不改變行為,然後看看先處理什麼以及順序。誠實的總結是:前面還有很多工作。

最後的話

我意識到這篇文章讀起來像Reddit上的吐槽。也許就是。但我寫下來有兩個原因。

第一個是倫敦的對話,我經常回想起來。15萬用户是一個真實的答案。這並沒有讓代碼不那麼痛苦,但將問題從“這是否可以接受”轉變為“修復它的成本是多少,團隊是否準備好付出”。我們正在付出,緩慢地,在邊緣上,同時保持發佈。大多數時候,這感覺像是正確的速度。

第二個是給任何讀到這篇文章的人,因為他們即將加入像我這樣的團隊,或者已經在一個團隊中,想知道自己是否孤軍奮戰。你不是。現在有很多這樣的代碼庫。出路並不光鮮:一次一個邊界,一個提取的鈎子,一個從模式生成的類型,一個移入Storybook的組件。這些都不會讓路線圖看起來令人印象深刻。但所有這些加起來。

如果你擔心AI會取代你的工作,我認為這並非行業現狀。AI正在產生更多代碼,而不是更少。總得有人能夠將其掌握在腦中。