No.09约 2,197 字 · 阅读 61 分钟LangGraph Basics 次阅读

【LangGraph基础】图构建与编译解析

LangGraph, Pregel, 源码解析, StateGraph
目录

本文从源码层面拆解 LangGraph 的 StateGraph 是如何从「几个 API 调用」变成「一张可执行的 Pregel 计算图」的。核心结论只有一句话:

add_node / add_edge / add_conditional_edges 只是「声明」,compile() 才是「构建」。声明阶段产出的是一堆普通的数据结构(StateNodeSpec、BranchSpec、edges 集合),编译阶段把它们翻译成执行引擎真正认的 PregelNode + channels + writers。

全文围绕三个示例展开(均位于 pregel/ 目录):

示例对应阶段回答的问题
graph_builder_internals.py构建add_node / add_edge / add_conditional_edges 内部做了什么、产物是什么
compile_internals.py编译compile() 有哪些步骤、步骤如何衔接
compiled_graph.py编译后编译出来的计算图长什么样、执行引擎怎么用它

一、两个阶段的心智模型

StateGraph 是一个 builder 类,不能直接执行(state.py:131-145 文档明确说明必须 .compile() 后才有 invoke/stream)。所以整个流程天然分两段:

flowchart LR
    subgraph build["① 构建阶段(builder)"]
        A["add_node"] --> N["self.nodes<br/>dict[str, StateNodeSpec]"]
        B["add_edge"] --> E["self.edges / waiting_edges<br/>set[tuple]"]
        C["add_conditional_edges"] --> BR["self.branches<br/>dict[source][name]=BranchSpec"]
    end
    subgraph compile["② 编译阶段(compile)"]
        N --> P["attach_node"]
        E --> AE["attach_edge"]
        BR --> AB["attach_branch"]
        P --> G["PregelNode + channels"]
        AE --> G
        AB --> G
    end
    G --> X["可执行 Pregel 图"]
  • 构建阶段:三个方法只往 StateGraph 实例的容器里塞数据,不产生任何可执行逻辑。
  • 编译阶段:compile() 读取这些容器,把它们翻译成 PregelNode / channels / writers。

二、构建阶段:三个 API 记录了什么

2.1 容器初始化

StateGraph.__init__(state.py:252-259)先建好几个空容器:

self.nodes = {}              # dict[str, StateNodeSpec]      ← add_node 的产物
self.edges = set()           # set[tuple[str, str]]          ← add_edge 单源的产物
self.branches = defaultdict(dict)  # dict[source][name]      ← add_conditional_edges 的产物
self.channels = {}           # dict[str, BaseChannel]        ← 状态字段通道
self.waiting_edges = set()   # set[tuple[tuple[str,...], str]] ← add_edge 多源的产物
self.compiled = False

这五个容器就是后续一切的「原料仓库」。

2.2 add_node:从函数到 StateNodeSpec

add_node(state.py:782-920)内部做了五件事:

flowchart TD
    A["解析节点名<br/>非 str 时取 __name__"] --> B["校验<br/>重名 / 保留名 / 非法字符 | 和 :"]
    B --> C["推断 schema<br/>input_schema + ends(Command[Literal])"]
    C --> D["若有 error_handler<br/>额外注册 __error_handler__{node}"]
    D --> E["coerce_to_runnable(action)<br/>包装成 Runnable"]
    E --> F["self.nodes[node] = StateNodeSpec"]

产物:StateNodeSpec(graph/_node.py:91-103):

class StateNodeSpec(Generic[NodeInputT, ContextT]):
    runnable: StateNode            # coerce_to_runnable 包装后的 Runnable
    metadata: dict[str, Any] | None
    input_schema: type             # 输入 schema(第一参数类型推断)
    retry_policy: ...              # 重试策略
    cache_policy: ...              # 缓存策略
    is_error_handler: bool = False
    error_handler_node: str | None = None   # 该节点的 error handler 节点名
    ends: tuple | dict | None      # 从 Command[Literal[...]] 推断的可达目标
    defer: bool = False
    timeout: TimeoutPolicy | None = None
    trace_policy: TracePolicy | None = None

