Spec-as-source 的理想與現實
credit: Nano Banana

Spec-as-source 的理想與現實

約 6,815 字

用人類的自然語言寫出精準的規格,跟用程式語言寫精確的程式碼,哪個比較簡單?

這個問題聽起來有點奇怪,程式語言不是本來就比自然語言難學嗎?如果我們能用自然語言跟電腦溝通,不就省下學程式語言的麻煩了?

其實並沒有,程式語言需要記的單字跟文法,遠比任何一個人類語言少很多。學程式語言不用背幾千個單字,不用搞懂動詞變化,更不用煩惱什麼時候該用敬語。電腦不會因為你少打一個 please 就不理你。

這正是 Spec-as-source 這個願景背後的核心假設。在我之前的文章「SDD 規格驅動開發」裡有提到 Spec-Driven Development 的三個層級,Spec-as-source 是最高的那一層,理想狀態 spec 就是 Single Source of Truth,人類只需要編輯 spec,程式碼完全由 spec 自動產生。聽起來很美好。人類負責「說」要做什麼,AI 負責「做」出來。

但實際上,這個願景遇到了一個根本性的問題,而這個問題不只是技術上的,更是語言本質上的。

同樣的 spec,不同的產出

我之前有做了個小實驗,我想測試看看 Spec-as-source 到底可以做到什麼程度。我請 AI 幫忙擬了一份很詳細的 spec,詳細到每一個檔案要怎麼命名、每一個 function 要做什麼事、每一個 component 要有什麼 props 都寫出來。如果你不是工程師,可能不知道上面這些在幹嘛,但如果你是工程師,你看到我寫到這種程度,應該會想這直接用程式語言寫可能還比較快。為了實驗,我還是讓 AI 根據這份 spec 產生程式碼。

第一次跑出來的結果還不錯,程式可以動。Good,這招好像有用。

然後我又跑了一次,完全相同的 spec,完全相同的 prompt。結果這次的結果不太一樣,例如有些 helper 被拆出來了,有些被寫成類別了。程式還是可以正常運作,但跟第一次的版本有點不一樣。

第三次呢?結果還是可以動,但結構又有點不太一樣了。整體來說,三次產出的程式碼功能都差不多,但細節上都有些差異。這不是操作失誤,也不是 spec 寫得不夠清楚(喔,可能也是寫的不夠清楚啦),這是 LLM 的本質,它天生就是非確定性的(non-deterministic),相同的輸入不保證相同的輸出。

看到這裡,可能會有兩派不同的看法:

一派是「可以動就好」派。雖然 AI 三次生出來的程式碼都不一樣,但功能是可以正常運作就好啦,何必這麼在意細節?的確,找三個不同的工程師來寫同一份需求,他們寫出來的程式碼也會不一樣,變數命名的風格不同、函式拆分的方式不同、甚至用的設計模式都可能不同。只要最後功能正確、可以維護,細節上的差異是可以接受的。AI 產出的變異性,其實跟人類開發者之間的變異性差不多,沒什麼好大驚小怪的。

另一派是「結構一致性」派。如果每次 AI 產出的結構都不一樣,長期下來專案會變成一團混亂。今天這個檔案用這種 pattern,明天那個檔案用另一種 pattern,累積起來會讓 codebase 變得很難維護。人類工程師雖然風格不同,但至少會參考現有的程式碼,盡量維持一致性。AI 不一定會這麼做。

這兩派都有道理,我自己比較偏「可以動就好」的支持者,在我看來這相對比較務實,也比較貼近實際上的日常開發情況。

當然,如果你的目標是 Spec-as-source,也就是把 spec 當作 Single Source of Truth,程式碼完全由 spec 自動產生,那 AI 的非確定性就會變成一個麻煩。目前我們沒辦法保證下次重新產生的時候,程式碼還是長一樣。版本控制會變得很奇怪,diff 會變得很難看,要花額外的時間進行 code review。

語言模型之所以能產生有創意、有彈性的回應,正是因為它是不確定性的。如果每次給相同的輸入都得到完全相同的輸出,那它就只是一個「對照表(lookup table)」,不是一個能理解語言、能推理、能創造的模型了。

程式碼的世界需要確定性,你不會希望同一份原始碼,今天編譯出一個版本,明天編譯出另一個版本。這剛好就是 Spec-as-source 矛盾的地方,我們試圖用一個本質上不確定的工具,來產生需要確定性的產物。

