生成回覆與異常處理:確保 AI 提供高效、可靠的建議回覆

Published February 10, 2025 by 徐培鈞
Python

在 AI 聊天機器人系統中,「建議回覆(Quick Replies)」的生成不僅需要準確符合使用者需求,還要確保穩定可靠。

然而,由於 OpenAI API 本身的特性與外部環境的不確定性,可能會遇到解析錯誤、API 超時、回應格式異常等問題。

如果這些異常未被妥善處理,可能導致系統崩潰或回覆質量下降,影響使用者體驗。

本篇文章將深入探討如何從 OpenAI API 解析回應數據,以及面對常見異常時,如何設計防禦性程式碼來確保系統穩定運行。

此外,我們還會提供一個完整的錯誤處理框架,幫助你優化 AI 生成 Quick Replies 的流程,提高回應的可靠性與準確性。


解析 OpenAI API 回應數據

OpenAI API 的回應格式

當你向 OpenAI API 發送請求時,回應通常是 JSON 格式,例如:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1700000000,
  "model": "gpt-4o",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"quick_replies\": [\"是的,請問有什麼需要幫助的嗎?\", \"可以提供更多細節嗎?\", \"請問是關於哪個產品呢?\"]}"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 100,
    "completion_tokens": 50,
    "total_tokens": 150
  }
}

此回應包含一個 choices 陣列,AI 生成的內容位於 choices[0].message.content,通常為 JSON 字串。我們需要解析它,以提取 quick_replies


防禦式編程:處理 API 可能發生的錯誤

當我們與 OpenAI API 互動時,可能會遇到以下問題:

可能原因網絡問題、伺服器負載過高
解決方案增加請求重試機制
可能原因AI 回覆格式不正確
解決方案加入回應格式驗證
可能原因無效或過期的 API 金鑰
解決方案檢查 API 金鑰並重新設定
可能原因請求超過 OpenAI 配額
解決方案設定 token 限制與日誌監控

我們可以透過 try-except 與重試機制來應對這些問題。


設計 API 重試機制

在與 OpenAI API 交互時,可能會遇到短暫的網絡不穩定、伺服器超時、速率限制(Rate Limit)等問題。
這些問題可能會導致 API 請求失敗,影響聊天機器人的穩定性。

為了讓系統具備更好的容錯能力,我們可以使用 Tenacity 套件來實現指數回退機制(Exponential Backoff)

這樣當 API 請求失敗時,系統會自動等待一段時間後重試,並且等待時間會隨著重試次數指數增加,減少對 API 伺服器的壓力。

什麼時候應該使用 API 重試機制?

  • 遇到短暫的網絡問題(如網絡不穩、DNS 查詢超時)
  • OpenAI API 負載過高,返回 429(Too Many Requests)錯誤
  • API 伺服器暫時性錯誤(500、503)
  • 避免用戶需要手動重新發送請求

📌 但是,若是 API 金鑰無效(401 錯誤)或參數錯誤(400 錯誤),應該立即報錯,而非重試!

使用 Tenacity 套件實現 API 重試

安裝 Tenacity

如果尚未安裝 Tenacity,可以使用以下指令安裝:

pip install tenacity

透過 Tenacity 設計 API 重試邏輯

import openai
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import openai.error

# 設定 API 重試機制
@retry(
    stop=stop_after_attempt(3),  # 最多重試 3 次
    wait=wait_exponential(multiplier=2, min=2, max=10),  # 指數回退,最小 2 秒,最大 10 秒
    retry=retry_if_exception_type(openai.error.OpenAIError),  # 只有 OpenAI API 錯誤才會重試
)
def call_openai_api(prompt):
    """向 OpenAI API 發送請求,失敗時會自動重試"""
    response = openai.ChatCompletion.create(
        model="gpt-4o",
        messages=[{"role": "system", "content": prompt}],
        temperature=0.7
    )
    return response

# 測試 API 調用
try:
    response = call_openai_api("請提供三個 Quick Replies 的範例")
    print(response)
except Exception as e:
    print(f"❌ API 最終請求失敗: {e}")

重點解釋

