AsyncCommandAction 详细分析

AsyncCommandAction 详细分析

源码路径:spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/action/AsyncCommandAction.java


1. 类的定位

AsyncCommandAction 是条件路由的核心函数接口,定义了一条边在运行时"如何决定下一个节点"的契约。

java 复制代码
public interface AsyncCommandAction
    extends BiFunction<OverAllState, RunnableConfig, CompletableFuture<Command>> {}

它是整个 action 接口族中能力最完整的路由接口:既能读取完整运行时配置(RunnableConfig),又能在返回路由结果的同时携带状态更新(Command.update)。


2. 函数签名拆解

swift 复制代码
BiFunction< OverAllState,  RunnableConfig,  CompletableFuture<Command> >
            ↑              ↑                ↑
            输入:当前状态   输入:运行时配置   输出:异步路由命令
参数/返回 类型 说明
state OverAllState 当前图的全量状态,路由逻辑从中读取数据
config RunnableConfig 运行时配置,含线程 ID、检查点、模型名等元数据
返回值 CompletableFuture<Command> 异步路由结果,Command 携带下一节点 key 和可选状态更新

Command record:

java 复制代码
record Command(String gotoNode, Map<String, Object> update) {}
//   gotoNode → mappings 中的逻辑 key(非真实节点 ID)
//   update   → 可选的状态更新,会合并进 OverAllState

3. 与其他 action 接口的对比

action 包中存在一组由简到繁的路由接口族,AsyncCommandAction 是其中能力最强的一档:

接口 函数签名 能读 config? 能更新 state? 异步?
EdgeAction (OverAllState) → String 否 否 否
AsyncEdgeAction (OverAllState) → CF<String> 否 否 是
EdgeActionWithConfig (OverAllState, RunnableConfig) → String 是 否 否
AsyncEdgeActionWithConfig (OverAllState, RunnableConfig) → CF<String> 是 否 是
CommandAction (OverAllState, RunnableConfig) → Command 是 是 否
AsyncCommandAction (OverAllState, RunnableConfig) → CF<Command> 是 是 是

4. 三个静态工厂方法

node_async(CommandAction syncAction) --- 同步转异步

java 复制代码
static AsyncCommandAction node_async(CommandAction syncAction) {
    return (state, config) -> {
        Context context = Context.current();  // 保留 OpenTelemetry 上下文
        var result = new CompletableFuture<Command>();
        try {
            result.complete(syncAction.apply(state, config));
        } catch (Exception e) {
            result.completeExceptionally(e);
        }
        return result;
    };
}

将同步的 CommandAction(可抛出受检异常)包装为异步接口。异常不会抛出,而是通过 completeExceptionally 传入 future,由调用方的 .get() / .join() 触发。

Context.current() 的捕获是为了在异步线程中保留 OpenTelemetry 的 trace 上下文,防止链路追踪断裂。

of(AsyncEdgeAction action) --- 简单路由适配

java 复制代码
static AsyncCommandAction of(AsyncEdgeAction action) {
    return (state, config) -> action.apply(state).thenApply(Command::new);
}

将只返回 String(节点 key)的轻量级 AsyncEdgeAction 提升为 AsyncCommandAction。Command::new 使用单参构造器,update 默认为空 Map,即不携带状态更新。

java 复制代码
// 等价写法
AsyncCommandAction.of(state -> completedFuture("approved"))
// 等同于
(state, config) -> completedFuture(new Command("approved"))

of(AsyncEdgeActionWithConfig action) --- 带配置的路由适配

java 复制代码
static AsyncCommandAction of(AsyncEdgeActionWithConfig action) {
    return (state, config) -> action.apply(state, config).thenApply(Command::new);
}

与上一个类似,但可以访问 RunnableConfig(如读取线程 ID、模型名等元数据来影响路由决策),同样不携带状态更新。


5. 四种创建方式对比

java 复制代码
// 方式 1:直接 lambda(最灵活,可携带状态更新)
AsyncCommandAction action = (state, config) ->
    completedFuture(new Command("approved", Map.of("audit_result", "passed")));

// 方式 2:node_async 包装同步逻辑(适合有 try/catch 的复杂路由)
AsyncCommandAction action = AsyncCommandAction.node_async((state, config) -> {
    String score = (String) state.value("score").orElse("0");
    return new Command(Integer.parseInt(score) > 80 ? "pass" : "fail");
});

// 方式 3:of(AsyncEdgeAction) 适配简单路由(无 config,无状态更新)
AsyncCommandAction action = AsyncCommandAction.of(
    state -> completedFuture(state.value("flag").orElse("").equals("Y") ? "yes" : "no")
);

// 方式 4:of(AsyncEdgeActionWithConfig) 适配带配置的路由
AsyncCommandAction action = AsyncCommandAction.of(
    (state, config) -> completedFuture(config.getMetadata("mode").equals("fast") ? "quick" : "slow")
);

6. 在 StateGraph 中的使用入口

AsyncCommandAction 通过以下两个入口进入图定义:

java 复制代码
// 入口 1:作为条件边(最常用)
graph.addConditionalEdges("nodeA", action, Map.of("yes", "nodeB", "no", "nodeC"));

// 入口 2:作为兼具路由能力的节点(语法糖)
graph.addNode("nodeA", action, Map.of("yes", "nodeB", "no", "nodeC"));
// 内部等价于:
graph.addNode("nodeA", (state, config) -> completedFuture(Map.of()))
     .addConditionalEdges("nodeA", action, mappings);

AsyncEdgeAction / AsyncEdgeActionWithConfig 最终也都通过 AsyncCommandAction.of() 转换后走同一路径:

