十五、Spring AI Alibaba · 高级 ·Graph

一、图(Graphs)三要素

Spring AI Alibaba Graph 把智能体工作流建模为「图」,由三个关键组件定义智能体行为:

组件 说明 关键类型
State 共享数据结构,表示应用当前快照 OverAllState
Nodes 编码智能体逻辑的函数式接口 AsyncNodeAction / AsyncNodeActionWithConfig
Edges 根据 State 决定下一个执行哪个 Node(条件分支或固定转换) AsyncEdgeAction / AsyncEdgeActionWithConfig
  • 组合 Nodes + Edges 可创建复杂循环工作流,工作流运行过程中持续更新 State。
  • 框架负责 State 的传递与持久化。
  • Node / Edge 本质就是"函数":内部可以是 LLM 调用 ,也可以是普通 Java 代码。
  • AsyncNodeActionWithConfig / AsyncEdgeActionWithConfig 额外接收 RunnableConfig 用于传递上下文(如 Store、threadId)。

二、StateGraph 与编译图

StateGraph 是核心定义类,通过用户定义的状态策略参数化。

构建流程

  1. 定义 State(Key + KeyStrategy)
  2. 添加 Nodes 和 Edges
  3. 编译(.compile())
java 复制代码
import com.alibaba.cloud.ai.graph.StateGraph;
import com.alibaba.cloud.ai.graph.CompiledGraph;

CompiledGraph graph = stateGraph.compile();

为什么要编译?

  • 提供图结构基本检查(无孤立节点等)
  • 指定运行时参数:检查点器(checkpointer)、中断点(interrupts)
  • 编译本身不复杂,只是做编排检查 + 预置 config
  • 使用图之前必须编译

三、Schema:OverAllState(状态)

定义图的第一件事:定义 State。

  • State = 图的 Key + KeyStrategy 函数
  • KeyStrategy 决定:多个节点更新同一个 key 时如何处理(合并 / 覆盖)
  • State 的 key 是所有 Nodes 和 Edges 的输入 schema
  • 节点通过返回 Map<String, Object>(一组 key-value)发出更新,图引擎按 KeyStrategy 应用到 State

KeyStrategy

每个 key 有独立 的策略。未显式指定则默认 ReplaceStrategy(覆盖)。

java 复制代码
import com.alibaba.cloud.ai.graph.KeyStrategyFactory;
import com.alibaba.cloud.ai.graph.KeyStrategy;
import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy;
import java.util.HashMap;
import java.util.Map;

public static KeyStrategyFactory createKeyStrategyFactory() {
  return () -> {
      Map<String, KeyStrategy> keyStrategyMap = new HashMap<>();
      keyStrategyMap.put("messages", new AppendStrategy());
      return keyStrategyMap;
  };
}

var graphBuilder = new StateGraph(createKeyStrategyFactory());

1. ReplaceStrategy(覆盖策略)

新值完全替换旧值,后执行节点覆盖先执行节点的值。

java 复制代码
KeyStrategyFactory keyStrategyFactory = () -> {
  Map<String, KeyStrategy> keyStrategyMap = new HashMap<>();
  keyStrategyMap.put("value", new ReplaceStrategy());
  return keyStrategyMap;
};

var nodeA = node_async(state -> Map.of("value", "初始值"));
var nodeB = node_async(state -> Map.of("value", "更新后的值"));

StateGraph stateGraph = new StateGraph(keyStrategyFactory)
      .addNode("node_a", nodeA)
      .addNode("node_b", nodeB)
      .addEdge(START, "node_a")
      .addEdge("node_a", "node_b")
      .addEdge("node_b", END);

CompiledGraph graph = stateGraph.compile();
RunnableConfig config = RunnableConfig.builder().threadId("replace-strategy-demo").build();
Optional<OverAllState> stateOptional = graph.invoke(Map.of(), config);

System.out.println("最终状态中的 value: " + (String) stateOptional.get().value("value"));
// 输出: 更新后的值

流程:value = "初始值" → 被覆盖为 "更新后的值" → 最终 "更新后的值"

2. AppendStrategy(追加策略)

新值追加到旧值之后,所有节点值保留。

java 复制代码
KeyStrategyFactory keyStrategyFactory = () -> {
  Map<String, KeyStrategy> keyStrategyMap = new HashMap<>();
  keyStrategyMap.put("messages", new AppendStrategy());
  return keyStrategyMap;
};

