經過前幾天的努力,我們的 Spring Boot 終於能跟 Gemini 順暢對話,並且擁有 Firestore 的記憶力了。但身為一個專業的馬拉松教練,Kakeru不能只記住你的名字,而是要能提出專業的知識,光靠單次生成與Hardcode 的 Prompt 是不夠的。
真實的訓練往往充滿變數:學員剛報名時可能連賽事時間或個人跑量都說不清;訓練過程中可能因為連續加班或肌肉痠痛無法吃下原訂計畫;此外,跑者最愛看的就是配速與心率數據截圖。
因此,今天我們將利用 Markdown (.md) 範本化 集中管理提示詞,一次實作三大核心情境:4 週月週期生成(含互動問診)、4 週動態微調、數據圖表視覺分析,並結合 Gemini 多模型分流與 HTTP 429 自動降級 (Quota Fallback) 機制,讓前後端對接絕對穩定!
複雜的系統提示詞 (System Prompt) 如果 Hardcode 寫死在 Java 程式碼裡,不僅難以閱讀,微調邏輯時還得重新編譯部署。
我們將提示詞獨立為 Markdown (.md) 範本存於 classpath:prompts/ 中。比起 .txt,.md 的 # Header 與區塊標籤更容易讓 Gemini 等大語言模型理解語意階層:
@Value("classpath:prompts/generate-marathon.md")
private Resource generatePromptResource;
@Value("classpath:prompts/adjust-schedule.md")
private Resource adjustPromptResource;
@Value("classpath:prompts/analyze-chart.md")
private Resource analyzePromptResource;
先前我們使用gemini-3.5-flash-lite,但在各種測試下,用單一模型的用量似乎過多,因此可以的話有另一個模型來作為緩衝我覺得是必要的選擇。但免費或付費 API 在尖峰時刻常遇到 HTTP 429 (Too Many Requests / Quota Exceeded) 限流。我們設計了雙模型備援機制:
gemini-flash-lite-latest。若觸發 429 或 503 額度限制,系統會印出 Log 警告並自動重試切換輕量模型 gemini-3.5-flash-lite。/chat):直接預設存取輕量模型 gemini-flash-lite-latest 節省額度。# application.yml
gemini:
api:
key: ${sm://projects/kakeru-ai/secrets/GEMINI_API_KEY/versions/latest}
base-url: https://generativelanguage.googleapis.com/v1beta/models
primary-model: gemini-flash-lite-latest
chat-model: gemini-3.5-flash-lite
我們要在發送請求時,利用 API 的 responseMimeType: "application/json" 參數,搭配以下精準的 Prompt 範本:
generate-marathon.md)不盲目給予課表,而是規劃 4 週週期(基礎適應 ➔ 跑量提升 ➔ 強度峰值 ➔ 減量恢復,共 28 天)。當學員輸入資訊不足時,教練會主動問診。
# 角色定義
你是一位擁有 20 年經驗的專業馬拉松教練。請評估以下學員資料:
- 目標賽事:{{TARGET_RACE}}
- 目前體能:{{CURRENT_LEVEL}}
## 評估與問診邏輯
1. **資訊不足**:若學員輸入的資料過於簡略(缺少目標時間、每週可練天數或跑量),請將 `status` 設為 `"NEEDS_CLARIFICATION"`,並在 `inquiryQuestions` 中列出 2-4 個具體問診追問。
2. **資訊充足**:若資料充足,請將 `status` 設為 `"READY"`,並規劃為期 4 週(第 1 週基礎適應、第 2 週跑量提升、第 3 週強度峰值、第 4 週減量恢復,共 28 天)的週期訓練計劃。
## 嚴格輸出限制
必須輸出為 JSON 物件,屬性包含:
- `status`: ("READY" 或 "NEEDS_CLARIFICATION")
- `coachAdvice`: 給學員的教練整體評估與提醒
- `inquiryQuestions`: 追問問題陣列(status 為 NEEDS_CLARIFICATION 時填寫,否則為空陣列)
- `schedule`: 課表陣列(包含 `weekNumber` (1-4), `dayOfWeek` (1-7), `workoutType`, `distanceKm`, `description`)
adjust-schedule.md)當學員遭遇突發狀況時觸發,餵入原訂 4 週課表與目前進度(CURRENT_WEEK, CURRENT_DAY),重新滾動規劃本月剩餘訓練。
# 角色定義
你是一位專業馬拉松教練。學員本月遭遇了突發狀況,無法完全執行原訂 4 週課表。
請根據以下資訊,重新規劃本月剩餘天數與週次的訓練,優先考慮運動修復與避免受傷。
## 輸入資訊
- **原始本月 4 週課表**:{{ORIGINAL_SCHEDULE_JSON}}
- **突發狀況描述**:{{USER_FEEDBACK}}
- **目前進度**:第 {{CURRENT_WEEK}} 週,星期 {{CURRENT_DAY}}
## 嚴格輸出限制
必須輸出為 JSON 物件,屬性包含:
- `adjustmentReason`: 給學員的鼓勵與調整原因說明
- `revisedSchedule`: JSON 陣列,格式同基礎課表,包含 `weekNumber` (1-4), `dayOfWeek` (1-7), `workoutType`, `distanceKm`, `description`,僅包含本月剩餘天數
- `recoveryAdvice`: 針對該突發狀況的物理或營養修復建議
analyze-chart.md)Gemini 多模態視覺診斷,觀察配速與心率趨勢。
# 角色定義
你是一位精通數據分析的跑步教練。使用者上傳了一張近期的跑步數據截圖(包含配速、心率或步頻)。
請仔細觀察圖表中的趨勢與波動,給予專業的數據診斷。
## 嚴格輸出限制
必須輸出為 JSON 物件,屬性包含:
- `detectedMetrics`: 字串陣列,例如 `["心率飄移", "配速穩定", "步頻過低"]`
- `performanceAnalysis`: 針對圖表趨勢的詳細分析
- `actionableAdvice`: 針對下一次訓練的具體改進建議
在實作層 MarathonCoachServiceImpl 中,實作超額捕獲與降級重試:
private String callGeminiApiWithFallback(List<GeminiRequest.Part> parts) {
try {
log.info("[Gemini Model] 嘗試呼叫主要模型: {}", primaryModel);
return executeCall(primaryModel, parts);
} catch (org.springframework.web.client.RestClientResponseException e) {
int statusCode = e.getStatusCode().value();
if (statusCode == 429 || statusCode == 503 || isQuotaExceededError(e.getResponseBodyAsString())) {
log.warn("[Gemini Fallback] 主要模型 ({}) 遭遇次數超額 [HTTP {}], 自動降級嘗試次要模型 ({})", primaryModel, statusCode, chatModel);
return executeCall(chatModel, parts);
}
throw e;
}
}
我們將端點收攏於Controller層 MarathonCoachController (/*),前端統一以此進行互動:
| HTTP Method | Endpoint | 說明 |
|---|---|---|
POST |
/generate |
4 週月課表生成與問診評估(帶入 sessionId 自動存入記憶) |
POST |
/adjust |
4 週月課表動態微調(帶入 currentWeek 與 currentDay) |
POST |
/analyze-chart |
數據圖表視覺分析(支援 Multipart 與 Base64,相容 file / base64Image key) |
POST |
/chat |
結合同一 sessionId 之多輪追問與對話 |
今天完成了 Prompt 的 Markdown 模組化解耦,並打造了高可用的馬拉松教練:從 4 週週期問診、月課表動態微調、多模態圖表診斷 到 Gemini HTTP 429 超額自動降級備援,極大化了系統韌性。
明天(Day 10),我們將針對整體 API 進行進一步的容錯與後備 (Fallback) 機制優化,確保就算在全線 AI 服務暫時不可用時,也能回傳預設結構化保底數據,敬請期待!