关键点:这一步只包装了函数、记录了配置,还没生成 PregelNode、也没有触发通道。

2.3 add_edge:边就是集合里的一个元组

add_edge(state.py:928-980)极其简单:

if isinstance(start_key, str):
    self.edges.add((start_key, end_key))        # 单源
else:
    self.waiting_edges.add((tuple(start_key), end_key))  # 多源

产物:

  • 单源边 → self.edges(set[tuple[str, str]]),如 ('__start__', 'agent')
  • 多源边 → self.waiting_edges(set[tuple[tuple[str,...], str]]),表示「等所有源都完成后才触发目标」

2.4 add_conditional_edges:路由函数到 BranchSpec

add_conditional_edges(state.py:982-1030):

flowchart TD
    A["coerce_to_runnable(path)<br/>包装路由函数"] --> B["取分支名<br/>path.name 或 'condition'"]
    B --> C["BranchSpec.from_path<br/>归一化 path_map"]
    C --> D["self.branches[source][name] = BranchSpec"]

BranchSpec.from_path(graph/_branch.py:88-120)把 path_map 归一化成 dict[Hashable, str]:

  • 传 dict → 直接 copy
  • 传 list → {name: name for name in path_map}
  • 都不传 → 从路由函数返回类型的 Literal[...] 推断

产物:BranchSpec(graph/_branch.py:83-86):

class BranchSpec(NamedTuple):
    path: Runnable                      # 包装后的路由函数
    ends: dict[Hashable, str] | None    # {返回值: 目标节点},如 {'tools': 'tools', 'end': '__end__'}
    input_schema: type | None = None

注意:BranchSpec 只是一个描述——「用这个函数路由,返回值按这个表映射到目标」。真正的动态路由逻辑要到编译时才被挂到源节点上。


三、编译阶段:compile() 的 11 个步骤

compile()(state.py:1177-1401)把上面的声明翻译成可执行图。整体分 11 步:

flowchart TD
    A["① ensure_valid_checkpointer<br/>处理 checkpointer"] --> B["② serde allowlist<br/>序列化白名单(可选)"]
    B --> C["③ validate<br/>验证图结构"]
    C --> D["④ 准备 output/stream channels<br/>__root__ 或字段列表"]
    D --> E["⑤ 应用 builder defaults<br/>默认 retry/cache/timeout/error_handler"]
    E --> F["⑥ node_error_handler_map<br/>{节点: handler节点}"]
    F --> G["⑦ 构造 CompiledStateGraph<br/>nodes={} 空,channels={状态字段+START}"]
    G --> H["⑧ attach_node × N<br/>建 branch:to:{key} + PregelNode"]
    H --> I["⑨ attach_edge × N<br/>普通边 → 源节点 writer"]
    I --> J["⑩ attach_branch × N<br/>条件边 → 源节点 branch writer"]
    J --> K["⑪ validate<br/>返回编译好的图"]

3.1 步骤 ①~⑥:纯数据准备

前六步不碰图结构,只在 builder 上做校验和配置下放:

步骤做什么产物
①ensure_valid_checkpointer规范化 checkpointer(None/False/实例)
②strict-msgpack 下收集所有 schema序列化白名单 serde_allowlist
③验证节点名、边端点存在性无(校验通过才继续)
④确定输出通道output_channels / stream_channels
⑤默认 retry_policy/cache_policy/timeout/error_handler 下放给 spec(per-node 优先)修改后的 StateNodeSpec
⑥收集「节点 → error handler 节点」node_error_handler_map

3.2 步骤 ⑦:构造空壳

state.py:1346-1370 构造 CompiledStateGraph,此时:

nodes={}                        # 空,等 ⑧ 填充
channels={
    **self.channels,            # 状态字段通道(如 messages → BinaryOperatorAggregate)
    **self.managed,             # managed values
    START: EphemeralValue(self.input_schema),  # 入口通道
}

这是关键:⑦ 时 channels 里只有「状态字段通道」,还没有 branch:to:{node}。那些触发通道是 ⑧ 才新建的。

3.3 步骤 ⑧⑨⑩:三者的衔接核心

这三个步骤是编译的精髓,它们都往源节点的 writers 列表里追加 writer:

flowchart LR
    subgraph attach["对每个节点执行"]
        N8["⑧ attach_node<br/>新建 branch:to:{key} 通道<br/>PregelNode(writers=[状态更新, Command路由])"]
    end
    subgraph edge["对每条边执行"]
        N9["⑨ attach_edge(start, end)<br/>给 start.writers 追加<br/>ChannelWrite(branch:to:{end})"]
    end
    subgraph branch["对每条条件边执行"]
        N10["⑩ attach_branch(source, name)<br/>给 source.writers 追加<br/>branch.run(...) 动态路由"]
    end
    N8 --> N9 --> N10

⑧ attach_node(state.py:1444-1549)为每个节点:

  1. 确定 output_keys(START 用 input_schema,普通节点用 builder.channels)
  2. 定义 _get_updates:把节点返回的 dict 转成 [(key, value), ...] 元组
  3. 构造两个 ChannelWriteTupleEntry(状态更新 + _control_branch 处理 Command 路由)
  4. 调用 _compile_node:新建 branch:to:{key} 通道(EphemeralValue),生成 PregelNode(triggers=[branch:to:{key}], channels=输入字段, writers=[ChannelWrite(write_entries)])

⑨ attach_edge(state.py:1551-1578)给源节点追加一个 writer:

self.nodes[starts].writers.append(
    ChannelWrite((ChannelWriteEntry(_CHANNEL_BRANCH_TO.format(end), None),))
)

即:「边」被翻译成「源节点执行完后,往目标节点的 branch:to:{end} 通道写一个信号」。

⑩ attach_branch(state.py:1578-1624)给源节点追加动态路由 writer:

self.nodes[start].writers.append(branch.run(get_writes, reader))

get_writes 把路由函数选中的目标包成 ChannelWriteEntry(branch:to:{target}),运行时动态决定写哪个通道。

3.4 用实际输出看衔接

以「ReAct 图」为例(compile_internals.py),编译后三个节点的 writers 分别是:

'__start__': writers = [ ['<tuple>','<tuple>'], ['branch:to:agent'] ]
                         └── ⑧ 状态更新+Command ──┘   └── ⑨ attach_edge(START→agent) ──┘

'agent':    writers = [ ['<tuple>','<tuple>'], <branch:RunnableCallable> ]
                         └── ⑧ ──┘             └── ⑩ attach_branch(agent) ──┘

'tools':    writers = [ ['<tuple>','<tuple>'], ['branch:to:agent'] ]
                         └── ⑧ ──┘             └── ⑨ attach_edge(tools→agent) ──┘

一眼能看出:writers 列表 = ⑧ 的状态更新 writer + ⑨/⑩ 追加的边/分支 writer。而目标节点的 triggers=[branch:to:{target}] 一旦被写就触发。这就是「边 → 触发」的编译本质。


四、编译后的计算图:执行引擎看到的东西

编译完成后,CompiledStateGraph 里有三样执行引擎真正关心的结构(见 compiled_graph.py)。

4.1 channels:状态通道

每个状态字段对应一个 channel,类型由 reducer 决定:

flowchart LR
    F["State 字段"] -->|"Annotated[x, add_messages]"| B["BinaryOperatorAggregate<br/>(追加消息)"]
    F -->|"Annotated[int, operator.add]"| A["BinaryOperatorAggregate<br/>(累加)"]
    F -->|"str(无 reducer)"| L["LastValue<br/>(覆盖写)"]
    F -->|"触发通道"| E["EphemeralValue<br/>(branch:to:{node})"]