var nodeA = node_async(state -> Map.of("messages", "消息1"));
var nodeB = node_async(state -> Map.of("messages", "消息2"));
var nodeC = node_async(state -> Map.of("messages", "消息3"));

StateGraph stateGraph = new StateGraph(keyStrategyFactory)
      .addNode("node_a", nodeA)
      .addNode("node_b", nodeB)
      .addNode("node_c", nodeC)
      .addEdge(START, "node_a")
      .addEdge("node_a", "node_b")
      .addEdge("node_b", "node_c")
      .addEdge("node_c", END);

// 最终结果:[消息1, 消息2, 消息3]
List<String> messages = (List<String>) stateOptional.get().value("messages").orElse(List.of());

流程:["消息1"] → ["消息1","消息2"] → ["消息1","消息2","消息3"]

3. AppendStrategy 下删除消息:RemoveByHash

通过比较 hashCode 删除消息。

java 复制代码
import com.alibaba.cloud.ai.graph.state.RemoveByHash;

var workflow = new StateGraph(createKeyStrategyFactory())
      .addNode("agent_1", node_async(state -> Map.of("messages", "message1")))
      .addNode("agent_2", node_async(state -> Map.of("messages", "message2.1")))
      .addNode("agent_3", node_async(state ->
              Map.of("messages", RemoveByHash.of("message2.1")))) // 删除
      .addEdge(START, "agent_1")
      .addEdge("agent_1", "agent_2")
      .addEdge("agent_2", "agent_3")
      .addEdge("agent_3", END);

4. 自定义 KeyStrategy

可以为特定状态属性指定自定义的 Strategy 实现。


四、序列化器(Serializer)

图执行期间状态需要被序列化(主要用于克隆),也支持跨执行持久化。

  • 现有实现:Jackson (默认)、JDK
  • 序列化关注点:
    • 不依赖不安全的标准序列化框架
    • 允许为第三方(非可序列化)类实现序列化
    • 尽可能避免类加载问题
    • 管理可空值

方案 1:为自定义数据类增加 Jackson 注解(最简单)

java 复制代码
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomMessage {
  private String content;
  private String type;

  @JsonCreator
  public CustomMessage(@JsonProperty("content") String content,
                       @JsonProperty("type") String type) {
      this.content = content;
      this.type = type;
  }
  // getter / setter ...
}

方案 2:定制 StateGraph 中的默认 Serializer(动态场景)

java 复制代码
StateGraph graph = new StateGraph(keyStrategyFactory);
StateSerializer stateSerializer = graph.getStateSerializer();
if (stateSerializer instanceof StateGraph.JacksonSerializer) {
  StateGraph.JacksonSerializer jacksonSerializer =
          (StateGraph.JacksonSerializer) stateSerializer;
  ObjectMapper objectMapper = jacksonSerializer.getObjectMapper();

  objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
  objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

  var module = new SimpleModule();
  objectMapper.registerModule(module);
}

方案 3:为 Graph 提供自定义序列化器(推荐)

java 复制代码
class CustomizedSerializer extends SpringAIJacksonStateSerializer {
  public CustomizedSerializer() {
      super(OverAllState::new);
      var module = new SimpleModule();
      module.addSerializer(CustomMessage.class, new CustomMessageSerializer());
      module.addDeserializer(CustomMessage.class, new CustomMessageDeserializer());
      objectMapper.registerModule(module);
  }
}

StateGraph graph = new StateGraph("demo", keyStrategyFactory, new CustomizedSerializer());
  • SpringAIJacksonStateSerializer 构造函数中已注册 Spring AI 相关消息类型序列化器
  • 继承后可继续注册自定义类型,并通过 objectMapper 字段完全控制序列化行为

五、节点(Nodes)

节点通常是:

  • AsyncNodeAction:入参 OverAllState,用 addNode 添加
  • AsyncNodeActionWithConfig:入参 OverAllState + RunnableConfig,用 addNode 添加
  • 由于 AsyncNodeAction 基于 CompletableFuture,可用 node_async 静态方法适配同步场景

特殊节点

  • START:表示将用户输入发送到图的节点,用于确定首先调用哪些节点

    java 复制代码
    import static com.alibaba.cloud.ai.graph.StateGraph.START;
  • END:终端节点,表示完成后不再有任何操作


六、边(Edges)

边定义逻辑如何路由 以及图如何决定停止。

四种关键类型

类型 说明
普通边 Normal Edges 直接从 A 到 B
条件边 Conditional Edges 调用函数决定下一个节点
入口点 Entry Point 用户输入到达时首先调用哪个节点
条件入口点 Conditional Entry Point 调用函数决定首先调用哪个节点

