iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Build on Google AI

用 Google AI 打造「因材施教」的個人化 AI 虛擬助教系列 第 12 篇

Day 12【Antigravity】讓 Terminal AI 幫我重構 API 架構

  • 分享至 

  • xImage
  •  

【今日開發目標】

隨著 AI 虛擬助教的功能越來越多,原本全部程式都放在 server.js 裡的做法開始變得不好維護。今天的主要目標,就是利用終端機中的 AI 程式開發工具 Antigravity CLI(agy),透過自然語言指令協助我們整理程式架構

【Google AI 工具實作過程】

步驟一:呼叫 Antigravity CLI 進行後端路由模組化

在專案根目錄下,我們透過終端機下達指令,要求 AI 將原本塞在 server.js 的聊天 API 邏輯抽離,並建立標準的路由檔案與型別說明:

agy -p "請只讀取 server.js,將聊天 API 的路由處理邏輯抽離到 routes/chatRoutes.js 中,並補上完整的 JSDoc 註解。不要動其他檔案。" --dangerously-skip-permissions
  • 執行成果:AI 自動新增了 routes/chatRoutes.js,將 @google/genai 初始化、請求解析與核心引導對話控制完整封裝,並在 server.js 中透過 Express 路由進行掛載。

https://ithelp.ithome.com.tw/upload/images/20260921/20183764btqVUcd9M5.png

https://ithelp.ithome.com.tw/upload/images/20260921/201837646EaKRsN2hs.png

步驟二:為 503 錯誤實作自動重試中繼機制(Retry Middleware)

在實際測試時,若遇到 Gemini 模型高負載而回傳 503 Service Unavailable,我們不希望直接把錯誤訊息丟給學生,破壞學習體驗。於是再次請 Antigravity CLI 幫忙:

agy -p "我在測試時出現503 error我不希望學生收到伺服器忙碌的訊息 希望程式自己重試直到有回覆為止 請只讀取 server.js,幫我修改程式" --dangerously-skip-permissions
  • 執行成果:AI 在 server.js 中加入了智慧攔截與自動重試迴圈。當偵測到模型忙碌時,會在背景自動以遞增延遲進行重試,直到順利取得 AI 回覆並安全送達前端,讓學生在網頁上不會感到被中斷。

https://ithelp.ithome.com.tw/upload/images/20260921/20183764LHT1DvLgkl.png

步驟三:全專案自動Code Review

為了確保重構後的程式碼沒有潛在 Bug、沒有發生邏輯誤判,我們請 CLI 進行了一次全面的安全與效能掃描:

agy -p "請再幫我掃描目前資料夾底下的程式碼,檢查有沒有任何潛在的 Bug 或可以優化的地方" --dangerously-skip-permissions
  • 執行成果:系統產出了完整的程式碼審查報告,協助我們盤點並優化了前後端的互動邏輯、XSS 防護與 Marked.js 渲染設定。

https://ithelp.ithome.com.tw/upload/images/20260921/20183764qQLrHTFSCU.png