java 复制代码
// StateGraph.addConditionalEdges 的两个适配重载
addConditionalEdges(sourceId, AsyncEdgeAction, mappings)
  └─→ addConditionalEdges(sourceId, AsyncCommandAction.of(action), mappings)

addConditionalEdges(sourceId, AsyncEdgeActionWithConfig, mappings)
  └─→ addConditionalEdges(sourceId, AsyncCommandAction.of(action), mappings)

7. 运行时执行流程

java 复制代码
// CompiledGraph.nextNodeId 中(简化)
var singleAction = edgeCondition.singleAction(); // 取出 AsyncCommandAction

// 1. 执行 action,传入当前状态和运行时配置
var command = singleAction.apply(derefState, config).get();

// 2. 从 Command 中取出逻辑 key
var newRoute = command.gotoNode(); // e.g. "approved"

// 3. 通过 mappings 查找真实节点 ID
String result = edgeCondition.mappings().get(newRoute); // e.g. "sendEmail"

// 4. 合并 Command 携带的状态更新(若有)
var currentState = OverAllState.updateState(state, command.update(), keyStrategyMap);

return new Command(result, currentState);

Command.update 的合并时机是路由决策之后、跳转之前,路由函数可以在决定下一节点的同时向状态中写入路由相关的元数据(如审核结果、决策原因等)。


8. 完整接口族关系图

arduino 复制代码
路由接口族(由简到繁):

EdgeAction               (state) → String                   同步,无 config,无更新
  └─ AsyncEdgeAction.edge_async() 包装
AsyncEdgeAction          (state) → CF<String>               异步,无 config,无更新
  └─ AsyncCommandAction.of() 适配 ──────────────────────────────────────┐
                                                                         │
EdgeActionWithConfig     (state, config) → String           同步,有 config,无更新  │
AsyncEdgeActionWithConfig (state, config) → CF<String>      异步,有 config,无更新  │
  └─ AsyncCommandAction.of() 适配 ──────────────────────────────────────┤
                                                                         ▼
CommandAction            (state, config) → Command          同步,有 config,有更新
  └─ AsyncCommandAction.node_async() 包装
AsyncCommandAction       (state, config) → CF<Command>  ← 本类  异步,有 config,有更新

9. 实际测试用例(来自 StateGraphTest)

java 复制代码
// 直接 lambda:同时路由到 C2,并向状态写入 "next_node"="C2"
AsyncCommandAction commandAction = (state, config) ->
    completedFuture(new Command("C2", Map.of("messages", "B", "next_node", "C2")));

var graph = new StateGraph()
    .addNode("A", makeNode("A"))
    .addNode("B", commandAction, Map.of(END, END, "C1", "C1", "C2", "C2"))
    .addNode("C1", makeNode("C1"))
    .addNode("C2", makeNode("C2"))
    .addEdge(START, "A")
    .addEdge("A", "B")
    .addEdge("C1", END)
    .addEdge("C2", END)
    .compile();
// 执行后:B 节点路由到 C2,状态中同时写入 next_node="C2"

10. 总结

维度 说明
类型 @FunctionalInterface,可用 lambda 直接实现
能力 路由接口族中最完整:异步 + 可读 config + 可更新 state
核心参数 OverAllState(读状态)+ RunnableConfig(读运行配置)
返回值 CompletableFuture<Command>,Command 携带路由 key 和可选状态更新
创建方式 直接 lambda、node_async(同步包装)、of(从简单接口适配)
使用入口 StateGraph.addConditionalEdges / addNode(id, action, mappings)
运行时 由 CompiledGraph.nextNodeId 调用,结果通过 mappings 映射为真实节点 ID

相关文件

  • EdgeCondition.md --- AsyncCommandAction 的存储容器
  • EdgeValue.md --- 条件边目标的联合类型
  • Edge.md --- 边的完整结构分析
  • StateGraph.md --- 图定义层整体分析
  • AsyncCommandAction.java --- 本文分析的源文件
  • CommandAction.java --- 同步版,被 node_async 包装
  • AsyncEdgeAction.java --- 轻量路由接口,被 of() 适配
  • AsyncEdgeActionWithConfig.java --- 带配置的轻量路由接口,被 of() 适配
  • Command.java --- 路由结果,携带 gotoNode + 状态更新
  • CompiledGraph.java --- 运行时 nextNodeId 的调用方
相关推荐
IT_陈寒9 小时前
Vue的watch把我坑惨了,原来要这样用才靠谱
前端·人工智能·后端
黃芢輝10 小时前
开源实战:基于 Spring Boot 3 + Vue 3 + Redis 的高校选修课管理系统(志愿填报 + 统一抽签)
后端
小雨的AI中台笔记10 小时前
502 还是 503?「无可用渠道」其实只有三个原因
后端
LCG元10 小时前
我让AI重写了这个老系统
后端
吴汐10 小时前
【CRMEB 社区版】二开第一问:这个需求该进 driver,还是进 modules?
后端
新*10 小时前
Elasticsearch 7.17 的 SQL 到底能查到什么?一次踩坑记录
后端·sql·elasticsearch·查询
卷无止境10 小时前
SALib敏感性分析的python库学习笔记
后端·python
ServBay10 小时前
从 Val Town 停服看,代码能下载不代表项目能搬家
后端·sqlite
我不会起名字32212 小时前
seq 空洞排查:Mongo 的 51 只是预留上界,Redis CURR 才是已消耗边界
redis·后端·mongodb·go·即时通讯·im·故障排查
特立独行的猫A12 小时前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python