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 --- 编译期子图展开逻辑
相关推荐
千里码aicood1 分钟前
flask-基于注意力机制的异常流量检测与自动防御系统
后端·python·flask
李迟3 分钟前
Golang实践录:工程框架实践:搭建工程模板
开发语言·后端·golang
Lynko9 分钟前
C语言指针高阶
后端
the局外人9 分钟前
学习 FastAPI 的 Day 2:用异步 ORM 完成增删改查
后端·python·fastapi
SamDeepThinking14 分钟前
if嵌套最好控制在3层以内
java·后端·程序员
苏渡苇17 分钟前
Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate
java·后端·spring·spring cloud·springboot·性能监控
tonydf17 分钟前
AI驱动历史项目安全审查
后端·ai编程
她的男孩33 分钟前
同一个接口,为什么销售只看到自己的订单?我拆了 681 行数据权限拦截器的源码
java·后端·架构
元界metalite35 分钟前
SpringBoot接口通用响应对象设计 - 一个 Resp 管理五种责任
后端
对象存储与RustFS38 分钟前
把 RustFS 跑在 Kubernetes 上:官方 Helm Chart 从单机到分布式
后端·rust·开源