我從新進工程師開始到現在,已經滿 1 年了。
當我請生成式 AI 協助實作時,實際寫程式的時間確實明顯縮短了,這點我很有感。
另一方面,像我這樣的初階工程師,因為要理解生成出來的程式碼需要花時間,開發整體並沒有如預期般變快,我也深刻感受到這一點。
因此,我先嘗試用 AI 做了一份用來閱讀生成程式碼的審查指南。
只要按照指南,就能確認每一段處理各自是在做什麼。
但即便如此,還是無法理解它們彼此怎麼串起來、以及為什麼會是那樣的架構。(一敗)
最後,還是得花時間把程式碼重讀一遍,而且即使如此,自己也不確定是不是真的理解了,結果還是會請前輩幫忙 review。
即使實作時間變短了,這樣也不能說整體開發變得更有效率。
於是我開始思考,問題或許不在於程式碼該怎麼讀,而是 在請 AI 實作之前的準備。
如果能先整理好要做的功能需求與處理流程,並把它變成可以用自己的話說明的狀態,就能把 AI 產生的程式碼和自己的預期做比較。
這篇文章會介紹我這個初階工程師,如何透過和 AI 來回討論,整理需求與實作範圍,並記錄成 ADR,讓生成的程式碼更容易理解。
目標不是 「讓輸出的程式碼更容易讀」,而是 「讓自己能預想輸出會長什麼樣子」
回到主題。
我認為,生成的程式碼難以理解的原因,出在請 AI 實作之前的階段。
如果腦中沒有整體處理流程,就無法想像處理會以什麼順序進行、每段程式碼各自扮演什麼角色。
在這種狀態下讀程式碼,就只會變成一段一段確認眼前的處理,最後反而失去處理彼此之間的連結。
實作範圍不清楚,也是讓程式碼變複雜的原因之一。
如果沒有先決定要實作什麼、不要實作什麼,就草率地把需求丟給 AI,有時會連本來沒打算要的例外處理或抽象化一起做進去。
但因為請求的一方自己也沒有畫出完成後的樣子,所以即使生成的程式碼偏離了自己的預期,也不會察覺到那個「落差」。
結果就是拿到一份比需要更複雜的實作。
「AI 好像思考得太多了……」
「感覺改動比想像中還長……」
在整體流程與實作範圍都模糊不清的情況下,根本沒辦法用自己的話說明生成的程式碼在做什麼。
每次讀程式碼都得問 AI「這段程式在幹嘛」,結果原本靠實作省下來的時間,又被拿去理解程式碼了。
而且問得越多,使用 AI 的成本也會跟著增加。
例如,假設你用下面這個 prompt 請 AI 實作:
讓使用者列表可以下載成 CSV
請求方原本想的是:從既有的列表取得流程中取出必要欄位,然後只做成 CSV 格式回傳的簡單修改。
但如果沒有把範圍說清楚,像是「只需要 CSV」「資料量不大」「不需要檔案儲存或非同步處理」,AI 有時會把未來擴充的可能性也一併考慮進去。
例如,可能會多出像下面這些機制:
原本只要一個處理就能完成的改動,結果膨脹成跨越多個類別與設定檔的實作。
如果未來真的要支援多種輸出格式或大量資料,這些機制或許是必要的。
但如果目前的需求只有 CSV 輸出,那這些都只是還不需要的擴充性。
這裡我把針對目前需求做得過頭的設計,稱為 過度工程化(Overengineering)。
近來 AI 越來越強,反而常常給人一種「好心辦壞事」的感覺
如果請求方沒有先決定實作範圍,即使能問 AI 每個類別是做什麼的,也沒辦法判斷這些類別是不是這次真的需要。
結果就會因為「AI 的實作」和「使用者的預期」之間產生落差,讓生成程式碼更難理解,理解與 review 都會花更多時間。
要理解生成的程式碼,不只是拿到程式後再想辦法讀懂,還必須在 請 AI 實作之前,就先把自己的預期說清楚。
首先,要先能用自己的話說明想要達成的需求與處理流程。
這裡不需要把實作細節全部決定好。
在實作前,只要能說明以下 3 點即可:
只要能說明這 3 點,就能看出哪些範圍可以交給 AI。
有了這個準備,就能把生成出來的實作和自己的預期做比較。
剛才那個 CSV 輸出的例子,也能判斷 JSON 切換或非同步處理不在本次範圍內。
因此可以請 AI 刪掉不必要的實作,更容易避免過度工程化。
這樣一來,原本在 review 前才會發現、但自己又說不清楚的部分,可以先被找出來並確認;前輩也不需要從頭讀解程式碼意圖,review 的負擔自然會降低。
為了讓每次實作前的準備都能用同一套流程進行,我註冊了 dig-and-write-adr Skill。
它是一個先用 dig Skill 和 AI 確認實作方針,接著把決定內容記錄成 .md 的 Skill。
各個 Skill 的細節之後再說明。
---
name: dig-and-write-adr
description: 透過徹底提問來精煉計畫與設計,同時也建立 ADR 與術語表。
disable-model-invocation: true
---
執行 Skill 工具的「dig」,在結束並輸出整理格式後,將對話內容以 ADR 的形式記錄成 md。
我參考的是這個 grill-with-docs Skill。
dig 階段)在請 AI 實作之前,先確認需求與處理流程。
決定這次要實作到哪裡、這次不實作什麼,目標是讓 AI 和自己都能想像出同一個完成樣貌。
不過,經驗尚淺的初階工程師,要一個人把所有該討論的論點都找出來是很困難的。
就算自己覺得已經整理得差不多了,還是可能漏掉例外情況的行為,或對既有功能造成的影響。
所以,我會在實作前的來回討論中使用 dig Skill。
用以下格式呼叫我這次做的 dig-and-write-adr,就可以觸發 dig:
/dig-and-write-adr <question>
接著,AI 會提出用來決定實作方針的問題,以及 4 個選項。
你不需要自己從零開始思考所有檢討項目。
可以一邊從提示的選項中選擇方針,一邊進行對話,因此即使經驗不多,也比較容易發現漏掉的考量。
在回答問題的過程中,需求、處理流程與實作範圍會逐漸變得明確,也能減少和 AI 之間的認知落差。
$ /dig-and-write-adr 想讓使用者列表可以下載成 CSV
---
### Q1: 「使用者列表」是指既有領域中的哪個列表?
這個 codebase 裡沒有 `User` entity 或認證使用者清單;若以人物列表來看,只有飼主(`Owner`)和獸醫師
(`Vet`)。如果對象弄錯,CSV 的欄位、流程與 API 設計都會跟著改變。
- **A** — 飼主列表(`/owners`)
- **B** — 獸醫師列表(`/vets.html`)
- **C** — 飼主與獸醫師兩者
- **D** — 新增認證使用者/利用者管理功能
**建議: A** — 因為目前 UI 中有可搜尋的人物列表,而且姓名、地址、城市、電話號碼這些 CSV 需要的欄位也都齊備。
CSV 輸出對象:
❯ A — 飼主列表
B — 獸醫師列表
C — 飼主與獸醫師兩者
D — 新的認證使用者管理
Other (type your answer)
如果有看不懂的詞,也可以隨時提問,AI 會補充說明。
$ 既有領域是什麼?
---
「既有領域」是指這個應用程式目前正在處理的業務對象/概念。以 Spring Petclinic 來說,主要是 **飼主
(Owner)・寵物(Pet)・就診(Visit)・獸醫師(Vet)**。
也就是說,剛才的問題是在確認「使用者」這個詞,在這個應用程式裡到底指的是誰。
### Q1: 想下載成 CSV 的人物是誰?
- **A** — 飼主列表
- **B** — 獸醫師列表
- **C** — 飼主與獸醫師兩者
- **D** — 登入使用者等新種類的使用者
**建議: A** — 因為可以直接把目前「Find owners」畫面顯示的列表,自然加上 CSV 下載功能。
CSV 要輸出的人物:
❯ A — 飼主
B — 獸醫師
C — 飼主與獸醫師
D — 新種類的使用者
Other (type your answer)
它問得滿細的,會有點累
在 dig 階段決定好的內容,會以 ADR(Architecture Decision Record) 的形式記錄成 Markdown 檔。
(我目前是用 Obsidian 管理。只要裝了 Obsidian Skill,它就會自動把相關檔案串成連結,這點很方便。)
只要把要實作的內容、不要實作的內容、採用的處理流程,以及選擇這個方針的理由留下來,之後回頭看也能追溯當初的判斷過程。
以 CSV 輸出的例子來說,會記錄像下面這樣的內容。
adr/user-list-csv-export.md
# 使用者列表的 CSV 輸出
## 決策
- 對象是飼主(`Owner`)。
- 使用既有的列表取得流程。
- 輸出格式僅限 CSV。
...
## 非目標
- 檔案儲存
- 非同步處理
- JSON 等非 CSV 的輸出格式
...
## 理由
由於目前資料量不大,而且暫時沒有要求 CSV 以外的輸出格式,因此
etc ...
在實作過程中如果迷路了,可以回到 ADR,確認生成的程式碼是否符合當初決定的方針。
就算 AI 遺失了前文脈絡,只要重新讀入 ADR,就能再次共享實作範圍與方針。
對要 review 的前輩工程師來說,這也是一份能輔助理解程式碼以外實作意圖與判斷理由的資料。
例:在 spring-petclinic 中實作使用者列表 CSV 輸出功能
$ 讓使用者列表可以下載成 CSV
只把這句話丟給 AI,然後直接請它實作。
生成的程式碼裡,包含了為了支援多種輸出格式的介面與 Factory、儲存檔案的機制、非同步處理等等。
但因為在請求前沒有先決定實作範圍,所以無法判斷哪些才是目前需求真正需要的。
如果逐一問 AI 每個類別的用途,雖然可以理解程式的角色,卻還是無法說明它為什麼是這次實作所必需。
到最後,還是會在沒有充分理解複雜架構的情況下,請前輩來 review。
「為什麼會有 Factory!」
「這個非同步處理是怎樣!」
「『幫我解釋這段程式碼』」
在請求實作前,先使用 dig-and-write-adr,一邊回答 AI 的問題,一邊整理需求與處理流程。
$ /dig-and-write-adr 想讓使用者列表可以下載成 CSV
接著 AI 會回傳幾個問題。
### Q3: CSV 的一列與欄位要如何構成?
`Owner` 除了有 ID、名字、姓氏、地址、城市、電話號碼之外,還會有多隻寵物。
如果把寵物也算進來,就不再是「一位飼主一列」這麼單純的列表了。
- **A** — 一位飼主一列,輸出 `ID, First Name, Last Name, Address, City, Telephone`
- **B** — 依照畫面顯示,輸出 `Name, Address, City, Telephone, Pets`(多個寵物名稱串在同一個儲存格)
- **C** — 每隻寵物一列,重複輸出飼主資訊
- **D** — 個別指定欄位
**建議: A** — 這樣會得到以飼主列表為基礎、較正規化且容易處理的 CSV,也不用另外定義多隻寵物的呈現規則。ID 也能作為後續處理中穩定的識別欄位。
CSV 的列/欄位構成:
❯ A — 飼主基本 6 項(建議)
B — 與畫面相同欄位+寵物名稱
C — 每隻寵物一列
D — 個別指定欄位
Other (type your answer)
像是只輸出 CSV、只輸出 6 個欄位、沿用既有的列表取得流程、不做檔案儲存與非同步處理,這些都會在回答 AI 的問題時逐一決定。
「基本 6 項雖然是建議,但以這次需求來看,其實還需要寵物名稱啊。」
將決定內容與理由記錄成 ADR,然後以那份 ADR 作為前提,請 AI 開始實作。
這時候,ADR 就成為判斷基準。
如果生成的程式碼裡包含切換輸出格式的機制或非同步處理,就能判斷這些不是本次需求所需。
petclinic-user-csv-export-ADR.md
## Context
Spring Petclinic 中沒有用於認證的 `User` 模型。
代表人物的既有模型是飼主 `Owner` 與獸醫師 `Vet`。
這次所說的「使用者列表」已確定是指客戶列表相當的 `Owner`。
...
## 決定因素
- 要能取得畫面上的頁面,而不是只限目前顯示的一頁
- 不能把數萬筆資料全部保留在記憶體中
etc ...
因為事先理解了需求與處理流程,所以可以用自己的話說明每個生成出來的處理是在做什麼。
在 review 時,也能把程式碼與 ADR 一起交給前輩,讓前輩能在理解實作意圖與判斷理由後再看程式碼。
兩者都是讓 AI 幫忙產生程式碼,差別在於 實作前的判斷基準。
只要有判斷基準,就更能看懂生成的程式碼,也更能看出哪些實作是不必要的。
我認為,AI 生成的程式碼之所以難懂,原因不只是程式本身很難而已。
如果在實作前沒有先整理好需求與處理流程,就沒辦法判斷生成的程式碼是否符合自己的預期、是否真的屬於這次需求所需。
加入這個步驟之後,讀 AI 生成的程式碼就不只是單純地讀,而是能對照自己事先決定的方針來閱讀。
如果不只知道每段程式碼的功能,還能說明「為什麼這次需要這樣實作」,在請人 review 時也能更清楚地共享實作意圖。
與其事後思考「產生程式碼之後該怎麼辦」,不如從實作前的需求定義開始,就先緩解「程式碼難懂」的問題。