实际输出:

messages        BinaryOperatorAggregate   reducer=add_messages
token_count     BinaryOperatorAggregate   reducer=add
weather         LastValue                  # 覆盖写
branch:to:weather  EphemeralValue          # 触发通道
  • BinaryOperatorAggregate:reducer 语义(追加/累加),多个节点写同一字段时合并而非覆盖。
  • LastValue:普通字段,后写覆盖先写。
  • EphemeralValue:触发通道,只做「本步有没有被写」的信号,用完即清。

4.2 trigger_to_nodes:通道 → 订阅节点

这是执行引擎的反向索引,回答「某通道被写时,该唤醒哪些节点」:

__start__         -> ['planner']
branch:to:planner -> ['planner']
branch:to:weather -> ['weather']
branch:to:news    -> ['news']
branch:to:summarize -> ['summarize']

prepare_next_tasks 每轮就是靠它,把「上一步被写的通道」映射成「下一步要跑的节点集合」。

4.3 节点的「订阅 / 读 / 写」三视图

每个 PregelNode(pregel/_read.py:97-150)有三组关键字段:

字段含义示例
triggers哪些通道被写会触发它['branch:to:weather']
channels读哪些通道作为输入['plan', 'token_count']
writers执行完往哪些通道写[状态更新, branch:to:summarize]

4.4 条件边的运行时形态

条件边在编译时被 attach_branch 包成一层动态 writer:路由函数 path() 运行时返回目标,再经 BranchSpec.ends 映射成 branch:to:{target} 写入。所以:

flowchart LR
    P["planner 执行完"] --> R["route() 返回 ['weather','news']"]
    R -->|"ends 映射"| W["写 branch:to:weather<br/>写 branch:to:news"]
    W --> T1["触发 weather"]
    W --> T2["触发 news"]
    T1 --> S["同一 superstep 内并行"]
    T2 --> S

一个路由返回列表,就会在同一 superstep 内并行唤醒多个分支。

4.5 运行时:reducer 与 superstep

编译只是静态结构,真正动起来看两个运行时现象:

reducer 合并并行写入(compiled_graph.py 场景 5):

输入 token_count=1
三个节点各写 token_count:planner=100, weather=50, news=50
最终 token_count = 201   ← 1+100+50+50(BinaryOperatorAggregate 累加)

superstep 边界(stream_mode="updates"):

flowchart LR
    S1["superstep 1<br/>planner"] --> S2["superstep 2<br/>weather + news 并行"]
    S2 --> S3["superstep 3<br/>summarize"]

并行分支在同一个 superstep 内完成,各自写入 token_count 后,在 superstep 边界由 reducer 一次性合并——这正是 Pregel/BSP(批量同步并行)模型的体现。


五、总结

整条链路可以压缩成一张图:

flowchart TD
    subgraph build["构建:只记录声明"]
        A["add_node"] --> NS["StateNodeSpec<br/>(runnable + 配置)"]
        B["add_edge"] --> ED["edges / waiting_edges<br/>(set[tuple])"]
        C["add_conditional_edges"] --> BS["BranchSpec<br/>(path + ends 映射)"]
    end
    subgraph compile["编译:翻译成执行图"]
        NS --> AT["attach_node<br/>→ PregelNode + branch:to:{key}"]
        ED --> AE["attach_edge<br/>→ 源节点追加 ChannelWrite"]
        BS --> AB["attach_branch<br/>→ 源节点追加动态路由 writer"]
    end
    AT --> G["channels + PregelNode + writers"]
    AE --> G
    AB --> G
    G --> R["执行:写 channel → 触发下一批节点<br/>(superstep 循环)"]

