EdgeCondition 详细分析

4 阅读4分钟

EdgeCondition 详细分析

源码路径:spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/internal/edge/EdgeCondition.java


1. 类的定位

EdgeCondition条件边路由逻辑的完整封装,包含两个要素:

  • action:运行时执行的路由函数,根据当前状态决定走哪个分支
  • mappingsaction 返回的逻辑 key → 实际节点 ID 的映射表

它只存在于 EdgeValue.value != null 的条件边场景,普通固定边不涉及此类。

StateGraph.addConditionalEdges()         → EdgeCondition.single(action, mappings)
StateGraph.addParallelConditionalEdges() → EdgeCondition.multi(action, mappings)

2. 字段结构与类型守卫

public record EdgeCondition(Object action, Map<String, String> mappings) {
    public EdgeCondition {
        if (action != null
            && !(action instanceof AsyncCommandAction)
            && !(action instanceof AsyncMultiCommandAction)) {
            throw new IllegalArgumentException("...");
        }
    }
}

action 声明为 Object 而非接口,是因为两种 action 类型没有共同父接口,用 Object + 运行时 instanceof 区分是 Java 中实现联合类型的标准做法。紧凑构造器充当类型守卫,在对象创建时拦截非法类型,而不是等到运行时路由才报错。

mappingsMap<String, String>,key 是 action 返回的逻辑名,value 是图中真实的节点 ID:

mappings = {
    "approved" -> "sendEmail",
    "rejected" -> "archiveRecord",
    "pending"  -> END
}

3. 两种路由模式对比

