隨著 AI 虛擬助教的功能越來越多,原本全部程式都放在 server.js 裡的做法開始變得不好維護。今天的主要目標,就是利用終端機中的 AI 程式開發工具 Antigravity CLI(agy),透過自然語言指令協助我們整理程式架構
在專案根目錄下,我們透過終端機下達指令,要求 AI 將原本塞在 server.js 的聊天 API 邏輯抽離,並建立標準的路由檔案與型別說明:
agy -p "請只讀取 server.js,將聊天 API 的路由處理邏輯抽離到 routes/chatRoutes.js 中,並補上完整的 JSDoc 註解。不要動其他檔案。" --dangerously-skip-permissions
routes/chatRoutes.js,將 @google/genai 初始化、請求解析與核心引導對話控制完整封裝,並在 server.js 中透過 Express 路由進行掛載。

在實際測試時,若遇到 Gemini 模型高負載而回傳 503 Service Unavailable,我們不希望直接把錯誤訊息丟給學生,破壞學習體驗。於是再次請 Antigravity CLI 幫忙:
agy -p "我在測試時出現503 error我不希望學生收到伺服器忙碌的訊息 希望程式自己重試直到有回覆為止 請只讀取 server.js,幫我修改程式" --dangerously-skip-permissions
server.js 中加入了智慧攔截與自動重試迴圈。當偵測到模型忙碌時,會在背景自動以遞增延遲進行重試,直到順利取得 AI 回覆並安全送達前端,讓學生在網頁上不會感到被中斷。
為了確保重構後的程式碼沒有潛在 Bug、沒有發生邏輯誤判,我們請 CLI 進行了一次全面的安全與效能掃描:
agy -p "請再幫我掃描目前資料夾底下的程式碼,檢查有沒有任何潛在的 Bug 或可以優化的地方" --dangerously-skip-permissions

透過 AI 幫忙掃描程式後,我們發現了幾個原本不太容易注意到的問題:
修正 503 錯誤判斷問題
原本程式是直接用文字去判斷是不是出現「503」或「忙碌」等字眼,結果如果學生問「公車 503 號怎麼搭」,也可能被誤認成伺服器發生錯誤,導致正常的回答被丟掉。這次重新整理錯誤處理方式,改成真正檢查 HTTP 狀態碼,避免這種誤判。
修正 AI 語音朗讀偶爾沒聲音的問題
原本在使用語音朗讀時,如果新的訊息進來,前一次的語音可能還沒有正確結束,導致後面的內容沒有順利播放。這次加入重新設定語音的機制,讓 AI 收到新訊息後可以正常播放最新的回答。
讓 JSON 回傳更穩定
AI 有時候會在 JSON 外面多加上 Markdown 的程式碼標記,例如 ````json`,這會讓程式在解析資料時出錯,所以我們加入自動清理的機制,先把這些多餘的標記移除,再進行解析,讓資料處理更加穩定。
這次透過 AI 幫忙檢查程式,也讓我們發現,系統真正做到「可以運作」之後,還有很多細節需要處理。AI 模型本身再厲害,如果程式沒有做好錯誤處理,實際使用時還是可能遇到各種問題。
透過 Antigravity CLI 的自動化協助,我們得到了乾淨俐落的模組化架構與高容錯的伺服器程式碼。以下是本次重構的核心成果展示:
routes/chatRoutes.jsJavaScript
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;
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 自動重試機制:
明天我們將進入第 13 天:【多模態教學】Gemini Vision 實戰:上傳手寫算式,AI 助教線上批改,教你如何讓學生拍下寫錯的數學作業並上傳,讓 AI 助教直接進行圖文並茂的線上批改與引導。