三个最值得记住的点:

  1. 两个阶段:add_* 只声明、compile 才构建,中间产物是 StateNodeSpec / BranchSpec / edges 集合。
  2. 边 = 触发:普通边和条件边最终都被翻译成「源节点的 writer 写往目标节点的 branch:to:{target} 通道」,而目标节点的 triggers 订阅了该通道。
  3. 通道类型决定语义:BinaryOperatorAggregate(reducer 合并)、LastValue(覆盖)、EphemeralValue(触发信号)、Topic(Send 队列)——同一个「写 channel」动作,因通道类型不同而语义不同。

掌握这条「声明 → 数据结构 → 触发图」的链路,就能看懂 LangGraph 源码里大部分构建与编译相关的实现。

附录:完整源代码

示例一:graph_builder_internals.py(构建阶段:三个 API 的中间产物)

"""详解 add_node / add_edge / add_conditional_edges 内部做了什么。

关键认知:StateGraph 是「builder」,三个方法只负责「记录声明」,真正「构建执行图」
发生在 compile()。所以产物分两层:

【builder 阶段产物】(调 add_* 时产生,存进 StateGraph 实例)
  - self.nodes         : dict[str, StateNodeSpec]              ← add_node 的产物
  - self.edges         : set[tuple[str, str]]                  ← add_edge(单源) 的产物
  - self.waiting_edges : set[tuple[tuple[str, ...], str]]      ← add_edge(多源) 的产物
  - self.branches      : dict[source][name] = BranchSpec       ← add_conditional_edges 的产物

【compile 阶段产物】(compile() 时由上面四条构建成可执行图)
  - self.nodes         : dict[str, PregelNode]   ← 执行节点(triggers/channels/writers/bound)
  - self.channels      : dict[str, BaseChannel]  ← 含 branch:to:{node} 触发通道
  - self.edges         : 执行边(含条件边展开)

本示例用一个最小 ReAct 图,在 compile 前后分别打印这些中间产物,看清每一步生成了什么。
"""

import operator
from typing import Annotated, Literal, TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]


# ---- 三个普通函数:节点 / 节点 / 路由 ----
def agent_node(state: State) -> dict:
    """节点1:调用模型(此处仅示意,不真的调 LLM)。"""
    return {"messages": [("ai", "agent 思考中...")]}


def tools_node(state: State) -> dict:
    """节点2:执行工具。"""
    return {"messages": [("tool", "工具结果")]}


def route(state: State) -> Literal["tools", "end"]:
    """条件路由:最后一条消息带 tool_calls 就去 tools,否则结束。"""
    return "end"


# ============================================================
# 构建图:调用三个方法,观察它们往 builder 里塞了什么
# ============================================================
builder = StateGraph(State)

# ---------- add_node:包装 runnable + 记录 StateNodeSpec ----------
builder.add_node("agent", agent_node)
builder.add_node("tools", tools_node)

# ---------- add_edge:记录一条固定边 ----------
builder.add_edge(START, "agent")

# ---------- add_conditional_edges:包装路由 + 记录 BranchSpec ----------
builder.add_conditional_edges("agent", route, {"tools": "tools", "end": END})

# ---------- add_edge:工具执行完回到 agent,形成循环 ----------
builder.add_edge("tools", "agent")


print("=" * 70)
print("① builder 阶段产物(compile 之前)")
print("=" * 70)

print("\n[add_node 的产物] builder.nodes:dict[str, StateNodeSpec]")
for name, spec in builder.nodes.items():
    print(f"  {name!r}:")
    print(f"    runnable 类型 = {type(spec.runnable).__name__}")
    print(f"    input_schema = {spec.input_schema.__name__}")
    print(f"    ends(从 Command[Literal] 推断) = {spec.ends}")

print("\n[add_edge 的产物] builder.edges:set[tuple[start, end]]")
print(f"  {builder.edges}")

print("\n[add_edge(多源) 的产物] builder.waiting_edges:set[tuple[tuple, end]]")
print(f"  {builder.waiting_edges}")

