节点和 State 搞定之后,下一关是控制图的流转

LangGraph 提供了三种层次的编排能力:边(Edge) 做常规路由,Send 做动态并行分发,Command 做「改状态 + 强制跳转」的特种操作。

日常开发里,90% 的路由用条件边就够了;但剩下 10% 的边界场景——工具强制收尾、中断恢复、子图交还父图——Command 是绕不开的。


边的四种形式

边决定 Node 之间的连接方式,分两类 API:

类型 API 说明
普通边 add_edge("A", "B") 固定从 A 到 B
普通入口 add_edge(START, "A") 固定从 START 到 A
条件边 add_conditional_edges(...) 根据函数返回值选路
条件入口 add_conditional_edges(START, router, {...}) 根据条件决定从哪开始

条件判断函数可以返回节点名,也可以只返回 True/False,然后在 add_conditional_edges 里映射:

1
2
3
4
5
graph.add_conditional_edges(
"node_a",
router_function,
{True: "node_1", False: "node_2"}
)

选路口诀

  • 只改 state → 普通 return
  • 只选路 → 条件边
  • 改完 state 还要立刻改道 → Command

Send:动态并行分发

前面的边都是「固定下一个节点是谁」。但有些场景任务数量和目标在运行时才确定——比如 Supervisor 拆出了 3 个故障主题,要分别派 3 个 RCA Agent 并行诊断。

这时候用 Send

1
2
def continue_to_jokes(state: OverallState):
return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]

Send 的本质是运行时动态创建边:有几个 subject 就派几个并行节点。

注意:分发时要带齐字段。Send 像共享数据——分发时不带的数据,子节点没法从父图里补拿。

配合父图的 Reducer(上一篇讲过),多个 Send 的结果会自动汇总到父 State 的列表字段里。这就是 LangGraph 的 Map-Reduce 模式。


Command:改状态 + 强制跳转

Command 是 LangGraph 里最容易被误解的 API。它有四个原语:

原语 作用
update 更新 State(类似 return)
goto 跳转到指定节点
graph 指定跳转发生在哪张图(当前图 or 父图)
resume 恢复被 interrupt 暂停的执行

Command 只有 update 时,是不是多此一举?

是的。只有 update 时 Command 等价于普通 return,日常直接 return 即可。Command 的价值在于 update + goto 捆在一起——改完状态立刻改道,这是 return + add_edge 拆不开的。

从「原来的理解」到「学会的理解」

几个典型的认知升级:

Command vs 条件边

原来的理解:Command 就是另一种写路由的方式,和 add_edge / 条件边差不多,很多场景可以互换。

学会的理解:日常选路用条件边最清楚;Command 是「改状态 + 强制跳转」捆在一起的特种工具。普通节点确实能拆成 return + add_edge,但工具在循环里要强制收尾时拆不开——工具 return 后默认还会回到 Agent,必须在工具返回值里直接 goto 收尾节点。

Command 的跳转范围

原来的理解:只要用了 Command,不管函数在不在图里都能跳;多个图时想从 tool 直接跳到子图内部的某个节点。

学会的理解:Command 只作用在当前正在执行的那张图。graph 只有当前图和 Command.PARENT,没有跳进子图内部的入口。从外面只能 goto 整张子图的父节点,子图从 START 重头跑。

interrupt 和 resume

原来的理解:Command(resume=…) 自己就能把执行接上,搞不清怎么知道「重续哪个」。

学会的理解:resume 只管塞回什么值;重续哪一次靠 config 里的 thread_id + checkpointer 找到同一次暂停。interrupt 写在节点里,invoke 跑到才停;第二次必须用同一个 config 再 invoke(Command(resume=…))。

什么时候需要 Command?

场景一:工具强制收尾

Agent 循环里,LLM 调完工具后默认还会回到 Agent 节点继续推理。但有些工具意味着「调查结束」——比如提交最终诊断报告,应该直接去 format-output,而不是再回 Agent 开会。

1
2
3
4
5
def submit_final_diagnosis(...) -> Command[Literal["format-output"]]:
return Command(
update={"diagnosis": result},
goto="format-output"
)

这就是「强制收尾」:不只记账(update),还要立刻改道离开循环(goto)

场景二:子图交还父图

Command 只作用在当前正在执行的那张图graph 合法值基本是 None(当前图)和 Command.PARENT

