# LangChainRust Agent 引擎:Graph 构建到执行

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,最灵活

关键区别:SyncNodeexecute() 把同步函数包在 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(&current_node) {
            return Err(GraphError::ExecutionInterrupted(current_node.clone()));
        }

        recursion_count += 1;

        // ★ 从 HashMap 找到当前节点的执行函数
        let node = self.nodes.get(&current_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(&current_node) {
            return Err(GraphError::ExecutionInterrupted(format!("after_{}", current_node)));
        }

        // ★ 路由决策:找下一个节点
        let next_node = self.find_next_node(&current_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(&current_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(&parallel_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 会:

  1. 如果中断点是 after_X:先执行 find_next_node(X) 跳到下一节点
  2. 如果中断点是 before_X:从 X 节点开始执行
  3. 继续主循环直到 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
  })

十一、代码中的边界与注意事项

  1. FanOut 在 invokestream 中不会被并行执行 --- find_next_node 遇到 FanOut 只返回 targets[0],其他分支被忽略。只有 invoke_parallel 才会真正并行。
  2. merge_parallel_states 顺序依赖 --- 分支顺序决定了合并后的消息顺序。如果使用 AppendMessagesReducer,messages 最终是 [分支1的消息..., 分支2的消息..., ...]
  3. 条件路由没有缓存 --- 如果多个条件边指向同一个路由函数名,每次 find_next_node 都重新调一次 router.route()
  4. find_next_node 是线性扫描 --- 每次循环都遍历整个 edges 列表。对于大规模图可能是性能瓶颈。
  5. 中断恢复不会重放检查点 --- 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,最灵活

关键区别:SyncNodeexecute() 把同步函数包在 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(&current_node) {
            return Err(GraphError::ExecutionInterrupted(current_node.clone()));
        }

        recursion_count += 1;

        // ★ 从 HashMap 找到当前节点的执行函数
        let node = self.nodes.get(&current_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(&current_node) {
            return Err(GraphError::ExecutionInterrupted(format!("after_{}", current_node)));
        }

        // ★ 路由决策:找下一个节点
        let next_node = self.find_next_node(&current_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 里用)
    
    把"当前节点"指向"下一个节点"
    继续循环
graph TD Start["invoke(initial_state)"] --> Check{"current == END

超过最大步数?"} 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(&current_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(&parallel_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 会:

  1. 如果中断点是 after_X:先执行 find_next_node(X) 跳到下一节点
  2. 如果中断点是 before_X:从 X 节点开始执行
  3. 继续主循环直到 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
  })

十一、代码中的边界与注意事项

  1. FanOut 在 invokestream 中不会被并行执行 --- find_next_node 遇到 FanOut 只返回 targets[0],其他分支被忽略。只有 invoke_parallel 才会真正并行。

  2. merge_parallel_states 顺序依赖 --- 分支顺序决定了合并后的消息顺序。如果使用 AppendMessagesReducer,messages 最终是 [分支1的消息..., 分支2的消息..., ...]

  3. 条件路由没有缓存 --- 如果多个条件边指向同一个路由函数名,每次 find_next_node 都重新调一次 router.route()

  4. find_next_node 是线性扫描 --- 每次循环都遍历整个 edges 列表。对于大规模图可能是性能瓶颈。

  5. 中断恢复不会重放检查点 --- 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,但概念类似)
相关推荐
彭于晏Yan2 小时前
Spring Boot 聚合MongoDB查询
spring boot·后端·mongodb
Nyarlathotep01132 小时前
并发集合类(3):LinkedBlockingQueue
java·后端
Apifox2 小时前
Apifox 近期更新|AI Agent Debugger、A2A Debugger、Postman API 导入、Ask AI 侧边栏对话
前端·人工智能·后端
知识浅谈2 小时前
面向方面编程(AOP)VS 面向对象编程(OOP)
后端
IT空门:门主2 小时前
spring ai alibaba -流式+invoke的人工介入的实现
java·后端·spring
fliter3 小时前
4 个字节拿到 root 权限:Linux 内核漏洞"Copy Fail"与 Cloudflare 的应急处置全记录
后端
fliter3 小时前
Cloudflare 推出 Flagship:为 AI 时代重新设计的功能开关服务
后端·算法
掘金者阿豪3 小时前
折腾了两天,终于把SQLAlchemy连上了金仓数据库
后端
SamDeepThinking3 小时前
RocketMQ消息可靠性的三道关卡
java·后端·程序员