前幾天的範例都建立在一個假設上:API呼叫一定會成功。但實際操作上不可能這麼順利,網路問題、模型服務繁忙、請求格式或內容出錯、呼叫太頻繁,都有可能導致API呼叫失敗。今天要來討論,遇到呼叫失敗時該怎麼處理。
429 Rate Limit:呼叫得太頻繁,超過帳號的速率限制。這是比較常見,也是最需要在使用者端好好處理的問題,因為不是「用錯」,只是因為「用太快」,而被API拒絕。
5xx 伺服器錯誤:模型服務端本身出問題,跟使用者請求內容無關。
Timeout:請求送出去,但在設定的時間內沒收到回應,可能是網路問題或模型正在處理超長的生成。
400 系列的請求錯誤:使用者送出的請求有誤,例如 context 太長超過模型上限、參數格式錯誤。同樣的內容送出後一樣會回傳錯誤,得先從請求內容本身開始修正。
分辨「錯誤該重試」跟「錯誤重試也沒用」,是寫錯誤處理邏輯前最重要的第一步。
對於「該重試」的錯誤,不能無腦馬上重複送——如果剛剛就是因為送太快才被限速,馬上再送一次只會讓情況更糟。常見的作法是 exponential backoff:每次重試前等待的時間,隨著重試次數指數增加(例如 1 秒、2 秒、4 秒),讓系統有時間喘口氣,也降低把問題搞得更嚴重的機率。
import time
def call_with_retry(fn, max_retries=3):
attempt = 0
while True:
try:
return fn()
except Exception as error:
status = getattr(error, "status_code", None)
is_retryable = status == 429 or (status is not None and 500 <= status < 600)
if not is_retryable or attempt >= max_retries:
raise
delay = 2 ** attempt
print(f"呼叫失敗(status: {status}),第 {attempt + 1} 次重試,等待 {delay} 秒")
time.sleep(delay)
attempt += 1
實際使用時,把呼叫包起來就好:
response = call_with_retry(lambda: client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "用一句話解釋什麼是 API"}],
))
實際上,Anthropic 跟 OpenAI 的官方 SDK 其實都內建了類似的重試機制(可以透過 max_retries 之類的參數設定),不一定要自己重新設計。這邊只是為了理解背後原理,在正式專案中,使用SDK內建的機制即可,除非有特別客製化的需求。
延續這幾天的觀察,兩家 API 回傳的錯誤資訊格式也不同。以 Anthropic 為例,錯誤物件裡會有一個 error.type 欄位,標明像 "rate_limit_error"、"overloaded_error" 這類字串;OpenAI 則是用 error.code 或 error.type,命名習慣不太一樣。兩家 Python SDK 也都各自把常見錯誤包成對應的例外類別(拋出的例外物件上會帶 status_code 屬性),但類別名稱背後對應的判斷邏輯、回應標頭裡帶的 rate limit 資訊欄位命名,都不是共用的一套規範。
也就是說,連在「判斷錯誤能不能重試」這件事上,也需要花心思對齊不同廠商的SDK。
從 Day 2 到今天,同一件事——呼叫、串流、結構化輸出、錯誤處理——在不同廠商手上,都得寫一次不同的程式碼。方法名稱不同、資料結構不同、錯誤格式不同。如果你的系統只用一家模型,這不是問題;但如果想要「同時管理多家 provider」「某個模型掛掉時自動切換到另一家」「統一追蹤所有呼叫的成本與延遲」,就會需要一層東西,把這些差異包起來,讓上層的程式邏輯不用管背後究竟是哪家模型在回應。
這正是這個系列後段(Part 6)要動手做的東西——一個個人版的 LLM Router。
Part 1 到這裡告一段落。現在我們已經知道如何「穩定呼叫 LLM」——知道怎麼送出請求、串流回應、拿到結構化資料、處理失敗。了解與模型交流的方法後,接下來要換一個方向:要如何好好向LLM提問與交談。
明天開始會進入Part 2,從Prompt的基本結構開始聊起。