Edge 详细分析
源码路径:
spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/internal/edge/Edge.java
1. 类的定位
Edge 是 StateGraph 中边的运行时表示 ,描述一条从源节点出发的有向连接关系。使用 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 为列表而非单值,统一表达普通边和并行边 |
| 联合类型 | EdgeValue 用 id / value 互斥字段区分固定路由和动态路由 |
| equals 策略 | 仅比较 sourceId,支持同 source 多 target 的累积语义 |
| 子图展开 | withSourceAndTargetIdsUpdated 是子图内联的核心工具方法 |
| 校验时机 | 编译期严格检查节点引用完整性,运行期不再重复校验 |
相关文件
StateGraph.md--- 图定义层整体分析Edge.java--- 本文分析的源文件EdgeValue.java--- 边目标的联合类型EdgeCondition.java--- 条件边的路由条件封装ParallelEdgeProcessor.java--- 运行时并行边处理器ProcessedNodesEdgesAndConfig.java--- 编译期子图展开逻辑