Edge 详细分析

Edge 详细分析

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


1. 类的定位

EdgeStateGraph边的运行时表示 ,描述一条从源节点出发的有向连接关系。使用 Java 16 record 定义,核心由两个字段组成:

java 复制代码
public record Edge(String sourceId, List<EdgeValue> targets) {}
字段 类型 说明
sourceId String 出发节点的 ID
targets List<EdgeValue> 目标列表,1 条边可以有多个目标(并行分叉)

2. 数据层次结构

三层嵌套关系:

yaml 复制代码
Edge
└── targets: List<EdgeValue>
         ├── EdgeValue(id)             ← 普通边,固定目标节点 ID
         └── EdgeValue(EdgeCondition)  ← 条件边,运行时动态决定目标
                   ├── action: AsyncCommandAction       ← 单路由
                   ├── action: AsyncMultiCommandAction  ← 并行多路由
                   └── mappings: Map<String, String>    ← 命令 key → 节点 ID

EdgeValue 是一个联合类型(union-like record):

java 复制代码
public record EdgeValue(String id, EdgeCondition value) {
    // id != null   → 普通边,目标是固定节点
    // value != null → 条件边,目标由运行时决定
}

两个字段互斥,只会有一个非 null。


3. 三类构造形态

java 复制代码
// 形态 1:1 对 1 普通边(最常见)
new Edge("nodeA", new EdgeValue("nodeB"))
//   sourceId = "nodeA"
//   targets  = [EdgeValue(id="nodeB", value=null)]

// 形态 2:并行分叉边(1 对多,同时触发多个节点)
new Edge("nodeA", List.of(
    new EdgeValue("nodeB"),
    new EdgeValue("nodeC")
))
//   targets.size() == 2  →  isParallel() == true

// 形态 3:条件边(目标由运行时 action 决定)
new Edge("nodeA", new EdgeValue(EdgeCondition.single(action, mappings)))
//   targets = [EdgeValue(id=null, value=EdgeCondition)]

4. 关键方法逐一分析

isParallel()

java 复制代码
public boolean isParallel() {
    return targets.size() > 1;
}

只要 targets 超过 1 个即为并行边。ParallelEdgeProcessor 在运行时通过此方法决定是否同时触发多个下游节点。


target()

java 复制代码
public EdgeValue target() {
    if (isParallel()) {
        throw new IllegalStateException(...); // 并行边禁止调用
    }
    return targets.get(0);
}

安全获取单目标。调用方(ParallelEdgeProcessor)须先判断 isParallel() == false 再调用,否则抛出异常。


anyMatchByTargetId(String targetId)

java 复制代码
public boolean anyMatchByTargetId(String targetId) {
    return targets().stream()
        .anyMatch(v -> (v.id() != null)
            ? Objects.equals(v.id(), targetId)
            : v.value().mappings().containsValue(targetId)
        );
}

两种查找路径

EdgeValue 类型 查找方式
普通边(id != null 直接比较节点 ID
条件边(value != null 检查 mappings 的 values 中是否包含目标 ID

Edges.edgesByTargetId() 调用,用于反向遍历(子图展开时查找所有指向某节点的边)。


withSourceAndTargetIdsUpdated(...) --- 子图展开核心

java 复制代码
public Edge withSourceAndTargetIdsUpdated(
    Node node,
    Function<String, String> newSourceId,
    Function<String, EdgeValue> newTarget
) {
    var newTargets = targets().stream()
        .map(t -> t.withTargetIdsUpdated(newTarget))
        .toList();
    return new Edge(newSourceId.apply(sourceId), newTargets);
}

子图内联展开时的核心方法,将子图的边"重命名"后拼接到父图。

展开示意

sql 复制代码
父图:  START → [subgraph] → nodeX
子图:  START → s1 → s2 → END

展开后:
  START → subgraph::s1 → subgraph::s2 → nodeX

withSourceAndTargetIdsUpdated 把子图每条边的 source/target 从 s1 重命名为 subgraph::s1,避免与父图节点 ID 冲突。

EdgeValue.withTargetIdsUpdated 中条件边的处理:

java 复制代码
// 条件边:重命名 mappings 中每个 value(节点 ID)
var newMappings = value.mappings().entrySet().stream()
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        e -> {
            var v = target.apply(e.getValue());
            return (v.id() != null) ? v.id() : e.getValue(); // 无法重命名则保留原值
        }
    ));

5. validate() 校验逻辑

编译期由 StateGraph.validateGraph() 触发,分三层校验:

sql 复制代码
validate(nodes)
  ├─ 1. sourceId 存在检查(START 豁免)
  ├─ 2. 并行边重复 target 检查
  └─ 3. 逐个 EdgeValue 校验
          ├─ 普通边:target 节点必须存在(END 豁免)
          ├─ 条件边:mappings 所有 value 节点必须存在(END 豁免)
          └─ 两者皆 null:抛出 invalidEdgeTarget
java 复制代码
public void validate(StateGraph.Nodes nodes) throws GraphStateException {
    // 1. source 节点必须存在
    if (!Objects.equals(sourceId(), START) && !nodes.anyMatchById(sourceId()))
        throw Errors.missingNodeReferencedByEdge.exception(sourceId());

    // 2. 并行边不能有重复 target
    if (isParallel()) {
        Set<String> duplicates = targets.stream()
            .collect(Collectors.groupingBy(EdgeValue::id, Collectors.counting()))
            .entrySet().stream()
            .filter(e -> e.getValue() > 1)
            .map(Map.Entry::getKey)
            .collect(Collectors.toSet());
        if (!duplicates.isEmpty())
            throw Errors.duplicateEdgeTargetError.exception(sourceId(), duplicates);
    }

    // 3. 每个 target 单独校验
    for (EdgeValue target : targets) validate(target, nodes);
}

