「AI 生成的程式碼看起來很亂,是不是因為模型能力還不夠?」
如果你也這樣想過,先重新想一次:AI 寫程式碼的能力,跟它寫出來的程式碼會長成什麼樣子,其實是兩件不同的事。AI 在沒有約束的情況下,會選擇「當下能讓需求動起來」的最短路徑——不是因為它寫不出更好的結構,而是因為沒有任何規則告訴它「這裡不該放這個」。 今天用一個具體案例,看清楚這件事實際發生時長什麼樣子。
假設有一個需求:「使用者送出訂單後,扣庫存、算折扣、寄通知信、寫一筆稽核紀錄」。把這個需求丟給一個沒有任何架構約束的 AI agent,只給它一句話的需求描述,它多半會寫出類似這樣的東西:
❌ 沒有架構邊界時,AI 寫出的版本:
class OrderController
{
public function submit(Request $request): Response
{
$order = $this->db->query(
"INSERT INTO orders ..."
);
$stock = $this->db->query(
"SELECT quantity FROM inventory WHERE product_id = ?"
);
if ($stock < $request->quantity) {
throw new Exception('庫存不足');
}
$this->db->query("UPDATE inventory SET ...");
$discount = $request->amount > 1000 ? 0.1 : 0;
$finalAmount = $request->amount * (1 - $discount);
$mailer = new SmtpMailer('smtp.example.com');
$mailer->send($request->email, '訂單確認', "...");
file_put_contents('/var/log/audit.log', json_encode([...]) . "\n");
return new Response(['order_id' => $order->id]);
}
}
→ 資料庫存取、業務規則(折扣計算)、外部服務(寄信)、
基礎設施(寫檔案 log)全部寫在同一個方法裡,
沒有任何一行程式碼是「錯」的,但整段程式碼沒有任何邊界
這段程式碼能動、邏輯也不難懂。但如果你請另一個 AI agent(或者是三個月後的你自己)去修改「折扣規則」,它得先讀懂整個方法在幹嘛,才敢動手改一行——因為沒有任何線索告訴它「折扣計算」跟「資料庫存取」是兩件不相干的事,可以分開改而不互相影響。
這不是 AI「不知道」該怎麼分層——如果你直接問它「Controller、Service、Repository 分別該負責什麼」,它答得出來一套教科書等級的答案。 問題在於:知道分層原則,跟在寫這一段程式碼的當下主動套用分層,是兩件事。
當需求本身就是一句話(「送出訂單後做這些事」),而沒有任何外部訊號告訴 AI「這個專案期待資料庫存取要放在哪裡、業務規則要放在哪裡」時,AI 會依循一個很自然的邏輯:把完成這個需求所需要的每一步,依照它們被提到的順序,依序寫進同一個函式裡。 這是完成單一需求的最短路徑,也是「看起來能動」的最快方式。如果 AI 有能力預測「這段程式碼一個月後會不會被罵」,它多半會選擇分層;但它沒有這個回饋訊號,它只有「這個需求現在動了嗎」這一個立即可驗證的訊號。
架構邊界要解決的,正是這個回饋訊號的缺口——它把「以後會不會被罵」提前變成「現在能不能通過某條具體規則」,讓 AI 不需要預測未來,只需要遵守一條當下就能檢查的規則。
同樣的需求,如果專案有清楚的分層邊界(Controller 只處理請求/回應、業務規則收斂進 Service、資料存取收斂進 Repository、外部服務收斂進各自的介面),AI 依循這些邊界寫出來的版本會長這樣:
✅ 有架構邊界時,AI 寫出的版本:
class OrderController
{
public function __construct(
private OrderService $orderService,
) {}
public function submit(Request $request): Response
{
$order = $this->orderService->placeOrder(
$request->productId,
$request->quantity,
$request->amount,
$request->email,
);
return new Response(['order_id' => $order->id]);
}
}
class OrderService
{
public function __construct(
private InventoryRepository $inventory,
private OrderRepository $orders,
private NotificationSender $notifier,
private AuditLogger $auditLogger,
) {}
public function placeOrder(
int $productId, int $quantity, float $amount, string $email
): Order {
$this->inventory->reserve($productId, $quantity);
$finalAmount = $this->calculateDiscount($amount);
$order = $this->orders->create($productId, $quantity, $finalAmount);
$this->notifier->send($email, '訂單確認');
$this->auditLogger->log('order.placed', $order->id);
return $order;
}
private function calculateDiscount(float $amount): float
{
return $amount > 1000 ? $amount * 0.9 : $amount;
}
}
→ 每一段職責都有自己的邊界;要改折扣規則,
只需要進 OrderService::calculateDiscount(),
完全不用碰資料庫存取或寄信的邏輯
這裡的重點不是「第二個版本比較長、比較『正規』」,而是第二個版本裡的每一段程式碼,都有一個明確的地方屬於它——而這件事不是 AI 自己想到的,是架構邊界(分層規則、介面約定)事先替它劃好的。同一個 AI、同樣的能力,寫出來的東西完全取決於有沒有邊界可以依循。
第一個版本的問題,很容易被籠統地歸類成「技術債」——但這個歸類其實模糊了問題的根源。真正的技術債,是團隊在某個時間點刻意選擇了一條較快、較不完美的路徑,並且知道自己欠了什麼;而架構缺席造成的債,是根本沒有人(或沒有規則)在場,讓「該分層」這件事連被考慮的機會都沒有。 前者是一筆記在帳上的債,後者連帳本本身都不存在。
這個區分很重要,因為兩者的解法不同:技術債的解法是排優先序、找時間償還;架構缺席的解法是先把帳本立起來——也就是先建立起分層規則、依賴方向、介面邊界這些東西,讓下一次同樣的需求進來時,AI(或任何人)有東西可以依循,而不是每次都要重新從零判斷「這段邏輯該放哪裡」。
回想你上一次請 AI 幫你加一個功能:它把邏輯全部塞進同一個地方,還是各自歸位到清楚的層級?如果是前者,你的專案裡有沒有任何東西(文件、範例程式碼、既有的分層慣例)告訴它「該怎麼分」?
明天要往下深挖一層:架構到底在「約束」什麼?答案不是規範「程式碼該怎麼寫」,而是限制「一次改動能碰到哪裡」——這個定義上的轉換,會讓後面 27 天講的每一個具體工具都有一個共同的判準。
iThome鐵人賽