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 --- 编译期子图展开逻辑
相关推荐
智驭未来掌门人17 分钟前
利用FFmpeg快速实现一个RTSP推流服务器
后端
垚垚学技术_聚焦云原生29 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide34 分钟前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_37 分钟前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐1 小时前
安全地生成验证码、密码和 Token
后端
小强19881 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak1 小时前
Python运算符与流程控制:if条件判断
后端