6. equals / hashCode 的设计取舍

java 复制代码
@Override
public boolean equals(Object o) {
    return Objects.equals(sourceId, node.sourceId); // 仅比较 sourceId
}

@Override
public int hashCode() {
    return Objects.hash(sourceId);
}

只用 sourceId 做相等判断,忽略 targets,这是刻意为之。

StateGraph.addEdge() 利用这一特性,通过 indexOf() 检测同 source 的边是否已存在:

  • 若不存在 → 直接添加新边
  • 若已存在 → 合并 targets(追加并行目标),自动形成并行边
java 复制代码
// StateGraph.addEdge() 的合并逻辑
int index = edges.elements.indexOf(newEdge);
if (index >= 0) {
    var newTargets = new ArrayList<>(edges.elements.get(index).targets());
    newTargets.add(newEdge.target()); // 追加新目标
    edges.elements.set(index, new Edge(sourceId, newTargets));
} else {
    edges.elements.add(newEdge);
}

因此 addEdge("A","B") + addEdge("A","C") 会自动合并为一条 isParallel() == true 的并行边。


7. EdgeCondition 详解

java 复制代码
public record EdgeCondition(Object action, Map<String, String> mappings) {}
方法 说明
EdgeCondition.single(action, mappings) 创建单路由条件(AsyncCommandAction
EdgeCondition.multi(action, mappings) 创建多路由条件(AsyncMultiCommandAction
isMultiCommand() 判断是否为多路由
singleAction() 获取单路由 action(多路由时返回 null)
multiAction() 获取多路由 action(单路由时返回 null)

action 字段类型为 Object,运行时通过 instanceof 区分两种路由类型,编译器在构造函数中做类型守卫:

java 复制代码
public EdgeCondition {
    if (action != null
        && !(action instanceof AsyncCommandAction)
        && !(action instanceof AsyncMultiCommandAction)) {
        throw new IllegalArgumentException("Action must be either AsyncCommandAction or AsyncMultiCommandAction");
    }
}

8. 完整调用关系

scss 复制代码
StateGraph.addEdge()
  └─→ new Edge(sourceId, EdgeValue(targetId))
        └─→ 存入 Edges.elements(LinkedList)

StateGraph.addConditionalEdges()
  └─→ new Edge(sourceId, EdgeValue(EdgeCondition.single(action, mappings)))

StateGraph.addParallelConditionalEdges()
  └─→ new Edge(sourceId, EdgeValue(EdgeCondition.multi(action, mappings)))

编译期(ProcessedNodesEdgesAndConfig):
  └─→ edge.isParallel()                    ← 子图入口不支持并行分叉,校验用
  └─→ edge.anyMatchByTargetId(END)          ← 过滤子图指向 END 的边
  └─→ edge.withSourceAndTargetIdsUpdated()  ← 子图节点 ID 重命名,内联到父图

运行时(ParallelEdgeProcessor):
  └─→ edge.isParallel()   ← 决定走并行分支还是单路径
  └─→ edge.target()       ← 单路径时取唯一目标
  └─→ edge.targets()      ← 并行时遍历所有目标,递归查找汇聚点

9. 总结

维度 说明
类型 Java Record(不可变)
核心设计 targets 为列表而非单值,统一表达普通边和并行边
联合类型 EdgeValueid / value 互斥字段区分固定路由和动态路由
equals 策略 仅比较 sourceId,支持同 source 多 target 的累积语义
子图展开 withSourceAndTargetIdsUpdated 是子图内联的核心工具方法
校验时机 编译期严格检查节点引用完整性,运行期不再重复校验

相关文件

  • StateGraph.md --- 图定义层整体分析
  • Edge.java --- 本文分析的源文件
  • EdgeValue.java --- 边目标的联合类型
  • EdgeCondition.java --- 条件边的路由条件封装
  • ParallelEdgeProcessor.java --- 运行时并行边处理器
  • ProcessedNodesEdgesAndConfig.java --- 编译期子图展开逻辑
相关推荐
倚栏听风雨9 小时前
AsyncCommandAction 详细分析
后端
倚栏听风雨9 小时前
CompiledGraph 详细分析
后端
装不满的克莱因瓶9 小时前
【项目亮点四】支付订单超时处理与状态补偿机制设计
java·开发语言·后端·rabbitmq·消息中间件
楼田莉子9 小时前
C#学习:分支与循环
服务器·后端·学习·c#
XovH9 小时前
Django 从 0 到 1 打造完整电商平台:商品列表页实现
后端
kunge20139 小时前
Claude Code Hooks 类型与使用指南
人工智能·后端·程序员
枕星而眠9 小时前
Linux 进程:虚拟内存、Fork原理、IPC通信与面试避坑
linux·运维·c语言·后端
倒流时光三十年9 小时前
JAVA 设计模式 之 责任链模式
后端
彦为君9 小时前
Spring AOP 原理深度解析:从动态代理到切面织入(最新!Spring6与Spring5的差异)
java·后端·spring