National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 08

串流與錯誤處理

逐字顯示、被截斷的回答、逾時與重試
Interaction實作 25 minute(s)

Learning Objectives

  • 用 stream=True 逐段接收回答,並把片段接回完整的文字
  • 發現 finish_reason 是 length 的回答,並選擇處理方式
  • 依錯誤種類判斷該不該重試
  • 寫出含指數退避、抖動與次數上限的重試函數

上線後才會遇到的問題

上一課的程式在順利時可以跑,但做成應用會遇到三件事:

  • 回答要等全部生成才顯示,使用者盯著空白畫面。
  • 回答可能被長度上限截斷,看起來卻像正常的文字。
  • 網路會斷、服務會忙、參數會寫錯。

這一課依序處理這三件事。串流、截斷與 400 錯誤是 2026-11-03 錄下來的真實回應;429 這類無法穩定錄到的錯誤,範例用自己寫的假函數模擬,範例裡會標明。

逐步看:一段一段收到回答

加上 stream=True,create 回傳的不再是回應物件,而是可以用 for 走訪的串流。注意第一段和最後一段的內容,還有文字是怎麼被切開的。

串流的幾個細節

從真實的輸出可以看到:

  • 切法不是一字一段。 大多是一個字('位'、'元'),也有 'byte'、' 個'、'資料'、'基本'。片段不一定對應你以為的「字」,不要依賴切法。
  • 第一段是空字串,最後一段是 None。 直接 pieces.append(text) 再 join 會出錯,所以範例先判斷 if text:。
  • finish_reason 要到最後一段才有值。 串流中途無法知道會不會被截斷。

串流沒有讓生成變快。 總時間差不多,但使用者很快就看到第一個字。程式要等全部結果才能處理時(例如解析 JSON),串流沒有好處。

用量: 串流預設不會帶 usage。要加 stream_options={"include_usage": True},API 會在結尾多送一個 choices 是空串列的 chunk,裡面有用量,所以 chunk.choices[0] 之前要先檢查(本站的預錄不含這個 chunk,範例沒有示範)。串流中途斷線時,可能收不到這個 chunk,見 API 參考。

接到網頁: 伺服器一段一段送給瀏覽器,常用的方式是 Server-Sent Events。Python SDK 的自動重試也不涵蓋已經開始接收的串流,中途斷線要由你的程式決定是保留已收到的部分,還是重新請求。

逐步看:回答被截斷

第 7 課看過 finish_reason。這裡把輸出上限設成 20 個 token,看真實被截斷的回答長什麼樣子,再試一種補救方式:請模型接著寫。

被截斷時怎麼辦

被截斷的回答最麻煩的地方是不會報錯。文字看起來正常,只是少了後面。所以只要程式依賴回答的完整性,就要檢查 finish_reason。

做法 適合 代價
調高 max_completion_tokens 上限本來就設得太緊 最壞情況的費用變高
提示詞要求短一點 不需要那麼長的回答 模型不一定做到(第 5 課的 83 字)
請模型接著寫 長文,接縫可以接受 又多一次呼叫;上面的範例就有接不順的地方
告訴使用者「回答不完整」 其他做法都不適合時 使用者要自己決定

回答如果是 JSON,被截斷的內容根本解析不了,下一課會看到。

逐步看:真實的 400 錯誤

這是真實 API 回傳的錯誤,不是模擬的。SDK 會把錯誤回應變成 Python 例外。

哪些錯誤該重試

判斷的原則:再送一次有沒有可能成功?

狀態 SDK 的例外 原因 重試?
400 BadRequestError 參數或內容有誤 不要,改請求
401 AuthenticationError 金鑰有誤或被撤銷 不要,檢查金鑰
429(速率) RateLimitError 送得太快 可以,要等一下
429(額度) RateLimitError 額度或消費上限用完 不要,要處理帳務
500 InternalServerError 服務端的問題 可以,等一下
503 InternalServerError 模型目前忙碌 可以,等一下
無回應 APITimeoutError、APIConnectionError 逾時或網路問題 可以