维度单路由(single多路由并行(multi
action 类型AsyncCommandActionAsyncMultiCommandAction
返回值类型CompletableFuture<Command>CompletableFuture<MultiCommand>
每次路由目标数1 个节点≥1 个节点(并行)
运行时处理直接跳转目标节点插入 ConditionalParallelNode 并行分发
isMultiCommand()falsetrue

4. action 接口族与返回值

AsyncCommandAction → 返回 Command

// 函数签名
BiFunction<OverAllState, RunnableConfig, CompletableFuture<Command>>

// Command record
record Command(String gotoNode, Map<String, Object> update) {}
//   gotoNode → 逻辑 key,通过 mappings 映射为真实节点 ID
//   update   → 可同时携带状态更新(会合并进 OverAllState)

创建方式:

// 方式1:直接 lambda(最常用)
AsyncCommandAction action = (state, config) ->
    completedFuture(new Command(state.value("score") > 80 ? "pass" : "fail"));

// 方式2:从同步 CommandAction 包装
AsyncCommandAction action = AsyncCommandAction.node_async((state, config) ->
    new Command(decide(state)));

// 方式3:从 AsyncEdgeAction 适配(只返回 key,不带状态更新)
AsyncCommandAction action = AsyncCommandAction.of(state ->
    completedFuture(state.value("score") > 80 ? "pass" : "fail"));

AsyncMultiCommandAction → 返回 MultiCommand

// 函数签名
BiFunction<OverAllState, RunnableConfig, CompletableFuture<MultiCommand>>

// MultiCommand record
record MultiCommand(List<String> gotoNodes, Map<String, Object> update) {}
//   gotoNodes → 多个逻辑 key,每个通过 mappings 映射为真实节点 ID
//   gotoNodes 不能为空(构造器有守卫)

创建方式:

// 方式1:直接 lambda
AsyncMultiCommandAction action = (state, config) ->
    completedFuture(new MultiCommand(List.of("nodeA", "nodeB")));

// 方式2:从返回 List<String> 的 Function 适配
AsyncMultiCommandAction action = AsyncMultiCommandAction.of(state ->
    completedFuture(selectBranches(state)));

5. 四个方法的职责

// 工厂方法:语义明确,防止直接用 new EdgeCondition() 写错类型
EdgeCondition.single(action, mappings)  // 单路由
EdgeCondition.multi(action, mappings)   // 多路由并行

// 类型判断:运行时分支派发的入口
boolean isMultiCommand()  // action instanceof AsyncMultiCommandAction

// 类型安全的 action 获取(非对应模式时返回 null)
AsyncCommandAction      singleAction()
AsyncMultiCommandAction multiAction()

singleAction() / multiAction() 返回 null 而非抛异常,调用方须配合 isMultiCommand() 先判断再取值:

// 标准调用模式(来自 CompiledGraph / GraphRunnerContext)
if (edgeCondition.isMultiCommand()) {
    // 多路由分支
} else {
    var singleAction = edgeCondition.singleAction(); // 此处保证非 null
    var command = singleAction.apply(state, config).get();
    String result = edgeCondition.mappings().get(command.gotoNode());
}

6. 编译期处理(CompiledGraph 构造器)

编译时,EdgeCondition 决定了最终的运行时节点图结构:

isMultiCommand() == false(单路由条件边)
  → 直接存入路由表:edges.put(sourceId, target)
  → 运行时由 nextNodeId 执行 singleAction

isMultiCommand() == true(多路由并行条件边)
  → 创建 ConditionalParallelNode,注入 edgeCondition
  → edges.put(sourceId, new EdgeValue(conditionalParallelNode.id()))
  → edges.put(conditionalParallelNode.id(), new EdgeValue(convergenceNodeId))
  → 运行时由 ConditionalParallelNode 内部执行 multiAction

多路由的编译期流程:

// CompiledGraph 构造器中(简化)
if (edgeCondition.isMultiCommand()) {

    // 1. 创建专用并行节点,将 edgeCondition 注入其中
    var conditionalParallelNode = new ConditionalParallelNode(
        e.sourceId(), edgeCondition, nodeFactories, keyStrategyMap, compileConfig);

    // 2. 注册并行节点
    nodeFactories.put(conditionalParallelNode.id(), conditionalParallelNode.actionFactory());

    // 3. 改写路由:原 sourceId → ConditionalParallelNode
    edges.put(e.sourceId(), new EdgeValue(conditionalParallelNode.id()));

    // 4. ConditionalParallelNode → 汇聚节点
    edges.put(conditionalParallelNode.id(), new EdgeValue(convergenceNodeId));
}

图结构变化示意:

编译前(定义层):
  nodeA ──[EdgeCondition.multi]──▶ {nodeB, nodeC}

编译后(运行层):
  nodeA ──▶ ConditionalParallelNode(nodeA)
                    ├──▶ nodeB
                    ├──▶ nodeC
                    └──▶ convergenceNode

7. 运行时执行(nextNodeId 中的单路由路径)

// 1. 取出 EdgeCondition
var edgeCondition = route.value();

// 2. 执行 action,得到 Command(含逻辑 key 和可选状态更新)
var singleAction = edgeCondition.singleAction();
var command = singleAction.apply(derefState, config).get();

// 3. 用 gotoNode(逻辑 key)查 mappings,得到真实节点 ID
String result = edgeCondition.mappings().get(command.gotoNode());
if (result == null) {
    throw RunnableErrors.missingNodeInEdgeMapping.exception(nodeId, command.gotoNode());
}

// 4. 合并 Command 携带的状态更新
var currentState = OverAllState.updateState(state, command.update(), keyStrategyMap);

return new Command(result, currentState);

mappings 在此充当解耦层:action 只需关心业务逻辑(返回 "approved" 还是 "rejected"),不需要知道真实节点叫什么名字,节点 ID 的绑定在图定义时由 mappings 完成。


8. 完整调用链总结

图定义期:
  StateGraph.addConditionalEdges(sourceId, action, mappings)
    └─→ EdgeCondition.single(action, mappings)
          └─→ EdgeValue(null, EdgeCondition)
                └─→ Edge(sourceId, [EdgeValue])
                      └─→ 存入 StateGraph.edges

编译期(CompiledGraph 构造器):
  遍历 processedData.edges()
    └─→ target.value() != null  → 是条件边
          ├─→ isMultiCommand() == true
          │     └─→ 创建 ConditionalParallelNode(edgeCondition)
          │           └─→ 改写 edges 路由表
          └─→ isMultiCommand() == false
                └─→ 直接写入 edges 路由表

运行时(nextNodeId):
  edges.get(nodeId) → EdgeValue
    └─→ value != null → 是条件边
          ├─→ isMultiCommand() == true
          │     └─→ 跳转 ConditionalParallelNode(内部执行 multiAction)
          └─→ isMultiCommand() == false
                └─→ singleAction.apply(state, config)
                      └─→ command.gotoNode() → mappings.get(key) → 真实节点 ID

9. 总结

维度说明
类型Java Record(不可变)
核心字段action(路由函数)+ mappings(key → 节点 ID 的解耦映射)
action 类型Object,用 instanceof 在运行时区分单路由/多路由
类型守卫紧凑构造器在对象创建时拦截非法 action 类型
单路由运行时直接执行 singleAction,查 mappings 得到节点 ID
多路由编译期插入 ConditionalParallelNode,运行时并行分发
mappings 的作用解耦 action 逻辑(返回逻辑 key)与图拓扑(节点 ID),两者独立变化

相关文件

  • StateGraph.md — 图定义层整体分析
  • Edge.md — 边的完整结构分析
  • EdgeValue.md — 边目标联合类型分析
  • EdgeCondition.java — 本文分析的源文件
  • AsyncCommandAction.java — 单路由 action 接口
  • AsyncMultiCommandAction.java — 多路由 action 接口
  • Command.java — 单路由返回值,携带 gotoNode + 状态更新
  • MultiCommand.java — 多路由返回值,携带 gotoNodes 列表
  • CompiledGraph.java — 编译期 ConditionalParallelNode 创建逻辑
  • GraphRunnerContext.java — 运行时 nextNodeId 派发实现