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 09

記住對話:狀態與 SQLite

每個使用者一份狀態,把多步驟流程存進資料庫
Interaction實作 30 minute(s)

Learning Objectives

  • 說明每則訊息都是新的請求,以及為什麼 webhook 不能靠 Cookie 或 Flask 的 session 記住使用者
  • 用 user_id 當鍵保存每個人的狀態,並說明存在字典裡的問題
  • 把多步驟流程寫成狀態機,存進 SQLite,處理取消與錯誤輸入
  • 存歷史訊息時只存需要的欄位,查詢帶 user_id,並讓使用者可以刪除自己的資料

下一句話是新的請求

使用者想預約討論室,機器人問「想預約哪個時段?」,使用者回「週三下午」。對你的程式來說,這兩句話是兩個毫不相干的 HTTP 請求:處理完第一句,on_text 就結束了,區域變數都消失了。收到「週三下午」時,程式不知道它是在回答什麼。

做網站時,這個問題靠 Cookie 或 Flask 的 session 解決(後端軌第 14 課)。機器人不能這樣做:webhook 是 LINE 平台送來的,不是使用者的瀏覽器,請求裡沒有屬於某位使用者的 Cookie。

每個事件都帶著 event.source.user_id,所以要自己準備一個以它為鍵的地方,記下「這個人走到哪一步、已經說了什麼」。這就是對話狀態。官方文件提到,使用者沒有同意提供個人檔案資訊時,webhook 裡不會有 user ID,真實的程式要能處理這個值是 None 的情況。

逐步看:用字典存每個人的狀態

最簡單的做法是一個全域字典。王小明和林怡君同時預約,交錯傳訊息。按播放,注意右邊記憶體圖裡的 STATES,它在兩個請求之間留了下來,兩個人各有一份。

互動練習:重新啟動後,字典裡的狀態不見了

手機右上角可以切換使用者。「伺服器紀錄」面板會印出每次處理完的 STATES。

真實環境的伺服器會重新啟動:部署新版本、程式當掉、主機更新都會。這裡用「存檔並重新啟動」實際看一次。

把狀態放進資料庫

字典還有另一個問題:正式環境常常跑好幾個 worker 程序(後端軌第 19 課),程序之間的記憶體不共用,同一個人的兩句話可能被不同程序處理。

所以狀態要放在程式之外。一張表就夠了,user_id 當主鍵,每人一列:

CREATE TABLE IF NOT EXISTS states (
    user_id TEXT PRIMARY KEY,
    step TEXT,        -- 走到哪一步
    data TEXT         -- 已經收集到的資料,存成 JSON 字串
);

連線的寫法和後端軌第 9 課相同:get_db() 把連線放在 g,請求結束時關閉。handler.handle 是在 callback 裡呼叫的,所以 on_text 裡可以直接用。不過,如果照第 3 課的建議把事件交給背景執行緒處理,執行緒裡沒有 Flask 的應用程式情境,就不能用 g,要在執行緒裡自己開關連線。

更新狀態用 UPSERT:沒有這一列就新增,已經有了就更新。