形式方法

Spec-as-source 這個想法其實不是最近才有的,在軟體工程的歷史上,有一個更早、更嚴謹的版本,叫做「形式方法(Formal Methods)」。

形式方法的核心理念是 Correctness by Construction,透過數學證明來保證軟體的正確性。先用形式化的規格語言寫出規格,然後透過數學推導來驗證你的實作符合規格。用數學來保證程式沒有 bug,聽起來很酷也很玄(數學好難),但為什麼好像沒有普及?

首先是開發模式,傳統上,形式方法多半假設一個相對穩定、前期定義完整的規格流程,這在實務上常被實作成接近瀑布式的開發模式。但現在主流的敏捷開發,需求是不斷變動的,規格也在不斷演進。如果每次規格變動都要重新做數學證明,成本有點高。

再來是覆蓋範圍的問題,真實的系統可能會用到沒有被驗證過的函式庫或是跟沒有被驗證過的 legacy 系統互動。你也許可以證明你自己寫的程式碼是正確的,但沒辦法證明整個系統是正確的。

另外,開發過程中的 False alarm 可能也會讓開發者失去信心,重構程式碼需要重新證明正確性,編譯時間變長會降低生產力,工具鏈的限制會影響技術選擇的自由度。

上面這些問題聽起來是不是有點熟悉?如果把「形式方法」換成「Spec-as-source」,把「數學證明」換成「AI 產生程式碼」,這些問題幾乎一模一樣。

形式方法只有在變得夠實用的時候才會普及,實用到我們甚至不會再叫它形式方法。如果各位曾經學過 TypeScript 的型別系統或是 Rust 的所有權(Ownership)機制,它們本質上就是在編譯的時候「證明」程式的某些性質是正確的。TypeScript 證明你不會把字串當數字用,Rust 證明你不會有記憶體洩漏。這其實就是形式方法在做的事,只是包裝成日常開發工具,讓你用起來不覺得在做數學證明。

自然語言的曖昧性

回到最開始的問題,用自然語言寫精準的規格,真的比用程式語言寫程式碼更容易嗎?

我舉一個例子:「我想要一顆按鈕,點擊後會顯示一個對話框」。這句話聽起來很簡單,但實際上有多少模糊的地方?

對話框是 modal 還是 modeless?要在按鈕的上面、下面、還是螢幕中間?對話框裡面要顯示什麼內容?要怎麼關閉對話框?點擊對話框外面會不會關閉?按 ESC 鍵會不會關閉?對話框出現的時候要不要有動畫效果?

這些問題在自然語言的描述裡完全沒有被回答,但在程式碼裡必須被明確決定。

AI 是非確定性的,所以即使團隊成員各自使用相同的工具,產出的程式碼也會有差異。這些差異在小專案裡可能無傷大雅,但在大型專案裡會累積成一致性問題。驗證 AI 產生的程式碼所需的時間,有時候比撰寫 prompt 的時間還長,5 分鐘的 prompt 可能需要 1 小時的驗證。

補充個小故事,我之前在做 ezBundle 系統的時候,AI 產出的程式碼風格和結構經常不一致,我是靠自己過去的開發經驗把這些硬拉成一致,這不一定每個人都做得到的事。如果沒有足夠的經驗去判斷什麼該改、什麼不該改,最後可能只是在製造更多混亂。

自然語言的曖昧性不是缺陷,是特性。人類用自然語言溝通的時候,會依賴大量的上下文、共同知識、和默契來填補這些模糊地帶。但 AI 並沒有與團隊共享的真實經驗、組織脈絡或未被寫下來的默契,它只能依賴明示的文字與訓練資料中的統計模式來「猜測」意圖。

虛假的控制感

Birgitta Böckeler 在 Martin Fowler 網站上的分析裡,對 SDD 工具做了深入的研究。她觀察到一個有趣的現象,就算有精心設計的工作流程、檢查清單和模板,AI Agent 最終還是不會完全遵循所有的指示。

她記錄了 AI agent 忽略現有程式碼文件並重複建立元件的情況,也有 AI 過於積極地遵循指示而做出不恰當行為的案例。這讓人產生一種「虛假的控制感(False sense of control)」,你以為你透過規格在控制 AI,但 AI 有時候還是會自己做決定。