作用最多重試 3 次,避免無限迴圈
作用指數回退機制,每次失敗後的等待時間會增加,例如:2 秒 → 4 秒 → 8 秒(但不超過 10 秒)
作用僅在 OpenAI API 發生錯誤時重試,避免不必要的重試
作用裝飾器(Decorator)方式,讓 call_openai_api 內的 API 請求具備自動重試功能

OpenAI API 返回錯誤種類

OpenAI API 可能返回的錯誤訊息主要分為 客戶端錯誤(4xx)伺服器錯誤(5xx)。以下是常見的錯誤類型及其原因:

1. 客戶端錯誤(4xx)—— 你的請求有問題

這些錯誤通常是由於 API 調用方式不正確、金鑰無效、超出使用限制等原因導致的。

HTTP 狀態碼400
說明無效的請求,例如請求參數錯誤、缺少必要欄位等。
是否應該重試?❌ 不應重試(應該修正請求內容)
HTTP 狀態碼401
說明API 金鑰無效或遺失,導致身份驗證失敗。
是否應該重試?❌ 不應重試(應該確認 API 金鑰是否有效)
HTTP 狀態碼403
說明API 金鑰沒有權限調用該資源,例如嘗試訪問 GPT-4o 但金鑰無權限。
是否應該重試?❌ 不應重試(應該確認權限)
HTTP 狀態碼429
說明觸發速率限制(請求太頻繁或超過配額)。
是否應該重試?✅ 應該重試,但需等待一段時間後再試

📌 如何處理 RateLimitError?

  • 若是 “You exceeded your current quota”,代表 API 配額用完,需要升級方案或增加額度。
  • 若是 “Too Many Requests”,代表短時間內請求過多,可以使用 Tenacity 來進行指數回退(Exponential Backoff) 後重試。

2. 伺服器錯誤(5xx)—— OpenAI 伺服器端問題

這些錯誤通常是 OpenAI 伺服器的問題,可能是暫時性的,可以考慮重試。

HTTP 狀態碼500
說明OpenAI 伺服器錯誤,可能是內部服務異常。
是否應該重試?✅ 應該重試(稍後再試)
HTTP 狀態碼503
說明OpenAI 伺服器忙碌或維護中,請稍後再試。
是否應該重試?✅ 應該重試(增加等待時間)

3. 其他可能的異常

這些錯誤可能來自 OpenAI API,也可能來自網絡環境或請求庫。

說明API 請求超時,可能是 OpenAI 伺服器回應過慢。
是否應該重試?✅ 應該重試(適當延長等待時間)
說明AI 回應的 JSON 格式錯誤,無法解析。
是否應該重試?❌ 不應重試(應該改進 Prompt 或加入錯誤處理)
說明OpenAI 的通用錯誤類別,可能來自任何 API 錯誤。
是否應該重試?❓ 視具體錯誤而定

4. 如何使用 Tenacity 來處理 API 錯誤?

基於上面的錯誤類型,我們可以設計一個更精細的重試機制:

import openai
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import openai.error

# 設定 API 重試機制(僅對可重試的錯誤進行重試)
@retry(
    stop=stop_after_attempt(5),  # 最多重試 5 次
    wait=wait_exponential(multiplier=2, min=1, max=16),  # 指數回退機制
    retry=retry_if_exception_type((
        openai.error.RateLimitError,       # 速率限制
        openai.error.APIConnectionError,   # 伺服器錯誤
        openai.error.ServiceUnavailableError, # 服務不可用
        openai.error.Timeout               # API 超時
    )),
)
def call_openai_api(prompt):
    """向 OpenAI API 發送請求,失敗時會自動重試"""
    response = openai.ChatCompletion.create(
        model="gpt-4o",
        messages=[{"role": "system", "content": prompt}],
        temperature=0.7
    )
    return response

# 測試 API 調用
try:
    response = call_openai_api("請提供三個 Quick Replies 的範例")
    print(response)
except openai.error.OpenAIError as e:
    print(f"❌ API 最終請求失敗: {e}")

總結

