上週發表 Kobako:讓 Agent 安全的操作 Rails 介紹 Kobako 這個 Gem 的目標後,繼續透過 Claude Code 推進開發進度,然而很快地就遇到要大幅度修改的狀況,這難道就是使用 AI 開發的宿命嗎?
這是很值得討論的問題,也就是使用 AI 協助開發,到底是因為 AI 能力不足所以做不好,還是人類給的設計太差。
不確定性
在 AI 時代之前,我們有許多運作軟體開發團隊的方式,像是敏捷開發(Agile)就是其中一種對不確定性處理的做法。
為什麼要處理不確定性,是因為在開發出實際可以運用的軟體之前,我們很難知道使用者是不是真正的喜歡這個軟體。同樣的,我們的實作跟設計在真正被運行、修改之前,也很難知道當下選擇的技術、架構是不是真的能持續運行。
也因此,一些軟體架構理論(如:Clean Architecture)被提出來,讓我們可以在符合條件的狀況下,很容易的修改或者調整。
到了 AI 時代,我們還是遭遇相同的問題,但是開發速度快十倍,遇到問題也同樣快了十倍。短短一週的時間,我就馬上遇到最初的設計沒考慮到未來加功能會遭遇的狀況。
Kobako 大概一週左右就到我覺得能釋出 0.1.0 版本的程度,可能不算快。但前面還有半個月的準備,像是做技術調查、概念驗證、原型實作等等,已經在傳統軟體開發的基礎跟經驗上,很好的做完該做的事情。
先入為主
上一篇介紹 Kobako 時有提到,從 druby 得到靈感,採取 RPC 的方式進行溝通,但最終讓我遭遇到了一個難以處理的問題。
因為 WebAssembly 中的 WASI Preview 1(WebAssembly System Interface)並沒有正常的 I/O 機制可用,所以從 druby 或者 RPC 的概念,建立一個 I/O Pipe 來溝通本身是比較複雜的,在 Claude Code 蒐集的資料來評估,採用 Host Linker 機制比較簡單。
先設定一個 Import Function 讓 WebAssembly 中可以呼叫這個 C 方法,要傳遞回傳值則利用 Export Function 呼叫 WASM 分配一塊記憶體,然後把回傳值放到裡面。這樣一來一回,就實現了把 Rails 物件綁定(Binding)給 WebAssembly 環境中 mruby 使用的機制。
但這就遇到一個問題,如果按照正常的 RPC 連線大概會像這樣。
1# Client-side
2conn = RPC::Client.connect("remote_addr")
3
4# Server-side
5server = RPC::Server.bind("address")
6server.accept do |conn|
7 req = conn.read
8 # ...
9 res = server.dispatch(req)
10 # ...
11 conn.write res
12end在 Host 端扮演的是 Server 角色,從「讀取」跟「寫入」都是 Server 拿到的這個連線,也就是一組 I/O Pipe 來負責處理。
但是在 Kobako 利用 WebAssembly 機制實現綁定的特性時,變成這樣的狀況。
1sandbox = Sandbox.new
2sandbox.on_dispatch do |req|
3 res = sandbox.server.dispatch(req)
4 sandbox.reply res
5end實際上握有扮演 I/O Pipe 的是 Sandbox 本身(原設計是 Kobako::Wasm::Instance)但是 Sandbox 本身又擁有 Kobako::RPC::Server 這個實例,如果要做將請求轉發,就得用上面的方式來實現,但這樣設計成 Sandbox#dispatch 反而更合理,而 RPC Server 本質上只是一個 Registry 並不具備 Server 的性質。
除此之外,RPC Server 還不足以負擔所有的 Dispatch(分發)任務,因為還有 Kobako::Handle 這個機制,可以讓 Host 端的物件像指標一樣被傳入到 Guest 裡面使用,在這 Binding 的物件會回傳物件,無法被 msgpack 編碼時很好用。
到這裡就會發現,物件之間的關係基本上變得一團亂,職責很難切開,概念也不符合物件的命名,還很高機率耦合在一起,這很難在初期注意到,但又是必須面對的問題。
最後讓我撞到這個問題,是因為我想讓 Ruby 重要的語言特性 Block 機制可以被支援,雖然無法做到百分之百,但有限的支援就能覆蓋很多情境,但上述的問題就直接卡死這個機制被實現。
重新來過
要解決這個問題,以前會很直覺的認為砍掉重練,畢竟已經知道新的限制在沒有包袱的前提下修改是很容易的。不過,AI 時代也有 AI 時代可以採取的策略,也就是重新分析。
我先讓 Claude Code 排除 SPEC.md 規格中的定義,從 Kobako 想設計一個 Ruby 語言沙盒(Sandbox)的角度重新分析,假設當下實作的樣子是這樣,我的目標這樣設定時,這些實作扮演怎樣的角色。
大概幾小時左右,我重新區分出了新的概念(模組)
- Codec - 解決 Ruby 和 mruby 差異,基本上不變繼續用 msgpack
- Runtime - 運行 WebAssembly 的環境
- Transport - Ruby 和 mruby 溝通的機制
- Catalog - 運行過程中提供的資源(如:Binding、Handle、Snippet 等)
最後統一由 Sandbox 物件進行調度,上述幾個物件的依賴關係幾乎是扁平,而且刻意維持單向(沒有循環依賴)來確保運作是保持在單純的狀態。
再重新設計的時候,我將 Sandbox 的介面鎖定在最後一次釋出的版本,盡可能限縮 Breaking Changes 的影響範圍,即使目前還沒釋出穩定版本。
這就還會遇到新的挑戰,要這樣修改會是「大幅度重構」在過去會有很多阻力讓整體變得非常漫長,然而透過 AI 工具的輔助,大約三天左右就完成基本的調整,也許這篇文章發佈的當下已經整理完畢釋出最新的版本也說不定。
回到最初的問題,與其說是 AI 能力不足,不如說 AI 把「設計」這個變數放大了——好的設計讓 AI 跑得更遠,壞的設計則更快讓專案翻車。
就結論來看,使用 AI 開發會更快的撞到過去軟體開發中本來就會遇到的挑戰,如果一直放任不管最終還是會發展成大泥球(Big Mud Ball)的狀態,傳統開發的理論依舊很有用,而且會比過去更快遇到需要用到的時間點。
喜歡這篇文章?請我喝杯奶茶 🧋