EdgeValue 详细分析

EdgeValue 详细分析

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


1. 类的定位

EdgeValue 是边目标的**联合类型(Union Type)**封装,用一个 record 同时表达两种互斥的路由语义:

yaml 复制代码
EdgeValue
  ├── id != null    → 普通边:目标节点在图定义时就确定
  └── value != null → 条件边:目标节点在运行时由 action 动态决定

两个字段永远只有一个非 null,是用 Java record 模拟代数数据类型(ADT)的惯用写法。


2. 字段与构造器

java 复制代码
public record EdgeValue(String id, EdgeCondition value) {}
构造器 id value 语义
new EdgeValue("nodeB") "nodeB" null 普通边,固定指向 nodeB
new EdgeValue(EdgeCondition.single(...)) null EdgeCondition 条件边,单路由
new EdgeValue(EdgeCondition.multi(...)) null EdgeCondition 条件边,多路由并行

3. 在 Edge.targets 中的位置

EdgetargetsList<EdgeValue>,每条边可以持有多个目标:

scss 复制代码
Edge("nodeA", targets)
        │
        ├── [EdgeValue("nodeB")]                     → 普通单边
        ├── [EdgeValue("nodeB"), EdgeValue("nodeC")]  → 并行分叉(isParallel=true)
        └── [EdgeValue(EdgeCondition)]                → 条件路由

EdgeValue 出现在列表中的每一项,是路由决策的最小单元。


4. 运行时消费路径:nextNodeId(EdgeValue route, ...)

EdgeValue 最核心的消费者是 CompiledGraphGraphRunnerContext 中的 nextNodeId 方法,对其做分支派发:

java 复制代码
private Command nextNodeId(EdgeValue route, ...) throws Exception {

    // 分支 1:普通边 ------ 直接返回固定节点 ID
    if (route.id() != null) {
        return new Command(route.id(), state);
    }

    // 分支 2:条件边 ------ 进一步区分单路由 vs 多路由
    if (route.value() != null) {
        var edgeCondition = route.value();

        if (edgeCondition.isMultiCommand()) {
            // 多路由:交给动态创建的 ConditionalParallelNode 处理
            String conditionalParallelNodeId = ParallelNode.formatNodeId(nodeId);
            return new Command(conditionalParallelNodeId, state);
        } else {
            // 单路由:执行 singleAction,用返回的 key 查 mappings 得到真实节点 ID
            var singleAction = edgeCondition.singleAction();
            var command = singleAction.apply(derefState, config).get();
            String result = route.value().mappings().get(command.gotoNode());
            return new Command(result, currentState);
        }
    }

    throw RunnableErrors.executionError.exception(...);
}

完整的派发决策树:

scss 复制代码
EdgeValue
  ├── id != null
  │     └─→ Command(id)                          直接跳转固定节点
  └── value != null (EdgeCondition)
        ├── isMultiCommand() == true
        │     └─→ Command(ConditionalParallelNode) 并行路由,交给并行节点
        └── isMultiCommand() == false
              └─→ singleAction.apply(state)
                    └─→ command.gotoNode() → mappings.get(key)
                          └─→ Command(realNodeId)  条件单路由

5. withTargetIdsUpdated 深度解析

包内访问权限,唯一实例方法,仅供 Edge.withSourceAndTargetIdsUpdated 调用:

java 复制代码
EdgeValue withTargetIdsUpdated(Function<String, EdgeValue> target) {
    if (id != null) {
        // 普通边:直接将当前 id 传给重命名函数,返回新 EdgeValue
        return target.apply(id);
    }

    // 条件边:遍历 mappings 中每个 value(节点 ID),逐个重命名
    var newMappings = value.mappings().entrySet().stream()
        .collect(Collectors.toMap(
            Map.Entry::getKey,           // key 不变(命令返回值)
            e -> {
                var v = target.apply(e.getValue());   // 对节点 ID 重命名
                return (v.id() != null) ? v.id() : e.getValue(); // 失败则保留原值
            }
        ));

    // 保留原 action,只更新 mappings 中的节点 ID
    return new EdgeValue(null, new EdgeCondition(value.action(), newMappings));
}