普通边

总是从 A 到 B → addEdge

条件边

java 复制代码
import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async;

graph.addConditionalEdges("nodeA", edge_async(state -> "nodeB"),
      Map.of("nodeB", "nodeB", "nodeC", "nodeC"));
  • 必须提供一个对象,将 routingFunction 的输出映射到下一个节点名称
  • 支持 AsyncEdgeAction 或 AsyncEdgeActionWithConfig

并行

一个节点可以拥有多个出边 → 形成并行关系,所有目标节点将作为下一批并行执行。


七、会话(Threads)

  • 会话支持对多个不同运行进行检查点,对多租户聊天等场景至关重要
  • 会话 = 分配给 checkpointer 保存的一系列检查点的唯一 ID
  • 使用 checkpointer 时必须 指定 thread_id
java 复制代码
RunnableConfig config = RunnableConfig.builder().threadId("unique-id-1").build();
Flux<NodeOutput> stream = graph.stream(Map.of("input", "你好"), config);

// 多次调用间传递同一个会话 ID
RunnableConfig config2 = RunnableConfig.builder().threadId("unique-id-1").build();
Flux<NodeOutput> stream2 = graph.stream(Map.of("input", "你好"), config2);

八、Checkpointer(检查点)

内置持久化层,每一步保存图状态的检查点,带来两大能力:

  1. 人机协作(human-in-the-loop):人类可检查、中断、批准步骤,图可在人类更新状态后恢复执行
  2. 交互间保持"记忆":创建会话并在执行后保存状态,后续消息可发送到该检查点,保留之前记忆

每条 Checkpoint 记录:

  • state:此时的状态值
  • nextNodeId:接下来要执行的节点标识

九、内存管理

两种内存:

  • 短期内存(会话级持久化):作为智能体 State 的一部分,支持多轮对话
  • 长期内存:跨会话存储的用户级 / 应用级数据

短期内存

java 复制代码
MemorySaver checkpointer = new MemorySaver();
SaverConfig saverConfig = SaverConfig.builder().register(checkpointer).build();

KeyStrategyFactory keyStrategyFactory = () -> {
  Map<String, KeyStrategy> keyStrategyMap = new HashMap<>();
  keyStrategyMap.put("messages", new AppendStrategy());
  return keyStrategyMap;
};

StateGraph stateGraph = new StateGraph(keyStrategyFactory)
  .addNode("agent", agentNode)
  .addEdge(START, "agent")
  .addEdge("agent", END);

CompiledGraph graph = stateGraph.compile(
  CompileConfig.builder().saverConfig(saverConfig).build());

RunnableConfig config = RunnableConfig.builder().threadId("user-session-1").build();
graph.invoke(Map.of("messages", List.of(Map.of("role","user","content","你好!我是 Bob"))), config);

生产环境:Redis 检查点器

java 复制代码
import com.alibaba.cloud.ai.graph.checkpoint.savers.RedisSaver;
import com.alibaba.cloud.ai.graph.checkpoint.constant.SaverConstant;

RedisSaver redisSaver = new RedisSaver("localhost", 6379);
SaverConfig saverConfig = SaverConfig.builder()
  .register(SaverConstant.REDIS, redisSaver)
  .build();

子图中使用

只需在编译父图 时提供检查点器,框架会自动传播到子图。若希望子图拥有自己的内存(如多智能体系统跟踪内部消息历史),可用适当的检查点器选项单独编译子图。

长期内存:Store

Store 是抽象接口(MemoryStore、RedisStore 等实现),跨会话持久化数据。

java 复制代码
import static com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async;

var userProfileNode = node_async((state, config) -> {
  String userId = (String) state.value("userId").orElse("");
  Store store = config.store();               // 可能为 null,需判空
  if (store != null) {
      Optional<StoreItem> itemOpt = store.getItem(List.of("user_profiles"), userId);
      if (itemOpt.isPresent()) {
          return Map.of("userProfile", itemOpt.get().getValue());
      }
  }
  return Map.of("userProfile", Map.of("name", "User", "preferences", "default"));
});

// 预填充长期记忆
MemoryStore memoryStore = new MemoryStore();
StoreItem profileItem = StoreItem.of(List.of("user_profiles"), "user_001", profileData);
memoryStore.putItem(profileItem);

RunnableConfig config = RunnableConfig.builder()
      .threadId("profile_thread")
      .store(memoryStore)
      .build();

