StateGraph.compile:把藍圖編譯成 Pregel 執行體
職責
StateGraph.compile 是構建器與執行時之間的「翻譯官」。它吃進去一個 StateGraph(節點 spec、邊、分支條件、通道字典),吐出來一個 CompiledStateGraph(CompiledStateGraph:1391)——後者繼承自 Pregel,帶 invoke / stream / astream / get_state 等所有執行期方法。compile 之前你的圖只是設定資料,compile 之後才變成可執行體。
它的核心工作是四件:1) 校驗圖結構 (validate),2) 決定輸出/串流通道,3) 注入執行期依賴 (checkpointer / store / cache / interrupt 設定),4) 把每個 StateNodeSpec / edge / BranchSpec 翻譯成 Pregel 的 PregelNode + 通道訂閱 (建構 CompiledStateGraph:1333-1357)。翻譯這一步是循環呼叫 compiled.attach_node / compiled.attach_edge / compiled.attach_branch 完成的——這些方法把 builder 裡的宣告式資料結構轉換成 PregelNode.writers / PregelNode.triggers 這類執行期欄位。
compile 還兼任兩個次要職責:1) 給所有未顯式指定 policy 的節點套用 set_node_defaults 設的預設值 (retry / cache / error_handler / timeout),2) 在啟用了嚴格 msgpack 序列化時(_serde.STRICT_MSGPACK_ENABLED),build 一個 serde allowlist 讓 checkpointer 只序列化 schema 裡出現的欄位(serde allowlist:1220-1241)。
設計動機
為什麼不直接讓 StateGraph 自己就是 Pregel,非要分兩步?
- 構建期 vs 執行期關注點分離:builder 階段只關心「圖長什麼樣」,執行期階段關心「怎麼跑」。分開後 builder 可以被多次編譯成不同執行體——同一個 StateGraph 配不同的 checkpointer / store / interrupt 節點跑出多個 Pregel 實例。
- 不可變執行體:compile 後的
CompiledStateGraph欄位在Pregel.__init__裡基本固定(Pregel.__init__:758-836),auto_validate=False讓 compile 自己控制何時校驗,避免被建構函式提前校驗。 - 延遲校驗:使用者
add_node時只 append 進容器,不立即檢查「邊的起點是否存在」——這樣允許使用者用任意順序註冊節點和邊;真正校驗在 compile 時一次性做 (validate 呼叫:1247-1254)。 - 中斷設定參數化:
interrupt_before/interrupt_after是 compile 參數,不是圖本身的屬性——同一張圖可以編譯出「帶中斷」和「不帶中斷」兩個版本,人機協作場景裡很常見 (interrupt 參數:1170-1171)。 - checkpointer 型別規範化:
compile(checkpointer=...)接受None/True/False/BaseCheckpointSaver四種,ensure_valid_checkpointer(ensure_valid_checkpointer:107-117)統一校驗,避免執行時炸。 - 預設值在編譯期才套用(
defaults 套用:1299-1325)——這樣允許使用者在add_node之後再調set_node_defaults,新預設值會覆蓋所有沒顯式指定的節點。
關鍵檔案
compile 簽名:1164-1217— 接收checkpointer/store/cache/interrupt_before/interrupt_after/debug/name/transformers。ensure_valid_checkpointer:107-117— 校驗 checkpointer 必須是None/True/False/BaseCheckpointSaver之一,否則TypeError。ensure_valid_checkpointer 呼叫:1218— compile 第一件事就是規範化 checkpointer。serde allowlist:1220-1241— 嚴格 msgpack 下,build allowlist 並 apply 到 checkpointer,讓 checkpoint 只存 schema 欄位。interrupt 合併 + validate:1243-1254—"*"表示 All(所有節點),把interrupt_before/interrupt_after合併成一個 list 傳給validate。output / stream channels:1256-1273— 單欄位且是__root__時直接用字串,否則用 list 過濾掉 managed value。預設 error handler 節點:1278-1297—set_node_defaults設的全域 error_handler 會被注入成一個名為__default_error_handler__的特殊節點。defaults 套用:1299-1325— 把set_node_defaults的 retry / cache / error_handler / timeout 套用到每個未顯式指定的 spec;cache 和 error_handler 不套用到 error-handler 節點本身。node_error_handler_map:1327-1331— 生成node_name -> handler_node_name映射,執行時根據這個 map 把失敗節點的執行路由到對應 handler。建構 CompiledStateGraph:1333-1357— 把 builder 的channels/managed合併,加START: EphemeralValue(input_schema)當入口通道,設stream_mode="updates"、input_channels=START。attach 三件套:1360-1388— 循環呼叫compiled.attach_node(START, None)+ 每個節點、attach_edge每條邊、attach_branch每個分支;最後compiled.validate()。Pregel.__init__:758-836—CompiledStateGraph.__init__透過super().__init__(**kwargs)委託給 Pregel,這裡展開所有執行期欄位並auto_validate=True時調self.validate()。
資料流
下面是 compile 真正建構執行體的關鍵段——把 builder 的所有欄位塞進 CompiledStateGraph,並加一個 START 入口通道:
compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT](
builder=self,
schema_to_mapper={},
context_schema=self.context_schema,
nodes={},
channels={
**self.channels,
**self.managed,
START: EphemeralValue(self.input_schema),
},
input_channels=START,
stream_mode="updates",
output_channels=output_channels,
stream_channels=stream_channels,
checkpointer=checkpointer,
interrupt_before_nodes=interrupt_before,
interrupt_after_nodes=interrupt_after,
auto_validate=False,
debug=debug,
store=store,
cache=cache,
node_error_handler_map=node_error_handler_map,
name=name or "LangGraph",
stream_transformers=transformers,
)
compiled._serde_allowlist = serde_allowlist
compiled.attach_node(START, None)
for key, node in self.nodes.items():
compiled.attach_node(key, node)最後把邊和分支也翻譯成 Pregel 的通道訂閱關係,再觸發一次 validate:
for start, end in self.edges:
compiled.attach_edge(start, end)
for starts, end in self.waiting_edges:
compiled.attach_edge(starts, end)
for start, branches in self.branches.items():
for name, branch in branches.items():
compiled.attach_branch(start, name, branch)
return compiled.validate()(attach edge/branch:1378-1388)
Pregel.__init__ 拿到這些欄位後,會把 nodes 裡的 NodeBuilder 翻成 PregelNode,給 TASKS 通道安一個 Topic(Send, accumulate=False),然後 auto_validate=True 時調 self.validate()(Pregel init:800-836):
self.nodes = {
k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items()
}
self.channels = channels or {}
if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic):
raise ValueError(
f"Channel '{TASKS}' is reserved and cannot be used in the graph."
)
else:
self.channels[TASKS] = Topic(Send, accumulate=False)邊界與失敗
checkpointer=True不能用於根圖(True 報錯:2583-2584)——True表示「從父圖繼承」,只有子圖才能用;根圖必須顯式給 saver 或False。interrupt_before="*"跟interrupt_after="*"互斥處理(* 處理:1249-1253)——只有interrupt_after != "*"時才把interrupt_before列表合進 interrupt;這是為了用*表示「所有節點」時的優先級約定。TASKS通道名被保留(TASKS 保留:804-807),使用者在 schema 裡定義名為__pregel_tasks的欄位會直接報錯——這是引擎內部用來扇出Send的 Topic 通道。- 重複 attach 同名節點:builder 階段已經擋了重名(
重名:792-793)——但__default_error_handler__這種自動生成的節點名也要避免跟使用者節點衝突(default handler 衝突:1280-1284)。 - cache 和 error_handler 預設值不應套用到 handler 節點本身(
cache 不给 handler:1313-1321)——因為「快取 handler 結果」不安全(失敗節點的 state 每次可能不同),「handler 抓自己」會死循環。 - 同一 builder 多次 compile 是允許的:每次 compile 都建立新的
CompiledStateGraph,builder 的compiled欄位雖然在validate()時被設成True(compiled=True:1161),但只是個標誌位元,不阻止再 compile 或繼續add_node(只是會 warning)。
小結
compile 是構建器和執行時之間的邊界——它把 StateGraph 裡 add_node / add_edge / add_conditional_edges 累積的 spec 全部翻譯成 Pregel 的執行期結構,繫結上 checkpointer / store / interrupt 設定,最後回傳一個可直接 invoke 的 Pregel 子類別。理解了它就理解了「圖宣告 → 可執行體」的整條鏈路。
接下來可以看 Pregel 引擎 裡 Pregel.__init__ 之後 invoke / stream 怎麼跑這個編譯產物,或看 StateSnapshot 怎麼從編譯產物配 checkpointer 後取狀態。
對照官方資料:LangGraph 文件 · README