RunnableConfig 详细分析
源码路径:
spring-ai-alibaba-graph-core/src/main/java/com/alibaba/cloud/ai/graph/RunnableConfig.java(约 505 行)
1. 类的定位
RunnableConfig 是图引擎的运行时配置,每一次 stream() / invoke() 调用都需要一个 RunnableConfig 实例。它回答的核心问题是:这次执行属于哪个线程、从哪个检查点恢复、用什么流模式、携带什么元数据。
与 CompileConfig(编译时配置)的分工:
| 维度 | CompileConfig | RunnableConfig |
|---|---|---|
| 生命周期 | 图编译时,一次性 | 每次执行,可多次 |
| 谁持有 | CompiledGraph 内部字段 | 调用方创建,作为参数传入 |
| 职责 | 定义"图能做什么"(中断点、Saver、递归上限) | 定义"这次执行怎么做"(线程 ID、恢复点、元数据) |
用户 ─── RunnableConfig ───▶ CompiledGraph.stream/invoke
│
CompileConfig ────┘(编译时已固定)
2. 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
threadId | String | 线程标识,checkpoint 存储的分区 key,不同线程互相隔离 |
checkPointId | String | 检查点 ID,从指定快照恢复时使用 |
nextNode | String | 下一个要执行的节点 ID,Human-in-the-loop 跳转用 |
streamMode | CompiledGraph.StreamMode | 流模式:VALUES(默认)/ SNAPSHOTS |
metadata | Map<String, Object> | 不可变,每次执行的环境信息(模型名、工具名、人类反馈等) |
context | ConcurrentMap<String, Object> | 可变,节点间传递的临时上下文,不持久化到 checkpoint |
store | Store | 长期记忆存储,跨执行持久化 |
interruptedNodes | Map<String, Object> | 被标记为中断的节点 ID → 中断数据 |
metadata与context的关键区别:metadata是不可变的,创建后不能修改,适合存放"这次执行的配置信息";context是ConcurrentHashMap,节点执行过程中可读写,适合"节点间传话",但不会持久化。
3. 元数据常量(公共 key 约定)
RunnableConfig 定义了一组标准 metadata key,供图引擎内部和用户使用:
| 常量 | 值 | 用途 |
|---|---|---|
AGENT_MODEL_NAME | "_AGENT_MODEL_" | 当前使用的 AI 模型名称 |
AGENT_TOOL_NAME | "_AGENT_TOOL_" | 当前工具名称 |
AGENT_HOOK_NAME_PREFIX | "_AGENT_HOOK_" | 钩子前缀,拼接节点 ID |
AGENT_NAME_KEY | "_AGENT_" | Agent 名称 |
HUMAN_FEEDBACK_METADATA_KEY | "HUMAN_FEEDBACK" | Human-in-the-loop 反馈数据 |
STATE_UPDATE_METADATA_KEY | "STATE_UPDATE" | 状态更新数据 |
DEFAULT_PARALLEL_EXECUTOR_KEY | "_DEFAULT_PARALLEL_EXECUTOR_" | 默认并行执行器 |
DEFAULT_PARALLEL_AGGREGATION_STRATEGY_KEY | "_DEFAULT_PARALLEL_AGGREGATION_STRATEGY_" | 默认并行聚合策略 |
DYNAMIC_TOOL_CALLBACKS_METADATA_KEY | "_DYNAMIC_TOOL_CALLBACKS_" | 动态工具回调 |
MERGE_REASONING_CONTENT_METADATA_KEY | "_MERGE_REASONING_CONTENT_" | 是否合并推理内容 |
这些常量本质上是 metadata Map 的标准化 key,避免硬编码字符串散落在代码各处。
4. 不可变设计 + with 方法
RunnableConfig 是 final class,字段在构造后不可变(metadata 用 Collections.unmodifiableMap 包装)。需要修改配置时,使用 with* 方法返回一个新实例:
// 原始 config
RunnableConfig config = RunnableConfig.builder()
.threadId("thread-1")
.build();
// 切换流模式 → 返回新 config,原 config 不变
RunnableConfig snapshotConfig = config.withStreamMode(StreamMode.SNAPSHOTS);
// 标记恢复 → 返回新 config
RunnableConfig resumeConfig = config.withResume();
// 更新检查点 ID → 返回新 config
RunnableConfig newConfig = config.withCheckPointId("cp-42");
这种设计保证了:
- 线程安全:并发场景下多个线程可共享同一个 config 而不互相影响
- 快照不可变:checkpoint 中保存的 config 引用不会被后续修改污染
唯一的例外是 context 字段(ConcurrentHashMap),它是有意的可变共享状态,用于节点间传递临时数据。
5. Builder 模式
RunnableConfig.Builder 继承自 HasMetadata.Builder<Builder>,提供链式构造:
RunnableConfig config = RunnableConfig.builder()
.threadId("thread-1")
.checkPointId("cp-42")
.nextNode("nodeB")
.streamMode(StreamMode.VALUES)
.addMetadata("configData", "test")
.addMetadata(RunnableConfig.AGENT_MODEL_NAME, "gpt-4")
.defaultParallelExecutor(ForkJoinPool.commonPool())
.store(myStore)
.build();
Builder 方法一览
| 方法 | 说明 |
|---|---|
threadId(String) | 设置线程 ID |
checkPointId(String) | 设置检查点 ID |
nextNode(String) | 设置下一节点 |
streamMode(StreamMode) | 设置流模式 |
addMetadata(String, Object) | 添加元数据(继承自 HasMetadata.Builder) |
addHumanFeedback(InterruptionMetadata) | 添加人类反馈 |
resume() | 设置 HUMAN_FEEDBACK 占位符,标记恢复执行 |
addStateUpdate(Map<String,Object>) | 添加状态更新 |
addParallelNodeExecutor(String, Executor) | 给指定并行节点设置执行器 |
defaultParallelExecutor(Executor) | 设置全局默认并行执行器 |
addParallelNodeAggregationStrategy(String, NodeAggregationStrategy) | 设置并行节点聚合策略 |
defaultParallelAggregationStrategy(NodeAggregationStrategy) | 设置默认聚合策略 |
mergeReasoningContent(boolean) | 设置推理内容合并标志 |
store(Store) | 设置长期存储 |
clearContext() | 清空上下文 |
build() | 构建 RunnableConfig |
Builder 有两个构造器:
Builder()— 空构造,从零开始Builder(RunnableConfig config)— 复制构造,从已有 config 拷贝所有字段
with* 方法内部就是使用复制构造器:
public RunnableConfig withStreamMode(StreamMode streamMode) {
return RunnableConfig.builder(this) // 复制当前 config
.streamMode(streamMode) // 覆盖目标字段
.build(); // 构建新实例
}
6. 中断(Interrupt)相关方法
中断是 Human-in-the-loop 的核心机制,RunnableConfig 负责追踪哪些节点被中断:
| 方法 | 行为 |
|---|---|
markNodeAsInterrupted(nodeId) | 在 interruptedNodes 中记录该节点 |
isInterrupted(nodeId) | 查询节点是否被中断 |
withNodeResumed(nodeId) | 创建新 config,将该节点标记为"已恢复" |
removeInterrupted(nodeId) | 移除中断标记 |
interruptData(key) | 读取中断时附带的数据 |
典型的中断-恢复流程
// 1. 首次执行,遇到中断
CompiledGraph graph = workflow.compile();
RunnableConfig config = RunnableConfig.builder().threadId("t1").build();
graph.stream(Map.of("input", "hello"), config).blockLast();
// → 执行到 nodeB 时被 interruptsBefore 中断
// 2. 获取中断时的快照
StateSnapshot snapshot = graph.getState(config);
// 3. 人工修改状态
config = graph.updateState(snapshot.config(), Map.of("approval", "yes"), "nodeB");
// 4. 恢复执行
RunnableConfig resumeConfig = config.withResume();
graph.stream(null, resumeConfig).blockLast();
7. 并行执行器配置
Builder 提供两级并行执行器配置:
// 方式一:给特定并行节点指定执行器
RunnableConfig.builder()
.addParallelNodeExecutor("parallelNode_A", customExecutor)
.build();
// 方式二:设置全局默认执行器
RunnableConfig.builder()
.defaultParallelExecutor(ForkJoinPool.commonPool())
.build();
// 同理,聚合策略也有两级
RunnableConfig.builder()
.addParallelNodeAggregationStrategy("targetNode", myStrategy)
.defaultParallelAggregationStrategy(defaultStrategy)
.build();
执行器存储在 metadata 中(以 _DEFAULT_PARALLEL_EXECUTOR_ 或 nodeId 为 key),运行时由 ParallelNode 读取。
8. HasMetadata 接口
RunnableConfig 实现了 HasMetadata<RunnableConfig.Builder> 接口:
public interface HasMetadata<B extends HasMetadata.Builder<B>> {
String INTERRUPT_PREFIX = "__NODE_INTERRUPT__";
static String formatNodeId(String nodeId); // → "__NODE_INTERRUPT__(nodeId)"
Map<String, Object> metadata();
Object metadata(String key);
<T> T metadata(String key, TypeRef<T> typeRef);
<T> T getMetadataAndRemove(String key, TypeRef<T> typeRef); // 读后删除
}
INTERRUPT_PREFIX 是中断节点在 metadata 中的 key 前缀,格式为 __NODE_INTERRUPT__(nodeId)。
9. 在图引擎中的流转路径
用户构建 RunnableConfig
↓
CompiledGraph.stream(inputs, config)
↓
GraphRunner(initialState, config, compiledGraph)
↓
GraphRunnerContext(state, config)
├─ config 含 HUMAN_FEEDBACK 或 checkPointId?
│ └─ Yes → initializeFromResume()(从 checkpoint 恢复)
│ └─ No → initializeFromStart()
↓
每个节点执行后:
├─ GraphLifecycleListener.afterNode(nodeId, state, config)
├─ BaseCheckpointSaver.put(config, checkpoint)
│ └─ 返回含新 checkPointId 的 RunnableConfig
└─ 中断判断 → config.markNodeAsInterrupted(nodeId)
↓
执行结束:
├─ GraphLifecycleListener.onComplete(state, config)
└─ CompileConfig.releaseThread = true?
└─ BaseCheckpointSaver.release(config) — 释放线程资源
10. 与 CompileConfig 的协作总结
┌─────────────────────────────────────────────────────┐
│ CompileConfig(编译时) │
│ saverConfig → 用什么 Saver 存 checkpoint │
│ interruptsBefore → 哪些节点前中断 │
│ interruptsAfter → 哪些节点后中断 │
│ recursionLimit → 最大迭代数 │
│ releaseThread → 执行完是否释放线程 │
└────────────────────────┬────────────────────────────┘
│ 编译固化
▼
┌─────────────────────────────────────────────────────┐
│ RunnableConfig(运行时) │
│ threadId → 存到哪个 thread 分区 │
│ checkPointId → 从哪个快照恢复 │
│ nextNode → 恢复后跳到哪个节点 │
│ metadata → 模型名/工具名/人类反馈 │
│ context → 节点间临时传话 │
│ streamMode → VALUES / SNAPSHOTS │
│ interruptedNodes → 运行时追踪中断状态 │
└─────────────────────────────────────────────────────┘
11. 总结
| 维度 | 说明 |
|---|---|
| 职责 | 图的运行时配置:线程标识、检查点定位、流模式、执行元数据 |
| 不可变性 | final class,字段构造后不可变,修改通过 with* 返回新实例 |
| 例外 | context 字段为 ConcurrentHashMap,有意允许节点间可变共享 |
| 中断追踪 | interruptedNodes 记录哪些节点被中断,配合 withResume() 恢复 |
| Builder | 链式构造 + 复制构造,支持并行执行器/聚合策略的两级配置 |
| 元数据约定 | 10+ 个标准常量 key,避免硬编码字符串 |
| 流转 | 用户创建 → CompiledGraph → GraphRunnerContext → Saver.put → 返回新 config |
相关文件
CompiledGraph.md— 执行层,RunnableConfig的主要消费方StateGraph.md— 图定义层,边动作接口接收RunnableConfigEdgeCondition.md— 条件边路由,singleAction/multiAction接收RunnableConfigAsyncCommandAction.md— 单路由 action,签名含RunnableConfig参数CompileConfig.java— 编译时配置,与RunnableConfig互补协作HasMetadata.java—RunnableConfig的父接口,定义 metadata 读取能力GraphRunnerContext.java— 持有RunnableConfig,根据其内容决定启动或恢复BaseCheckpointSaver.java—list/get/put/release均以RunnableConfig为参数GraphLifecycleListener.java— 所有生命周期回调传入RunnableConfig