昨天講 conditional edge 怎麼讓流程繞回去重試。今天講另一個機制,checkpointer,它解決的是 conditional edge 解決不了的問題:流程能不能先暫停,等人核准之後再繼續。
compile(checkpointer=...) 讓每個節點執行完存一份 state 快照。
這一行帶來的能力:
要做 human-in-the-loop 的系統,這幾乎是硬需求。想想一個需要主管簽核的流程:agent 跑到「等簽核」那一步,然後呢?沒有 checkpointer 的話,你得自己把整個中間狀態序列化存起來,主管簽完再反序列化重建,而且要處理版本不一致。有 checkpointer 的話這是框架的事。
這也是 LangGraph 相對於宣告式框架最難被取代的地方。 宣告角色那類框架把流程控制權交給 LLM,而 LLM 沒有「暫停三天再繼續」這個概念。
checkpointer 存的快照要分辨「這是誰的」,靠的是 thread_id。呼叫 compile 出來的 app 時,設定裡帶一個 configurable.thread_id,同一個 id 的多次呼叫會接續同一份狀態,不同 id 各自獨立。這也是「跑到一半停下來等人審核,明天再繼續」能實現的原因:只要把同一個 thread_id 傳回去,框架就知道該接哪一份快照續跑,不需要自己管理任何 session 狀態。
反過來說,沒帶 thread_id 卻期待系統記得上一輪,是我看過最常見的踩空之一。每次呼叫都像第一次,因為框架真的把它當第一次處理,不會有任何錯誤訊息提醒你少帶了這個欄位。
checkpointer 不是只有一種實作。開發階段最常用 MemorySaver,存在記憶體裡,重啟就沒了,拿來驗證流程正確性很夠用,一行就能掛上,幾乎沒有成本。
正式環境要換成會落地的版本,SqliteSaver 或 PostgresSaver 都是選項。差別在於團隊本來的資料庫慣例,以及要不要跨多個服務實例共享同一份 checkpoint。單機單實例用 SqliteSaver 通常就夠,多實例、需要高可用的場景才會考慮 PostgresSaver。三種背後是同一個介面,換的時候流程圖不用改,只換 compile() 那一行傳進去的物件。
我的建議是:即使一開始不需要 human-in-the-loop,compile() 的時候還是把 checkpointer 掛上。
理由是掛它的成本接近零(記憶體版的 checkpointer 一行就好),而需要它的時候通常是系統已經上線、需求突然變了、你不想大改的時候。這是少數 YAGNI 不太適用的地方,因為它不是「多寫一個功能」,是「不要把門關上」。
Day 20 到 22 合起來:LangGraph 給你的是一台可控的狀態機,你自己決定流程,模型只是節點裡的一種實作。代價是樣板碼比較多。
這個代價值不值得,取決於你在做 prototype 還是在做要維護三年的系統。明天用一張表把這個取捨講清楚。