Spring AI Alibaba 1.x 系列【60】检查点机制原理与全流程剖析

文章目录

  • [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(内存保存器)

MemorySaverBaseCheckpointSaver 接口的内存实现,将检查点存储在内存中的 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 初始化执行引擎上下文

ReactAgentStateGraph 执行时,底层都是调用 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 个检查点

  1. START 初始检查点
  2. A 执行完成
  3. B 执行完成
  4. C 执行完成

相关推荐
极光代码工作室2 小时前
基于机器学习的二手商品价格预测系统
人工智能·python·深度学习·机器学习
YueJoy.AI2 小时前
AI应用的隐私保护:从设计开始的隐私
人工智能·ai·语言模型
小当家.1052 小时前
PostgreSQL 做向量数据库:pgvector 在 RAG 中的实战与多场景适配
数据库·人工智能·postgresql·rag
ForgeAI码匠2 小时前
Maven 多模块项目如何避免越写越乱?Forge Admin 的模块边界实践
java·人工智能·开源·maven
Dola_Zou2 小时前
工业软件防破解避坑指南:CodeMeter 全流程入门与选型(上)
人工智能·自动化·视觉检测·软件工程·软件加密
生成论实验室2 小时前
我们给AI装上了判断力
人工智能·深度学习·语言模型·机器人·自动驾驶
掘金安东尼2 小时前
国内通用智能体(本地操作型 Agent)深度测评对比
人工智能
完成大叔2 小时前
Agent感知模式的情景化联想应用
人工智能
z落落2 小时前
C# 数组 最终完整版全套笔记(一维+多维+交错+引用类型+对象数组)
java·笔记·c#