是否應該重試?❌ 不應重試
處理方式修正 API 參數
是否應該重試?❌ 不應重試
處理方式檢查 API 金鑰
是否應該重試?❌ 不應重試
處理方式檢查權限
是否應該重試?✅ 應該重試
處理方式指數回退機制,避免連續請求
是否應該重試?✅ 應該重試
處理方式稍後再試
是否應該重試?✅ 應該重試
處理方式增加等待時間
是否應該重試?✅ 應該重試
處理方式增加請求超時設定
是否應該重試?❌ 不應重試
處理方式檢查 AI 回應格式

透過 Tenacity 設計適當的重試機制,確保 API 服務的穩定性與可靠性,避免不必要的請求失敗,提升 AI 聊天機器人的使用體驗!🚀


優雅處理解析錯誤

在與 AI 互動的過程中,回應的 JSON 格式可能會因各種原因出錯,例如 AI 產生的結構不符合預期、API 服務異常或網絡不穩定等。

為了確保系統的穩定性,我們可以透過 預設值 (fallback values) 來應對這類情況,避免因錯誤導致程式崩潰。

安全解析 Quick Replies

在解析 OpenAI API 回應時,我們需要確保即使 AI 回傳的格式異常,系統仍能順利運行。

因此,我們可以設計一個函式來安全提取 Quick Replies:

import json

def safe_extract_quick_replies(api_response):
    """從 OpenAI API 回應中提取 Quick Replies,並處理異常"""
    try:
        data = json.loads(api_response) if isinstance(api_response, str) else api_response
        content = data.get("choices", [{}])[0].get("message", {}).get("content", "{}")
        quick_replies = json.loads(content).get("quick_replies", [])
        
        if isinstance(quick_replies, list) and all(isinstance(qr, str) for qr in quick_replies):
            return quick_replies
        else:
            raise ValueError("quick_replies 格式錯誤")

    except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e:
        print(f"⚠️ 解析 quick_replies 時發生錯誤: {e}")
        return ["請問有什麼需要幫忙的?", "可以提供更多資訊嗎?"]

1️⃣ 判斷 API 回應的類型

data = json.loads(api_response) if isinstance(api_response, str) else api_response
  • 如果 api_response 是字串(string),則使用 json.loads(api_response) 解析成 Python 字典 (dict)。
  • 如果 api_response 已經是字典,則直接使用,避免重複解析。

📌 為什麼要這樣做?

有時候 API 回應的內容可能是 JSON 格式的字串,而不是 Python 字典,所以需要解析。

2️⃣ 提取 content 欄位

content = data.get("choices", [{}])[0].get("message", {}).get("content", "{}")

這行程式碼的作用是:

  1. 取得 choices 陣列(如果不存在則提供一個空字典 [{}] 來避免 KeyError)。
  2. 選取第一個 choice[0])。
  3. 取得 message.content 欄位的值(如果不存在則回傳 "{}",即一個空 JSON 字串)。

📌 為什麼 content 可能是 JSON 格式的字串?

OpenAI API 回應的 message.content 可能是一個 JSON 格式的字串,而不是直接的 Python 結構。例如:

"content": "{\"quick_replies\": [\"是的,請問有什麼需要幫助的嗎?\", \"可以提供更多細節嗎?\"]}"

所以我們需要進一步解析這個 content

3️⃣ 解析 quick_replies

quick_replies = json.loads(content).get("quick_replies", [])
  • json.loads(content):將 content 解析成 Python 字典。
  • .get("quick_replies", []):從解析後的字典中提取 "quick_replies",如果不存在則回傳空列表 []

📌 這樣做的原因

  • quick_replies 可能不存在,所以 .get() 方法能避免 KeyError
  • 確保函式總是返回一個列表,即使 API 回應異常。

4️⃣ 確保 quick_replies 格式正確

if isinstance(quick_replies, list) and all(isinstance(qr, str) for qr in quick_replies):
    return quick_replies
else:
    raise ValueError("quick_replies 格式錯誤")
  • 檢查 quick_replies 是否為列表 (list)
  • 檢查列表內的每個元素是否都是字串 (str)
  • 如果格式錯誤,則拋出 ValueError,進入 except 區塊處理錯誤

5️⃣ 處理錯誤,提供預設值

except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e:
    print(f"⚠️ 解析 quick_replies 時發生錯誤: {e}")
    return ["請問有什麼需要幫忙的?", "可以提供更多資訊嗎?"]