同樣是 429,意思可能不同:要看錯誤內容裡的 code,額度用完時重試也沒用(OpenAI 錯誤代碼指南)。

立刻一直重送會更糟。 官方的速率限制指南提醒,失敗的請求也算進每分鐘的限制(說明)。所以每次失敗後要等一下,而且越等越久。

逐步看:指數退避加上抖動

這個範例的 429 是模擬的。 flaky_api 是我們自己寫的假函數,前兩次丟出 RateLimitError,沒有呼叫任何模型。例外的建立方式也是這裡的模擬環境才有;真實環境的例外由 SDK 依 API 回應建立,你只需要 except。

在這裡 time.sleep 不會真的等,秒數請看輸出。

重試的要點

  • 指數退避: 等待時間 1、2、4、8 秒逐次加倍,服務端有時間恢復。
  • 抖動(jitter): 同時被擋的程式如果等一樣久,會在同一時間一起回來。在等待時間裡加入隨機,可以把重試打散,Marc Brooker 2015 年的文章用模擬比較過。
  • 次數上限: 一直失敗就回報錯誤,不要無限重試。
  • Retry-After: 回應裡有這個標頭時,依它指定的時間等待(官方指南)。

SDK 已經內建重試。 OpenAI 的 Python SDK 預設對連線錯誤、408、409、429、5xx 自動重試 2 次,並使用短暫的指數退避;預設逾時是 10 分鐘(SDK 說明)。這兩項都可以調整:

client = OpenAI(timeout=30, max_retries=3)   # 使用者在等,逾時要短一點

10 分鐘對批次工作也許可以,對網頁上等待的使用者太久。自己再寫一層重試時,通常要設 max_retries=0,否則兩層會相乘。

重試前想一下: 同一個請求送兩次會不會造成問題?純問答只是多花一點錢;如果這個請求會讓工具寄信或扣款(第 15 課起),重試可能造成重複的動作。

實作:寫一個重試函數

完成 retry(fn, max_attempts=4, base=1):

  • 呼叫 fn(),成功就回傳結果。
  • 遇到 RETRYABLE 裡的錯誤:如果已經是最後一次嘗試,把錯誤往外丟(raise);否則印出 第 N 次重試前等 W 秒,等待 base * 2 ** attempt 秒後再試。
  • 其他錯誤(例如 400)不要接住,讓它直接往外丟。

這一題不呼叫模型,make_flaky、bad_request 都是模擬的假函數。裡面建立例外的寫法只有這裡的模擬環境能用,真實 SDK 的例外由 SDK 自己丟出,不要照抄。為了讓輸出固定,這一題不加抖動。

你的輸出
預期輸出

小測驗:串流與錯誤

1.

串流時為什麼要先判斷 if text: 才加進串列?

2.

使用者的回答看起來少了結尾,程式也沒有報錯。最先該檢查什麼?

3.

哪一種錯誤不該自動重試?

4.

為什麼指數退避要加上抖動?

5.

下面的程式會印出什麼?

base = 2
print([base * 2 ** attempt for attempt in range(4)])
第 1 次作答

重點整理

  • stream=True 讓使用者很快看到文字;片段切法不固定,空字串與 None 要先判斷,finish_reason 在最後一段。
  • 被截斷不會報錯,要檢查 finish_reason;依用途調高上限、接著寫,或告訴使用者。
  • 依錯誤種類決定是否重試:400、401、額度用完不重試;速率限制、5xx、逾時可以重試。
  • 重試要有指數退避、抖動與次數上限。SDK 預設已有重試,自己再寫一層時要避免相乘。

下一課: 程式要的不是文字,而是欄位。下一課讓模型輸出 JSON,並處理它不符合預期的情況。

這一課完成了!
Next lesson:結構化輸出
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。