優秀的工程師會在建構前進行規劃。我過去常見的工作流程是:撰寫 RFC 文件描述功能,將其拆分成較小的問題,然後逐一建構,每個問題經常會阻礙下一個問題的進展。工作結構在寫下第一行程式碼之前就已確定。

這是合理的。它能讓程式碼審查更容易管理,並避免一次性的大規模合併。但這也意味著你必須在對問題了解最少的時候,做出最關鍵的結構性決策。

在你建構任何東西之前,你都在猜測:哪些部分可以分離,每個部分會有多複雜,第三步是否會迫使你重新思考第一步。有時候你會猜對。但很多時候你會猜錯,第一步會被捨棄。你透過建構學到了一些東西,但如果先建構整個東西,你會學得更快。

我們過去付出這個代價是有充分理由的:另一種選擇是建構所有東西然後手動拆解,而拆解一週的工作比提前規劃更困難。事先決定界線從來都不是為了讓建構更容易。它是為了讓審查成為可能,而且是唯一能達到此目的的經濟實惠方式。這部分已經改變了。

有三件事的成本大幅降低了。建構:AI 助理能在幾小時內,甚至幾分鐘內,將一個明確的問題轉化為可運行的程式碼。設計:你可以用對話的速度來審視計畫並進行重塑。而這裡最重要的,是拆解已完成的分支:將一週的零散工作拆分成一系列小型 PR,過去這是工作中 O最繁瑣的部分,這也是我們過去為何會避免它的原因。現在這只需要一個提示。

有兩件事的成本沒有降低。第一是程式碼審查中的判斷力。AI 助理幾乎免費地處理了機械性的部分(一致性、小錯誤、明顯的 bug),但它們無法解決微妙的正確性問題或圍繞它的問題:這個變更是否應該在這裡,這個端點的結構在六個月後是否會造成問題。機器人批准你的 PR 與你理解程式碼是不同的,如果你沒有編寫程式碼,閱讀它是你掌握它的方式。狹窄的 PR 讓這種閱讀成為可能。

第二是產品驗證。運行程式碼並決定這是正確的建構方向仍然很慢。新的是能夠在早期就讓整個功能運行起來,並在任何人閱讀程式碼之前就展示給他人。

所以,停止為了規避一個已不存在的成本而預先決定界線。

需要澄清的是:這不是「跳過規劃直接開始編碼」。在我觸碰編輯器之前,我有一個計畫,每個功能都始於一次審問。我運行 `grill-me`,這是一個技能,它會以對抗性的回合方式審問你的想法,直到它有真正的決策。如果 API 調用失敗,備用方案是什麼?我對所有內容都運行它,包括小的變更,它會不斷浮現我不知道存在的漏洞。

當設計新穎時,計畫就變成了一個在任何程式碼存在之前就提交的規格。在另一個專案中,第一個 PR 是一個文件:功能是什麼,它將如何工作,信任邊界在哪裡。它在任何實施存在前幾天就合併了,所以團隊可以先提出反對意見。

永不提交的是對 PR 的拆解。在建構之前決定要建構什麼;在建構之後決定如何拆分。

一旦設計確定,我就開始建構。我經常分階段進行,但我不會在每個階段都停下來打開一個 PR 並等待審查。所有東西都保留在一個分支上,直到它能夠端對端運行,跨越任何必要的檔案。

提交會發生,但它們不是給別人的里程碑。它們是儲存點:一個概念被證明,或者我即將嘗試一些有風險的事情,並希望有一個可以回退的點。我最近完成的一個重構,在一天內跨越了十幾個檔案,有十幾個提交,並附有操作訊息。這些是路標,讓我看到我走過的路,而不是給審查者的故事。

這是讓一些工程師感到不安的部分,我理解為什麼:git 歷史應該是記錄。但這個歷史從未成為記錄。最後的 PR 是從 `main` 分支新鮮切出的,而建構分支是你可以丟棄的草稿紙。兩個不同時間點的受眾,我(開發者)和審查者,讓他們分開可以讓你為兩者進行優化。

一旦工作進入良好狀態,我就停止並展示它。不是以 PR 或程式碼審查的形式,而是以 Slack 中的短影片,或者如果變更需要點擊,則進行預覽部署。在任何程式碼審查開始之前,從使用者那裡獲得對運行軟體的反饋。

現在審查的成本很高,因為便宜的部分已經自動化了。在審查時才發現你建構了錯誤的東西(令人困惑的 UI 模式,前端使用資料時的端點結構不符)會浪費審查者和你的時間。演示可以在變更成本較低時發現問題。如果它浮現了需要重新思考的東西,我會先對反饋運行 `grill-me`,這樣迭代就不僅僅是感覺。