print("\n[add_conditional_edges 的产物] builder.branches:dict[source][name]=BranchSpec")
for source, branches in builder.branches.items():
    for name, b in branches.items():
        print(f"  {source!r} -> {name!r}:")
        print(f"    path 类型 = {type(b.path).__name__}")
        print(f"    ends(path_map) = {b.ends}")

# ============================================================
# compile:把上面的声明构建成可执行图
# ============================================================
graph = builder.compile()

print("\n" + "=" * 70)
print("② compile 阶段产物(compile 之后)")
print("=" * 70)

print("\n[结果产物] graph.nodes:dict[str, PregelNode]")
for name, node in graph.nodes.items():
    writers = []
    for w in node.writers:
        if hasattr(w, "writes"):
            # ChannelWriteEntry 有 .channel;ChannelWriteTupleEntry 只有 .mapper
            writers.append([getattr(e, "channel", "<tuple>") for e in w.writes])
    print(f"  {name!r}:")
    print(f"    triggers = {node.triggers}   # 哪些 channel 被写会触发它")
    print(f"    channels = {node.channels}   # 读哪些 channel 作为输入")
    print(f"    writers 写往 = {writers}     # 执行后往哪些 channel 写")

print("\n[结果产物] graph.channels:dict[str, BaseChannel]")
for ch_name, ch in graph.channels.items():
    print(f"  {ch_name!r}: {type(ch).__name__}")

print("\n[结果产物] graph.get_graph().edges:执行边(Edge 结构)")
for e in graph.get_graph().edges:
    print(f"  Edge(source={e.source!r}, target={e.target!r}, conditional={e.conditional})")

print("\n[图结构] mermaid:")
print(graph.get_graph().draw_mermaid())

示例二:compile_internals.py(编译阶段:compile 的 11 个步骤)

"""compile() 内部发生了什么:11 个步骤的逐步拆解。

compile() 把 builder 阶段的声明(StateNodeSpec / edges / waiting_edges / branches)
转成可执行图(PregelNode / channels / writers)。步骤链见 state.py:1177-1401:

  ① ensure_valid_checkpointer    处理 checkpointer 参数(None/False/实例)
  ② serde allowlist              strict-msgpack 下构建序列化白名单(可选)
  ③ validate                     验证图结构(节点名 / 边端点存在性)
  ④ 准备 output/stream channels  确定输出通道(__root__ 或状态字段列表)
  ⑤ 应用 builder defaults        默认 error_handler/retry/cache/timeout 下放给 spec
  ⑥ node_error_handler_map       生成 {节点名: error_handler 节点名}
  ⑦ 构造 CompiledStateGraph      nodes={} 空、channels={状态字段 + managed + START}
  ⑧ attach_node × N             每个节点 → branch:to:{key} 通道 + PregelNode
  ⑨ attach_edge × N             普通边 → 给【源节点】追加 writer(写 branch:to:{目标})
  ⑩ attach_branch × N           条件边 → 给【源节点】追加 branch writer(动态路由)
  ⑪ validate                     最终验证,返回编译好的图

核心衔接:⑧⑨⑩ 都往「源节点的 writers 列表」里追加 writer,
每个 writer 都写往【目标节点的 branch:to:{target}】通道,从而把边变成「触发」。

本示例构建一个 ReAct 图,打印 compile 前后每个步骤的产物,看 writer 如何一步步累积。
"""

import operator
from typing import Annotated, Literal, TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]


def agent_node(state: State) -> dict:
    return {"messages": [("ai", "思考中...")]}


def tools_node(state: State) -> dict:
    return {"messages": [("tool", "工具结果")]}


def route(state: State) -> Literal["tools", "end"]:
    return "end"


# ---- builder 阶段:声明(前一个示例已详解,这里快速带过)----
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tools_node)
builder.add_edge(START, "agent")                                  # → self.edges
builder.add_conditional_edges("agent", route, {"tools": "tools", "end": END})  # → self.branches
builder.add_edge("tools", "agent")                                # → self.edges