要点:

  • 用 AsyncNodeActionWithConfig.node_async 访问 RunnableConfig → config.store()
  • store.getItem(namespace, key) → Optional<StoreItem>
  • StoreItem.of(namespace, key, value) + store.putItem(item)

用 Store 实现缓存

java 复制代码
var cacheNode = node_async((state, config) -> {
  String key = (String) state.value("cacheKey").orElse("");
  Store store = config.store();
  if (store != null) {
      Optional<StoreItem> itemOpt = store.getItem(List.of("cache"), key);
      if (itemOpt.isPresent()) {              // 命中
          return Map.of("result", itemOpt.get().getValue().get("value"));
      }
  }
  Object computedData = performExpensiveOperation(key);   // 未命中
  if (store != null) {
      store.putItem(StoreItem.of(List.of("cache"), key,
              Map.of("value", computedData)));
  }
  return Map.of("result", computedData);
});

短期 + 长期内存结合

  • State:userId(Replace) + messages(Append) + userPreferences(Replace)
  • 节点 1 load_preferences:从 Store 加载长期偏好
  • 节点 2 chat:结合偏好 + 短期消息历史调用 LLM
  • 编译图时配置 MemorySaver(短期),RunnableConfig 配置 MemoryStore(长期)

十、持久化(Persistence)

使用检查点编译图时,检查点会在**每个超级步骤(super-step)**保存状态快照,保存到 thread(会话)中,执行后可访问。

由此带来:人在回路 、内存 、时间旅行 、容错。

会话(Thread)

  • 唯一 ID / 会话标识符,包含一系列运行的累积状态
  • 使用检查点调用图时必须 在 RunnableConfig 指定 threadId

检查点(Checkpoints)

会话在特定时间点的状态,由 StateSnapshot 表示:

属性 含义
config 与此检查点关联的配置
metadata 元数据
values 此时状态通道的值
next 下一个要执行的节点名称元组
tasks PregelTask 对象元组,含下一个任务信息

示例 :START → node_a → node_b → END,foo(Replace) / bar(Append) 会产生 恰好 4 个检查点:

  1. 空检查点,START 为下一个节点
  2. 用户输入 {'foo':'', 'bar':[]},node_a 为下一个节点
  3. node_a 输出 {'foo':'a','bar':['a']},node_b 为下一个节点
  4. node_b 输出 {'foo':'b','bar':['a','b']},无下一个节点

bar 包含两个节点输出,因为使用了 AppendStrategy。

获取状态

java 复制代码
RunnableConfig config = RunnableConfig.builder().threadId("1").build();
StateSnapshot stateSnapshot = graph.getState(config);
System.out.println(stateSnapshot.state());
System.out.println(stateSnapshot.node());

// 指定 checkpoint_id
RunnableConfig configWithCheckpoint = RunnableConfig.builder()
  .threadId("1")
  .checkPointId("1ef663ba-28fe-6528-8002-5a559208592c")
  .build();

获取状态历史

java 复制代码
List<StateSnapshot> history = (List<StateSnapshot>) graph.getStateHistory(config);
// 按时间顺序排序,最近的检查点在列表第一个位置

重放(Replay)

用 thread_id + checkpoint_id 调用 invoke:

java 复制代码
StateSnapshot snapshot = graph.getState(config);
RunnableConfig replayConfig = RunnableConfig.builder()
  .threadId("1")
  .checkPointId(snapshot.config().checkPointId().orElse(""))
  .build();
graph.invoke(Map.of(), config);
  • 框架知道某步骤是否执行过
  • checkpoint_id 之前的步骤只重放,不重新执行
  • checkpoint_id 之后的步骤会真正执行(新分支),即使之前执行过

更新状态(时间旅行核心)

java 复制代码
RunnableConfig config = RunnableConfig.builder().threadId("unique-id-1").build();
var updatedConfig = graph.updateState(config, Map.of("the_key_to", "newValue"), "the-next-node-to-to");
  • thread_id 指定更新哪个会话
  • 第三个参数可覆盖当前 Checkpoint 所在节点,让图从指定节点开始执行
  • 注意 :updateState 不直接覆盖,而是按该 key 的 KeyStrategy 决策(Replace 覆盖 / Append 追加)

示例:当前 {"foo":1,"bar":["a"]},foo=Replace、bar=Append

java 复制代码
updates.put("foo", 2);
updates.put("bar", List.of("b"));
graph.updateState(config, updates, null);
// 新状态:{"foo": 2, "bar": ["a", "b"]}