建構順序只是拆分的粗略草稿:步驟會被重新組合和重新切割。刪除 PR 是最清晰的例子,一個只在最後存在的單元,一旦新的路徑到位。

將當前工作拆分成最小的可獨立審查的 PR 集,每個 PR 都可以安全地合併,並且在工作允許時,每個 PR 都能為使用者提供價值。創建一個 git worktree 並從 `main` 分支為每個 PR 分支。僅在依賴關係真實存在時堆疊;否則從 `main` 分支創建。任何對正在替換的程式碼的刪除都放在其自己的最終 PR 中。在創建任何內容之前,請向我展示建議的拆分。

所需時間取決於工作的複雜程度,從幾分鐘到幾輪來回。無論哪種情況,都沒有手動的 cherry-picking。

那個重構最終變成了五個 PR:兩個後端端點作為 `main` 的兄弟節點,兩個前端視圖各自建立在需要其資料的後端 PR 之上,以及一個純粹刪除舊路徑的最終 PR。刪除的程式碼行數比整個功能添加的程式碼行數多幾百行。這就是以這種順序完成重構的形狀。

我通過反覆執行得出了兩個規則:

僅在依賴關係真實存在時堆疊。前端視圖 PR 依賴於其後端端點 PR,並且分支反映了這一點。其他所有內容都來自 `main`。為了方便而堆疊會創建一個 rebase 鏈,一旦底部的 PR 收到反饋,你就會後悔。

清理工作最後發布。舊程式碼在其自己的 PR 中被刪除,在新路徑上線後。將刪除與創建混合會讓審查者感到困惑,使回滾變得模糊,並將清理工作淹沒在功能的噪音中。

拆分也是我閱讀自己工作的時候。逐一 PR 地查看 diff,以一個我能在腦海中掌握的大小,這是在交付 AI 編寫的程式碼與理解它之間的區別。我寧願在那裡找到自己的問題。

一個實際的注意事項:管理堆疊會快速消耗上下文,所以將拆分工作交給子代理,然後由主代理報告。在 Cursor 中,`split-to-prs` 技能會處理這個問題。

沉重的 PR 仍然沉重,而且應該如此。後端 PR 承擔了真正的架構風險(新的資料模型、新的 API 介面、信任邊界),這也是審查者應該花費注意力的地方。消費它們的前端 PR 幾分鐘就能讀完。小型、專注的 PR 流動順暢。大型 PR 則停滯不前。

在這個規模下,代理審查循環也更快。在 Adapt,我們使用 Adapt 本身作為審查者,一個具有先前工作業務背景的代理。它在 PR 打開後幾分鐘內就會發表評論,交換過程(問題、澄清、小修復)會在十分鐘內解決。這只有在 PR 足夠小以便快速閱讀時才有效;一個混合了多個問題的 2000 行 PR 根本不會得到人類的這種待遇。

逐步合併也使部署更容易管理。如果出現問題,你可以回滾一個專注的變更,你的錯誤追蹤會指向該變更,而不是整個合併的堆疊。

這些是收益。它們伴隨著兩種成本。

Rebasing。當審查者要求對一個 PR 進行變更,而另一個 PR 疊在其之上時,上面的每個分支都需要 rebase。這並不經常發生,因為審查者通常只會審查葉節點 PR,但當它發生時,你會感受到它的影響。保持產生拆分的聊天會話打開:它仍然持有拆分,所以你可以重新提示它來更新上面的分支,而不是手動操作。

拆分不是交付。在我寫這篇文章時,那個重構的五個 PR 都還開著。一個好的拆分使每個 PR 都易於審查,並且值得單獨合併。代理為我做了前者;後者是我的決定,當我決定每個 PR 包含什麼時,以及我的團隊的決定,當他們決定合併什麼時。我的 PR 懸而未決,因為我只做了前者。即使所有五個 PR 在同一天合併,我仍然獲得了審查的好處和每個變更的乾淨回滾目標;我失去的是增量交付,因為沒有任何東西更早地到達使用者,並且部署是作為一個批次進行的。

適合場景:跨越多個後端和前端表面的功能,你不知道最終形狀直到完成的重構,任何你原本會猜測問題界線的工作。

較難的場景:必須在生產環境中排序的遷移和架構變更,其中順序是真實的,你應該先規劃它。有一個明顯的切割點的工作。功能 A 無法單獨發布,永遠;如果所有東西同時落地,晚期拆解只為你帶來增量審查,沒有其他好處。

啟發式方法:如果你在建構任何東西之前就在寫 RFC 並猜測如何將其分解成問題,那麼這些時間可能更好地用於建構。你將在結束時獲得更好的答案。我一直在幾個專案中測試這個方法,它一直有效。

結構性決策並未消失。當你利用已有的程式碼來做決策時,它的成本會大大降低。