排程跟進
智慧代理如何把工作排到稍後執行,並把結果送回它原本進來的那個管道——持久化任務、原子性認領,以及故障復原。
有些事沒辦法在一輪對話裡答完:一項要跑好幾分鐘的查核、下週二的一則提醒、六週後才該開啟的續約對話。智慧代理可以把這些工作登記下來,等結果備妥再送出答案。
運作機制
對話進行到一半,智慧代理針對這一輪內做不完的工作呼叫 schedule_followup:一項要跑好幾分鐘的查核、下週二的一則提醒、六週後的續約對話。
任務是 Postgres 裡的一列資料,不是訊息佇列裡的一則訊息。待處理的任務就是已提交的資料,重新部署或當機都不會弄丟它——持久性由本來就在用的儲存層免費附贈。
輪詢器每隔幾秒醒來一次,尋找到期的任務。代價是時間精細度:任務會在下一次輪詢時觸發,而不是精確到毫秒。對於以分鐘到數週計的跟進來說,這不是有意義的損失。
到期任務以 FOR UPDATE SKIP LOCKED 認領。單一輪詢器其實用不到這個,但現在就寫對,代表日後多跑一個實例也不會重複送出——一則跟進被送兩次,賠掉的是客戶對寄件者的信任。
結果被生成出來。若某個行程認領了任務之後掛掉,會有一輪復原掃描把它退回待處理,而不是讓它永遠卡在處理中。
送達時會回到請求原本進來的那個管道:網頁小工具裡的一則對話泡泡、Telegram 或 WhatsApp 裡的一則訊息。在 WhatsApp 上問的人,就在 WhatsApp 上得到回覆。
工具會立即返回——客戶現在就拿到回覆,排程的結果稍後才到。
這個工具不會卡住這一輪。它寫入一列資料就返回,所以客戶現在就拿到回覆,排程的結果稍後才到。
為什麼佇列是一張資料庫表
沒有 Redis,也沒有訊息中介。任務就是 Postgres 裡的資料列,這是刻意的取捨:
- 重啟不會弄丟工作。 待處理的任務是一列已提交的資料。行程可以在執行途中被重新部署,輪詢器回來時會把任務接手過去——持久性由本來就在用的資料儲存層免費附贈,而不是靠另一套有自己失效模式的系統。
- 少一樣要維運的東西。 對單行程部署來說,一個輪詢迴圈就夠了。
代價是延遲的精細度:任務在下一次輪詢時觸發,而不是精確到毫秒。對於以分鐘到數週計的跟進來說,這不是有意義的損失。
認領是原子性的
到期任務以 FOR UPDATE SKIP LOCKED 認領。單一輪詢器不可能需要這個,但現在就寫對,代表日後多跑一個實例也不會重複送出。一則跟進被送兩次不是外觀上的小瑕疵:那是客戶收到兩次同樣的「您的合約即將續約」,然後對寄件者失去信任。
SKIP LOCKED 同時也代表一個跑很慢的任務不會把整條佇列擋在後面;其他工作者會跳過被鎖住的那一列,直接取下一筆。
故障復原
某個行程認領了任務之後掛掉,這筆任務原本會永遠卡在處理中。一輪復原掃描會把卡住的任務退回待處理,讓它們被重試,而不是無聲無息地被丟掉。
送達跟著來源走
結果會被推回請求原本進來的那個管道,而不是平台自己挑一個。在 WhatsApp 上問的人,就在 WhatsApp 上得到回覆。租戶自己排程的主動聯繫——續約提醒、期限催辦——也是同樣的機制,依你設定的節奏執行。
實務上的意義
- 智慧代理可以說「這件事我回頭再跟您說」,然後真的做到。
- 提醒事項撐得過部署、重啟與當機。
- 一則跟進只會送達一次,而且會找到對方原本跟你講話的那個地方。