Learning Objectives
- 用 requests 呼叫外部 API,設定逾時並處理失敗,不讓例外跑到 webhook
- 在機器人裡呼叫語言模型,設定回答上限,並用資料庫保存對話上文
- 用 loading 動畫與 reply、push 的搭配處理等待,說明各自的代價
- 為模型的回答做長度截斷、每人限次與來源標示,說明資料送到了哪裡
別人的服務會慢、會壞,也可能要花錢
到目前為止,機器人的答案都在自己的程式或資料庫裡。這一課讓它去問別人:呼叫一個查資料的 API,或呼叫語言模型。程式只多幾行,要處理的事情卻變多了:
| 狀況 | 沒處理的結果 |
|---|---|
| 對方連不上或出錯 | 例外跑到 webhook,回 500,使用者什麼都收不到 |
| 對方很慢 | 程式一直等,reply token 可能過期 |
| 回答太長 | LINE API 回 400,回答送不出去 |
| 有人一直問 | 每次呼叫模型都要付費 |
這一課的外部 API 用 requests 呼叫,它是 Python 常用的 HTTP 第三方套件。瀏覽器連不到外面,所以課程準備了假資料:網址 api.example.com 和裡面的座位數都是虛構的,不是任何學校的系統。沒有準備的網址會丟 ConnectionError,正好拿來看連不上時會怎樣。
逐步看:外部 API 成功與失敗
使用者依序傳「座位」「自習室」「影印」三句話:第一個成功,第二個對方回 503,第三個連不上。注意後兩次都進到 except,而且 webhook 仍然回 200。
requests 會丟哪些例外
| 例外 | 什麼時候 | 這個課程看得到嗎 |
|---|---|---|
requests.ConnectionError |
網路問題,例如 DNS 失敗、連線被拒 | 看得到:沒準備的網址 |
requests.HTTPError |
呼叫 raise_for_status() 而對方回 4xx、5xx |
看得到:studyrooms 回 503 |
requests.Timeout |
超過 timeout 秒沒有回應 |
看不到:假資料立刻回傳,要在真實環境測 |
它們都繼承 requests.RequestException。timeout 不是整個下載的時間上限,而是多久沒收到資料就放棄。requests 官方文件寫明正式環境幾乎每個請求都應該設定,沒設定的話程式可能一直卡住。
呼叫語言模型
語言模型也是一個外部 API,只是回答要等比較久,而且按用量計費。下面的程式寫法和真實環境相同(from openai import OpenAI),不過這裡的回應是事先錄好的真實輸出,不會真的呼叫模型,也不用金鑰。提示詞或參數改動後對不上錄音,會丟 RecordingNotFoundError;想試自己寫的提示詞,到生成式 AI 應用軌。
真實環境的金鑰放環境變數 OPENAI_API_KEY,SDK 會自己讀取,不要寫在程式裡。
逐步看:模型的回答、上限與上文
這支程式還沒接上 LINE,只看模型本身。注意三件事:finish_reason 什麼時候變成 length、沒有上文時模型怎麼回答「舉個例子」、系統提示詞多交代一句之後回答有什麼不同。
使用者在等的時候
模型要算一陣子,使用者看到的是一片空白。Messaging API 提供 loading 動畫:
api.show_loading_animation(ShowLoadingAnimationRequest(chat_id=user_id, loading_seconds=20))
依官方文件:秒數是 5 到 60 之間 5 的倍數;機器人傳出新訊息或時間到就消失;只能用在一對一聊天,而且使用者正在看這個聊天畫面才看得到。
如果真的算很久,reply token 可能過期。官方的說法是一次、一分鐘內,一分鐘後不保證可用,這個時限也可能變動。下面的範例用 time.sleep(70) 假裝模型算了 70 秒,看看會發生什麼事。
逐步看:回答太慢,reply 失敗
這個課程裡 time.sleep 只推進模擬時鐘,不會真的等 70 秒。注意最後印出的 API 呼叫順序與額度。
reply 和 push 怎麼搭配
| 作法 | 好處 | 代價 |
|---|---|---|
| loading 動畫,算完 reply | 不計入額度,使用者看到動畫 | 要在 token 有效時送出 |
| 算完 reply,失敗才改 push(上面的範例) | 平常不計入額度 | 算很久時 webhook 也一直等著 |
| 先 reply「處理中」,背景算完再 push | webhook 馬上回應,不怕 token 過期 | 每則回答都計入額度 |
額度的規則與數字看官方定價頁,這裡不寫數字,因為會變動。
官方建議非同步處理 webhook 事件,免得後面的請求要等前一個處理完。最簡單的寫法是開一條執行緒:
@handler.add(MessageEvent, message=TextMessageContent)
def on_text(event):
threading.Thread(target=answer_later, args=(event,)).start() # 馬上回到 callback,回 200
這個課程的瀏覽器沒有執行緒,start() 會原地執行完才往下走;真實環境才是真的背景執行。背景執行緒裡沒有請求,g 不能用,要自己開資料庫連線。伺服器重新啟動時,還在跑的工作會消失,量大的話要改用工作佇列。
逐步看:有記憶的模型機器人
把前面的東西接起來:loading 動畫、呼叫模型、把對話存進 SQLite。資料庫的部分放在 store.py(輸出區的分頁看得到),寫法同第 9 課與後端軌。第二句話送出時,注意送給模型的訊息多了上一輪。
費用、資料去向與答錯的時候
費用與濫用。 模型按 token 計費(官方定價頁),機器人的帳單由你付,任何人加好友就能一直問。要有的防線:max_completion_tokens 限制每次的輸出、只送需要的歷史(對話愈長輸入愈貴)、每人每天限次。剛才的限次用 usage_log 分開記,使用者刪除對話紀錄也不會重新計算。
資料去向。 使用者傳的文字會送到模型供應商,不是只留在你的伺服器。要在隱私說明裡寫清楚,也要提醒使用者不要傳個人敏感資料。供應商怎麼處理資料,看它的官方說明(例如 OpenAI 的資料控制)。對話紀錄只存需要的,並讓使用者能刪除,也就是這裡的「清除」。
答錯。 語言模型可能講錯,而且講得很有把握。剛才的法律問題,模型直接回答「可以」,還列了程序;多交代一句系統提示詞後會提醒找專業人員,但提示詞不保證每次都做到。所以:
- 查得到的事實(座位數、課表)由程式查好交給模型,或直接回資料,不要讓模型自己想
- 法律、醫療這類問題,回答要標示這是 AI 產生的、僅供參考,並提醒找專業人員確認
- 不要讓機器人只憑模型的回答就執行動作(報名、改資料),先讓使用者確認
怎麼判斷與負責使用 AI,可以看 AI 素養軌。
動手做:截斷、失敗處理與 loading
完成三件事:truncate 截斷長回答;ask_model 失敗時呼叫 log_error(e) 並回傳 None;on_text 先顯示 loading 動畫,失敗時回 FALLBACK。
模型的請求不要改,改了會對不上錄音。最下面的測試傳了一句有錄音的話和一句沒有錄音的話。
想一想
requests.get(url) 沒有寫 timeout,對方的伺服器一直不回應。會發生什麼事?
外部 API 回 503,JSON 內容是 {"error": "維護中"}。程式只寫了 resp.json()["rooms"],沒有 raise_for_status()。結果是?
使用者問了第二句「可以舉個例子嗎?」,模型卻答了不相關的主題。最可能的原因是?
程式先算了 70 秒才呼叫 reply_message,LINE 回 400 Invalid reply token。下列哪個改法最合理?
下面的 truncate 會印出什麼?
要做一個回答校規問題的機器人。下列哪一種作法比較可靠?
重點整理
- 呼叫外部服務一定要設
timeout、檢查狀態碼、接住例外,失敗時回使用者聽得懂的話,細節留在伺服器紀錄 - 語言模型也是外部 API:設
max_completion_tokens、自己設定逾時與重試,上文要每次送出,對話存資料庫 - 等待久時用 loading 動畫;reply token 過期就要 push,push 計入額度
- 長回答要截斷,LINE 的文字上限是 5000 字元
- 每人限次控制費用;讓使用者知道文字會送到模型供應商,也能刪除自己的紀錄
- 模型可能答錯,查得到的事實由程式提供,專業領域要標示並提醒找專業人員
下一課處理另一種輸入:使用者傳語音訊息時,怎麼在自己的機器上轉成文字。