1. 子图如何动态跳回父图?
前面学习子图时,我们主要关注:
text
父图调用子图
子图执行
结果返回父图
但 LangGraph 还支持另一种更动态的方式:
子图可以直接把执行流路由到父图中的某个节点。
核心就是:
python
Command(
goto="node_b",
graph=Command.PARENT
)
例如子图节点:
python
def sub_node(state):
return Command(
update={
"data": state["data"] + " → 子图"
},
goto="node_b",
graph=Command.PARENT
)
这里:
text
update
=
更新状态
goto
=
指定接下来执行父图中的哪个节点
graph=Command.PARENT
=
告诉 LangGraph:
goto 指向的不是当前子图,而是父图
于是执行路径可以变成:
text
父图 START
↓
进入子图
↓
sub_node
↓
Command.PARENT
↓
父图 node_b
↓
END
这和普通子图调用最大的区别是:
text
普通子图
=
执行完以后按照父图原本的边继续
动态路由
=
子图自己决定回到父图哪个节点
有一个细节需要注意。
子图本身并不知道父图有哪些节点,因此返回类型不能简单写成:
python
Command[Literal["node_b"]]
否则编译子图时可能认为 node_b 是一个不存在的子图节点。
另外,如果希望渲染出来的图能够正确显示这条动态路径,可以在添加子图节点时声明:
python
builder.add_node(
"sub_graph",
sub_graph,
destinations=("node_b",)
)
destinations 主要影响拓扑渲染,不会改变真正的动态路由行为。
2. Routing 不应该让 LLM 直接返回节点名
Routing 的基本结构我们已经学习过:
text
输入
↓
路由判断
↓
选择不同业务流程
但是工程中更推荐把它拆成三层:
text
LLM 节点
负责语义判断
↓
结构化结果
↓
路由函数
把判断结果映射成节点
↓
业务节点
真正执行任务
例如先定义结构化输出:
python
class Route(BaseModel):
step: Literal[
"poem",
"story",
"joke"
]
然后让 LLM 只负责回答:
text
story
joke
poem
而不是直接返回:
text
model_call_1
model_call_2
model_call_3
之后再由普通 Python 函数映射:
python
def route_decision(state):
if state["decision"] == "story":
return "model_call_1"
if state["decision"] == "joke":
return "model_call_2"
if state["decision"] == "poem":
return "model_call_3"
return END
这样做的核心价值是:
text
LLM
只理解业务语义
图结构
由程序控制
如果直接让模型返回节点名:
text
LLM 输出
和
LangGraph 图结构
就会耦合得很紧。
后面一旦改节点名称,Prompt 也可能需要一起改,而且类型检查、异常兜底都会更麻烦。
所以 Routing 最好记成:
text
语义判断和图路由分离
3. Parallelization 和 Orchestrator-worker 到底差在哪?
这两个模式看起来都像:
text
一个任务
↓
拆成多个并行任务
↓
汇总
但最大的区别是:
text
Parallelization
任务数量在编译图时已经确定
Orchestrator-worker
任务数量在运行时才确定
例如固定并行:
text
START
├── 查天气
├── 查新闻
└── 查汇率
↓
汇总
无论用户问什么,都是三个固定节点。
这就是:
text
Parallelization
而 Orchestrator-worker 可能是:
text
用户:
帮我写一份关于 LangGraph 的研究报告
↓
Orchestrator 分析任务
↓
动态生成:
章节1
章节2
章节3
章节4
章节5
↓
动态创建 Worker
↓
并行生成
↓
Synthesizer 汇总
下次换一个问题,可能只需要两个 Worker,也可能需要八个。
所以它的本质就是我们前面学习过的:
text
Map-Reduce
对应关系:
text
Orchestrator
=
生成 Map 任务
Send
=
动态分发
Worker
=
执行 Map
Reducer
=
合并中间结果
Synthesizer
=
执行 Reduce
因此:
text
任务数量固定
→ Parallelization
任务数量动态
→ Orchestrator-worker
4. Evaluator-optimizer 为什么一定要限制循环?
Evaluator-optimizer 的流程是:
text
Generator
↓
Evaluator
↓
合格?
├── 是 → END
└── 否
↓
带反馈重新生成
↓
Evaluator
例如:
text
生成代码
↓
执行测试
↓
测试失败
↓
反馈错误
↓
重新生成代码
或者:
text
生成文案
↓
合规检查
↓
不合格
↓
修改
↓
再次检查
这里的问题很明显:
如果一直不合格怎么办?
流程就可能一直循环。
因此这种模式必须有退出控制。
资料中把方法分为两类:
text
主动退出
=
在达到某个条件前主动结束
被动退出
=
触发递归限制后异常终止
这和前面学过的循环防护正好对应。
所以 Evaluator-optimizer 的完整设计不应该只有:
text
Generator + Evaluator
还应该包含:
text
退出条件
可以把它理解为:
text
生成能力
+
质量标准
+
反馈机制
+
循环边界
缺少最后一个,工作流就不完整。
5. Workflow 和 Agent 应该怎么组合?
资料最后把 Agent 和前面的设计模式做了一个很重要的区分。
前面的:
text
Prompt Chaining
Parallelization
Routing
Orchestrator-worker
Evaluator-optimizer
本质上都属于:
text
Workflow
也就是:
开发者提前决定图大体上怎么走。
而 Agent 不一样:
text
LLM 根据当前 messages
以及工具执行结果
动态决定下一步
典型结构:
text
用户输入
↓
LLM
↓
是否调用工具?
├── 否 → END
│
└── 是
↓
ToolNode
↓
ToolMessage
↓
LLM
这就是 ReAct 循环。
但这并不意味着:
text
Agent 比 Workflow 高级
所以所有流程都应该交给 Agent
这些模式解决的是不同问题。
例如可以组合成:
text
用户请求
↓
Routing
↓
判断任务类型
↓
┌───────────────┐
│ │
固定业务流程 开放式复杂任务
│ │
Workflow Agent
│ │
└───────┬───────┘
↓
ToolNode
↓
Evaluator
↓
END
如果任务流程本身非常明确:
text
A
↓
B
↓
C
就没必要让 LLM 每一步重新思考。
而当:
text
需要哪些工具未知
调用顺序未知
任务步骤无法提前确定
Agent 才更合适。
所以可以记成:
text
确定性强的部分
→ Workflow
需要自主决策的部分
→ Agent