add_node / add_edge / add_conditional_edges:往蓝图里填内容
职责
StateGraph 提供三个核心注册方法:add_node 把一个函数 / Runnable 包成节点 (add_node:662),add_edge 加一条确定的边 (add_edge:915),add_conditional_edges 加一个分支条件 (add_conditional_edges:969)。这三个方法都返回 Self,所以可以链式调:builder.add_node("a", a).add_node("b", b).add_edge("a", "b").add_conditional_edges("b", route)。它们都只往 builder.nodes / builder.edges / builder.branches 这几个容器里追加,真正的翻译成 PregelNode + 通道订阅要等 compile() 时由 attach_node / attach_edge / attach_branch 完成。
它们的位置在用户 API 层最外面:你写图时的每一行几乎都在调这三个方法。add_node 是最复杂的——它会从函数签名推 input_schema、从返回类型 Literal[...] 推 destinations、把 error_handler 包成一个独立的 __error_handler__{node} 节点 (error handler 注入:856-870)。add_edge 比较直接,要么进 edges set,要么进 waiting_edges set (多入边,要等所有起点都完成)。add_conditional_edges 最薄,只把 path 包成 Runnable,再交给 BranchSpec.from_path(from_path:89)算出 ends 字典存进 branches[source]。
设计动机
为什么 add_node 要从函数签名推这么多东西?
- 少写样板:不显式传
input_schema时,从第一个参数的类型注解推 (inferred input schema:815-825),让def my_node(state: MyState): ...自动用MyState;返回类型是Literal["a", "b", "__end__"]时自动当作 destinations (Literal destinations:840-846),不用写destinations=...。 - 节点名可省:
node参数既可以是字符串也可以是函数本身,函数对象会取__name__当节点名 (node 名推断:768-773)——Runnable则用get_name()。 error_handler是独立节点(error_handler 节点:857-870)——它不是 spec 的字段,而是生成一个名为__error_handler__{node}的常规节点,再在error_handler_node字段里记下指针。这样 handler 自己也是 Pregel 节点,享受 retry / metadata / tracing 等所有节点能力。- 多入边语义统一:
add_edge(["a", "b"], "c")不是「a 跑完跑 c,b 跑完也跑 c」,而是「a 和 b 都跑完才跑 c」(multi-start 语义:917-921),存进waiting_edgesset 等编译时翻译成NamedBarrierValue通道。 - 条件边的返回值可省 path_map:不传
path_map时,从path函数的返回类型Literal["a", "b"]推 (Literal 推 path_map:103-115),让def route(state) -> Literal["a", "b"]: ...自动工作;都没给时图可视化会假设它可能跳到任意节点 (warning:994-997)。 - 同节点同名的 branch 报错(
branch 重名:1009-1012)——一个节点可以有多个 conditional edge,但每个的 condition name 必须唯一,因为branches[source]是 dict by name。
关键文件
add_node 签名:662-676— 接收node/action/defer/metadata/input_schema/retry_policy/cache_policy/error_handler/destinations/timeout。node 名推断:768-790— 字符串就用字符串,函数用__name__,Runnable用get_name();保留字START/END/NS_SEP/NS_END会报错。签名类型推断:803-848— 从__call__的 type hints 推inferred_input_schema和返回类型里的Command[Literal[...]]destinations。error_handler 注入:856-870— 生成__error_handler__{node}独立节点存进self.nodes,字段is_error_handler=True。存 StateNodeSpec:872-907— 按input_schema/inferred_input_schema/self.state_schema三档把 spec 存进self.nodes。add_edge:915-967— 单起点进edgesset,多起点进waiting_edgesset,起点不能是END、终点不能是START。add_conditional_edges:969-1017— 把path包成 Runnable,BranchSpec.from_path算ends,存进self.branches[source][name]。BranchSpec.from_path:83-120— 处理path_map三种形态:dict/list/ 都没给就从返回类型Literal推;另外推input_schema。add_sequence:1019-1044— 一次性注册一串节点(语法糖,内部还是循环add_node)。attach_edge:1537-1561— 编译期:单入边向起点节点的 writers 加一条ChannelWrite指向终点通道;多入边注册一个NamedBarrierValue通道当 join。attach_branch:1563-1596— 编译期:把BranchSpec翻成ChannelWrite.register_writer,根据ends决定写到哪个目标通道。
数据流
add_node 真正干活的就这两段——推断 input schema 和返回类型里的 destinations,然后把 spec 存进 self.nodes:
if (
isfunction(action)
or ismethod(action)
or ismethod(getattr(action, "__call__", None))
) and (
hints := get_type_hints(getattr(action, "__call__"))
or get_type_hints(action)
):
if input_schema is None:
first_parameter_name = next(
iter(inspect.signature(cast(FunctionType, action)).parameters.keys())
)
if input_hint := hints.get(first_parameter_name):
if isinstance(input_hint, type) and get_type_hints(input_hint):
inferred_input_schema = input_hint
if rtn := hints.get("return"):
rtn_origin = get_origin(rtn)
if rtn_origin is Union:
rtn_args = get_args(rtn)
for arg in rtn_args:
arg_origin = get_origin(arg)
if arg_origin is Command:
rtn = arg
rtn_origin = arg_origin
break
if (
rtn_origin is Command
and (rargs := get_args(rtn))
and get_origin(rargs[0]) is Literal
and (vals := get_args(rargs[0]))
):
ends = vals最后按推断结果三档存进 self.nodes 字典——input_schema 显式 / inferred_input_schema 推出 / fallback 到 self.state_schema:
if input_schema is not None:
self.nodes[node] = StateNodeSpec[NodeInputT, ContextT](
coerce_to_runnable(action, name=node, trace=False),
metadata,
input_schema=input_schema,
retry_policy=retry_policy,
cache_policy=cache_policy,
error_handler_node=handler_node_name,
ends=ends,
defer=defer,
timeout=timeout,
)add_conditional_edges 倒是非常薄——核心是把 path 包成 Runnable,再让 BranchSpec.from_path 算 ends 字典:
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
if name in self.branches[source]:
raise ValueError(
f"Branch with name `{path.name}` already exists for node `{source}`"
)
self.branches[source][name] = BranchSpec.from_path(path, path_map, True)
if schema := self.branches[source][name].input_schema:
self._add_schema(schema)
return self(add_conditional_edges 实现:1005-1017)
边界与失败
- 节点名重复直接报错(
重名:792-793)——Node \` already present`,不允许覆盖。 - 节点名保留字(
reserved:794-801)——START/END/NS_SEP(|) /NS_END(:) 都不能用,后两个会破坏checkpoint_ns的拼接。 add_edge起点是END报错(END 不能当起点:939-940),同理START不能当终点(START 不能当终点:941-942)——这两个常量是图入口出口,不能被反转。- 多入边要求所有起点先
add_node(多入边校验:956-964)——不然编译时attach_edge找不到起点的writers。 - 同节点同名 branch 报错(
branch 重名:1009-1012),但不同名的多个 branch 可以共存——这是 conditional + deterministic 混合路由的基础。 add_node在已编译图上只 warning 不报错(compiled warning:778-782),改动不会反映到已编译实例——典型顺序坑。
小结
add_node / add_edge / add_conditional_edges 是 StateGraph 的写入接口,本身不复杂,复杂的是从函数签名推 schema 和 destinations 的「魔法」。理解了这三个方法,基本就读完了 StateGraph 的所有 mutable API;下一步是 compile 怎么把这些 spec 翻译成 PregelNode,运行模型见 Pregel 引擎。
对照官方资料:LangGraph 文档 · README。