一、图(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 是核心定义类,通过用户定义的状态策略参数化。
构建流程
- 定义 State(Key + KeyStrategy)
- 添加 Nodes 和 Edges
- 编译(
.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:表示将用户输入发送到图的节点,用于确定首先调用哪些节点
javaimport 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(检查点)
内置持久化层,每一步保存图状态的检查点,带来两大能力:
- 人机协作(human-in-the-loop):人类可检查、中断、批准步骤,图可在人类更新状态后恢复执行
- 交互间保持"记忆":创建会话并在执行后保存状态,后续消息可发送到该检查点,保留之前记忆
每条 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 个检查点:
- 空检查点,
START为下一个节点 - 用户输入
{'foo':'', 'bar':[]},node_a为下一个节点 node_a输出{'foo':'a','bar':['a']},node_b为下一个节点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 出流。**