iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Build on Google AI

單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?系列 第 10

Day 10 | 錯誤處理與防禦性設計:API 重試策略、AI 護欄與實戰除錯全紀錄

  • 分享至 

  • xImage
  •  

前言:現實世界充滿變數,你的 API 夠強壯嗎?

經過前幾天的實作,我們已經打造出一個具備記憶力、且能穩定輸出 JSON 格式課表的 AI 教練 API。但在準備將專案打包上線前,我們必須面對現實世界的兩大挑戰:

  1. 邏輯層面的惡意或白目使用:使用者把我們的馬拉松 App 當成免費的 ChatGPT,叫 AI 幫他「寫一份行銷企劃」或「算數學」。
  2. 系統層面的意外:網路不穩導致 API Timeout,或使用者上傳了過大的圖片直接塞爆伺服器。

在將 AI 教練 API 真正推向 Production 環境前,後端必須具備「邏輯層面防禦能力」與「系統層面抗壓能力」。本文將彙整 Day 10 的核心知識,並結合開發測試過程中遇到的真實問題進行更完整的整理!

核心防禦策略 1:AI 領域護欄 (AI Guardrails)

為了防止 API 額度被濫用,我們不需要寫複雜的 NLP 演算法,而是透過 Prompt Engineering 結合後端 Service 邏輯攔截,打造出堅不可摧的雙層護欄。

系統提示詞規範 (Prompt Engineering)

我們在 Prompt 範本(如 generate-marathon.md)及多輪對話的 System Instruction 中,強硬注入嚴格的教練角色與領域規則:

你是一位嚴格且專業的馬拉松教練。
你的唯一職責是處理「馬拉松訓練、課表調整、跑步技術分析、運動修復與身體放鬆」相關的問題。

【極度重要:領域防禦規則】
如果使用者詢問的內容與上述「跑步及修復領域」無關,請你**絕對拒絕回答**,並在 JSON 中回傳錯誤狀態:
- "isRelevant": (布林值,使用者問題是否與跑步領域相關)
- "errorMessage": (如果不相關,請放入一段嚴厲但幽默的教練警告)
- "scheduleData": (如果不相關,請設為 null)

後端 Service 邏輯攔截 (省下 DB 寫入資源)

Spring Boot 收到 Gemini 的 JSON 回應後,會立刻在 Service 層進行檢驗。一旦發現 isRelevant == false,就立即拋出 DomainNotSupportedException,藉此省下後續不必要的資料庫寫入運算:

private <T extends GuardrailResponseDto> T parseAndValidateGuardrail(String cleanedJson, String rawJson, Class<T> responseType) {
    T dto = objectMapper.readValue(cleanedJson, responseType);

    if (dto != null && Boolean.FALSE.equals(dto.getIsRelevant())) {
        String msg = dto.getErrorMessage();
        // 主動拋出例外,跳過後續的 saveHistory()
        throw new DomainNotSupportedException(msg);
    }
    return dto;
}

全域例外處理 (Global Exception Handling)

接著,透過 @RestControllerAdvice 捕捉這個自定義例外,並優雅地回傳 HTTP 400 Bad Request 給前端:

@ExceptionHandler(DomainNotSupportedException.class)
public ResponseEntity<Map<String, Object>> handleDomainNotSupportedException(DomainNotSupportedException e) {
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
            "status", "error",
            "code", "DOMAIN_NOT_SUPPORTED",
            "message", e.getMessage()
    ));
}

核心防禦策略 2:Spring Retry 自動重試與降級 (Fallback)

呼叫外部 LLM API 時遭遇網路超時(Timeout)是很常見的事情。透過 Spring Retry,我們可以避免因單次網路波動就直接跳出 500 Error 嚇壞使用者。

啟用與設定

  1. pom.xml 引入 spring-retryspring-boot-starter-aop 依賴。
  2. 在 Spring Boot 主程式加上 @EnableRetry 註解。

實作 @Retryable 與 @Recover

在我們的 GeminiServiceImpl 中加上重試與降級的註解:

// 遭遇 RestClientException 時,最多重試 3 次,每次間隔 2000ms
@Retryable(
    retryFor = {RestClientException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 2000)
)
@Override
public String generateText(String prompt) {
    // 發送 RestClient 請求邏輯...
}

// 3 次重試皆失敗後觸發降級機制
@Recover
public String recover(RestClientException e, String prompt) {
    log.error("Gemini API 完全失去連線,執行降級處理", e);
    // 回傳預設的 JSON,安撫使用者
    return "{ \"isRelevant\": true, \"errorMessage\": \"教練現在有點累,請稍後再試!\", \"scheduleData\": null }";
}

踩坑與除錯筆記

在開發與測試階段,凡走過必留下 Bug。我們遇到了很現實的問題:

圖片上傳報錯 Maximum upload size exceeded

  • 問題情況:透過實際上傳手機截圖進行跑步數據圖表分析時,伺服器回傳 Maximum upload size exceeded
  • 原因分析:Spring Boot 預設的 Multipart 單一檔案上傳上限僅為 1MB (spring.servlet.multipart.max-file-size)。現代智慧型手機的高解析度截圖隨時都可能超過 1MB,因而被 Spring 底層直接攔截。
  • 解決方案
  1. application.yml 將上限放寬至 10MB
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

2.在 GlobalExceptionHandler.java 捕捉例外,回傳友善提示:

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
            "status", "error",
            "code", "MAX_UPLOAD_SIZE_EXCEEDED",
            "message", "上傳圖片檔案過大,請提供小於 10MB 的圖片檔案"
    ));
}

今日總結與明日預告

完成上述的重試、護欄與除錯修復後,透過 API 測試工具進行了驗證。
我們不僅確認了 AI 護欄能成功擋下無關提問並阻斷 DB 寫入,也驗證了 Retry 機制能在模擬 Timeout 時精確重試 3 次並進入降級模式。更棒的是,現在系統已經能完美支援容量更大的高畫質數據圖表解析。
我們的後端 API 有逐漸變成一個穩健的鐵人了,本地端開發正式功德圓滿!
明天(Day 11)我們將準備把專案帶上雲端,進入容器化實戰:撰寫 Dockerfile,將這個 Java 21 + Spring Boot 應用程式打包成輕巧的 Docker Image,敬請期待!

爛笑話角落

我:「第10天終於結束了!」
日友:「ですよね。」(得蘇優餒)
我:「Day 10 よね..」
日友:………👏👏 ですよね!!🙄🙄


上一篇
Day 9 | Prompt Engineering 實戰:設計能穩定產出 JSON 格式「馬拉松課表」的 Prompt 範本
下一篇
Day 11 | 容器化實戰:撰寫 Dockerfile,將 Spring Boot 應用程式輕量打包
系列文
單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言