iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Vibe Coding

從零打造 AI 全端應用:Vibe Coding 結合 n8n 視覺化工作流實戰系列 第 20 篇

Day 20:處理 AI 的「慢」:實作非同步 Timeout 與前端錯誤處理最佳實踐

  • 分享至 

  • xImage
  •  

Day 20:處理 AI 的「慢」:實作非同步 Timeout 與前端錯誤處理最佳實踐

嗨,大家今天過得好嗎?歡迎來到鐵人賽 Day 20。

昨天我們用 JSON Schema 強制解析了 n8n 的輸出,我們的無程式碼後端基本上已經進化成了「商業完全體」。現在,是時候把前幾天在 SwiftUI 寫的 APIService 換成真實的 n8n Webhook 網址,讓前後端正式大會師了!

但是,當你興高采烈地把網址換掉,按下「產生推薦菜單」時,你很快就會面臨全端 AI 開發的第一個震撼教育:AI 實在是太慢了。

殘酷的現實:AI API 不是傳統的 CRUD

以前我們打傳統後端 API(例如去資料庫撈個會員資料),回應時間通常在 100 毫秒以內。但在我們現在的 n8n 工作流中,系統要去 PostgreSQL 撈資料、去抓即時匯率、最後還要丟給 OpenAI / Gemini 思考。

這個「思考」的過程,根據當下的網路狀況與 Token 數量,通常會耗時 3 到 10 秒,甚至更久。

如果我們的前端沒有做好對應的防護,會發生兩件災難:

  1. 使用者狂點按鈕: 以為 App 當機了,狂點送出,結果你的 n8n 瞬間收到 10 個 Request,不僅把資料庫連線卡死,還燒光了你的 LLM API 額度。
  2. 網路 Timeout 閃退: 原生的 URLSession 若遇到網路波動,很容易拋出 URLError.timedOut,如果你的 Controller 沒有接住這個 Error,畫面就會直接卡死在轉圈圈。

今天,我們要用 Vibe Coding 搭配工程師心法,為我們的 App 加上強韌的「錯誤防護罩」。

第一步:優化 APIService,拉長 Timeout 限制

還記得我們 Day 11 寫的 APIService 嗎?當時我們使用的是 URLSession.shared。這個預設的 Session 對於長時間等待的 LLM 請求來說不太友善。

我們需要自己建立一個帶有 Timeout 設定的 Session。你可以請 AI 幫你這樣改,或是直接手動更新 APIService.swift:

import Foundation

class APIService {
    static let shared = APIService()
    
    // 建立一個自訂的 URLSession
    private let session: URLSession
    
    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30.0 // 把 Timeout 拉長到 30 秒,給 AI 思考的時間
        config.timeoutIntervalForResource = 60.0
        self.session = URLSession(configuration: config)
    }
    
    func fetchRecommendation(budget: Int, purpose: String, sessionId: String) async throws -> HardwareMenu {
        // 請換成你的 n8n Webhook Test URL (記得加上 ngrok 或是內網 IP)
        guard let url = URL(string: "[http://192.168.](http://192.168.)X.X:5678/webhook-test/recommend") else {
            throw URLError(.badURL)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        // 把 Session ID 也一起包進 Payload 送給後端記憶體
        let payload: [String: Any] = [
            "budget": budget, 
            "purpose": purpose,
            "sessionId": sessionId
        ]
        request.httpBody = try JSONSerialization.data(withJSONObject: payload)
        
        // 改用我們自訂的 session 發送請求
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        
        return try JSONDecoder().decode(HardwareMenu.self, from: data)
    }
}

第二步:在 Controller 接住錯誤 (Error Handling)

網路層設定好了,接下來是邏輯層。
當 API 發生錯誤時(例如 n8n 掛了、AI API Key 沒錢了),我們必須把錯誤訊息接下來,通知前端畫面。

打開你的 HomeController.swift,我們新增兩個專門用來處理錯誤的狀態:

import Foundation
import SwiftUI

@MainActor
class HomeController: ObservableObject {
    @Published var requestData = HardwareRequest()
    @Published var isLoading = false
    @Published var resultMenu: HardwareMenu?
    
    // 新增:錯誤處理狀態
    @Published var showError = false
    @Published var errorMessage = ""
    
    let purposeOptions = ["3A 遊戲", "影音剪輯", "文書處理"]
    
    func submitRequest() {
        guard let budgetInt = Int(requestData.budget), budgetInt > 0 else {
            self.errorMessage = "請輸入有效的預算金額"
            self.showError = true
            return
        }
        
        isLoading = true
        resultMenu = nil
        
        Task {
            do {
                let menu = try await APIService.shared.fetchRecommendation(
                    budget: budgetInt, 
                    purpose: requestData.purpose,
                    sessionId: requestData.sessionId
                )
                self.resultMenu = menu
                self.isLoading = false
            } catch {
                // 接住錯誤,並更新給 UI 顯示
                print("API 錯誤:\(error.localizedDescription)")
                self.errorMessage = "AI 伺服器連線失敗,請稍後再試。\n(\(error.localizedDescription))"
                self.showError = true
                self.isLoading = false // 記得關閉轉圈圈
            }
        }
    }
}

第三步:Vibe Coding 召喚 UI 錯誤彈窗

狀態都準備好了,我們終於可以請 AI 幫忙修飾畫面了。
這一步非常簡單,把這段 Prompt 餵給 AI:

這是我目前的 ContentView.swift (貼上 Day 9 的程式碼)。
我的 HomeController 新增了兩個錯誤狀態:`showError` (Bool) 與 `errorMessage` (String)。
請幫我在 ContentView 加上 SwiftUI 原生的 `.alert` 彈窗。當 showError 為 true 時,顯示 errorMessage。

AI 會在你的 ContentView 的大括號結尾處,優雅地加上這段 Modifier:

.alert(isPresented: $controller.showError) {
    Alert(
        title: Text("發生錯誤"),
        message: Text(controller.errorMessage),
        dismissButton: .default(Text("確定"))
    )
}

** UX 守門員:禁用連點**
記得我們在 Day 9 已經偷偷在送出按鈕加上了 .disabled(controller.isLoading)。這個小小的 Modifier,在真實接上 AI API 後將成為你的救命恩人,它徹底阻絕了使用者在等待的 10 秒內連點 100 次按鈕的慘劇。

小結

今天我們補齊了全端開發中經常被新手忽略,卻是上線前最重要的一環:Error Handling 與 Timeout 防護機制。

這就是為什麼在 Day 1 我就強調,Vibe Coding 雖然快,但「工程師的架構思維」才是專案成敗的關鍵。AI 不會主動提醒你設定 Timeout,也不會考慮你的使用者會不會狂點按鈕,這些都需要我們用經驗去引導它。

明天,我們的前後端整合已經正式宣告完成!我們將實作一個超酷的進階商業場景:「結合 AI 審核機制與資料庫回寫」。不只要讓 AI 讀資料,還要讓 AI 把算好的菜單「存回 PostgreSQL」作為歷史訂單!

我們 Day 21 見!


上一篇
# Day 19:馴服 AI 的輸出:強制解析 JSON 格式與 Schema 驗證
下一篇
# Day 21:進階商業場景 (一):讓 AI 寫入資料庫!實作 n8n 的回寫與歷史紀錄
系列文
從零打造 AI 全端應用:Vibe Coding 結合 n8n 視覺化工作流實戰 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言