另外,她也把 Spec-as-source 跟早期的 MDD(Model-Driven Development)做了類比。MDD 也曾經承諾過類似的願景,用模型來產生程式碼,人類只需要維護模型。但 MDD 最後因為缺乏彈性和過高的維護成本而沒有成為主流。Spec-as-source 可能會繼承 MDD 的弱點,同時還要面對 LLM 非確定性帶來的新問題。

她用了一個德文詞 Verschlimmbesserung,意思是「想要改善卻讓事情變得更糟」,很精準地描述了可能的風險。

非技術人 vs 技術人

Spec-as-source 的吸引力對不同背景的人來說是不一樣的。

對非技術人來說,這個願景可能非常吸引人,因為終於可以不用學程式語言,只要用自然語言就能做出自己想要的軟體了,這也是 Vibe Coding 能引起這麼大迴響的主要原因。Vibe Coding 對兩種人很有幫助,一種是很有經驗的開發者,他們懂得怎麼除錯和解決問題,透過 AI 幫忙加速開發。另一種是完全的初學者,他們把想法變成可以運作的軟體,即使不懂程式設計也做的到。

問題在於,用自然語言描述得「夠不夠精準」完全取決於個人的品味和經驗,產出的品質好壞也得看 LLM 心情,這正是 Vibe Coder 目前容易卡關的地方。當開發者不懂程式碼在做什麼的時候,大概也不會知道它有什麼問題。

對已經會寫程式的人來說,情況又不一樣了。程式語言本來就是一種精準描述邏輯的工具,它被設計成沒有歧義的。變數有型別、函式有參數和回傳值、控制流程有明確的語義。換成自然語言,不一定更輕鬆,反而可能更麻煩,因為你要花更多精力去消除歧義。

不只是指揮 AI,同時留下脈絡

有個常被忽略的觀點,是很多人對 SDD 的認知可能是「用規格來指揮 AI 做事」。是的,但這只說對了一半。

SDD 更重要的價值,是把規格留下來備查。想像一下你接手了一個專案,之前的開發者已經離職了。程式碼在那裡,但你完全不知道當初為什麼要這樣寫。這個 if 判斷是在處理什麼邊界條件?這個看起來很奇怪的邏輯是 bug 還是 feature?這個被註解掉的程式碼可以刪掉嗎?

傳統的做法是靠程式碼註解、版本控制的 commit message 或是 issue ticket,不過這些東西有時候不完整、過時、或是根本找不到。如果當初開發的時候有留下 spec,spec 記錄的不只是「要做什麼」,還有「為什麼要這樣做」。它是開發當下的決策脈絡,是需求和實作之間的橋樑。

重點是,這個 spec 不只是給人看的,現在還能給 AI 看,對我來說這可能才是 SDD 真正的亮點。

當我們要修改一個功能的時候,可以試著把相關的 spec 餵給 AI,或是請 AI 自己去搜尋,讓 AI 更容易理解這個功能的背景和意圖。AI 不再是從零開始猜測你要什麼,它有脈絡可以參考了,這比每次都重新掃描 codebase 有效率得多。或是,當遇到 bug 的時候,可以讓 AI 比對一下 spec 和實際的程式碼,看看是實作偏離了規格,還是規格本身就有問題,這也比盲目地 debug 有方向多了。

當新人加入團隊的時候,他可以透過 spec 快速理解系統的設計決策,而不是只能從程式碼反推。AI 可以幫他解讀這些 spec,或是把 spec 丟進去做 RAG 讓它回答新人的問題,這比傳統的 onboarding 文件有用多了。

這就是為什麼 Spec-anchored 可能是目前比較務實的選擇。它不只是把 spec 當作一次性的 prompt,而且還把 spec 當作專案的知識資產,持續維護、更新。規格成為「活文件」,消除個人依賴,讓團隊開發可以規模化。當規格跟程式碼一起被版本控制,你就有了完整的演進歷史,可以追溯每一個決策的來龍去脈。

所以 SDD 的價值不只是指揮 AI 做事,而且還能讓專案的知識不會流失。程式碼會被重構、會被刪除、會被完全改寫。但如果 spec 有被好好維護,決策的脈絡就會留下來。

在這 AI 時代,AI 產生的程式碼變得相對低成本,容易被當作「可拋棄式」的產出,反正重新再叫 AI 生一次就好。但需求的理解、設計的決策,這些不應該被拋棄,spec 就是保存這些東西的地方。