INSERT INTO states (user_id, step, data) VALUES (?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET step = excluded.step, data = excluded.data

excluded 指的是這次原本要新增的那一列。SQL 的語法可以到 SQL 軌學,不過那一軌用 MySQL,寫法不完全相同。

狀態機

把流程畫出來,每個步驟等於一個狀態,收到訊息就依規則換到下一個狀態:

目前狀態 收到 回覆 下一個狀態
idle 預約 想預約哪個時段? ask_time
idle 其他 傳「預約」開始 idle
ask_time 任何文字 請問幾個人? ask_people
ask_people 數字 確認嗎? confirm
ask_people 不是數字 請輸入數字 ask_people(不動)
confirm 確認 預約完成 idle
任何狀態 取消 已取消 idle

兩個容易漏掉的情況:輸入不合法時留在原地,讓使用者重答,而不是當掉或跳到下一步;隨時都能取消,不要把人困在流程裡。「取消」要在進入各步驟之前先判斷。

逐步看:狀態機與 SQLite

這段邏輯用不到 LINE:handle 收到 user_id 與文字,回傳要回覆的話,所以範例拿掉機器人的部分,只留狀態機,也省略確認那一步。為了聚焦,這裡只用一條連線;放進機器人時要換成上面的 get_db(),下一個互動練習就是完整的版本。

每次都會印出:使用者傳的話 → 回覆,以及 states 表的內容。

互動練習:討論室預約

這個機器人把狀態存進 SQLite,流程多了「確認」這一步,用快速回覆當按鈕。它還有兩個問題要由你修好:輸入不是數字會當掉,而且中途無法取消。

資料庫不在程式檔裡,所以「存檔並重新啟動」不會清掉資料。要全部重來時按「清空資料庫」。

修改 on_text 後按「存檔並重新啟動」。注意第 3 個任務:你預約到一半時改了程式、重新啟動,機器人還記得你,這就是把狀態放進資料庫的差別。

歷史訊息與使用者的資料

除了狀態,機器人也可能需要留下訊息紀錄,例如讓使用者查自己說過什麼。一張表,每則訊息一列。

存之前先想過這三件事:

  • 只存需要的欄位。 這裡只存 user_id 與訊息內容。顯示名稱、大頭貼這類不需要的資料就不要存。user ID 可以對應到一位真實的使用者,要當成個人資料保管;使用者傳的訊息裡也可能有個資
  • 查詢一定要帶 user_id。 SELECT msg FROM messages 會把所有人的訊息回給正在查詢的人,這是真實會發生的外洩。LINE 的訊息一對一傳給你,容易忘記資料表裡是所有人的資料
  • 讓使用者可以刪除自己的資料,並告訴他們你存了什麼、存多久。不再需要的狀態與紀錄也要定期清掉,例如走到一半就放棄的預約

另外,第 3 課提過 LINE 可能重送事件。對狀態機來說,同一句話處理兩次會多走一步,所以要照第 3 課的做法,用 webhookEventId 去重。

逐步看:每個人只看到自己的紀錄

兩位使用者各留一則訊息,再由王小明查詢。看 SQL 的 WHERE user_id = ?,以及印出來的結果裡有沒有林怡君的訊息。

動手做:只能看自己的紀錄,也能刪除

完成兩個函數。起始碼的 history 少了條件,會把所有人的訊息都查出來,執行一次就看得到。

  1. history(uid):回傳這個人所有訊息內容組成的串列,依 id 由舊到新
  2. delete_mine(uid):刪除這個人的所有訊息並回傳刪了幾筆。別人的資料不能動

cursor.rowcount 可以取得影響的列數。記得 commit。

你的輸出
預期輸出

想一想

1.

有人用一個全域變數 current_step = 'idle' 記錄流程走到哪一步。會發生什麼問題?

2.

這段程式會印出什麼?

STATES = {}

def on_text(uid, text):
    state = STATES.setdefault(uid, {'step': 'ask_time'})
    if state['step'] == 'ask_time':
        state['time'] = text
        state['step'] = 'ask_people'
        return '幾人'
    del STATES[uid]
    return state['time'] + text

print(on_text('A', '週三'), on_text('B', '週四'), on_text('A', '3'), len(STATES))
3.

問人數時,使用者傳了「不知道」。最合適的處理是?

4.

INSERT … ON CONFLICT(user_id) DO UPDATE SET step = excluded.step 裡的 excluded.step 指的是什麼?

5.

機器人要提供「想看歷史訊息」。下面哪一句 SQL 才對?

第 1 次作答

重點整理

  • 每則訊息都是新的請求;webhook 來自 LINE 平台,不能用 Cookie 與 Flask 的 session,要以 user_id 為鍵自己保存狀態
  • 字典重新啟動就不見,多個程序也不共用,所以存進資料庫
  • 多步驟流程寫成狀態機:輸入不合法時留在原地,「取消」最先判斷
  • 只存需要的欄位,查詢帶 user_id,讓使用者可以刪除自己的資料

下一課讓機器人查外部資料、接上語言模型,並處理等待與失敗。

這一課完成了!
Next lesson:接上外部服務與語言模型
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。