文章目录
- [1. 概述](#1. 概述)
- [2. 核心概念](#2. 核心概念)
-
- [2.1 Checkpoint(检查点)](#2.1 Checkpoint(检查点))
- [2.2 MemorySaver(内存保存器)](#2.2 MemorySaver(内存保存器))
- [2.3 StateSnapshot(状态快照)](#2.3 StateSnapshot(状态快照))
- [2.4 RunnableConfig(运行配置)](#2.4 RunnableConfig(运行配置))
- [3. 加载流程](#3. 加载流程)
-
- [3.1 编译配置](#3.1 编译配置)
- [3.2 状态图编译](#3.2 状态图编译)
- [4. 执行流程](#4. 执行流程)
-
- [4.1 初始化执行引擎上下文](#4.1 初始化执行引擎上下文)
-
- [4.1.1 恢复模式(获取历史检查点更新状态)](#4.1.1 恢复模式(获取历史检查点更新状态))
- [4.1.1 全新启动模式(获取历史检查点更新状态)](#4.1.1 全新启动模式(获取历史检查点更新状态))
- [4.2 开始节点添加检查点(第一次)](#4.2 开始节点添加检查点(第一次))
- [4.3 节点执行器保存检查点(第二次)](#4.3 节点执行器保存检查点(第二次))
- [4.4 执行完成](#4.4 执行完成)
- [5. 检查点保存规则](#5. 检查点保存规则)
-
- [5.1 保存时机](#5.1 保存时机)
- [5.2 保存频率](#5.2 保存频率)
- [5.3 检查点数量](#5.3 检查点数量)
1. 概述
检查点机制是实现持久化、中断恢复、时间旅行(Time Travel)等高级功能的核心基础设施。本文档详细分析 Spring AI Alibaba 框架中 MemorySaver 进行检查点(Checkpoint)的加载和执行全流程。
ReactAgent 使用示例:
java
MemorySaver memorySaver = new MemorySaver();
ReactAgent my_agent = ReactAgent.builder()
.name("my_agent")
.model(zhiPuAiChatModel)
.saver(memorySaver)
.build();
RunnableConfig config = RunnableConfig.builder()
.threadId("user_123456") // 会话唯一标识
.build();
String res1 = my_agent.call("你好!我叫 Bob。", config).getText();
String res2 = my_agent.call("我是谁", config).getText();
Graph 使用示例:
java
SaverConfig saverConfig = SaverConfig.builder()
.register(new MemorySaver())
.build();
CompileConfig compileConfig = CompileConfig.builder()
.saverConfig(saverConfig)
.build();
CompiledGraph compiledGraph = workflow.compile(compileConfig);
RunnableConfig runnableConfig = RunnableConfig.builder()
.threadId("11111111111")
.addMetadata("user_id", "user-001")
.build();
Map<String, Object> inputs = Map.of("email_content", "我的订阅被收费两次了,这很紧急",
"send_email", "123456@qq.com",
"email_id", "123456");
Flux<NodeOutput> graphStream = compiledGraph.stream(inputs, runnableConfig);
2. 核心概念
2.1 Checkpoint(检查点)
Checkpoint 是图执行过程中某一时刻的完整状态快照,包含以下核心信息:
java
public class Checkpoint {
private final String id; // 检查点唯一标识符 (UUID)
private Map<String, Object> state; // 当前状态数据 (OverAllState.data())
private String nodeId; // 当前节点 ID
private String nextNodeId; // 下一个要执行的节点 ID
}
关键特性:
- 唯一标识 : 使用
UUID生成唯一ID,支持版本追踪 - 状态捕获: 保存完整的图状态,包括消息列表、变量等
- 位置记录 : 记录执行位置(
nodeId+nextNodeId),支持精确恢复 - 不可变性 : 构造后状态不可变,通过
copyOf()创建新版本
2.2 MemorySaver(内存保存器)
MemorySaver 是 BaseCheckpointSaver 接口的内存实现,将检查点存储在内存中的 Map<String, LinkedList<Checkpoint>> 结构中:
java
public class MemorySaver implements BaseCheckpointSaver {
// 核心数据结构:threadId -> 检查点链表
final Map<String, LinkedList<Checkpoint>> _checkpointsByThread = new HashMap<>();
private final ReentrantLock _lock = new ReentrantLock(); // 线程安全锁
}
存储结构:
text
_checkpointsByThread = {
"thread-1": [checkpoint3, checkpoint2, checkpoint1], // 按时间倒序
"thread-2": [checkpoint5, checkpoint4],
...
}
核心操作:
get(config)- 根据threadId/checkPointId获取检查点put(config, checkpoint)- 保存新检查点或更新现有检查点list(config)- 列出线程的所有检查点release(config)- 释放线程的所有检查点
2.3 StateSnapshot(状态快照)
StateSnapshot 继承 NodeOutput,是面向用户的检查点封装:
java
public final class StateSnapshot extends NodeOutput {
private final RunnableConfig config; // 包含 checkPointId、nextNode
public String next() {
return config.nextNode().orElse(null); // 下一个要执行的节点
}
public RunnableConfig config() {
return config; // 用于恢复执行的配置
}
}
2.4 RunnableConfig(运行配置)
RunnableConfig 是执行配置载体,包含会话、检查点的关键信息:
java
public final class RunnableConfig {
private final String threadId; // 线程标识符
private final String checkPointId; // 检查点 ID
private final String nextNode; // 下一个节点(恢复后设置)
private final CompiledGraph.StreamMode streamMode; // 流模式
private final Map<String, Object> metadata; // 元数据
}
3. 加载流程
3.1 编译配置
在 DefaultBuilder#build() 构建 ReactAgent 方法最后会构建编译配置:
java
return new ReactAgent(llmNode, toolNode, buildConfig(), this);
}
上面的示例没有传递 CompileConfig ,所以会构建默认的:
java
protected CompileConfig buildConfig() {
if (compileConfig != null) {
return compileConfig;
}
SaverConfig saverConfig = SaverConfig.builder()
.register(saver)
.build();
return CompileConfig.builder()
.saverConfig(saverConfig)
.recursionLimit(Integer.MAX_VALUE)
.releaseThread(releaseThread)
.build();
}
CompileConfig 实例对象中封装了 Saver 实例:

CompileConfig 实例又被传递给 ReactAgent 对象:

3.2 状态图编译
ReactAgent 第一次运行时会进行状态图编译:
java
public synchronized CompiledGraph getAndCompileGraph() {
if (compiledGraph != null) {
return compiledGraph;
}
StateGraph graph = getGraph();
try {
if (this.compileConfig == null) {
this.compiledGraph = graph.compile();
}
else {
this.compiledGraph = graph.compile(this.compileConfig);
}
} catch (GraphStateException e) {
throw new RuntimeException(e);
}
return this.compiledGraph;
}
自定义状态图 StateGraph 则需要手动进行编译:
java
/**
* 根据传入的配置,将状态图编译为已编译的图对象
* @param config 编译配置参数
* @return 编译完成后的图对象
* @throws GraphStateException 当图状态存在相关错误时抛出该异常
*/
public CompiledGraph compile(CompileConfig config) throws GraphStateException {
// 校验配置对象不能为空,为空则抛出空指针异常
Objects.requireNonNull(config, "config cannot be null");
// 校验当前状态图的合法性
validateGraph();
// 构造并返回新的编译完成的图对象
return new CompiledGraph(this, config);
}
CompileConfig 又传递到可执行的编译图中:

4. 执行流程
状态图执行过程之前我们已经介绍过了,现在主要介绍关于检查点相关。
4.1 初始化执行引擎上下文
ReactAgent 、 StateGraph 执行时,底层都是调用 GraphRunner:
java
public Flux<GraphResponse<NodeOutput>> run(OverAllState initialState) {
return Flux.defer(() -> {
try {
GraphRunnerContext context = new GraphRunnerContext(initialState, config, compiledGraph);
// Delegate to the main execution handler - demonstrates polymorphism
return mainGraphExecutor.execute(context, resultValue);
}
catch (Exception e) {
return Flux.error(e);
}
});
}
在初始化图运行上下文时, 从断点恢复模式初始化上下文分支中,会涉及到 CheckpointSaver :
java
/**
* 构造图运行上下文对象
* 根据配置判断是从头初始化上下文,还是从断点恢复初始化上下文
* @param initialState 图的初始整体状态
* @param config 图运行的配置参数
* @param compiledGraph 已编译完成的图对象
* @throws Exception 初始化上下文过程中发生异常时抛出
*/
public GraphRunnerContext(OverAllState initialState, RunnableConfig config, CompiledGraph compiledGraph)
throws Exception {
// 赋值已编译的图对象
this.compiledGraph = compiledGraph;
// 赋值运行配置参数
this.config = config;
// 判断配置中是否包含人工反馈元数据 或 检查点ID(用于断点恢复)
if (config.metadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY).isPresent() || config.checkPointId().isPresent()) {
// 从断点恢复模式初始化上下文
initializeFromResume(initialState, config);
} else {
// 全新启动模式初始化上下文
initializeFromStart(initialState, config);
}
}
4.1.1 恢复模式(获取历史检查点更新状态)
从检查点恢复模式初始化运行上下文中,会获取检查点保存器 (恢复执行必须配置检查点保存器)、从保存器中获取检查点数据,若不存在有效检查点则抛出异常:
java
/**
* 从检查点恢复模式初始化运行上下文
* 用于处理任务中断后继续执行的场景,加载历史检查点并恢复运行状态
* @param initialState 初始整体状态
* @param config 运行配置
*/
private void initializeFromResume(OverAllState initialState, RunnableConfig config) {
// 打印日志:标记当前为恢复执行请求
log.trace("RESUME REQUEST");
// 获取检查点保存器,若未配置则抛出异常(恢复执行必须配置检查点保存器)
var saver = compiledGraph.compileConfig.checkpointSaver()
.orElseThrow(() -> new IllegalStateException("Resume request without a configured checkpoint saver!"));
// 从保存器中获取检查点数据,若不存在有效检查点则抛出异常
var checkpoint = saver.get(config)
.orElseThrow(() -> new IllegalStateException("Resume request without a valid checkpoint!"));
// 获取检查点记录的下一个要执行的节点动作
var startCheckpointNextNodeAction = compiledGraph.getNodeAction(checkpoint.getNextNodeId());
// 判断是否为可恢复的子图动作
if (startCheckpointNextNodeAction instanceof ResumableSubGraphAction resumableAction) {
// 检测到需要从子图恢复执行
this.config = RunnableConfig.builder(config)
.checkPointId(null) // 重置检查点ID,避免重复恢复
.addMetadata(resumableAction.getResumeSubGraphId(), true) // 添加子图恢复元数据标记
.build();
// 清空配置上下文
this.config.clearContext();
} else {
// 普通恢复模式:仅重置检查点ID
this.config = config.withCheckPointId(null);
}
// 初始化当前节点ID为null
this.currentNodeId = null;
// 从检查点中恢复下一个执行节点ID
this.nextNodeId = checkpoint.getNextNodeId();
// 使用检查点中的状态更新整体状态
this.overallState = initialState.input(checkpoint.getState());
// 记录从哪个节点开始恢复
this.resumeFrom = checkpoint.getNodeId();
// 打印日志:输出恢复执行的节点ID
log.trace("RESUME FROM {}", checkpoint.getNodeId());
}
4.1.1 全新启动模式(获取历史检查点更新状态)
如果不是中断,则会全新启动模式初始化运行上下文:
java
/**
* 全新启动模式初始化运行上下文
* 用于任务第一次启动时,初始化状态和节点信息
* @param initialState 初始整体状态
* @param config 运行配置
*/
private void initializeFromStart(OverAllState initialState, RunnableConfig config) {
// 打印日志:标记当前为全新启动请求
log.trace("START");
// 获取初始状态中的输入数据
Map<String, Object> inputs = initialState.data();
// 判断输入数据是否非空
if (!CollectionUtils.isEmpty(inputs)) {
// 简单校验输入数据,打印输入数据的键集合(不调用受保护方法)
log.debug("Initializing with inputs: {}", inputs.keySet());
}
// 调用编译图的初始化状态方法,创建并赋值当前整体状态
this.overallState = stateCreate(compiledGraph.getInitialState(inputs, config), initialState);
// 将当前节点ID设置为启动节点(START 常量)
this.currentNodeId = START;
// 初始化下一个节点ID为null(启动时暂无下一个执行节点)
this.nextNodeId = null;
}
getInitialState方法中也会进行状态合并,之前已经介绍过
4.2 开始节点添加检查点(第一次)
第一次执行(非中断),先进入到 MainGraphExecutor#handleStartNode 方法处理 START 节点,添加第一个检查点:
java
/**
* 处理图的起始节点执行逻辑
* @param context 图运行上下文,包含状态、节点、配置等核心信息
* @return 包含节点输出的GraphResponse响应式流
*/
private Flux<GraphResponse<NodeOutput>> handleStartNode(GraphRunnerContext context) {
try {
// 触发起始节点对应的监听器执行
context.doListeners(START, null);
// 获取图的入口执行命令
Command nextCommand = context.getEntryPoint();
// 设置下一个要执行的节点ID
context.setNextNodeId(nextCommand.gotoNode());
// 为起始节点添加检查点,记录执行快照
Optional<Checkpoint> cp = context.addCheckpoint(START, context.getNextNodeId());
// 构建起始节点的执行输出结果
NodeOutput output = context.buildOutput(START, cp);
// 将当前节点ID更新为下一个节点ID,完成起始节点跳转
context.setCurrentNodeId(context.getNextNodeId());
// 封装执行结果,并递归调用主执行方法继续执行后续节点
return Flux.just(GraphResponse.of(output))
.concatWith(Flux.defer(() -> execute(context, new AtomicReference<>())));
}
catch (Exception e) {
// 异常捕获:封装异常信息并返回响应式错误结果
return Flux.just(GraphResponse.error(e));
}
}
GraphRunnerContext#addCheckpoint 添加执行检查点:
java
/**
* 添加执行检查点(快照)
* 用于记录当前节点执行状态、下一个节点信息,支持任务中断后恢复
* @param nodeId 当前执行的节点ID
* @param nextNodeId 下一个要执行的节点ID
* @return 封装后的检查点对象(Optional,未启用检查点则返回空)
* @throws Exception 创建/保存检查点过程中发生异常时抛出
*/
public Optional<Checkpoint> addCheckpoint(String nodeId, String nextNodeId) throws Exception {
// 判断是否配置了检查点保存器(只有配置了才需要保存检查点)
if (compiledGraph.compileConfig.checkpointSaver().isPresent()) {
// 构建检查点对象:记录当前节点ID、克隆后的状态数据、下一个节点ID
var cp = Checkpoint.builder()
.nodeId(nodeId)
.state(cloneState(overallState.data()))
.nextNodeId(nextNodeId)
.build();
// 强制将检查点ID置空,确保执行**追加新检查点**操作,而非替换现有检查点
RunnableConfig appendConfig = RunnableConfig.builder(config).checkPointId(null).build();
// 调用保存器将检查点持久化,并更新配置(保存后会返回新的配置)
this.config = compiledGraph.compileConfig().checkpointSaver().get().put(appendConfig, cp);
// 返回已创建的检查点
return Optional.of(cp);
}
// 未配置检查点保存器时,返回空Optional
return Optional.empty();
}
构建的 cp 对象:

调用 SaverConfig#get 方法获取 CheckpointSaver 实例对象:
java
public BaseCheckpointSaver get() {
if (savers.isEmpty()) {
return null;
}
if (savers.size() == 1) {
return savers.get(0);
}
throw new IllegalStateException("Multiple savers configured, but no specific one requested.");
}
调用 MemorySaver#put 方法进行持久化,并构建新的运行配置,将当前检查点 ID 设置到配置中并返回:
java
/**
* 保存/更新检查点(重写父类方法)
* 根据配置中的检查点ID判断:存在则替换,不存在则新增
* @param config 运行配置
* @param checkpoint 要保存的检查点对象
* @return 更新后的运行配置(包含最新的检查点ID)
* @throws Exception 保存检查点过程中发生异常时抛出
*/
@Override
public final RunnableConfig put(RunnableConfig config, Checkpoint checkpoint) throws Exception {
// 加载或初始化检查点列表,执行检查点保存逻辑
return loadOrInitCheckpoints(config, checkpoints -> {
// 判断配置中是否存在检查点ID → 执行【替换检查点】逻辑
if (config.checkPointId().isPresent()) {
String checkPointId = config.checkPointId().get();
// 查找与ID匹配的检查点在列表中的索引位置
int index = IntStream.range(0, checkpoints.size())
.filter(i -> checkpoints.get(i).getId().equals(checkPointId))
.findFirst()
// 未找到对应ID的检查点,抛出异常
.orElseThrow(() -> (new NoSuchElementException(format("Checkpoint with id %s not found!", checkPointId))));
// 使用新的检查点替换列表中旧的检查点
checkpoints.set(index, checkpoint);
// 执行检查点更新后的回调逻辑
updatedCheckpoint(config, checkpoints, checkpoint);
// 替换模式:直接返回原配置
return config;
}
// 无检查点ID → 执行【新增检查点】逻辑,将检查点加入列表
checkpoints.push(checkpoint);
// 执行检查点新增后的回调逻辑
insertedCheckpoint(config, checkpoints, checkpoint);
// 新增模式:构建新配置,将当前检查点ID设置到配置中并返回
return RunnableConfig.builder(config)
.checkPointId(checkpoint.getId())
.build();
});
}
loadOrInitCheckpoints 加锁保证多线程安全:
java
/**
* 加载或初始化检查点列表(线程安全)
* 提供线程隔离的检查点管理,加锁保证多线程安全,执行检查点操作的转换逻辑
* @param config 运行配置
* @param transformer 检查点列表处理函数,用于执行具体的增删改查逻辑
* @return 处理后返回的结果
* @throws Exception 执行过程中抛出的异常
* @param <T> 返回值泛型类型
*/
protected final <T> T loadOrInitCheckpoints(RunnableConfig config,
TryFunction<LinkedList<Checkpoint>, T, Exception> transformer) throws Exception {
// 加锁:保证多线程环境下检查点操作的线程安全
_lock.lock();
try {
// 获取线程ID,未配置则使用默认线程ID
var threadId = config.threadId().orElse(THREAD_ID_DEFAULT);
// 根据线程ID获取/创建对应的检查点链表(线程隔离)
// 加载检查点数据后,传入自定义函数执行具体逻辑
return transformer.tryApply(
loadedCheckpoints(config, _checkpointsByThread.computeIfAbsent(threadId, k -> new LinkedList<>()))
);
} finally {
// 无论是否发生异常,最终都会释放锁
_lock.unlock();
}
}
最后 GraphRunnerContext 中的 RunnableConfig 是最新的。
4.3 节点执行器保存检查点(第二次)
进入到 NodeExecutor 执行时,执行完成后会调用到 handleActionResult 方法
java
private Flux<GraphResponse<NodeOutput>> handleActionResult(GraphRunnerContext context,
Map<String, Object> updateState, AtomicReference<Object> resultValue) {
try {
// ... 状态更新处理 ...
// 合并状态更新
context.mergeIntoCurrentState(updateState);
// 计算下一个节点
if (context.getCompiledGraph().compileConfig.interruptBeforeEdge()
&& context.getCompiledGraph().compileConfig.interruptsAfter().contains(context.getCurrentNodeId())) {
context.setNextNodeId(INTERRUPT_AFTER);
} else {
Command nextCommand = context.nextNodeId(context.getCurrentNodeId(), context.getCurrentStateData());
context.setNextNodeId(nextCommand.gotoNode());
}
// ★ 关键:添加检查点
NodeOutput output = context.buildNodeOutputAndAddCheckpoint(updateState);
context.doListeners(NODE_AFTER, null);
// 递归执行下一个节点
return Flux.just(GraphResponse.of(output))
.concatWith(Flux.defer(() -> mainGraphExecutor.execute(context, resultValue)));
} catch (Exception e) {
return Flux.just(GraphResponse.error(e));
}
}
在 buildNodeOutputAndAddCheckpoint 方法中也会调用 addCheckpoint 添加检查点:
java
public NodeOutput buildNodeOutputAndAddCheckpoint(Map<String, Object> updateStates) throws Exception {
// 为当前节点、下一个节点创建并保存检查点
Optional<Checkpoint> cp = addCheckpoint(currentNodeId, nextNodeId);
// 构建并返回节点输出结果(传入当前节点ID、状态更新数据、检查点)
return buildOutput(currentNodeId, updateStates, cp, false);
}
4.4 执行完成
图执行完成后,每个节点都会生成一个检查点保存到 Saver 中:

5. 检查点保存规则
在 Spring AI Alibaba Graph 中,检查点的保存遵循固定规则。
5.1 保存时机
- 每执行完成一个普通节点,立即保存一次检查点
- END 节点不保存检查点
- 中断节点(Interrupt)执行后会保存检查点
- 流式节点(LLM 流式)在流完全消费完成后,才会保存检查点
- 多次
invoke()同一threadId,会追加检查点历史
5.2 保存频率
- 一步一存:节点执行 → 状态合并 → 保存检查点 → 进入下一个节点
- 流式节点 = 执行完成后存一次 :不会每一个
token都存检查点 - 中断节点 = 立即存检查点
- 并行节点 = 所有分支完成后统一存一次
5.3 检查点数量
一次完整执行 START → A → B → C → END 会生成 4 个检查点:
START初始检查点A执行完成B执行完成C执行完成