如果發生錯誤(例如 JSON 解析失敗、鍵不存在、資料格式錯誤等),則:

  1. 印出錯誤訊息,方便開發者排查問題。
  2. 回傳預設的 Quick Replies,確保系統不會崩潰。

記錄日誌

當我們與 AI 服務進行互動時,將 API 回應記錄下來,可以幫助我們在錯誤發生時進行分析與調試。我們可以使用 Python 內建的 logging 模組來實現這點:

import logging

# 設定日誌記錄,將資訊存入 quick_replies.log 檔案
logging.basicConfig(filename="quick_replies.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

def log_response(response):
    """記錄 API 回應內容"""
    logging.info(f"API 回應: {response}")

🔹 這段程式碼的作用

  • logging.basicConfig(...)
    • filename="quick_replies.log":指定日誌輸出檔案名稱。
    • level=logging.INFO:記錄 INFO 級別的訊息(可以改成 DEBUG 來記錄更詳細資訊)。
    • format="%(asctime)s - %(levelname)s - %(message)s":設定日誌格式,包括時間 (asctime)、日誌級別 (levelname)、訊息內容 (message)。
  • log_response(response)
    • 呼叫此函式時,會將 API 的回應記錄到 quick_replies.log 檔案中,方便後續排查問題。

📌 這樣做的好處

  • 方便分析 AI 回應的結構,找出異常模式。
  • 若 AI 回傳的格式發生變更,可透過日誌快速發現問題。
  • 若系統在某個時間點發生錯誤,可以查詢該時間的日誌來定位問題。

補充說明quick_replies.log 檔案

quick_replies.log 這個檔案不需要手動建立,Python 的 logging 模組會自動創建它。

logging.basicConfig(...) 中指定了 filename="quick_replies.log",如果該檔案不存在,Python 會自動建立它;如果檔案已經存在,則會將新的日誌內容追加 (append) 到檔案中,而不會覆蓋原有內容。

  • 如果你在執行程式後找不到 quick_replies.log
    可能的原因:
    1. 程式沒有執行 log_response(response),導致沒有內容被寫入日誌。
    2. 程式沒有寫入權限,例如你在某些受限目錄下運行程式。可以嘗試指定完整路徑,例如: logging.basicConfig(filename="/path/to/quick_replies.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
    3. 日誌級別 (logging level) 設定過高logging.INFO 只能記錄 INFO 級別及以上的訊息,如果你設定 logging.WARNING,那麼 INFO 級別的日誌就不會被記錄。

📌 結論:不用手動建立檔案,logging 會自動幫你處理!


驗證輸出

在處理 AI 回應時,為了確保其格式符合預期,我們可以建立一個 驗證函式 (validation function),來確認 AI 產生的 quick_replies 是否正確:

def validate_quick_replies(quick_replies):
    """驗證 quick_replies 是否符合格式 (應為字串列表)"""
    if isinstance(quick_replies, list) and all(isinstance(qr, str) for qr in quick_replies):
        return True
    print("❌ quick_replies 格式錯誤")
    return False

🔹 這段程式碼的作用

  • isinstance(quick_replies, list)
    • 檢查 quick_replies 是否為 列表 (list),因為 Quick Replies 應該是一組選項,而非單一值。
  • all(isinstance(qr, str) for qr in quick_replies)
    • 確保 quick_replies 的每個元素 (qr) 都是字串 (str),避免出現 None 或其他型別的錯誤資料。
  • 若格式錯誤,則輸出警告訊息,幫助開發者快速發現問題。

📌 這樣做的好處

  • 確保系統的數據完整性,避免出現錯誤格式導致 UI 顯示異常或系統崩潰。
  • 防止錯誤傳播,如果某個 API 版本回應的格式變更,這個驗證函式能及早發現問題,避免影響其他系統模組。

結論

透過這篇文章,我們學習了:

  1. 如何解析 OpenAI API 回應,提取 quick_replies
  2. 如何設計防禦式程式碼來處理 API 可能發生的錯誤
  3. 如何建立 API 重試機制來提升穩定性
  4. 如何透過日誌與驗證機制來確保回覆品質

這些技巧將確保你的 AI 聊天機器人在面對異常時仍能穩定運行,提供高效、可靠的 Quick Replies!🚀