Spec-anchored 更務實的選擇

所以,如果全自動的 Spec-as-source 可能還要再等等,也許目前 Spec-anchored 會是比較好的解法,也就是我在上一篇文章裡提到的第二層。在這個層級裡,spec 是開發輔助道具,用來引導 AI 產生程式碼,但程式碼本身還是被維護和版本控制的。spec 不是用完即丟,而是會隨著專案演進持續更新,作為「活文件」保留下來,這些 spec 也是要進到版本控制系統的。

GitHub 的 Spec Kit 文章描述了這個流程。首先是 Specify,開發者用自然語言描述目標,著重在使用者體驗和預期成果,AI 產生詳細的規格。然後是 Plan,提供技術限制、架構偏好和技術棧細節,AI 建立完整的技術藍圖。接著是 Tasks,規格和計畫被拆解成小的、可以審查的、可以測試的任務。最後是 Implement,AI 逐一處理這些任務,開發者審查的是專注的小改動,而不是大量的程式碼。

另一個工具 OpenSpec 強調的是 fluid not rigid、iterative not waterfall。核心理念還是 "Agree before you build",但流程變得更靈活。你可以用 /opsx:new 開一個新的變更,用 /opsx:ff(fast-forward)一口氣產生 proposal、specs、design、tasks,然後用 /opsx:apply 實作,最後用 /opsx:archive 歸檔。每個變更都有自己的資料夾,完成後整包歸檔,留下完整的決策紀錄。這正好呼應了前面說的,SDD 的價值不只是指揮 AI,更是留下脈絡讓未來可以追溯。有興趣的話可參閱我之前寫的另一篇文章:「OpenSpec 讓 SDD 變簡單的三個指令

這兩個工具的流程細節不同,但關鍵都是人類都有在這個 loop 裡面(human-in-the-loop)。AI 產出初版,然後人工審查、調整。這不是全自動化,但也不是 Vibe Coding 的隨性而為,這是在兩者之間抓一個人機協作的節奏。

角色轉變

如果你跟我一樣也是工程師,以前我們是寫程式碼的人,我們想的是 how,怎麼實作這個功能、怎麼解決這個問題。現在這些 how 可以交給 AI 來做,我們的角色從「寫程式碼」轉變成「設計架構和審查結果」。我們需要想的是 what,這個系統應該做什麼、這個功能的預期行為是什麼、這個改動會影響到哪些地方。

這是一個很有趣的轉變。我們不需要控制每一行程式碼長什麼樣子,只需要確保最終的結果符合 spec 的預期。這跟管理人類團隊成員其實有點像,我們不用「微管理(Micromanagement)」每個人都用完全相同的方式寫程式,只要確保大家的產出符合設計規範就好。

小結

回到最一開始的問題,用自然語言寫精準的規格,跟用程式語言寫精確的程式碼,哪個比較簡單?

這得看你是誰。

對非技術人來說,用自然語言可能比較簡單,但描述得夠不夠精準就得看個人的品味,產出的品質好壞也得看 LLM 心情,這是 Vibe Coder 目前容易卡關的地方。

對已經會寫程式的人來說,程式語言本來就是一種精準描述邏輯的工具,換成自然語言不一定更輕鬆。比較務實的做法是用自然語言描述大方向和意圖,讓 AI 產出初版,然後人為審查、調整產出的成果。

這大概就是目前 AI Coding 比較平衡的工作模式,不追求 Spec-as-source 的全自動化,但也不是 Vibe Coding 的隨性而為,而是在兩者之間抓個人機協作的節奏。

說到底,寫程式一直都是在「翻譯」,就是把人類的想法翻譯成電腦可以執行的指令。工具變了,從組合語言到高階語言,從純文字編輯器到 IDE,現在又加上了 AI 輔助,不過翻譯的過程還是存在的,還是需要人類來把關。

也許在不久的將來,這個翻譯過程會變得完全自動化,精準到我們可以放心地把程式碼完全交給 AI。在那之前,我們還是得在自然語言和程式語言之間來回穿梭,在人類意圖和機器執行之間搭起友誼的橋樑。

認清現實,才能找到最好的工作方式,讓 AI 真正成為助力而不是負擔。

合作夥伴

留言討論