为什么条件边只重命名 mappings 而不重命名 action

action 是用户定义的 lambda,其内部逻辑和返回的 key 名称固定不变;mappings 中的 value 才是实际节点 ID,子图展开时需要加命名空间前缀,因此只重命名 mappings 的 values。

展开示例:

rust 复制代码
子图内条件边 mappings:
  { "yes" -> "approve", "no" -> "reject" }

子图展开后(命名空间前缀 "subgraph::"):
  { "yes" -> "subgraph::approve", "no" -> "subgraph::reject" }

6. 在 CompiledGraph 中的存储形式

编译后,StateGraphList<Edge> 被展平为一张平坦的路由表:

java 复制代码
// CompiledGraph 中
final Map<String, EdgeValue> edges = new LinkedHashMap<>();
//   key   = sourceId(节点 ID)
//   value = EdgeValue(该节点的出边目标)

并行分叉边(isParallel() == true)在编译期被处理为中间的 ParallelNode,最终每个节点只对应一个 EdgeValue

java 复制代码
// 并行边 → 插入 ParallelNode,展平为两条普通 EdgeValue
edges.put(e.sourceId(), new EdgeValue(parallelNode.id()));       // A → ParallelNode
edges.put(parallelNode.id(), new EdgeValue(convergenceNodeId));  // ParallelNode → 汇聚节点

getEdge(nodeId) 方法暴露这张路由表供 GraphRunnerContext 查询:

java 复制代码
public EdgeValue getEdge(String nodeId) {
    return edges.get(nodeId);
}

7. anyMatchByTargetId 中的角色

Edge.anyMatchByTargetId 通过 EdgeValue 的两个字段做双路查找:

java 复制代码
.anyMatch(v ->
    (v.id() != null)
        ? Objects.equals(v.id(), targetId)             // 普通边:直接比 id
        : v.value().mappings().containsValue(targetId) // 条件边:查 mappings values
)

这保证了条件边的间接引用也能被反向追踪,子图展开时不会遗漏任何指向目标节点的边。


8. 总结

维度 说明
类型 Java Record(不可变),两字段互斥的联合类型
核心作用 统一表达"固定目标"和"动态目标"两种路由语义
生产者 StateGraph.addEdge / addConditionalEdges / addParallelConditionalEdges
消费者 CompiledGraph.nextNodeIdGraphRunnerContext.nextNodeId 做分支派发
变换工具 withTargetIdsUpdated 用于子图展开时的节点 ID 重命名,包内可见
存储形式 编译后展平为 Map<String, EdgeValue>,每个源节点对应一条路由

相关文件

  • StateGraph.md --- 图定义层整体分析
  • Edge.md --- 边的完整结构分析
  • EdgeValue.java --- 本文分析的源文件
  • EdgeCondition.java --- 条件路由的 action 与 mappings 封装
  • CompiledGraph.java --- 编译后的路由表存储与 nextNodeId 派发
  • GraphRunnerContext.java --- 运行时 nextNodeId 的另一实现
相关推荐
独泪了无痕17 小时前
MyBatis魔法堂:结果集映射
后端·mybatis
copyer_xyf17 小时前
LangChain 调用 LLM
后端·python·agent
copyer_xyf17 小时前
Prompt 组织管理
后端·python·agent
摇滚侠19 小时前
SpringMVC 入门到实战 文件上传 75-77
java·后端·spring·maven·intellij-idea
fox_lht21 小时前
15.3.改进我们之前的输入、输出项目
开发语言·后端·学习·rust
大鸡腿同学21 小时前
用 AI 肝了一个星期的智能客服助手,看看怎么个事
后端
IT_陈寒21 小时前
Python的os.path.join居然能这么坑?
前端·人工智能·后端
张忠琳21 小时前
【Go 1.26.4】Golang Channel 深度解析
开发语言·后端·golang
Rain5091 天前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js