检查点器实现

  • MemorySaver:内存保存
java 复制代码
SaverConfig saverConfig = SaverConfig.builder()
  .register(SaverConstant.MEMORY, new MemorySaver())
  .build();
  • RedisSaver:Redis 保存(生产环境)

十一、流式输出

框架统一使用 Flux 定义和传递流(与 Spring 生态一致)。

  • .stream() 返回 Flux<NodeOutput>
  • 流返回后不会立即触发执行 ,需 subscribe() / blockLast() 才真正启动
  • NodeOutput 报告执行的节点名称和结果状态
  • Flux 支持流的合并、转换、组合(Reactor 能力)

在节点中返回 Flux

java 复制代码
public static class StreamingNode implements NodeAction {
  @Override
  public Map<String, Object> apply(OverAllState state) {
      String query = (String) state.value("query").orElse("");
      Flux<ChatResponse> chatResponseFlux = chatClient.prompt()
              .user(query).stream().chatResponse();
      return Map.of("messages", chatResponseFlux);
  }
}

关键:引擎运行到下游节点时,框架已自动订阅并消费 上一个节点的 Flux,将汇总结果按 AppendStrategy 追加到 messages。

java 复制代码
public static class ProcessStreamingNode implements NodeAction {
  @Override
  public Map<String, Object> apply(OverAllState state) {
      Object messages = state.value("messages").orElse("");
      return Map.of("result", "流式响应已处理完成: " + messages);
  }
}

完整流式调用

java 复制代码
graph.stream(Map.of("query", "请用一句话介绍 Spring AI"), config)
  .doOnNext(output -> {
      if (output instanceof StreamingOutput<?> streamingOutput) {
          String chunk = streamingOutput.chunk();
          if (chunk != null && !chunk.isEmpty()) System.out.print(chunk);
      } else {
          System.out.println("节点 '" + output.node() + "' 执行完成");
      }
  })
  .doOnComplete(() -> System.out.println("流式输出完成"))
  .doOnError(error -> System.err.println("错误: " + error.getMessage()))
  .blockLast();

流式输出的三个层次

1. 节点级别 :单个 LLM 节点产生 token 流 → 包装成 StreamingOutput 成为图流一部分

复制代码
Query → LLM API → Token1 → Token2 → ... → TokenN

2. 图级别:多个节点依次执行形成流

复制代码
NodeOutput(node="A") → StreamingOutput("Hello") → StreamingOutput(" World")
→ StreamingOutput("!") → NodeOutput(node="C") → NodeOutput(node="__END__")
方法 返回类型 场景
graph.stream() Flux<NodeOutput> 普通图执行
graph.graphResponseStream() Flux<GraphResponse<NodeOutput>> 嵌套子图场景

3. 流输出数据类型

复制代码
NodeOutput(基类)
  - node: String        节点 ID
  - state: OverallState 全局状态
  - message: Object     节点消息
    ├── StreamingOutput:chunk(流式 Token),框架自动为 LLM 流式节点创建
    ├── NodeOutput:普通节点标准输出
    └── CustomOutput:用户自定义扩展

并行节点的流式输出

多个并行节点(普通节点或嵌套子图)的流式处理参见「并行节点的流式处理」文档。Edge 指路,Checkpoint 存档,Store 记长期,Flux 出流。**

相关推荐
明志数科1 小时前
具身智能机器人从演示验证到常态化部署的工程差距分析
人工智能·机器人
晨间晓1 小时前
图片边缘主要朝哪里?用华为云码道开发 OpenCV 梯度方向分析工具
人工智能·opencv·华为云
果霸大叔1 小时前
AI网关第8篇:AI 代理与负载均衡--多模型实例如何调度?
人工智能·算法·自然语言处理·负载均衡·ai-native
东离与糖宝1 小时前
Kafka流算湖一体化重磅发布,AI实时数据链路迎来大变革
人工智能
释厄6231 小时前
胶球实验本体论解码··力直接构成物质悖论·切割 A让B=B
人工智能
IvorySQL1 小时前
IvorySQL 校园行走进山东大学:从 PostgreSQL 到 AI,连接技术实践与职业成长
数据库·人工智能·postgresql
传人once1 小时前
从 PPT 到可交互原型:用 AI 做小程序原型的实践 | 深度实践版
人工智能·powerpoint·交互
金銀銅鐵1 小时前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程
磊 子1 小时前
什么是虚假唤醒(spurious wakeup)?从 POSIX 标准讲到 futex 内核
java·开发语言