不能从工具节点一枪打进子图内部的某个中途节点——从外面只能 goto 整张子图的父节点,子图从 START 重头跑。

场景三:interrupt 恢复

节点里写了 interrupt(),第一次 invoke 跑到这里会暂停。第二次必须用同一个 config(thread_id) 再 invoke:

1
graph.invoke(Command(resume="人工输入的内容"), config={"configurable": {"thread_id": "xxx"}})

resume 只管塞回什么值;重续哪一次靠 thread_id + checkpointer 找到同一条暂停记录。config 不是触发中断的开关——interrupt 写在节点函数里,invoke 跑到才会停。

Command 的坑

1. 不要和静态边混用

Command 的 goto 是追加动态边,静态 add_edge 仍会执行——同一节点两条出口可能都跑,副作用翻倍、State 乱。

每个节点要么 Command 出边,要么 add_edge/条件边,不要混用

2. 返回类型要标注

用 Command 时函数要声明返回类型,图才能正确推断跳转目标:

1
2
def my_node(...) -> Command[Literal["format-output", "other"]]:
...

3. invoke 入参里 Command 几乎只能 resume

多轮接着聊用普通 dict,别用 Command(update=...)

4. 改图要当心进行中的会话

有 checkpointer 的活 thread 时:已结束的随便改;中断中的别删/改名关键节点;state 加减字段一般 OK,改名会丢旧值。


Runtime Context:全局配置面板

Runtime Context 是一个所有节点都能读的配置面板,通过 invoke 传入:

1
2
3
4
graph.invoke(inputs, context={"llm_provider": "anthropic"})

def node_a(state: State, runtime: Runtime[ContextSchema]):
llm = get_llm(runtime.context.llm_provider)

适合放 LLM 提供商、环境标识、用户权限等不属于 State 但需要全局访问的配置。


Recursion Limit:步数限制

Agent 循环最怕无限跑。LangGraph 用 recursion_limit 控制最大步数:

1
graph.invoke(inputs, config={"recursion_limit": 5})

config 分层记忆

  • 顶层recursion_limit / tags / callbacks)→ 框架的
  • configurablethread_id / 业务参数)→ 你的

撞限前的两条路

1. 主动收尾(推荐)

条件边判断 remaining_steps <= 2 → 转收尾节点 → 正常 END。checkpoint 保住,部分结果能拿。

State 里声明 remaining_steps: RemainingSteps,框架自动填充剩余步数。当前步数在 config["metadata"]["langgraph_step"] 里。

2. 被动兜底

图外 try/except GraphRecursionError——简单,但执行被腰斩,只当保险。


子图:模块化你的 Agent

复杂 Agent 不要把所有节点堆在一张图里。LangGraph 支持子图(Subgraph)

  • 子图有自己的 State 和节点
  • 父图把子图当作一个节点挂载
  • 典型用法:triage_agent_graph 作为子图挂到 parent_graph

子图的好处是职责隔离——Triage 只管分类,RCA 只管诊断,Supervisor 只管调度。

以 SRE Agent 为例,一张典型的多 Agent 图长这样:

Triage 子图从 Prometheus / Jaeger / K8s 拉原始遥测,交给 LLM 聚合统一结构;Supervisor 根据故障主题 Send 并行分发;所有 RCA Worker 返回后 Reducer 汇总,最后 format 输出报告。

Persistence(持久化) 在这个阶段变得重要:compile(checkpointer=...) 让每次执行状态都被保存,调试时可以回溯、中断时可以恢复。config 里的 thread_id 标识同一次会话。


常见坑汇总

# 解法
1 父图汇总子图结果时列表被覆盖 给列表字段加 Reducer
2 Command 返回没标类型,框架推断跳转出错 -> Command[Literal["node_a", "node_b"]]
3 invoke 入参误用 Command(update=...) 多轮对话用普通 dict,Command 只用于 resume
4 Command goto 和静态 add_edge 混用 每个节点只选一种出边方式
5 有活 thread 时删/改名节点 已结束的随便改,中断中的别动关键节点

小结

图编排的三层能力:

能力 适用场景
条件边 日常路由,最清晰
Send 运行时动态并行分发(Map-Reduce)
Command 工具强制收尾、interrupt 恢复、子图交还父图

记住总口诀:只改 state → return;只选路 → 条件边;改完还要立刻改道 → Command。