print("=" * 72)
print("步骤 ⑦ 之前:builder 阶段的输入")
print("=" * 72)
print(f"  nodes    = {list(builder.nodes)}")
print(f"  edges    = {builder.edges}")
print(f"  branches = { {s: list(ns) for s, ns in builder.branches.items()} }")


# ---- compile:一次性跑完 ①~⑪ ----
graph = builder.compile()


print("\n" + "=" * 72)
print("步骤 ⑦⑧ 的产物:channels + PregelNode")
print("=" * 72)

print("\n⑦ 构造 CompiledStateGraph 时初始化的 channels:")
print("  (状态字段 + managed + START 的 EphemeralValue)")
for ch, val in graph.channels.items():
    tag = "  ← ⑦ 初始就有" if ch in ("messages", "__pregel_tasks", "__start__") else "  ← ⑧ attach_node 新建"
    print(f"    {ch!r}: {type(val).__name__}{tag}")

print("\n⑧ attach_node 给每个节点构建 PregelNode:")
print("  (triggers = [branch:to:{key}],writers = [状态更新, Command 路由])")
for name, node in graph.nodes.items():
    writers = []
    for w in node.writers:
        if hasattr(w, "writes"):
            writers.append([getattr(e, "channel", "<tuple>") for e in w.writes])
        else:
            writers.append(f"<branch:{type(w).__name__}>")
    print(f"    {name!r}:")
    print(f"      triggers = {node.triggers}")
    print(f"      writers  = {writers}")


print("\n" + "=" * 72)
print("步骤 ⑨⑩ 的产物:普通边 / 条件边如何「追加」到源节点 writers")
print("=" * 72)
print("""
⑨ attach_edge(START, 'agent')   → 给 __start__.writers 追加 ChannelWrite(branch:to:agent)
⑨ attach_edge('tools', 'agent') → 给 tools.writers     追加 ChannelWrite(branch:to:agent)
⑩ attach_branch('agent', route) → 给 agent.writers     追加 branch.run(...)(动态路由 writer)

所以最终:
  __start__ 触发时,往 branch:to:agent 写 → 触发 agent
  agent 的条件分支 writer 往 branch:to:tools 或 branch:to:__end__ 写 → 路由
  tools 执行完,往 branch:to:agent 写 → 回到 agent(循环)
""")


print("=" * 72)
print("步骤 ⑪ 的产物:最终验证后的执行边(Edge 结构)")
print("=" * 72)
for e in graph.get_graph().edges:
    print(f"  Edge(source={e.source!r}, target={e.target!r}, conditional={e.conditional})")

print("\n图结构 mermaid:")
print(graph.get_graph().draw_mermaid())

示例三:compiled_graph.py(编译后的计算图内部结构)

"""展示 graph.compile() 之后,Pregel 计算图的内部结构。

用接近真实 agent 的复杂 State,覆盖:
- LastValue channel(普通字段,覆盖写)
- BinaryOperatorAggregate channel(reducer:追加 / 累加)
- 边 → branch:to:xxx 信号 channel
- 条件边 → ends 映射 + _control_branch 动态写
- 并行分支 → 一个 superstep 内唤醒多个节点
"""

from operator import add as add_reducer
from typing import Annotated, TypedDict

from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages


class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]  # 累积
    plan: str                                              # 覆盖
    token_count: Annotated[int, add_reducer]               # 累加
    weather: str                                           # 覆盖
    news: str                                              # 覆盖


def planner(state: AgentState) -> dict:
    return {"plan": "并行查询天气和新闻", "token_count": 100}


def call_weather(state: AgentState) -> dict:
    return {"weather": "sunny", "token_count": 50}


def call_news(state: AgentState) -> dict:
    return {"news": "market up", "token_count": 50}


def summarize(state: AgentState) -> dict:
    return {"messages": [("ai", f"{state['weather']} / {state['news']}")]}


