本节在全册位置:风格选择。声明式是用 Builder API 显式 add 节点与边,结构一眼可见;编程式在节点内部用 Command 动态决定流向,图随运行数据生长。两者不是互斥,常混合。我们给的判断:结构已知就用声明式,流向由数据决定才用编程式。
声明式优点是可静态校验、易读、易可视化;缺点是结构在编译期固定。编程式(Command/动态边)适合流向依赖运行时内容的场景,如按模型输出跳到任意节点;缺点是可视化只能看到「可能」的边。选择原则:结构已知用声明式,流向数据驱动用编程式。下表对比。我们见过把整张图都写成动态路由的团队,结果完全无法静态调试。
# 声明式:结构固定,编译期可见 from langgraph.graph import StateGraph, START, END from typing import TypedDict class S(TypedDict): v: int def a(s): return {"v": 1} b = StateGraph(S); b.add_node("a", a) b.add_edge(START, "a"); b.add_edge("a", END) # 编程式:节点内用 Command 决定下一步 from langgraph.graph import Command def dyn(s): target = "a" if s["v"] < 3 else END return Command(goto=target, update={"v": s["v"] + 1})
# 混合:声明式骨架 + 个别编程式节点 b.add_node("dyn", dyn) b.add_edge(START, "dyn") # 这是生产里最常见的折中形态
背景:根据用户一句话跳到不同处理节点,节点集很大,写几十条条件边不现实。
操作:用编程式,在入口节点里解析意图并返回目标节点名。
结果:免写几十个条件边,结构随意图生长,新增节点不改边。
解读:动态路由牺牲一点可静态可视化性,换来管理大量分支的简洁,取舍要清楚。
变式:折中:把意图聚类成 5 类,声明式条件边即可,避免过度动态导致调试黑盒。

声明式结构固定、可读、可校验;编程式流向由运行期数据决定,两者不互斥常混用。
结构已知用声明式,流向数据驱动才用编程式,不要为了灵活把整张图写成动态路由。
混合形态最常见:声明式骨架加个别编程式节点,兼顾可读与灵活,调试成本可控。
为了灵活把整张图都写成 Command 动态路由,结果完全无法静态调试;只在流向确实由数据决定时才用编程式,否则优先声明式。
声明式的优势在编译期就能画出完整结构;编程式的边在运行期才生成,静态图只能看到「可能」。
# 声明式:编译后即可出完整 mermaid b = StateGraph(S); b.add_node("a", a); b.add_node("b", b_) b.add_edge(START, "a"); b.add_conditional_edges("a", route, ["b", END]) print(b.compile().get_graph().draw_mermaid()) # 编程式:动态 goto 在节点内决定,静态图看不到 def dyn(s): return Command(goto=pick(s)) # pick 运行期才算 # draw_mermaid 只能画出「可能经过的节点」,不能保证覆盖
| 维度 | 声明式 | 编程式 |
|---|---|---|
| 可静态校验 | 强 | 弱 |
| 可视化完整度 | 高 | 低 |
| 灵活度 | 中 | 高 |
⚠️ 常见坑:把整张图都写成 Command 动态路由,静态图一片空白,排障只能靠跑;只在流向确实由数据决定处用编程式。
💡 关键直觉:可视化能力是调试成本的直接映射;能用声明式表达的结构,就别为了「优雅」换成动态路由。
编程式的核心是 Command,把它的参数逐一说清,用起来才不会绕:
| 参数 | 作用 | 说明 |
|---|---|---|
| goto | 下一步去向 | 节点名或 END,覆盖普通边 |
| update | 状态增量 | 与节点普通返回等价,归约合并 |
| resume | 恢复中断 | 用于 interrupt 之后继续(第四章) |
goto 是「覆盖」而非「建议」:一旦节点返回 Command(goto=...),执行器不再参考该节点的普通出边。这既是灵活也是风险——想在两条路径间做细粒度切换时很好用,但也意味着图的可视化无法预知去向。如果发现你的 Command 里 goto 指向的节点数量超过两三个,并且每次由数据决定,这通常是从编程式重构成声明式(或折中)的信号。
编程式是手段不是目的,出现下面任何一种情况,就该停下来考虑重构:
重构方向不是全盘重写,而是「把动态点收敛」:把分散在各节点的 goto 判断收成入口处的一个路由函数,用声明式条件边表达;节点内部只保留真正数据驱动的极少数跳转。收敛之后,图的可读性恢复,动态能力仍留在最关键的地方,调试成本回到可控区间。
生产里最常见的折中是「声明式骨架 + 一两个编程式节点」,用一个具体例子说明怎么拆:客服图的主干是固定的「分流 -> 处理 -> 回执」,只有「按用户意图跳转」这一步需要动态。
# 主干:声明式 b.add_node("gate", gate) # 意图解析,输出 target b.add_node("handle", handle) b.add_node("receipt", receipt) b.add_edge(START, "gate") b.add_conditional_edges("gate", route_by_target, {"order": "handle", "refund": "handle", "other": "receipt"}) # 处理节点内部:只有处理中需要再次跳转的少数情况用 Command # 关键:动态范围被压缩到一个节点,静态结构仍然可读
拆分原则一句话:把「图往哪走」的决策尽量提到条件边,把「一个节点内部怎么走」的动态留到 Command。前者可视化、可测试、可审计,后者负责灵活性。两者边界清楚了,图式编排的可维护性才真正落地。