透過 AI 幫忙掃描程式後,我們發現了幾個原本不太容易注意到的問題:

  1. 修正 503 錯誤判斷問題

    原本程式是直接用文字去判斷是不是出現「503」或「忙碌」等字眼,結果如果學生問「公車 503 號怎麼搭」,也可能被誤認成伺服器發生錯誤,導致正常的回答被丟掉。這次重新整理錯誤處理方式,改成真正檢查 HTTP 狀態碼,避免這種誤判。

  2. 修正 AI 語音朗讀偶爾沒聲音的問題

    原本在使用語音朗讀時,如果新的訊息進來,前一次的語音可能還沒有正確結束,導致後面的內容沒有順利播放。這次加入重新設定語音的機制,讓 AI 收到新訊息後可以正常播放最新的回答。

  3. 讓 JSON 回傳更穩定

    AI 有時候會在 JSON 外面多加上 Markdown 的程式碼標記,例如 ````json`,這會讓程式在解析資料時出錯,所以我們加入自動清理的機制,先把這些多餘的標記移除,再進行解析,讓資料處理更加穩定。

這次透過 AI 幫忙檢查程式,也讓我們發現,系統真正做到「可以運作」之後,還有很多細節需要處理。AI 模型本身再厲害,如果程式沒有做好錯誤處理,實際使用時還是可能遇到各種問題。

【核心 Code / Prompt 展示】

透過 Antigravity CLI 的自動化協助,我們得到了乾淨俐落的模組化架構與高容錯的伺服器程式碼。以下是本次重構的核心成果展示:

1. 抽離後的聊天路由模組:routes/chatRoutes.js

JavaScript


export async function handleChat(req, res) {
  try {
    const { message } = req.body || {};
    // 同時相容 history 與 chatHistory 命名
    const history = (req.body && (req.body.history || req.body.chatHistory)) || [];

    if (!message || typeof message !== 'string' || message.trim() === '') {
      return res.status(400).json({ error: 'Message 欄位不能為空' });
    }

    const trimmedMessage = message.trim();
    console.log(`📥 收到前端訊息: ${trimmedMessage}`);
    console.log(`📜 目前歷史紀錄筆數: ${history.length} 條`);

    // 呼叫 Gemini 服務層
    const jsonResponse = await generateTutorReply({ message: trimmedMessage, history });

    console.log('✅ 成功輸出結構化資料:', jsonResponse);

    // 回傳給前端
    return res.json({
      success: true,
      data: jsonResponse,
      response: jsonResponse.reply,
      reply: jsonResponse.reply,
    });
  } catch (error) {
    console.error('❌ API 處理發生錯誤 (最終失敗):', error);

    const status = error.status || error.statusCode || 500;
    const is429 = status === 429 || /resource.*exhaust|quota|rate limit/i.test(error.message || '');
    const is503 = status === 503 || /overloaded|service unavailable|503/i.test(error.message || '');

    if (is429) {
      return res.status(429).json({
        error: 'AI 服務目前使用量已達上限或連線過於頻繁,請稍候片刻再試!⏱️',
        details: error.message || error.toString(),
      });
    }

    if (is503) {
      return res.status(503).json({
        error: 'AI 老師現在伺服器滿載,已經幫你自動重試多次仍無法連線,請稍後再試!☕',
        details: error.message || error.toString(),
      });
    }

    return res.status(status >= 400 && status < 600 ? status : 500).json({
      error: error.message || '伺服器處理失敗',
      details: error.message || error.toString(),
    });
  }
}

/**
 * 註冊聊天 API 路由
 * @name post/
 * @path POST /api/chat
 */
router.post('/', handleChat);

export default router;

2. 主程式加入 503 自動重試與防錯機制:server.js(片段)

JavaScript

import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import chatRoutes from './routes/chatRoutes.js';

// 正確宣告 ESM 的 __dirname 並載入絕對路徑之 .env
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '.env') });

const app = express();
app.use(express.json());

// 讓 Express 自動去 public 資料夾裡面找所有靜態檔案(HTML, CSS, JS)
app.use(express.static(path.join(__dirname, 'public')));

// 根路由設定:當有人造訪 http://localhost:3000 時,自動回傳 public/index.html
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

/**
 * 判斷是否為 503 服務忙碌或暫時不可用之錯誤
 * 注意:嚴格限制只有非 2xx 回應或 err 存在時才判定,絕不誤判正常的 HTTP 200 內容
 */
export function is503Error(err, statusCode = 200, body = null) {
  // 正常的 2xx 狀態碼絕對不是 503 錯誤,直接放行
  if (!err && statusCode >= 200 && statusCode < 300) {
    return false;
  }
  if (statusCode === 503) return true;
  if (err) {
    if (err.status === 503 || err.statusCode === 503 || err.code === 503 || err.code === '503') {
      return true;
    }
    const msg = String(err.message || err.error || err || '');
    if (/overloaded|service unavailable|high demand/i.test(msg)) {
      return true;
    }
  }
  if (body && statusCode >= 400) {
    const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
    if (/overloaded|service unavailable|伺服器忙碌/i.test(bodyStr)) {
      return true;
    }
  }
  return false;
}

// 註冊聊天 API 路由(重試邏輯已收攏至 services/geminiService.js)
app.use('/api/chat', chatRoutes);

// 404 處理
app.use((req, res) => {
  res.status(404).json({ error: '找不到請求的資源 (404 Not Found)' });
});

// 全域錯誤處理中間件 (Error Handling Middleware)
app.use((err, req, res, next) => {
  console.error('伺服器發生錯誤:', err);

  const status = err.status || err.statusCode || (is503Error(err) ? 503 : 500);

  let message = err.message || '伺服器內部發生錯誤';
  if (status === 503 || is503Error(err, status)) {
    message = 'AI 服務目前忙碌或暫時無法使用(503 Service Unavailable),請稍候片刻再試。';
  } else if (status === 429) {
    message = 'AI 服務目前使用量已達上限或請求過於頻繁,請稍候片刻再試。';
  }

  res.status(status).json({
    error: message,
    message: message,
    statusCode: status
  });
});

// 捕捉全域未處理的例外與 Promise 拒絕,防止伺服器崩潰
process.on('unhandledRejection', (reason, promise) => {
  console.error('⚠️ 未處理的 Promise Rejection:', reason);
});

process.on('uncaughtException', (err) => {
  console.error('💥 未捕捉的例外 (Uncaught Exception):', err);
});

const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
  console.log(`🚀 伺服器已成功啟動!請開啟瀏覽器前往:http://localhost:3000`);
});

server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`❌ 連接埠 ${PORT} 已被佔用,請更換 PORT 或關閉佔用該 Port 的程式。`);
  } else {
    console.error('❌ 伺服器啟動時發生錯誤:', err);
  }
});

【未來教育反思】

在教育現場中,「穩定性」與「流暢度」是學生能否持續專注學習的關鍵。想像一下,當學生正在深夜苦讀、好不容易鼓起勇氣向 AI 助教提問時,畫面上卻不斷跳出「503 Service Unavailable」或「伺服器忙碌中」,這往往會瞬間澆熄學生的學習熱情,甚至讓他們對數位工具失去信任。

透過今天的架構重構與 503 自動重試機制:

  1. 無縫的使用者體驗:學生再也不會被繁雜的後端錯誤訊息干擾,每一次的提問都能獲得平穩的對應與引導。
  2. 高維護性的程式碼結構:將 API 路由模組化並加上完整的 JSDoc,讓未來要擴充多學科導引或是錯題本功能時,能夠以最低的維護成本進行迭代。

【明日預告】

明天我們將進入第 13 天:【多模態教學】Gemini Vision 實戰:上傳手寫算式,AI 助教線上批改,教你如何讓學生拍下寫錯的數學作業並上傳,讓 AI 助教直接進行圖文並茂的線上批改與引導。


上一篇
Day 11|【Antigravity】不用切視窗,直接在終端機叫 AI 寫 Code
下一篇
Day 13|【多模態教學】Gemini Vision 實戰:上傳手寫算式,AI 助教線上輔導
系列文
用 Google AI 打造「因材施教」的個人化 AI 虛擬助教 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言