def route(state: AgentState) -> list[str]:
    """返回列表 → 同一 superstep 内并行触发多个分支。"""
    return ["weather", "news"]


g = StateGraph(AgentState)
g.add_node("planner", planner)
g.add_node("weather", call_weather)
g.add_node("news", call_news)
g.add_node("summarize", summarize)

g.add_edge(START, "planner")
g.add_conditional_edges(
    "planner", route, {"weather": "weather", "news": "news"}
)
g.add_edge("weather", "summarize")
g.add_edge("news", "summarize")
g.add_edge("summarize", END)

app = g.compile()

BAR = "=" * 72

# ============ 1. channels ============
print(BAR)
print("1. channels(状态通道)")
print(BAR)
for name, ch in app.channels.items():
    detail = ""
    if hasattr(ch, "reducer"):
        # 看 reducer 是哪个函数
        fn = getattr(ch.reducer, "func", ch.reducer)
        detail = f"  reducer={getattr(fn, '__name__', fn)}"
    print(f"    {name:<22} {type(ch).__name__:<26}{detail}")

# ============ 2. trigger_to_nodes ============
print("\n" + BAR)
print("2. trigger_to_nodes(channel → 订阅它的节点)")
print(BAR)
for trigger, nodes in app.trigger_to_nodes.items():
    print(f"    {trigger:<22} -> {list(nodes)}")

# ============ 3. 条件边的定义(builder 上的 BranchSpec)============
print("\n" + BAR)
print("3. 条件边:builder.branches 里的 BranchSpec")
print(BAR)
for src, branches in g.branches.items():
    for name, spec in branches.items():
        print(f"    source={src}  name={name}")
        print(f"      ends(path 返回值 → 目标节点): {spec.ends}")

# ============ 4. 节点详情 ============
print("\n" + BAR)
print("4. 节点详情(订阅 / 读 / 写)")
print(BAR)
for name, node in app.nodes.items():
    static_writes = []      # 编译时就能确定的 channel(静态边)
    dynamic_mappers = []    # 运行时才能确定(state 更新 / Command / Send)
    for w in node.writers:
        for entry in getattr(w, "writes", ()):
            if ch := getattr(entry, "channel", None):
                static_writes.append(ch)
            elif mapper := getattr(entry, "mapper", None):
                dynamic_mappers.append(getattr(mapper, "__name__", str(mapper)))
    # 条件边:编译时被 attach_branch 包成一层 Runnable,写入目标由 path 运行时决定
    has_branch = name in g.branches
    print(f"\n    node [{name}]")
    print(f"      订阅 triggers     = {node.triggers}")
    print(f"      读取 channels     = {node.channels}")
    print(f"      静态边写入        = {static_writes}")
    print(f"      动态写入 mapper   = {dynamic_mappers}")
    if has_branch:
        print(f"      条件边            = path() 返回值经 ends 映射决定")

# ============ 5. 运行时:reducer 的实际效果 ============
print("\n" + BAR)
print("5. 运行时:看 reducer 怎么合并并行写入")
print(BAR)
result = app.invoke({"messages": [("user", "查询")], "token_count": 1})
print(f"    输入 token_count=1")
print(f"    三个节点各写 token_count: planner=100, weather=50, news=50")
print(f"    最终 token_count = {result['token_count']}   ← 1+100+50+50(累加)")
print(f"    最终 messages 条数 = {len(result['messages'])}   ← 追加而来")
print(f"    最终 {result['messages'][-1].content!r}")

# ============ 6. 执行顺序(superstep)============
print("\n" + BAR)
print("6. 执行顺序:superstep 边界")
print(BAR)
for chunk in app.stream(
    {"messages": [("user", "查询")], "token_count": 0}, stream_mode="updates"
):
    for node, update in chunk.items():
        print(f"    superstep 内节点 [{node}] 更新: {list(update.keys())}")