LangChainRust Agent 引擎:Graph 构建到执行
项目地址:github.com/atliliw/lan... 框架源码:github.com/atliliw/lan... --- 基于
crates/langchainrust/src/langgraph/源码逐层拆解
一、整体架构:两阶段设计
css
┌──────────────────────────────────────────────────────────────┐
│ 阶段1: 构建 (StateGraph) → 阶段2: 执行 (CompiledGraph) │
│ │
│ add_node() / add_edge() invoke() → GraphInvocation │
│ add_conditional_edges() stream() → Vec<StreamEvent> │
│ add_fan_out() / add_fan_in() invoke_parallel() → ... │
│ │
│ compile() → CompiledGraph resume() → 继续执行 │
└──────────────────────────────────────────────────────────────┘
设计意图:构建期只做"描述图",不执行任何逻辑。编译期做验证,通过后才能执行。
二、StateGraph 构建(graph.rs)
2.1 核心数据结构
StateGraph<S> 内部维护 5 个数据结构:
swift
pub struct StateGraph<S: StateSchema> {
nodes: HashMap<String, Arc<dyn GraphNode<S>>>, // 节点名 → 节点实现
edges: Vec<GraphEdge>, // 所有边(有序)
entry_point: Option<String>, // 入口节点名
reducers: HashMap<String, Arc<dyn Reducer<S>>>, // 字段级 Reducer
default_reducer: Arc<dyn Reducer<S>>, // 默认 Reducer (ReplaceReducer)
conditional_routers: HashMap<String, Arc<dyn ConditionalEdge<S>>>, // 路由函数
}
2.2 添加节点的三种方式
| 方法 | 签名 | 内部实现 |
|---|---|---|
add_node_fn |
Fn(&S) -> Result<StateUpdate<S>> |
包成 SyncNode,不支持 async |
add_async_node |
AsyncFn<S> trait,即 Fn(&S) -> Future<...> |
包成 AsyncNode,在 .await 中执行 |
add_node |
自定义 GraphNode 实现 |
直接存 Arc,最灵活 |
关键区别:SyncNode 的 execute() 把同步函数包在 async 块里返回,而 AsyncNode 直接 await 异步函数。
2.3 添加边的五种方式
| 方法 | 效果 | 边类型 |
|---|---|---|
add_edge(A, B) |
A 执行完一定走 B | GraphEdge::Fixed |
add_conditional_edges(A, router, map, default) |
A 执行完,根据状态选择走哪个 | GraphEdge::Conditional |
add_fan_out(A, [B,C,D]) |
A 执行完,分叉成 B/C/D 并行 | GraphEdge::FanOut |
add_fan_in([B,C,D], E) |
B/C/D 全完了,合并到 E | GraphEdge::FanIn |
2.4 compile() 的入口推断逻辑
scss
fn compile(&self) -> GraphResult<CompiledGraph<S>> {
// 1. 没节点 → 拒绝
if self.nodes.is_empty() { return Err(...); }
// 2. 找入口点:优先用 set_entry_point() 设的,否则找 START 指向的第一个节点
let entry = self.entry_point.clone()
.or_else(|| self.find_first_node_after_start())
.ok_or_else(|| "No entry point defined")?;
// 3. 把构建期的所有数据 clone 进 CompiledGraph
let mut compiled = CompiledGraph::new(nodes, edges, entry, reducer);
for (name, router) in &self.conditional_routers {
compiled.add_router(name.clone(), router.clone());
}
// 4. ★★★ 最关键一步:调用 validate() 做三遍校验
compiled.validate()?;
Ok(compiled)
}
三、编译期验证(validate)
compile() 返回的 CompiledGraph 一定通过全部 5 项检查。每项的具体算法如下:
validate() 入口(compiled.rs 第142-229行)
先遍历每条边,检查其引用的节点、路由函数是否存在且合法:
rust
fn validate(&self) -> GraphResult<()> {
for edge in &self.edges {
match edge {
GraphEdge::Fixed { source, target } => {
// source 如果是 START 则跳过检查(START 是个 sentinel,不在 nodes 里)
// 否则 source 必须在 nodes 中
if source != START && !self.nodes.contains_key(source) {
return Err("Source node not found");
}
// target 如果是 END 则跳过检查(END 也是个 sentinel)
// 否则 target 必须在 nodes 中
if target != END && !self.nodes.contains_key(target) {
return Err("Target node not found");
}
// target 不能是 START
if target == START {
return Err("Edge cannot target START node");
}
}
GraphEdge::Conditional { source, router_name, targets, default_target } => {
// source 检查同上
if source != START && !self.nodes.contains_key(source) { ... }
// ★ router_name 必须在 conditional_routers 里有对应的路由函数
if !self.conditional_routers.contains_key(router_name) {
return Err("Router not found");
}
// targets 中的每个 target,要么是 END,要么在 nodes 里
for (route, target) in targets {
if target != END && !self.nodes.contains_key(target) { ... }
if target == START { ... }
}
// default_target 同理
}
GraphEdge::FanOut { source, targets } => {
// source 检查同上
// 每个 target 必须是已有节点或 END
}
GraphEdge::FanIn { sources, target } => {
// sources 中的每个 source 必须是已有节点或 START
// target 必须是已有节点或 END
}
}
}
// 做完边检查后,再跑三项独立校验
self.validate_duplicate_edges()?; // 检查2:无重复边
self.validate_unreachable_nodes()?; // 检查3:无孤立节点
self.validate_cycles()?; // 检查4:无死循环
Ok(())
}
检查2:无重复边(compiled.rs 第231-249行)
rust
fn validate_duplicate_edges(&self) -> GraphResult<()> {
let mut seen_fixed: HashSet<(String, String)> = HashSet::new();
for edge in &self.edges {
// 只检查 Fixed 类型边(Conditional/FanOut/FanIn 允许同名 source 多条)
if let GraphEdge::Fixed { source, target } = edge {
let key = (source.clone(), target.clone());
if seen_fixed.contains(&key) {
return Err(GraphError::DuplicateEdgeError(
format!("Duplicate edge: {} -> {}", source, target)
));
}
seen_fixed.insert(key);
}
}
Ok(())
}
算法描述 :用 HashSet<(String, String)> 记录所有 (source, target) 对,发现重复就报错。只检查 Fixed 边------因为 Conditional / FanOut / FanIn 本身就允许多条共享同一个 source。
检查3:无孤立节点(compiled.rs 第251-314行)
rust
fn validate_unreachable_nodes(&self) -> GraphResult<()> {
let reachable = self.compute_reachable_nodes();
for node_name in self.nodes.keys() {
if !reachable.contains(node_name) {
return Err(GraphError::OrphanNodeError(
format!("Unreachable node: {}", node_name)
));
}
}
Ok(())
}
从入口点出发,做 DFS 遍历(depth-first search),收集所有能到达的节点。最后检查每个节点是否都在这个集合里。
compute_reachable_nodes() 算法
rust
输入: self.entry_point(入口节点名), self.edges(所有边), self.nodes(所有节点)
输出: reachable(从入口可达的节点集合)
reachable = {}
to_visit = [entry_point] // 待访问栈,初始只有入口
while to_visit 不为空:
current = to_visit.pop() // 从栈顶取一个
if current 已在 reachable 中 或 current == END:
continue // 跳过
reachable.insert(current) // 标记为可达
// 遍历所有从 current 出发的边
for edge in edges where edge.source == current:
match edge.type:
Fixed { target }:
if target 不在 reachable 且 target != END:
to_visit.push(target)
Conditional { targets, default_target }:
for target in targets.values():
if target 不在 reachable 且 target != END:
to_visit.push(target)
if default_target 存在且不在 reachable 且 != END:
to_visit.push(default_target)
// 注意:把所有可能分支都加入,不考虑路由函数的实际结果
FanOut { targets }:
for target in targets:
if target 不在 reachable 且 target != END:
to_visit.push(target)
FanIn { sources, target }:
if sources 中的所有节点都已可达:
if target 不在 reachable 且 target != END:
to_visit.push(target)
return reachable
完整执行示例
图结构:START → A, A → [B, C] (FanOut), B → END, C → D, D → END
ini
初始: reachable = {}, to_visit = [A]
第1次弹出: current = A
reachable = {A}
边: A → FanOut([B, C]) → 推入 B, C
to_visit = [B, C]
第2次弹出: current = C
reachable = {A, C}
边: C → Fixed(D) → D 不在 reachable → 推入 D
to_visit = [B, D]
第3次弹出: current = D
reachable = {A, C, D}
边: D → Fixed(END) → END 跳过
to_visit = [B]
第4次弹出: current = B
reachable = {A, C, D, B}
边: B → Fixed(END) → END 跳过
to_visit = []
结果: reachable = {A, B, C, D}
所有节点都在 reachable 中 → 验证通过 ✅
检查4:无死循环(compiled.rs 第316-374行)
核心思想:如果每个节点最终都能走到 END,图里就没有"吞掉执行流程"的死循环。
rust
fn validate_cycles(&self) -> GraphResult<()> {
let reachable = self.compute_reachable_nodes(); // 步骤1
let end_reachable = self.compute_end_reachable_nodes(); // 步骤2
for node in &reachable {
if !end_reachable.contains(node) {
return Err(GraphError::InfiniteCycleError(
format!("Node '{}' in cycle with no path to END", node)
));
}
}
Ok(())
}
这里的关键是 compute_end_reachable_nodes()------一个 反向传播(逆向 BFS) 算法。
compute_end_reachable_nodes() 算法
ini
输入: self.edges(所有边)
输出: end_reachable(能到达 END 的节点集合,反向计算)
// 初始条件:END 当然能到达 END 自己
end_reachable = {END}
changed = true
while changed:
changed = false
// 遍历每条边,检查是否可以从已知"能到 END"的节点反向推导
for edge in edges:
match edge.type:
Fixed { source, target }:
// 如果 target 能到 END,那么 source 也能到 END
if target ∈ end_reachable AND source ∉ end_reachable:
end_reachable += source
changed = true
Conditional { source, targets, default_target }:
// 只要有一个分支能到 END,source 就能到 END
cnt = count { t ∈ targets.values() where t ∈ end_reachable }
if (cnt > 0 OR default_target ∈ end_reachable) AND source ∉ end_reachable:
end_reachable += source
changed = true
FanOut { source, targets }:
// ★ 所有分支都必须能到 END,source 才能到 END
// 因为 FanOut 是并行的,有一条分支卡死,整个流程就卡死
if targets 全部 ∈ end_reachable AND source ∉ end_reachable:
end_reachable += source
changed = true
FanIn { sources, target }:
// 如果 target 能到 END,那么所有 sources 都能到 END
// (因为 FanIn 的 sources 必须先执行完,target 才能执行)
if target ∈ end_reachable:
for source in sources:
if source ∉ end_reachable:
end_reachable += source
changed = true
return end_reachable
示例1:正常线性图
less
图: A → B, B → C, C → END
css
初始: end_reachable = {END}
第1轮:
Fixed(C, END): END ∈ set, C ∉ → end_reachable = {END, C}, changed = true
Fixed(B, C): C ∈ set, B ∉ → end_reachable = {END, C, B}, changed = true
Fixed(A, B): B ∈ set, A ∉ → end_reachable = {END, C, B, A}, changed = true
第2轮: 没有变化 → 停止
结果: end_reachable = {END, C, B, A}
验证: 所有节点 ∈ end_reachable → 通过 ✅
示例2:循环图 A→B→A(没有指向 END)
less
图: A → B, B → A
css
初始: end_reachable = {END}
第1轮:
Fixed(A, B): B ∉ set → 不处理
Fixed(B, A): A ∉ set → 不处理
没有变化 → 停止
结果: end_reachable = {END}
验证: A ∉ end_reachable, B ∉ end_reachable
→ 报错:InfiniteCycleError("Node 'A' in cycle with no path to END") ❌
示例3:A→B→A 且 A→END
less
图: A → B, B → A, A → END
less
初始: end_reachable = {END}
第1轮:
Fixed(A, END): END ∈ set, A ∉ → end_reachable = {END, A}, changed = true
Fixed(A, B): END ∈ set 但这里只看 target 是 A 的 → 不对,A→B 中 target=B ∉ set
Fixed(B, A): A ∈ set, B ∉ → end_reachable = {END, A, B}, changed = true
第2轮:
Fixed(A, B): B ∈ set → 但 A 已经在 set 中了,不变
没有变化 → 停止
结果: end_reachable = {END, A, B}
验证: A 和 B 都在 end_reachable 中
→ 通过 ✅(因为存在 A→END 这条出路)
示例4:FanOut 全部都要能到 END
less
图: START → A, A → FanOut([B, C]), B → END, C → END
css
初始: end_reachable = {END}
第1轮:
Fixed(B, END): END ∈, B ∉ → set = {END, B}
Fixed(C, END): END ∈, C ∉ → set = {END, B, C}
FanOut(A → [B,C]): B∈set, C∈set, A ∉ → A 的两个分支都能到 END
→ set = {END, B, C, A}
第2轮: 不变
结果: end_reachable = {END, A, B, C} → 通过 ✅
示例5:FanOut 有一条分支卡死
less
图: START → A, A → FanOut([B, C]), B → END, C → D
(C 到 D 后没路了)
sql
初始: end_reachable = {END}
第1轮:
Fixed(B, END): END ∈, B ∉ → set = {END, B}
其他边:C→D, D 没有出边 → 无变化
FanOut(A → [B,C]): B∈set, C∉set → 不是全部 → 不处理
第2轮: 不变
结果: end_reachable = {END, B}
验证: A ∉ set, C ∉ set, D ∉ set
→ 报错:InfiniteCycleError("Node 'A' in cycle with no path to END") ❌
五类校验总结
| 检查 | 检测什么 | 算法 | 复杂度 |
|---|---|---|---|
| 边合法性 | 边引用的节点/路由不存在 | 线性扫描 | O(E) |
| 无重复边 | 相同的 Fixed 边出现两次 | HashSet 去重 | O(E) |
| 无孤立节点 | 从入口不可达的节点 | DFS 正向遍历 | O(N + E) |
| 无死循环 | 存在无法走到 END 的节点 | 反向传播(逆向 BFS) | O(K * E),K 是迭代轮数 |
| target≠START | 边指向了 START 这个 sentinel | 线性扫描 | O(E) |
注:四种检查不能合并。一条边合法 ≠ 所有节点可达(可能有孤立节点)。所有节点可达 ≠ 没有死循环(可能有环但入口可达)。孤立的环同时触发检查3和检查4。
为什么不用"正向 DFS"代替逆向 BFS?
有人可能会问:检查4 不就是要看"每个节点能不能到 END"吗?那我从每个节点做一次正向 DFS 不就完了?
方案对比
ini
图:N 个节点,E 条边
方案A:正向 DFS 每个节点 方案B:逆向 BFS(当前实现)
───────────────────────────── ─────────────────────────────
对每个节点:从它出发 DFS 看能不能到 END 从 END 出发,逆着箭头往回走
见到一个节点就标记"能到 END"
复杂度:O(N × (N + E)) 复杂度:O(N + E)
举例 N=100, E=200:
100 × 300 = 30,000 次操作 ~300 次操作
直观理解
正向 DFS 每个节点 = 你是一个快递员,每次从不同的路口出发,看能不能走到终点站。走了 4 次路,路线大部分重叠:
css
从 A 出发走一遍 → 能到终点
从 B 出发走一遍 → 能到终点
从 C 出发走一遍 → 能到终点
从 D 出发走一遍 → 能到终点
逆向 BFS = 你从终点站往所有路口贴告示。告示传一圈,全部知道,只走了一次:
arduino
终点站贴告示:"我能到终点"
→ 旁边 D 看到了:"那我也能到终点"
→ 旁边 C 看到了:"那我也能到终点"
→ 旁边 B 看到了:"那我也能到终点"
→ 旁边 A 看到了:"那我也能到终点"
根本原因
正向 DFS 的问题在于:你不知道一个节点能不能到 END,除非你真正从它开始走一遍。即使多个节点的路径高度重叠(如 A→B→C→END,A 走过的路径 B 还得再走一次),正向方案没有复用中间结果。
逆向 BFS 一次遍历就标记了所有"能到 END"的节点。每条边最多被处理一次。
那用标准的"检测环"算法(DFS 找 back edge)不行吗?
标准环检测会告诉你"图里有环",但它分不清两种环:
css
有问题的环:
START → A → B → C ← 没有任何节点指向 END
↑____↓
无害的环:
START → A → B → C → END
↑_______↓
两种都有环,但后者每个节点都能到 END,完全没问题。我们要的不是"有没有环",而是 "有没有节点无法到达 END" 。
四、执行引擎核心:invoke
一句话说清楚
invoke 就是一个 while 循环,每次循环干三件事:
markdown
当前在哪个节点 → 执行这个节点 → 决定下一个去哪个节点
↕ ↕
从 HashMap 里找 通过边和路由函数找
直到走到 END 或者超过最大步数。
实际源码(compiled.rs 第376-436行)
rust
pub async fn invoke(&self, input: S) -> GraphResult<GraphInvocation<S>> {
let mut state = input;
let mut current_node = self.entry_point.clone();
let mut steps = Vec::new();
let mut recursion_count = 0;
// 可选:初始状态存检查点
if let Some(ref checkpointer) = self.checkpointer {
let checkpoint_id = checkpointer.lock().await.save(&state).await?;
steps.push(ExecutionStep::checkpoint(checkpoint_id, current_node.clone()));
}
// ★★★ 核心 while 循环
while current_node != END && recursion_count < self.recursion_limit {
// 可选:进节点前打断(Human-in-the-loop)
if self.interrupt_before.contains(¤t_node) {
return Err(GraphError::ExecutionInterrupted(current_node.clone()));
}
recursion_count += 1;
// ★ 从 HashMap 找到当前节点的执行函数
let node = self.nodes.get(¤t_node)
.ok_or_else(|| GraphError::ExecutionError(
format!("Node '{}' not found", current_node)
))?;
let config = NodeConfig {
recursion_limit: self.recursion_limit,
debug: false,
metadata: HashMap::new(),
};
// ★ 执行节点!虚表 dispatch 到 SyncNode::execute 或 AsyncNode::execute
let update = node.execute(&state, Some(config)).await?;
// ★ Reducer 合并状态
if let Some(new_state) = update.update {
state = self.default_reducer.reduce(&state, &new_state);
}
steps.push(ExecutionStep::node(current_node.clone(), update.metadata.clone()));
// 可选:出节点后打断
if self.interrupt_after.contains(¤t_node) {
return Err(GraphError::ExecutionInterrupted(format!("after_{}", current_node)));
}
// ★ 路由决策:找下一个节点
let next_node = self.find_next_node(¤t_node, &state).await?;
// 可选:每步完成存检查点
if let Some(ref checkpointer) = self.checkpointer {
let checkpoint_id = checkpointer.lock().await.save(&state).await?;
steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
}
current_node = next_node; // 跳转
}
if recursion_count >= self.recursion_limit {
return Err(GraphError::RecursionLimitReached(self.recursion_limit));
}
Ok(GraphInvocation {
final_state: state,
steps,
recursion_count,
})
}
拿个真实例子跑一遍
假设构建了这样一个图:
sql
START → step1 → step2 → END
step1 的逻辑:记一条消息 "第一步完成"
step2 的逻辑:记一条消息 "第二步完成",把输出设成 "done"
编译后的 CompiledGraph 内部数据:
swift
nodes: {
"step1": Arc<SyncNode<...>>, // 存了闭包: |state| { ... 记消息 ... }
"step2": Arc<SyncNode<...>>, // 存了闭包: |state| { ... 记消息, 设输出 ... }
}
edges: [
Fixed("__start__", "step1"),
Fixed("step1", "step2"),
Fixed("step2", "__end__"),
]
entry_point: "step1"
recursion_limit: 25
default_reducer: ReplaceReducer
调用 invoke(AgentState::new("hello"))。
逐轮跟踪
准备工作
ini
state = AgentState { input: "hello", messages: ["hello"], output: None }
current_node = "step1" ← 从 entry_point 拿到的
recursion_count = 0
steps = []
第1轮循环
① 检查 :current_node = "step1",不是 END,继续。
② 找节点 :从 nodes HashMap 里取 "step1",拿到 Arc<SyncNode<...>>。
③ 执行节点:
scss
nodes["step1"].execute(&state).await
内部调闭包:
css
|state| {
let mut new_state = state.clone();
new_state.add_message(MessageEntry::ai("第一步完成".to_string()));
Ok(StateUpdate::full(new_state))
}
返回 StateUpdate { update: Some(新的 state), metadata: {} }。
④ Reducer 合并:
css
旧的 state:{ input: "hello", messages: ["hello"], output: None }
新的 state:{ input: "hello", messages: ["hello", AI("第一步完成")], output: None }
↑ 新增了一条消息
用 ReplaceReducer:旧的整个被替换成新的。
⑤ 记步骤 :steps.push(("step1", 执行记录))。
⑥ 找下一个节点:
bash
find_next_node("step1", state)
扫描 edges,找到 source == "step1" 的边
→ Fixed { source: "step1", target: "step2" }
→ 返回 "step2"
⑦ 跳转 :current_node = "step2"。
第2轮循环
① 检查 :current_node = "step2",不是 END,继续。
② 找节点 :从 nodes 取 "step2"。
③ 执行节点:
scss
nodes["step2"].execute(&state).await
内部调闭包:
less
|state| {
let mut new_state = state.clone();
new_state.add_message(MessageEntry::ai("第二步完成".to_string()));
new_state.set_output("done".to_string());
Ok(StateUpdate::full(new_state))
}
返回 StateUpdate { update: Some(更新的 state), ... }。
④ Reducer 合并:
css
旧的 state:{ input: "hello", messages: ["hello", AI("第一步完成")], output: None }
新的 state:{ input: "hello", messages: ["hello", AI("第一步完成"), AI("第二步完成")], output: Some("done") }
⑤ 记步骤 :steps.push(("step2", ...))。
⑥ 找下一个节点:
bash
find_next_node("step2", state)
→ Fixed { source: "step2", target: "__end__" }
→ 返回 "__end__"
⑦ 跳转 :current_node = "__end__"。
第3轮:循环条件检查
current_node = "__end__",条件 current_node != END 不成立 → 退出循环。
返回结果
css
Ok(GraphInvocation {
final_state: AgentState {
input: "hello",
messages: ["hello", AI("第一步完成"), AI("第二步完成")],
output: Some("done"),
},
steps: [ExecutionStep::Node("step1"), ExecutionStep::Node("step2")],
recursion_count: 2,
})
整个流程用时间线看
ini
时间 ──────────────────────────────────────────────────────────────→
invoke() 被调用
│
├─ 初始状态: state = { input: "hello", messages: [], output: None }
│
├─ [第1轮] current_node = "step1"
│ ├─ nodes["step1"].execute(&state) ← 执行节点
│ ├─ reducer.reduce(state, update) ← 合并状态
│ ├─ find_next_node("step1") → "step2" ← 路由决策
│ └─ current_node = "step2"
│
├─ [第2轮] current_node = "step2"
│ ├─ nodes["step2"].execute(&state)
│ ├─ reducer.reduce(state, update)
│ ├─ find_next_node("step2") → "__end__"
│ └─ current_node = "__end__"
│
├─ [第3轮] current_node == END → 退出
│
└─ 返回 GraphInvocation { final_state, steps, recursion_count }
invoke 在做什么
把上面这个例子抽象一下,invoke 本质上就是:
sql
有一个"当前节点"指针,初始指向 entry_point
循环:
如果当前节点是 END → 结束
从 HashMap 里找到当前节点对应的"执行函数"
调用它,传入当前状态
拿到返回值(状态更新)
用 Reducer 把更新合并进当前状态
找"下一个节点去哪":
- Fixed 边:直接去目标节点
- Conditional 边:调路由函数,根据返回值决定去哪个
- FanOut 边:取第一个目标(主要在 invoke_parallel 里用)
把"当前节点"指向"下一个节点"
继续循环
makefile
否
是
invoke(initial_state)
current == END
或
超过最大步数?
从 HashMap 找当前节点
执行: node.execute(&state)
合并: reducer.reduce(state, update)
路由: find_next_node(current, state)
current_node = next_node
返回 GraphInvocation
Reducer 是干什么的
节点执行完返回的不是"新状态",而是"状态更新"(StateUpdate<S>):
rust
pub struct StateUpdate<S> {
pub update: Option<S>, // 新的状态数据
pub metadata: HashMap<String, JsonValue>, // 调试信息
}
Reducer 决定「怎么把更新合并进当前状态」。两种最常见的:
css
ReplaceReducer(默认):
旧状态 = { name: "张三", score: 10 }
新状态 = { name: "李四", score: 20 }
─────────────────────────────────
结果 = { name: "李四", score: 20 } ← 完全替换
AppendMessagesReducer(对话历史用):
旧状态 = { messages: ["你好"] }
新状态 = { messages: ["我在吗?"] }
─────────────────────────────────
结果 = { messages: ["你好", "我在吗?"] } ← 追加而不是替换
为什么需要这个机制? 如果两个节点各自加了一条消息,都用 ReplaceReducer,最后只有第二个节点的消息存活。用了 AppendMessagesReducer,两条消息都保留。
五、路由机制(find_next_node)
rust
async fn find_next_node(&self, current: &str, state: &S) -> GraphResult<String> {
for edge in &self.edges {
if edge.source() == current {
match edge {
GraphEdge::Fixed { target, .. } => {
return Ok(target.clone()); // 直接返回固定目标
}
GraphEdge::Conditional { router_name, targets, default_target, .. } => {
let router = self.conditional_routers.get(router_name)?;
let route_key = router.route(state).await?; // ★ 执行路由函数
let target = targets.get(&route_key)
.or_else(|| default_target.as_ref())
.ok_or_else(|| GraphError::RoutingError(...))?;
return Ok(target.clone());
}
GraphEdge::FanOut { targets, .. } => {
return Ok(targets[0].clone()); // ★ 只返回第一个!其他在 invoke_parallel 处理
}
GraphEdge::FanIn { .. } => {
continue; // ★ 跳过,FanIn 由 merge 逻辑处理
}
}
}
}
Err(GraphError::RoutingError("No outgoing edge"))
}
5.1 条件路由完整流程
scss
Analyze 节点执行完毕
↓
find_next_node("analyze", state)
↓
condition_routers["length_router"].route(&state)
↓
FunctionRouter::route // 实际就是调闭包
↓
如果 input.len() < 10 返回 "short",否则 "long"
↓
在 targets 中查找 "short" → "quick_process",或 "long" → "detailed_process"
↓
返回对应的下一个节点名
六、并行执行(invoke_parallel + FanOut/FanIn)
rust
pub async fn invoke_parallel(&self, input: S) -> GraphResult<ParallelInvocation<S>> {
// ... 与 invoke 相同的主循环 ...
while current_node != END && recursion_count < self.recursion_limit {
let fan_out_targets = self.find_fan_out_targets(¤t_node);
if let Some(targets) = fan_out_targets {
// ★★★ 遇到 FanOut 边:并行执行所有分支
let branch_results = self.execute_parallel_branches(&targets, &state).await?;
// 收集分支结果
for (name, inv) in branch_results {
parallel_branches.push(ParallelBranch { name, final_state, steps });
}
// ★ 找 FanIn 合并点
let merge_target = self.find_fan_in_target(&targets);
if let Some(merge_node) = merge_target {
state = self.merge_parallel_states(¶llel_branches);
current_node = merge_node;
} else {
current_node = END;
}
} else {
// 普通节点,跟 invoke 一样
}
}
}
6.1 并行分支执行
rust
async fn execute_parallel_branches(&self, targets: &[String], state: &S) -> ... {
// ★★★ 用 join_all 同时启动 N 个分支,每个分支独立跑 invoke_from_node
let futures: Vec<_> = targets.iter()
.filter(|t| *t != END)
.map(|target| {
let state_clone = state.clone();
async move {
self.invoke_from_node(target.clone(), state_clone).await
}
})
.collect();
let results = join_all(futures).await; // 所有分支真正并行
// ...
}
6.2 并行分支合并
ini
fn merge_parallel_states(&self, branches: &[ParallelBranch<S>]) -> S {
let mut merged = branches[0].final_state.clone();
for branch in branches.iter().skip(1) {
merged = self.default_reducer.reduce(&merged, &branch.final_state);
}
merged
}
逐个用 Reducer 合并,相当于把所有分支的状态累加起来。如果用的是 AppendMessagesReducer,每个分支追加的消息都会被保留。
七、流式执行(stream)
rust
pub async fn stream(&self, input: S) -> GraphResult<Vec<StreamEvent<S>>> {
let mut events = Vec::new();
events.push(StreamEvent::start(state.clone())); // ❶ 开始
while current_node != END && ... {
events.push(StreamEvent::enter_node(name, state)); // ❷ 进入节点
let update = node.execute(&state, ...).await?;
events.push(StreamEvent::node_complete(name, update)); // ❸ 节点完成
if let Some(new_state) = update.update {
state = reducer.reduce(&state, &new_state);
events.push(StreamEvent::state_update(state.clone())); // ❹ 状态更新
}
current_node = find_next_node(...);
}
events.push(StreamEvent::end(state)); // ❺ 结束
Ok(events)
}
事件类型枚举:
scss
pub enum StreamEvent<S> {
Start(S), // 图开始,附带初始状态
EnterNode(String, S), // 进入某个节点,附带当前状态
NodeComplete(String, StateUpdate<S>), // 节点执行完毕,附带状态更新
StateUpdate(S), // Reducer 合并后的新状态
End(S), // 图执行完毕,附带最终状态
}
八、中断与恢复(Human-in-the-loop)
csharp
compiled_graph
.with_interrupt_before(vec!["review_step".into()]) // 进入前打断
.with_interrupt_after(vec!["llm_call".into()]); // 退出后打断
执行时效果:
scss
invoke 开始
↓ 执行 entry_point
↓ 检查 interrupt_before → 命中则返回 ExecutionInterrupted("review_step")
↓ 用户可以检查/修改状态
↓ 调用 resume(execution_context) 继续
↓ 执行 review_step
↓ 检查 interrupt_after → 命中则返回 ExecutionInterrupted("after_llm_call")
↓ ...
恢复执行的 invoke_with_execution 会:
- 如果中断点是
after_X:先执行find_next_node(X)跳到下一节点 - 如果中断点是
before_X:从 X 节点开始执行 - 继续主循环直到 END
九、三种执行模式对比
| 特征 | invoke |
stream |
invoke_parallel |
|---|---|---|---|
| 返回值 | GraphInvocation { state, steps } |
Vec<StreamEvent> |
ParallelInvocation { state, steps, branches } |
| FanOut 处理 | 取第一个分支,忽略其他 | 取第一个分支,忽略其他 | 所有分支并行,结果合并 |
| Checkpointer | 每步检查点 | 否 | 每步检查点 |
| 适用场景 | 完整一次执行 | 实时进度展示 | 分叉合并型工作流 |
| 性能 | 快 | 中等(收集事件有开销) | 分支并行,总体快 |
十、完整执行示例
构建一个简单的线性图:
ruby
let compiled = GraphBuilder::<AgentState>::new()
.add_node_fn("step1", |s| { /* 处理 */ Ok(StateUpdate::full(...)) })
.add_node_fn("step2", |s| { /* 处理 */ Ok(StateUpdate::full(...)) })
.add_edge(START, "step1")
.add_edge("step1", "step2")
.add_edge("step2", END)
.compile()?;
let result = compiled.invoke(AgentState::new("hello")).await?;
内部执行流程:
css
StateGraph 的内存状态(编译前):
nodes: { "step1" => Arc<SyncNode>, "step2" => Arc<SyncNode> }
edges: [ Fixed("__start__", "step1"), Fixed("step1", "step2"), Fixed("step2", "__end__") ]
entry_point: None (compile 时会自动找 START 的第一个 target)
conditional_routers: {}
编译后:
CompiledGraph.entry_point = "step1"
CompiledGraph.recursion_limit = 25
调用 invoke(AgentState { input: "hello", messages: [...], steps: [], output: None }):
┌─ 循环开始 ──────────────────────────────────────────────────┐
│ │
│ 第1轮: current_node = "step1" │
│ node = nodes["step1"] │
│ update = step1.execute(&state) → StateUpdate { update: Some(new_state) } │
│ state = ReplaceReducer.reduce(state, new_state) │
│ steps.push(ExecutionStep::node("step1")) │
│ next = find_next_node("step1") │
│ → 扫描 edges,找到 Fixed { source: "step1", target: "step2" } │
│ → 返回 "step2" │
│ current_node = "step2" │
│ │
│ 第2轮: current_node = "step2" │
│ node = nodes["step2"] │
│ update = step2.execute(&state) │
│ state = reducer.reduce(state, new_state) // 比如 set_output("done") │
│ next = find_next_node("step2") │
│ → 扫描 edges,找到 Fixed { source: "step2", target: "__end__" } │
│ → 返回 "__end__" │
│ current_node = "__end__" │
│ │
│ 第3轮: current_node == END → 退出循环 │
└──────────────────────────────────────────────────────────────┘
Ok(GraphInvocation {
final_state: AgentState { input: "hello", output: Some("done"), messages: [..., ...] },
steps: [Node("step1"), Node("step2")],
recursion_count: 2
})
十一、代码中的边界与注意事项
- FanOut 在
invoke和stream中不会被并行执行 ---find_next_node遇到 FanOut 只返回targets[0],其他分支被忽略。只有invoke_parallel才会真正并行。 merge_parallel_states顺序依赖 --- 分支顺序决定了合并后的消息顺序。如果使用AppendMessagesReducer,messages 最终是[分支1的消息..., 分支2的消息..., ...]。- 条件路由没有缓存 --- 如果多个条件边指向同一个路由函数名,每次
find_next_node都重新调一次router.route()。 find_next_node是线性扫描 --- 每次循环都遍历整个edges列表。对于大规模图可能是性能瓶颈。- 中断恢复不会重放检查点 ---
resume()直接从上次中断的位置继续,之前的状态由调用方通过GraphExecution传递。
十二、关键源码文件索引
| 文件 | 内容 |
|---|---|
crates/langchainrust/src/langgraph/graph.rs |
StateGraph 构建器 + GraphBuilder 流畅 API |
crates/langchainrust/src/langgraph/compiled.rs |
CompiledGraph 执行引擎 + invoke/stream/invoke_parallel |
crates/langchainrust/src/langgraph/state.rs |
StateSchema trait + AgentState + Reducer 系列 |
crates/langchainrust/src/langgraph/node.rs |
GraphNode trait + SyncNode/AsyncNode/FunctionNode |
crates/langchainrust/src/langgraph/edge.rs |
GraphEdge 枚举 + ConditionalEdge trait + FunctionRouter |
crates/langchainrust/src/langgraph/mod.rs |
模块导出 + 核心概念文档 |
src/services/langgraph_service.rs |
应用层:并行/条件/流式三种演示 + LLM 任务拆解 |
src/services/agent_executor.rs |
应用层:真实 Agent 引擎(非 LangGraph,但概念类似)# LangChainRust Agent 引擎:Graph 构建到执行 |
项目地址:github.com/atliliw/lan... 框架源码:github.com/atliliw/lan... --- 基于
crates/langchainrust/src/langgraph/源码逐层拆解
一、整体架构:两阶段设计
css
┌──────────────────────────────────────────────────────────────┐
│ 阶段1: 构建 (StateGraph) → 阶段2: 执行 (CompiledGraph) │
│ │
│ add_node() / add_edge() invoke() → GraphInvocation │
│ add_conditional_edges() stream() → Vec<StreamEvent> │
│ add_fan_out() / add_fan_in() invoke_parallel() → ... │
│ │
│ compile() → CompiledGraph resume() → 继续执行 │
└──────────────────────────────────────────────────────────────┘
设计意图:构建期只做"描述图",不执行任何逻辑。编译期做验证,通过后才能执行。
二、StateGraph 构建(graph.rs)
2.1 核心数据结构
StateGraph<S> 内部维护 5 个数据结构:
rust
pub struct StateGraph<S: StateSchema> {
nodes: HashMap<String, Arc<dyn GraphNode<S>>>, // 节点名 → 节点实现
edges: Vec<GraphEdge>, // 所有边(有序)
entry_point: Option<String>, // 入口节点名
reducers: HashMap<String, Arc<dyn Reducer<S>>>, // 字段级 Reducer
default_reducer: Arc<dyn Reducer<S>>, // 默认 Reducer (ReplaceReducer)
conditional_routers: HashMap<String, Arc<dyn ConditionalEdge<S>>>, // 路由函数
}
2.2 添加节点的三种方式
| 方法 | 签名 | 内部实现 |
|---|---|---|
add_node_fn |
Fn(&S) -> Result<StateUpdate<S>> |
包成 SyncNode,不支持 async |
add_async_node |
AsyncFn<S> trait,即 Fn(&S) -> Future<...> |
包成 AsyncNode,在 .await 中执行 |
add_node |
自定义 GraphNode 实现 |
直接存 Arc,最灵活 |
关键区别:SyncNode 的 execute() 把同步函数包在 async 块里返回,而 AsyncNode 直接 await 异步函数。
2.3 添加边的五种方式
| 方法 | 效果 | 边类型 |
|---|---|---|
add_edge(A, B) |
A 执行完一定走 B | GraphEdge::Fixed |
add_conditional_edges(A, router, map, default) |
A 执行完,根据状态选择走哪个 | GraphEdge::Conditional |
add_fan_out(A, [B,C,D]) |
A 执行完,分叉成 B/C/D 并行 | GraphEdge::FanOut |
add_fan_in([B,C,D], E) |
B/C/D 全完了,合并到 E | GraphEdge::FanIn |
2.4 compile() 的入口推断逻辑
rust
fn compile(&self) -> GraphResult<CompiledGraph<S>> {
// 1. 没节点 → 拒绝
if self.nodes.is_empty() { return Err(...); }
// 2. 找入口点:优先用 set_entry_point() 设的,否则找 START 指向的第一个节点
let entry = self.entry_point.clone()
.or_else(|| self.find_first_node_after_start())
.ok_or_else(|| "No entry point defined")?;
// 3. 把构建期的所有数据 clone 进 CompiledGraph
let mut compiled = CompiledGraph::new(nodes, edges, entry, reducer);
for (name, router) in &self.conditional_routers {
compiled.add_router(name.clone(), router.clone());
}
// 4. ★★★ 最关键一步:调用 validate() 做三遍校验
compiled.validate()?;
Ok(compiled)
}
三、编译期验证(validate)
compile() 返回的 CompiledGraph 一定通过全部 5 项检查。每项的具体算法如下:
validate() 入口(compiled.rs 第142-229行)
先遍历每条边,检查其引用的节点、路由函数是否存在且合法:
rust
fn validate(&self) -> GraphResult<()> {
for edge in &self.edges {
match edge {
GraphEdge::Fixed { source, target } => {
// source 如果是 START 则跳过检查(START 是个 sentinel,不在 nodes 里)
// 否则 source 必须在 nodes 中
if source != START && !self.nodes.contains_key(source) {
return Err("Source node not found");
}
// target 如果是 END 则跳过检查(END 也是个 sentinel)
// 否则 target 必须在 nodes 中
if target != END && !self.nodes.contains_key(target) {
return Err("Target node not found");
}
// target 不能是 START
if target == START {
return Err("Edge cannot target START node");
}
}
GraphEdge::Conditional { source, router_name, targets, default_target } => {
// source 检查同上
if source != START && !self.nodes.contains_key(source) { ... }
// ★ router_name 必须在 conditional_routers 里有对应的路由函数
if !self.conditional_routers.contains_key(router_name) {
return Err("Router not found");
}
// targets 中的每个 target,要么是 END,要么在 nodes 里
for (route, target) in targets {
if target != END && !self.nodes.contains_key(target) { ... }
if target == START { ... }
}
// default_target 同理
}
GraphEdge::FanOut { source, targets } => {
// source 检查同上
// 每个 target 必须是已有节点或 END
}
GraphEdge::FanIn { sources, target } => {
// sources 中的每个 source 必须是已有节点或 START
// target 必须是已有节点或 END
}
}
}
// 做完边检查后,再跑三项独立校验
self.validate_duplicate_edges()?; // 检查2:无重复边
self.validate_unreachable_nodes()?; // 检查3:无孤立节点
self.validate_cycles()?; // 检查4:无死循环
Ok(())
}
检查2:无重复边(compiled.rs 第231-249行)
rust
fn validate_duplicate_edges(&self) -> GraphResult<()> {
let mut seen_fixed: HashSet<(String, String)> = HashSet::new();
for edge in &self.edges {
// 只检查 Fixed 类型边(Conditional/FanOut/FanIn 允许同名 source 多条)
if let GraphEdge::Fixed { source, target } = edge {
let key = (source.clone(), target.clone());
if seen_fixed.contains(&key) {
return Err(GraphError::DuplicateEdgeError(
format!("Duplicate edge: {} -> {}", source, target)
));
}
seen_fixed.insert(key);
}
}
Ok(())
}
算法描述 :用 HashSet<(String, String)> 记录所有 (source, target) 对,发现重复就报错。只检查 Fixed 边------因为 Conditional / FanOut / FanIn 本身就允许多条共享同一个 source。
检查3:无孤立节点(compiled.rs 第251-314行)
rust
fn validate_unreachable_nodes(&self) -> GraphResult<()> {
let reachable = self.compute_reachable_nodes();
for node_name in self.nodes.keys() {
if !reachable.contains(node_name) {
return Err(GraphError::OrphanNodeError(
format!("Unreachable node: {}", node_name)
));
}
}
Ok(())
}
从入口点出发,做 DFS 遍历(depth-first search),收集所有能到达的节点。最后检查每个节点是否都在这个集合里。
compute_reachable_nodes() 算法
rust
输入: self.entry_point(入口节点名), self.edges(所有边), self.nodes(所有节点)
输出: reachable(从入口可达的节点集合)
reachable = {}
to_visit = [entry_point] // 待访问栈,初始只有入口
while to_visit 不为空:
current = to_visit.pop() // 从栈顶取一个
if current 已在 reachable 中 或 current == END:
continue // 跳过
reachable.insert(current) // 标记为可达
// 遍历所有从 current 出发的边
for edge in edges where edge.source == current:
match edge.type:
Fixed { target }:
if target 不在 reachable 且 target != END:
to_visit.push(target)
Conditional { targets, default_target }:
for target in targets.values():
if target 不在 reachable 且 target != END:
to_visit.push(target)
if default_target 存在且不在 reachable 且 != END:
to_visit.push(default_target)
// 注意:把所有可能分支都加入,不考虑路由函数的实际结果
FanOut { targets }:
for target in targets:
if target 不在 reachable 且 target != END:
to_visit.push(target)
FanIn { sources, target }:
if sources 中的所有节点都已可达:
if target 不在 reachable 且 target != END:
to_visit.push(target)
return reachable
完整执行示例
图结构:START → A, A → [B, C] (FanOut), B → END, C → D, D → END
ini
初始: reachable = {}, to_visit = [A]
第1次弹出: current = A
reachable = {A}
边: A → FanOut([B, C]) → 推入 B, C
to_visit = [B, C]
第2次弹出: current = C
reachable = {A, C}
边: C → Fixed(D) → D 不在 reachable → 推入 D
to_visit = [B, D]
第3次弹出: current = D
reachable = {A, C, D}
边: D → Fixed(END) → END 跳过
to_visit = [B]
第4次弹出: current = B
reachable = {A, C, D, B}
边: B → Fixed(END) → END 跳过
to_visit = []
结果: reachable = {A, B, C, D}
所有节点都在 reachable 中 → 验证通过 ✅
检查4:无死循环(compiled.rs 第316-374行)
核心思想:如果每个节点最终都能走到 END,图里就没有"吞掉执行流程"的死循环。
rust
fn validate_cycles(&self) -> GraphResult<()> {
let reachable = self.compute_reachable_nodes(); // 步骤1
let end_reachable = self.compute_end_reachable_nodes(); // 步骤2
for node in &reachable {
if !end_reachable.contains(node) {
return Err(GraphError::InfiniteCycleError(
format!("Node '{}' in cycle with no path to END", node)
));
}
}
Ok(())
}
这里的关键是 compute_end_reachable_nodes()------一个 反向传播(逆向 BFS) 算法。
compute_end_reachable_nodes() 算法
ini
输入: self.edges(所有边)
输出: end_reachable(能到达 END 的节点集合,反向计算)
// 初始条件:END 当然能到达 END 自己
end_reachable = {END}
changed = true
while changed:
changed = false
// 遍历每条边,检查是否可以从已知"能到 END"的节点反向推导
for edge in edges:
match edge.type:
Fixed { source, target }:
// 如果 target 能到 END,那么 source 也能到 END
if target ∈ end_reachable AND source ∉ end_reachable:
end_reachable += source
changed = true
Conditional { source, targets, default_target }:
// 只要有一个分支能到 END,source 就能到 END
cnt = count { t ∈ targets.values() where t ∈ end_reachable }
if (cnt > 0 OR default_target ∈ end_reachable) AND source ∉ end_reachable:
end_reachable += source
changed = true
FanOut { source, targets }:
// ★ 所有分支都必须能到 END,source 才能到 END
// 因为 FanOut 是并行的,有一条分支卡死,整个流程就卡死
if targets 全部 ∈ end_reachable AND source ∉ end_reachable:
end_reachable += source
changed = true
FanIn { sources, target }:
// 如果 target 能到 END,那么所有 sources 都能到 END
// (因为 FanIn 的 sources 必须先执行完,target 才能执行)
if target ∈ end_reachable:
for source in sources:
if source ∉ end_reachable:
end_reachable += source
changed = true
return end_reachable
示例1:正常线性图
less
图: A → B, B → C, C → END
css
初始: end_reachable = {END}
第1轮:
Fixed(C, END): END ∈ set, C ∉ → end_reachable = {END, C}, changed = true
Fixed(B, C): C ∈ set, B ∉ → end_reachable = {END, C, B}, changed = true
Fixed(A, B): B ∈ set, A ∉ → end_reachable = {END, C, B, A}, changed = true
第2轮: 没有变化 → 停止
结果: end_reachable = {END, C, B, A}
验证: 所有节点 ∈ end_reachable → 通过 ✅
示例2:循环图 A→B→A(没有指向 END)
less
图: A → B, B → A
css
初始: end_reachable = {END}
第1轮:
Fixed(A, B): B ∉ set → 不处理
Fixed(B, A): A ∉ set → 不处理
没有变化 → 停止
结果: end_reachable = {END}
验证: A ∉ end_reachable, B ∉ end_reachable
→ 报错:InfiniteCycleError("Node 'A' in cycle with no path to END") ❌
示例3:A→B→A 且 A→END
less
图: A → B, B → A, A → END
less
初始: end_reachable = {END}
第1轮:
Fixed(A, END): END ∈ set, A ∉ → end_reachable = {END, A}, changed = true
Fixed(A, B): END ∈ set 但这里只看 target 是 A 的 → 不对,A→B 中 target=B ∉ set
Fixed(B, A): A ∈ set, B ∉ → end_reachable = {END, A, B}, changed = true
第2轮:
Fixed(A, B): B ∈ set → 但 A 已经在 set 中了,不变
没有变化 → 停止
结果: end_reachable = {END, A, B}
验证: A 和 B 都在 end_reachable 中
→ 通过 ✅(因为存在 A→END 这条出路)
示例4:FanOut 全部都要能到 END
less
图: START → A, A → FanOut([B, C]), B → END, C → END
css
初始: end_reachable = {END}
第1轮:
Fixed(B, END): END ∈, B ∉ → set = {END, B}
Fixed(C, END): END ∈, C ∉ → set = {END, B, C}
FanOut(A → [B,C]): B∈set, C∈set, A ∉ → A 的两个分支都能到 END
→ set = {END, B, C, A}
第2轮: 不变
结果: end_reachable = {END, A, B, C} → 通过 ✅
示例5:FanOut 有一条分支卡死
less
图: START → A, A → FanOut([B, C]), B → END, C → D
(C 到 D 后没路了)
sql
初始: end_reachable = {END}
第1轮:
Fixed(B, END): END ∈, B ∉ → set = {END, B}
其他边:C→D, D 没有出边 → 无变化
FanOut(A → [B,C]): B∈set, C∉set → 不是全部 → 不处理
第2轮: 不变
结果: end_reachable = {END, B}
验证: A ∉ set, C ∉ set, D ∉ set
→ 报错:InfiniteCycleError("Node 'A' in cycle with no path to END") ❌
五类校验总结
| 检查 | 检测什么 | 算法 | 复杂度 |
|---|---|---|---|
| 边合法性 | 边引用的节点/路由不存在 | 线性扫描 | O(E) |
| 无重复边 | 相同的 Fixed 边出现两次 | HashSet 去重 | O(E) |
| 无孤立节点 | 从入口不可达的节点 | DFS 正向遍历 | O(N + E) |
| 无死循环 | 存在无法走到 END 的节点 | 反向传播(逆向 BFS) | O(K * E),K 是迭代轮数 |
| target≠START | 边指向了 START 这个 sentinel | 线性扫描 | O(E) |
注:四种检查不能合并。一条边合法 ≠ 所有节点可达(可能有孤立节点)。所有节点可达 ≠ 没有死循环(可能有环但入口可达)。孤立的环同时触发检查3和检查4。
为什么不用"正向 DFS"代替逆向 BFS?
有人可能会问:检查4 不就是要看"每个节点能不能到 END"吗?那我从每个节点做一次正向 DFS 不就完了?
方案对比
ini
图:N 个节点,E 条边
方案A:正向 DFS 每个节点 方案B:逆向 BFS(当前实现)
───────────────────────────── ─────────────────────────────
对每个节点:从它出发 DFS 看能不能到 END 从 END 出发,逆着箭头往回走
见到一个节点就标记"能到 END"
复杂度:O(N × (N + E)) 复杂度:O(N + E)
举例 N=100, E=200:
100 × 300 = 30,000 次操作 ~300 次操作
直观理解
正向 DFS 每个节点 = 你是一个快递员,每次从不同的路口出发,看能不能走到终点站。走了 4 次路,路线大部分重叠:
css
从 A 出发走一遍 → 能到终点
从 B 出发走一遍 → 能到终点
从 C 出发走一遍 → 能到终点
从 D 出发走一遍 → 能到终点
逆向 BFS = 你从终点站往所有路口贴告示。告示传一圈,全部知道,只走了一次:
arduino
终点站贴告示:"我能到终点"
→ 旁边 D 看到了:"那我也能到终点"
→ 旁边 C 看到了:"那我也能到终点"
→ 旁边 B 看到了:"那我也能到终点"
→ 旁边 A 看到了:"那我也能到终点"
根本原因
正向 DFS 的问题在于:你不知道一个节点能不能到 END,除非你真正从它开始走一遍。即使多个节点的路径高度重叠(如 A→B→C→END,A 走过的路径 B 还得再走一次),正向方案没有复用中间结果。
逆向 BFS 一次遍历就标记了所有"能到 END"的节点。每条边最多被处理一次。
那用标准的"检测环"算法(DFS 找 back edge)不行吗?
标准环检测会告诉你"图里有环",但它分不清两种环:
css
有问题的环:
START → A → B → C ← 没有任何节点指向 END
↑____↓
无害的环:
START → A → B → C → END
↑_______↓
两种都有环,但后者每个节点都能到 END,完全没问题。我们要的不是"有没有环",而是**"有没有节点无法到达 END"**。
四、执行引擎核心:invoke
一句话说清楚
invoke 就是一个 while 循环,每次循环干三件事:
markdown
当前在哪个节点 → 执行这个节点 → 决定下一个去哪个节点
↕ ↕
从 HashMap 里找 通过边和路由函数找
直到走到 END 或者超过最大步数。
实际源码(compiled.rs 第376-436行)
rust
pub async fn invoke(&self, input: S) -> GraphResult<GraphInvocation<S>> {
let mut state = input;
let mut current_node = self.entry_point.clone();
let mut steps = Vec::new();
let mut recursion_count = 0;
// 可选:初始状态存检查点
if let Some(ref checkpointer) = self.checkpointer {
let checkpoint_id = checkpointer.lock().await.save(&state).await?;
steps.push(ExecutionStep::checkpoint(checkpoint_id, current_node.clone()));
}
// ★★★ 核心 while 循环
while current_node != END && recursion_count < self.recursion_limit {
// 可选:进节点前打断(Human-in-the-loop)
if self.interrupt_before.contains(¤t_node) {
return Err(GraphError::ExecutionInterrupted(current_node.clone()));
}
recursion_count += 1;
// ★ 从 HashMap 找到当前节点的执行函数
let node = self.nodes.get(¤t_node)
.ok_or_else(|| GraphError::ExecutionError(
format!("Node '{}' not found", current_node)
))?;
let config = NodeConfig {
recursion_limit: self.recursion_limit,
debug: false,
metadata: HashMap::new(),
};
// ★ 执行节点!虚表 dispatch 到 SyncNode::execute 或 AsyncNode::execute
let update = node.execute(&state, Some(config)).await?;
// ★ Reducer 合并状态
if let Some(new_state) = update.update {
state = self.default_reducer.reduce(&state, &new_state);
}
steps.push(ExecutionStep::node(current_node.clone(), update.metadata.clone()));
// 可选:出节点后打断
if self.interrupt_after.contains(¤t_node) {
return Err(GraphError::ExecutionInterrupted(format!("after_{}", current_node)));
}
// ★ 路由决策:找下一个节点
let next_node = self.find_next_node(¤t_node, &state).await?;
// 可选:每步完成存检查点
if let Some(ref checkpointer) = self.checkpointer {
let checkpoint_id = checkpointer.lock().await.save(&state).await?;
steps.push(ExecutionStep::checkpoint(checkpoint_id, next_node.clone()));
}
current_node = next_node; // 跳转
}
if recursion_count >= self.recursion_limit {
return Err(GraphError::RecursionLimitReached(self.recursion_limit));
}
Ok(GraphInvocation {
final_state: state,
steps,
recursion_count,
})
}
拿个真实例子跑一遍
假设构建了这样一个图:
sql
START → step1 → step2 → END
step1 的逻辑:记一条消息 "第一步完成"
step2 的逻辑:记一条消息 "第二步完成",把输出设成 "done"
编译后的 CompiledGraph 内部数据:
swift
nodes: {
"step1": Arc<SyncNode<...>>, // 存了闭包: |state| { ... 记消息 ... }
"step2": Arc<SyncNode<...>>, // 存了闭包: |state| { ... 记消息, 设输出 ... }
}
edges: [
Fixed("__start__", "step1"),
Fixed("step1", "step2"),
Fixed("step2", "__end__"),
]
entry_point: "step1"
recursion_limit: 25
default_reducer: ReplaceReducer
调用 invoke(AgentState::new("hello"))。
逐轮跟踪
准备工作
ini
state = AgentState { input: "hello", messages: ["hello"], output: None }
current_node = "step1" ← 从 entry_point 拿到的
recursion_count = 0
steps = []
第1轮循环
① 检查 :current_node = "step1",不是 END,继续。
② 找节点 :从 nodes HashMap 里取 "step1",拿到 Arc<SyncNode<...>>。
③ 执行节点:
scss
nodes["step1"].execute(&state).await
内部调闭包:
rust
|state| {
let mut new_state = state.clone();
new_state.add_message(MessageEntry::ai("第一步完成".to_string()));
Ok(StateUpdate::full(new_state))
}
返回 StateUpdate { update: Some(新的 state), metadata: {} }。
④ Reducer 合并:
css
旧的 state:{ input: "hello", messages: ["hello"], output: None }
新的 state:{ input: "hello", messages: ["hello", AI("第一步完成")], output: None }
↑ 新增了一条消息
用 ReplaceReducer:旧的整个被替换成新的。
⑤ 记步骤 :steps.push(("step1", 执行记录))。
⑥ 找下一个节点:
bash
find_next_node("step1", state)
扫描 edges,找到 source == "step1" 的边
→ Fixed { source: "step1", target: "step2" }
→ 返回 "step2"
⑦ 跳转 :current_node = "step2"。
第2轮循环
① 检查 :current_node = "step2",不是 END,继续。
② 找节点 :从 nodes 取 "step2"。
③ 执行节点:
scss
nodes["step2"].execute(&state).await
内部调闭包:
rust
|state| {
let mut new_state = state.clone();
new_state.add_message(MessageEntry::ai("第二步完成".to_string()));
new_state.set_output("done".to_string());
Ok(StateUpdate::full(new_state))
}
返回 StateUpdate { update: Some(更新的 state), ... }。
④ Reducer 合并:
css
旧的 state:{ input: "hello", messages: ["hello", AI("第一步完成")], output: None }
新的 state:{ input: "hello", messages: ["hello", AI("第一步完成"), AI("第二步完成")], output: Some("done") }
⑤ 记步骤 :steps.push(("step2", ...))。
⑥ 找下一个节点:
bash
find_next_node("step2", state)
→ Fixed { source: "step2", target: "__end__" }
→ 返回 "__end__"
⑦ 跳转 :current_node = "__end__"。
第3轮:循环条件检查
current_node = "__end__",条件 current_node != END 不成立 → 退出循环。
返回结果
rust
Ok(GraphInvocation {
final_state: AgentState {
input: "hello",
messages: ["hello", AI("第一步完成"), AI("第二步完成")],
output: Some("done"),
},
steps: [ExecutionStep::Node("step1"), ExecutionStep::Node("step2")],
recursion_count: 2,
})
整个流程用时间线看
ini
时间 ──────────────────────────────────────────────────────────────→
invoke() 被调用
│
├─ 初始状态: state = { input: "hello", messages: [], output: None }
│
├─ [第1轮] current_node = "step1"
│ ├─ nodes["step1"].execute(&state) ← 执行节点
│ ├─ reducer.reduce(state, update) ← 合并状态
│ ├─ find_next_node("step1") → "step2" ← 路由决策
│ └─ current_node = "step2"
│
├─ [第2轮] current_node = "step2"
│ ├─ nodes["step2"].execute(&state)
│ ├─ reducer.reduce(state, update)
│ ├─ find_next_node("step2") → "__end__"
│ └─ current_node = "__end__"
│
├─ [第3轮] current_node == END → 退出
│
└─ 返回 GraphInvocation { final_state, steps, recursion_count }
invoke 在做什么
把上面这个例子抽象一下,invoke 本质上就是:
sql
有一个"当前节点"指针,初始指向 entry_point
循环:
如果当前节点是 END → 结束
从 HashMap 里找到当前节点对应的"执行函数"
调用它,传入当前状态
拿到返回值(状态更新)
用 Reducer 把更新合并进当前状态
找"下一个节点去哪":
- Fixed 边:直接去目标节点
- Conditional 边:调路由函数,根据返回值决定去哪个
- FanOut 边:取第一个目标(主要在 invoke_parallel 里用)
把"当前节点"指向"下一个节点"
继续循环
或
超过最大步数?"} Check -->|否| FindNode["从 HashMap 找当前节点"] FindNode --> Execute["执行: node.execute(&state)"] Execute --> Reduce["合并: reducer.reduce(state, update)"] Reduce --> Route["路由: find_next_node(current, state)"] Route --> Jump["current_node = next_node"] Jump --> Check Check -->|是| Return["返回 GraphInvocation"]
Reducer 是干什么的
节点执行完返回的不是"新状态",而是"状态更新"(StateUpdate<S>):
rust
pub struct StateUpdate<S> {
pub update: Option<S>, // 新的状态数据
pub metadata: HashMap<String, JsonValue>, // 调试信息
}
Reducer 决定「怎么把更新合并进当前状态」。两种最常见的:
css
ReplaceReducer(默认):
旧状态 = { name: "张三", score: 10 }
新状态 = { name: "李四", score: 20 }
─────────────────────────────────
结果 = { name: "李四", score: 20 } ← 完全替换
AppendMessagesReducer(对话历史用):
旧状态 = { messages: ["你好"] }
新状态 = { messages: ["我在吗?"] }
─────────────────────────────────
结果 = { messages: ["你好", "我在吗?"] } ← 追加而不是替换
为什么需要这个机制? 如果两个节点各自加了一条消息,都用 ReplaceReducer,最后只有第二个节点的消息存活。用了 AppendMessagesReducer,两条消息都保留。
五、路由机制(find_next_node)
rust
async fn find_next_node(&self, current: &str, state: &S) -> GraphResult<String> {
for edge in &self.edges {
if edge.source() == current {
match edge {
GraphEdge::Fixed { target, .. } => {
return Ok(target.clone()); // 直接返回固定目标
}
GraphEdge::Conditional { router_name, targets, default_target, .. } => {
let router = self.conditional_routers.get(router_name)?;
let route_key = router.route(state).await?; // ★ 执行路由函数
let target = targets.get(&route_key)
.or_else(|| default_target.as_ref())
.ok_or_else(|| GraphError::RoutingError(...))?;
return Ok(target.clone());
}
GraphEdge::FanOut { targets, .. } => {
return Ok(targets[0].clone()); // ★ 只返回第一个!其他在 invoke_parallel 处理
}
GraphEdge::FanIn { .. } => {
continue; // ★ 跳过,FanIn 由 merge 逻辑处理
}
}
}
}
Err(GraphError::RoutingError("No outgoing edge"))
}
5.1 条件路由完整流程
scss
Analyze 节点执行完毕
↓
find_next_node("analyze", state)
↓
condition_routers["length_router"].route(&state)
↓
FunctionRouter::route // 实际就是调闭包
↓
如果 input.len() < 10 返回 "short",否则 "long"
↓
在 targets 中查找 "short" → "quick_process",或 "long" → "detailed_process"
↓
返回对应的下一个节点名
六、并行执行(invoke_parallel + FanOut/FanIn)
rust
pub async fn invoke_parallel(&self, input: S) -> GraphResult<ParallelInvocation<S>> {
// ... 与 invoke 相同的主循环 ...
while current_node != END && recursion_count < self.recursion_limit {
let fan_out_targets = self.find_fan_out_targets(¤t_node);
if let Some(targets) = fan_out_targets {
// ★★★ 遇到 FanOut 边:并行执行所有分支
let branch_results = self.execute_parallel_branches(&targets, &state).await?;
// 收集分支结果
for (name, inv) in branch_results {
parallel_branches.push(ParallelBranch { name, final_state, steps });
}
// ★ 找 FanIn 合并点
let merge_target = self.find_fan_in_target(&targets);
if let Some(merge_node) = merge_target {
state = self.merge_parallel_states(¶llel_branches);
current_node = merge_node;
} else {
current_node = END;
}
} else {
// 普通节点,跟 invoke 一样
}
}
}
6.1 并行分支执行
rust
async fn execute_parallel_branches(&self, targets: &[String], state: &S) -> ... {
// ★★★ 用 join_all 同时启动 N 个分支,每个分支独立跑 invoke_from_node
let futures: Vec<_> = targets.iter()
.filter(|t| *t != END)
.map(|target| {
let state_clone = state.clone();
async move {
self.invoke_from_node(target.clone(), state_clone).await
}
})
.collect();
let results = join_all(futures).await; // 所有分支真正并行
// ...
}
6.2 并行分支合并
rust
fn merge_parallel_states(&self, branches: &[ParallelBranch<S>]) -> S {
let mut merged = branches[0].final_state.clone();
for branch in branches.iter().skip(1) {
merged = self.default_reducer.reduce(&merged, &branch.final_state);
}
merged
}
逐个用 Reducer 合并,相当于把所有分支的状态累加起来。如果用的是 AppendMessagesReducer,每个分支追加的消息都会被保留。
七、流式执行(stream)
rust
pub async fn stream(&self, input: S) -> GraphResult<Vec<StreamEvent<S>>> {
let mut events = Vec::new();
events.push(StreamEvent::start(state.clone())); // ❶ 开始
while current_node != END && ... {
events.push(StreamEvent::enter_node(name, state)); // ❷ 进入节点
let update = node.execute(&state, ...).await?;
events.push(StreamEvent::node_complete(name, update)); // ❸ 节点完成
if let Some(new_state) = update.update {
state = reducer.reduce(&state, &new_state);
events.push(StreamEvent::state_update(state.clone())); // ❹ 状态更新
}
current_node = find_next_node(...);
}
events.push(StreamEvent::end(state)); // ❺ 结束
Ok(events)
}
事件类型枚举:
rust
pub enum StreamEvent<S> {
Start(S), // 图开始,附带初始状态
EnterNode(String, S), // 进入某个节点,附带当前状态
NodeComplete(String, StateUpdate<S>), // 节点执行完毕,附带状态更新
StateUpdate(S), // Reducer 合并后的新状态
End(S), // 图执行完毕,附带最终状态
}
八、中断与恢复(Human-in-the-loop)
rust
compiled_graph
.with_interrupt_before(vec!["review_step".into()]) // 进入前打断
.with_interrupt_after(vec!["llm_call".into()]); // 退出后打断
执行时效果:
scss
invoke 开始
↓ 执行 entry_point
↓ 检查 interrupt_before → 命中则返回 ExecutionInterrupted("review_step")
↓ 用户可以检查/修改状态
↓ 调用 resume(execution_context) 继续
↓ 执行 review_step
↓ 检查 interrupt_after → 命中则返回 ExecutionInterrupted("after_llm_call")
↓ ...
恢复执行的 invoke_with_execution 会:
- 如果中断点是
after_X:先执行find_next_node(X)跳到下一节点 - 如果中断点是
before_X:从 X 节点开始执行 - 继续主循环直到 END
九、三种执行模式对比
| 特征 | invoke |
stream |
invoke_parallel |
|---|---|---|---|
| 返回值 | GraphInvocation { state, steps } |
Vec<StreamEvent> |
ParallelInvocation { state, steps, branches } |
| FanOut 处理 | 取第一个分支,忽略其他 | 取第一个分支,忽略其他 | 所有分支并行,结果合并 |
| Checkpointer | 每步检查点 | 否 | 每步检查点 |
| 适用场景 | 完整一次执行 | 实时进度展示 | 分叉合并型工作流 |
| 性能 | 快 | 中等(收集事件有开销) | 分支并行,总体快 |
十、完整执行示例
构建一个简单的线性图:
rust
let compiled = GraphBuilder::<AgentState>::new()
.add_node_fn("step1", |s| { /* 处理 */ Ok(StateUpdate::full(...)) })
.add_node_fn("step2", |s| { /* 处理 */ Ok(StateUpdate::full(...)) })
.add_edge(START, "step1")
.add_edge("step1", "step2")
.add_edge("step2", END)
.compile()?;
let result = compiled.invoke(AgentState::new("hello")).await?;
内部执行流程:
css
StateGraph 的内存状态(编译前):
nodes: { "step1" => Arc<SyncNode>, "step2" => Arc<SyncNode> }
edges: [ Fixed("__start__", "step1"), Fixed("step1", "step2"), Fixed("step2", "__end__") ]
entry_point: None (compile 时会自动找 START 的第一个 target)
conditional_routers: {}
编译后:
CompiledGraph.entry_point = "step1"
CompiledGraph.recursion_limit = 25
调用 invoke(AgentState { input: "hello", messages: [...], steps: [], output: None }):
┌─ 循环开始 ──────────────────────────────────────────────────┐
│ │
│ 第1轮: current_node = "step1" │
│ node = nodes["step1"] │
│ update = step1.execute(&state) → StateUpdate { update: Some(new_state) } │
│ state = ReplaceReducer.reduce(state, new_state) │
│ steps.push(ExecutionStep::node("step1")) │
│ next = find_next_node("step1") │
│ → 扫描 edges,找到 Fixed { source: "step1", target: "step2" } │
│ → 返回 "step2" │
│ current_node = "step2" │
│ │
│ 第2轮: current_node = "step2" │
│ node = nodes["step2"] │
│ update = step2.execute(&state) │
│ state = reducer.reduce(state, new_state) // 比如 set_output("done") │
│ next = find_next_node("step2") │
│ → 扫描 edges,找到 Fixed { source: "step2", target: "__end__" } │
│ → 返回 "__end__" │
│ current_node = "__end__" │
│ │
│ 第3轮: current_node == END → 退出循环 │
└──────────────────────────────────────────────────────────────┘
Ok(GraphInvocation {
final_state: AgentState { input: "hello", output: Some("done"), messages: [..., ...] },
steps: [Node("step1"), Node("step2")],
recursion_count: 2
})
十一、代码中的边界与注意事项
-
FanOut 在
invoke和stream中不会被并行执行 ---find_next_node遇到 FanOut 只返回targets[0],其他分支被忽略。只有invoke_parallel才会真正并行。 -
merge_parallel_states顺序依赖 --- 分支顺序决定了合并后的消息顺序。如果使用AppendMessagesReducer,messages 最终是[分支1的消息..., 分支2的消息..., ...]。 -
条件路由没有缓存 --- 如果多个条件边指向同一个路由函数名,每次
find_next_node都重新调一次router.route()。 -
find_next_node是线性扫描 --- 每次循环都遍历整个edges列表。对于大规模图可能是性能瓶颈。 -
中断恢复不会重放检查点 ---
resume()直接从上次中断的位置继续,之前的状态由调用方通过GraphExecution传递。
十二、关键源码文件索引
| 文件 | 内容 |
|---|---|
crates/langchainrust/src/langgraph/graph.rs |
StateGraph 构建器 + GraphBuilder 流畅 API |
crates/langchainrust/src/langgraph/compiled.rs |
CompiledGraph 执行引擎 + invoke/stream/invoke_parallel |
crates/langchainrust/src/langgraph/state.rs |
StateSchema trait + AgentState + Reducer 系列 |
crates/langchainrust/src/langgraph/node.rs |
GraphNode trait + SyncNode/AsyncNode/FunctionNode |
crates/langchainrust/src/langgraph/edge.rs |
GraphEdge 枚举 + ConditionalEdge trait + FunctionRouter |
crates/langchainrust/src/langgraph/mod.rs |
模块导出 + 核心概念文档 |
src/services/langgraph_service.rs |
应用层:并行/条件/流式三种演示 + LLM 任务拆解 |
src/services/agent_executor.rs |
应用层:真实 Agent 引擎(非 LangGraph,但概念类似) |