Skip to content

StateGraph.compile:把藍圖編譯成 Pregel 執行體

源码版本1.2.9

職責

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 真正建構執行體的關鍵段——把 builder 的所有欄位塞進 CompiledStateGraph,並加一個 START 入口通道:

python
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)

(建構 + attach_node:1333-1362)

最後把邊和分支也翻譯成 Pregel 的通道訂閱關係,再觸發一次 validate:

python
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):

python
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)

(Pregel init nodes:800-809)

邊界與失敗

  • 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 是構建器和執行時之間的邊界——它把 StateGraphadd_node / add_edge / add_conditional_edges 累積的 spec 全部翻譯成 Pregel 的執行期結構,繫結上 checkpointer / store / interrupt 設定,最後回傳一個可直接 invokePregel 子類別。理解了它就理解了「圖宣告 → 可執行體」的整條鏈路。

接下來可以看 Pregel 引擎Pregel.__init__ 之後 invoke / stream 怎麼跑這個編譯產物,或看 StateSnapshot 怎麼從編譯產物配 checkpointer 後取狀態。

對照官方資料:LangGraph 文件 · README