文章目录
- [1. 概述](#1. 概述)
-
- [1.1 Human-in-the-Loop(人在回路)](#1.1 Human-in-the-Loop(人在回路))
- [1.2 Interrupts(执行中断)](#1.2 Interrupts(执行中断))
- [1.3 执行中断 VS 人在回路](#1.3 执行中断 VS 人在回路)
- [2. interruptBefore 模式](#2. interruptBefore 模式)
-
- [2.1 配置项说明](#2.1 配置项说明)
-
- [2.1.1 CompileConfig](#2.1.1 CompileConfig)
- [2.1.2 RunnableConfig](#2.1.2 RunnableConfig)
- [2.2 配置中断节点](#2.2 配置中断节点)
- [2.3 定义中断反馈结果](#2.3 定义中断反馈结果)
- [2.4 流程中断](#2.4 流程中断)
- [2.5 批准执行](#2.5 批准执行)
-
- [2.5.1 情况 1 :不修改状态](#2.5.1 情况 1 :不修改状态)
- [2.5.2 状态合并 BUG](#2.5.2 状态合并 BUG)
- [2.5.3 情况 2 :修改状态](#2.5.3 情况 2 :修改状态)
- [2.6 人工拒绝](#2.6 人工拒绝)
- [3. interruptsAfter 模式](#3. interruptsAfter 模式)
-
- [3.1 简单案例](#3.1 简单案例)
- [3.2 interruptBeforeEdge 配置](#3.2 interruptBeforeEdge 配置)
1. 概述
1.1 Human-in-the-Loop(人在回路)
Human-in-the-Loop 为智能体的工具调用 增加人工审核机制。当大模型拟执行存在风险的操作(如写入文件、执行 SQL 语句)时,该中间件可暂停流程执行,等待人工决策。
内置三种人工响应中断的决策方式:
| 决策类型 | 说明 | 适用场景示例 |
|---|---|---|
| ✅ 批准(approve) | 直接认可原操作,无修改执行 | 原样发送邮件草稿 |
| ✏️ 编辑(edit) | 修改工具调用参数后再执行 | 发送邮件前修改收件人 |
| ❌ 拒绝(reject) | 驳回工具调用,并在对话中附上拒绝理由 | 拒绝邮件草稿并说明改写建议 |
在之前我们介绍过 React 基于 HumanInTheLoopHook 模型后置钩子(模型生成响应后、工具调用执行前触发)实现,处理逻辑:
- 智能体调用大模型生成响应
- 中间件解析响应中的工具调用
- 若需人工介入,构造含操作请求、审核配置的
HITL请求并触发中断 - 智能体挂起,等待人工决策
- 根据人工决策:执行批准/编辑的调用、为拒绝操作生成工具消息、将人工回复直接作为工具返回结果,最终恢复流程执行
1.2 Interrupts(执行中断)
执行中断 允许在流程图的指定节点暂停执行,等待外部输入后再继续运行。该机制是实现人在回路 流程的核心,适用于需要外部人工介入才能继续执行的场景。触发中断时,Graph 会通过持久化层保存流程图完整状态,并无限等待直到手动恢复执行。
中断核心价值:暂停执行、等待外部输入
典型应用场景:
- 审批流程:执行高危操作前暂停(接口调用、数据库变更、金融交易)
- 多中断并行处理:单次调用批量恢复多个并行分支的中断
- 审核与编辑:人工审核、修改大模型输出或工具调用参数后再继续
- 工具调用中断:工具执行前暂停,人工审核编辑后再执行
- 输入校验:步骤流转前校验人工输入,非法则重新询问
Spring AI Alibaba Graph 提供了提供了两种方式来实现重点:
- 通过实现
InterruptableAction接口来控制中断时机,可以在任意时刻返回InterruptionMetadata来中断执行 - 编译
Graph时指定中断点(interruptBefore、interruptsAfter),在指定节点执行前后自动中断
两种中断模式对比:
| 特性 | InterruptionMetadata 模式 | interruptBefore\ interruptsAfter模式 |
|---|---|---|
| 中断时机 | 运行时动态决定 | 编译时预先定义 |
| 节点要求 | 需要实现 InterruptableAction 接口 |
普通节点即可 |
| 灵活性 | 高,可根据运行时状态动态中断 | 中等,需在编译时固定中断位置 |
| 配置复杂度 | 较高,需实现接口方法 | 低,仅配置节点名称即可 |
| 适用场景 | 需依据运行时业务状态动态判断是否中断 | 业务流程中已知、固定的中断点位 |
HITL规定了只能在工具调用前暂停 ,而Interrupts更灵活支持任意节点、任意时机、任意触发条件都能暂停
1.3 执行中断 VS 人在回路
| 对比维度 | 执行中断(Interrupts) | 人在回路(Human-in-the-Loop, HITL) |
|---|---|---|
| 核心定位 | Graph 底层技术原语、基础工具 | AI 流程的业务设计模式、人机协作规范 |
| 本质 | 底层实现恢复指令,实现暂停/恢复 | 受控的人机协作机制,封装了固定的审批逻辑(批准/编辑/驳回/应答) |
| 核心作用 | 在流程图任意位置暂停执行、保存状态,等待外部输入后恢复运行 | 将人纳入自动化流程,关键节点(多为工具调用)需人工审批/编辑后才能继续 |
| 灵活性 | 极高,可在任意节点、任意时机、任意条件下触发暂停 | 较低,仅针对工具调用场景,按预设规则触发暂停,决策方式固定 |
| 触发场景 | 调试断点、多轮对话等待用户输入、任意节点条件暂停(如风险判断)、非工具环节人工确认等 | 工具调用前审批(如执行SQL、写文件、发邮件)、高危操作把关、合规校验、工具调用参数审核修改 |
| 实现方式 | 执行中断机制,自定义暂停时机和恢复逻辑 | 集成 HITL 钩子,配置 interrupt_on 定义需审批的工具及允许的决策类型 |
| 依赖关系 | 独立技术能力,不依赖 HITL |
依赖 Interrupts 实现暂停功能,是 Interrupts 的业务化封装 |
| 核心用途 | 满足所有需要暂停流程的场景,通用且灵活 | 生产级安全场景,重点管控工具调用,保障流程合规、安全 |
一句话总结:
- Interrupts :
Graph的底层暂停/恢复技术,万能工具 - HITL :基于
Interrupts的人机协作安全模式,专用于执行时人工审批
2. interruptBefore 模式
在编译 Graph 时提前指定中断点,在指定节点执行前后自动中断。这种方式适合已知的中断点,配置简单直接。
优势:
- 配置简单:只需在编译配置中指定中断点
- 无需修改节点:普通节点即可,不需要实现特殊接口
- 明确的中断点:中断位置在编译时确定,易于理解和维护
接下来,我们在之前【邮件处理工作流】案例的基础上进行中断演示!
2.1 配置项说明
2.1.1 CompileConfig
CompileConfig 中断配置项说明:
| 配置项 | 类型 | 作用 | 示例 |
|---|---|---|---|
interruptsBefore |
Set<String> |
在指定节点执行前中断 | interruptBefore("llm_node") |
interruptsAfter |
Set<String> |
在指定节点执行后中断 | interruptAfter("tool_node") |
interruptBeforeEdge |
boolean |
恢复执行后动态计算下一个节点(配合 interruptsAfter) |
interruptBeforeEdge(true) |
示例:
java
CompileConfig config = CompileConfig.builder()
// 方式1:执行前中断
.interruptBefore("approval_node", "review_node")
// 方式2:执行后中断
.interruptAfter("tool_call_node")
// 方式3:边评估前中断(需配合 interruptsAfter)
.interruptAfter("decision_node")
.interruptBeforeEdge(true)
.saverConfig(SaverConfig.builder().register(MemorySaver.builder().build()).build())
.build();
StateGraph graph = new StateGraph(...)
.compile(config);
2.1.2 RunnableConfig
RunnableConfig 是流程执行、中断、断点续跑、人工反馈 的配置载体,所有中断相关的状态、标记、元数据都通过它传递和管理。
定义了中断/续跑场景下的固定元数据 Key,是引擎识别人工反馈、状态更新的唯一标识:
java
// 1. 人工反馈标记:标识流程为【断点续跑】模式
public static final String HUMAN_FEEDBACK_METADATA_KEY = "HUMAN_FEEDBACK";
// 2. 状态更新标记:续跑时合并人工输入的状态数据
public static final String STATE_UPDATE_METADATA_KEY = "STATE_UPDATE";
作用:
HUMAN_FEEDBACK:触发断点续跑 的核心标记(GraphRunnerContext据此初始化续跑上下文)STATE_UPDATE:InterruptableAction模式专用,续跑时合并人工反馈的业务状态
中断节点状态管理(核心字段):
java
// 并发Map:存储【节点ID -> 中断状态】,线程安全
private final Map<String, Object> interruptedNodes;
- 存储格式:
格式化节点ID → true/false true:节点处于中断暂停状态false:节点已恢复执行- 线程安全:使用
ConcurrentHashMap,支持多线程场景
中断状态操作核心方法,引擎/业务侧用于标记、判断、恢复、清除节点中断状态:
- isInterrupted(String nodeId):判断节点是否中断
markNodeAsInterrupted(String nodeId):标记节点为中断状态withNodeResumed(String nodeId):将节点标记为已恢复,引擎继续执行removeInterrupted(String nodeId):清空节点的中断标记,流程执行完成,清理中断状态
流程续跑配置方法,用于构建续跑配置,告诉引擎「这是一次中断恢复执行」:
withResume():自动添加HUMAN_FEEDBACK元数据,触发断点续跑addHumanFeedback(InterruptionMetadata humanFeedback):添加人工反馈,携带中断元数据,恢复时关联原始中断信息addStateUpdate(Map<String, Object> stateUpdate):添加续跑状态更新(InterruptableAction专用),携带人工输入的状态数据,续跑时自动合并
链式构建中断/续跑配置,业务侧最常用:
java
RunnableConfig config = RunnableConfig.builder()
.threadId("thread_123") // 会话ID
.checkPointId("checkpoint_456") // 断点ID
.resume() // 标记续跑(添加HUMAN_FEEDBACK)
.addStateUpdate(Map.of("approve", true)) // 添加工单审批状态
.build();
2.2 配置中断节点
CompileConfig 提供了两种【节点执行前触发中断】的配置方法:
java
/**
* 从集合中设置【节点执行前触发】的多个中断点
* @param interruptsBefore 存储中断点标识的字符串集合
* @return 当前构建器实例,用于方法链式调用
*/
public Builder interruptsBefore(Collection<String> interruptsBefore) {
// 将集合转换为不可修改的Set集合,赋值给中断点配置
this.config.interruptsBefore = interruptsBefore.stream().collect(Collectors.toUnmodifiableSet());
return this;
}
/**
* 通过可变参数方式,设置【节点执行前触发】的单个/多个中断点
* @param interruptBefore 一个或多个表示中断点的字符串
* @return 当前构建器实例,用于方法链式调用
*/
public Builder interruptBefore(String... interruptBefore) {
// 将可变参数转换为不可变Set集合,赋值给中断点配置
this.config.interruptsBefore = Set.of(interruptBefore);
return this;
}
节点名称就是我们在添加节点时指定的名称:
java
workflow.addNode("read_email", readEmail)
ReactAgent 只有模型、工具节点,是在 ReactAgent#initGraph 方法中定义的:
java
graph.addNode(AGENT_MODEL_NAME, node_async(this.llmNode));
if (hasTools) {
graph.addNode(AGENT_TOOL_NAME, node_async(this.toolNode));
}
常量定义在 RunnableConfig 类中:
java
public static final String AGENT_MODEL_NAME = "_AGENT_MODEL_";
public static final String AGENT_TOOL_NAME = "_AGENT_TOOL_";
自定义状态图、ReactAgent 都需要通过 CompileConfig 配置中断节点,这里的配置标识在 read_email (读取邮件)节点执行前进行中断:
java
// 自定义状态图
CompileConfig compileConfig = CompileConfig.builder()
.saverConfig(saverConfig)
.interruptBefore("read_email")
.store(new MemoryStore())
.build();
CompiledGraph compiledGraph = workflow.compile(compileConfig);
// ReactAgent
ReactAgent chatAgent = ReactAgent.builder()
.name("email-chat-agent")
.observationRegistry(observationRegistry)
.compileConfig(compileConfig)
.enableLogging(true)
.tools()
.saver(new MemorySaver())
.model(chatModel)
.instruction("你是一个邮件处理助手,可以帮助用户处理邮件分类、文档搜索、Bug追踪和回复起草等问题。请用中文回答用户的问题。")
.build();
2.3 定义中断反馈结果
在 HITL 中定义了三种工具审批结果:
java
// ==================== FeedbackResult 枚举 ====================
/**
* FeedbackResult - 工具审批结果枚举
*
* <p>表示用户对工具调用的审批决定。
*/
public enum FeedbackResult {
/**
* 已批准 - 允许执行工具
*/
APPROVED,
/**
* 已拒绝 - 拒绝执行工具
*/
REJECTED,
/**
* 已编辑 - 用户修改了工具参数后批准
*/
EDITED;
}
Interrupts 更加灵活,我们可以自定义多种处理结果,比如在以上三种基础上加:
| 决策类型 | 说明 | 适用场景示例 |
|---|---|---|
| 💬 回复(respond) | 跳过节点执行,人工输入内容直接作为节点返回结果 | 直接回复「询问用户」类提示 |
自定义中断反馈结果枚举类:
java
public enum InterruptFeedbackResult {
APPROVED("批准", "APPROVED"),
EDITED("编辑", "EDITED"),
REJECTED("拒绝", "REJECTED"),
RESPOND("回复", "RESPOND");
private final String name;
private final String code;
InterruptFeedbackResult(String name, String code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public String getCode() {
return code;
}
public static InterruptFeedbackResult fromCode(String code) {
for (InterruptFeedbackResult action : values()) {
if (action.getCode().equals(code)) {
return action;
}
}
return null;
}
}
后端可以根据不同的审批策略,执行对应逻辑:
java
switch (action) {
case APPROVED:
// 人工批准:恢复执行
break;
case EDITED:
// 人工编辑:更新状态并设置
break;
case REJECTED:
// 人工拒绝:更新状态标记拒绝后终止执行
return Flux.just("❌ 执行已被拒绝终止。");
default:
throw new IllegalArgumentException("Unknown action: " + action);
}
}
2.4 流程中断
执行前中断执行逻辑:
- 流程在
interruptBefore调用前精准挂起 - 检查点器保存完整状态,支持后续恢复;生产环境需使用数据库等持久化检查点
- 返回
InterruptionMetadata数据,我们需要根据输出类型进行处理
interruptBefore 模式下只要执行 Graph 到中断节点前,一定会执行自动中断并返回 InterruptionMetadata 数据,我们需要根据输出类型进行处理:
- 如果是
InterruptionMetadata类型说明流程暂停了 - 检测到中断点时,输出相关处理页面
部分处理代码:
java
return stream
.map(output -> {
String nodeName = output.node();
OverAllState state = output.state();
boolean isInterrupted = output instanceof InterruptionMetadata;
StringBuilder result = new StringBuilder();
// 检测中断点
if (isInterrupted) {
result.append("⏸️ 执行已中断,等待审核...\n");
result.append("节点: ").append(nodeName).append("\n");
result.append("sessionId: ").append(sessionId).append("\n");
result.append("\n请选择审核操作:\n");
result.append("<div class=\"action-buttons\">\n");
result.append(" <button class=\"action-btn approved-btn\" onclick=\"handleInterruption('APPROVED')\">");
result.append("✅ 批准 - 继续执行</button>\n");
result.append(" <button class=\"action-btn rejected-btn\" onclick=\"handleInterruption('REJECTED')\">");
result.append("❌ 拒绝 - 终止执行</button>\n");
result.append(" <button class=\"action-btn edited-btn\" onclick=\"showEditStateForm()\">");
result.append("✏ 编辑 - 修改状态后继续</button>\n");
result.append("</div>\n");
result.append("<div id=\"editStateForm\" style=\"display:none;margin-top:10px;\">\n");
result.append(" <textarea id=\"editedStateInput\" placeholder='输入JSON格式的状态更新,例如:{\"draft_response\":\"修改后的回复\"}' style=\"width:100%;height:80px;padding:8px;border:1px solid #ddd;border-radius:8px;resize:vertical;\"></textarea>\n");
result.append(" <button class=\"action-btn confirm-edited-btn\" onclick=\"handleInterruption('EDITED')\" style=\"margin-top:8px;\">确认修改并继续</button>\n");
result.append("</div>\n");
// 输出当前状态信息供审核
state.value("classification").ifPresent(v -> {
result.append("\n分类结果: ").append(v).append("\n");
});
state.value("draft_response").ifPresent(v -> {
result.append("\n回复草稿: ").append(v).append("\n");
});
}
InterruptionMetadata 数据:

json
{
nodeId = 'read_email',
state = {
"OverAllState": {
"data": {
"email_id": "7eb310d1-6e77-43a2-9ddb-de63fa9586db",
"_graph_execution_id_": "041fa408-6131-4b6a-9b2d-b372a2582e95",
"email_content": "你好",
"sender_email": "user@example.com"
}
}
},
metadata = {}
}
前端显示效果:

2.5 批准执行
AI 流程运行到指定审批节点时主动中断暂停,让出执行权给人工;人工完成审批 / 填写参数后,基于原断点恢复流程继续往下执行。
继续往下执行时又分为多种情况:
- 不修改状态数据,使用原数据继续执行
- 修改状态数据,替换掉原数据后再继续执行
恢复中断关键要点:
-
恢复时必须使用触发中断时相同的
thread_id,为了获取到对应的检查点数据,获取使用默认$default,会造成紊乱 -
构建
HUMAN_FEEDBACK元数据,标识这是一个恢复中断操作 -
传递
checkPointId值(检查点ID),获取中断时保存的状态数据,否则取最后一条
2.5.1 情况 1 :不修改状态
构建 RunnableConfig :
java
// 人工批准:恢复执行
RunnableConfig approvedResumeConfig = RunnableConfig.builder()
.threadId(sessionId) // 会话 ID
.resume() // 构建 `HUMAN_FEEDBACK` 元数据
.build();
支持传递 RunnableConfig 配置的流式方法只有一个:
java
public Flux<NodeOutput> stream(Map<String, Object> inputs, RunnableConfig config) {
return streamFromInitialNode(stateCreate(inputs), config);
}
不修改状态可以直接传入 null 会使用原状态数据继续执行(官网中提供的方法):
java
stream = emailAgentGraph.stream(null, approvedResumeConfig);
对话结果中,发现原先的状态数据消失了 ,在分类节点时中没有任何数据,说明恢复执行后检查点中的数据没有合并到全局状态中:

可以先查询检查点状态,再次传入:
java
StateSnapshot stateSnapshot = emailAgentGraph.getState(config);
Map<String, Object> input = stateSnapshot.state().data();
stream = emailAgentGraph.stream(input, approvedResumeConfig);
再次对话,发现正常了:

2.5.2 状态合并 BUG
在流程恢复执行时,会将原先检查点状态,合并到用户传入的状态中,在上面不修改状态运行中,因为是执行前中断,所以检查点中保存的是 _START 节点的数据:

合并策略只会为将检查点中配置了 Key 策略的数据,添加到新的全局状态中,恢复执行时,我们传入的 null 的全局状态为:

检查点中没有 input 数据,所以导致全局状态完全是空的。按照正常逻辑来说,不应该是以新传入的数据为准:
- 用户没有传数据时,将检查点中的所有数据都合并到全局状态中
- 用户传了数据时,以新数据为主,检查点中的数据为辅
在 GitHub 中可以看到,这是一个 BUG ,把「旧状态」和「新状态」传反了,导致恢复流程时,数据合并(追加 / 累加 / 替换)结果错误,并在 2026-2 月提交了 PR ,最新版本(1.1.2.2)尚未修复:

2.5.3 情况 2 :修改状态
用户提交修改后在恢复执行的场景中,第一步需要显示给用户可编辑操作和内容,比如,这里演示直接将所有的状态数据返回用户,用户点击编辑后,输入新的状态数据恢复执行:

将修改后的状态传递给流程进行恢复执行:
java
@SuppressWarnings("unchecked")
Map<String, Object> stateUpdate = (Map<String, Object>) request.get("stateUpdate");
if (stateUpdate == null) {
stateUpdate = Map.of();
}
RunnableConfig resumeWithEditConfig = RunnableConfig.builder()
.threadId(sessionId)
.resume()
.build();
stream = emailAgentGraph.stream(stateUpdate, resumeWithEditConfig);
2.6 人工拒绝
如果是人工拒绝,可以在状态元数据中添加一个拒绝标识,会调用检查点进行更新和持久化,下次再执行这个会话时,可以提示当前会话流程已终止。
添加元数据:
java
emailAgentGraph.updateState(
config,
Map.of("human_feedback", "rejected"),
null
);
log.info("Rejected - terminating execution for sessionId: {}", sessionId);
return Flux.just("❌ 执行已被拒绝终止。");
在恢复执行前,需要自定义判断逻辑:
java
var currentState = emailAgentGraph.getState(config);
Map<String, Object> stateData = currentState.state().data();
String human_feedback = stateData.get("human_feedback").toString();
if ("rejected".equals(human_feedback)){
throw new RuntimeException("当前流程已终止,无法进行恢复!!");
}
3. interruptsAfter 模式
interruptsAfter 用于配置【节点执行后】执行中断,指定节点执行完成结束后再暂停流程。
适用场景:
- 文案生成、合同解析、内容抽取等节点跑完,暂停供人工校对、修改;
- 节点产出业务结果后,需要外部系统回调、消费数据,再继续往下走;
- 子图 / 子流程执行完毕后暂停,等待外部指令再进入主流程下一环节;
- 数据加工、统计计算节点执行完,需先落库、发通知,再手动恢复续跑。
用法和
interruptBefore基本一致,很多一样的地方就不赘述了
3.1 简单案例
示例,定义classify_intent (邮件分类)节点执行后进行中断:
java
CompileConfig compileConfig = CompileConfig.builder()
.saverConfig(saverConfig)
.interruptAfter("classify_intent")
.store(new MemoryStore())
.build();
输入测试:

3.2 interruptBeforeEdge 配置
中断时只暂停不规划,恢复时再动态算下一步,专门为「动态条件路由边」设计,等人工改完数据状态后,再让流程做条件判断,保证路由正确。
对配置了 interruptsAfter 的节点,interruptBeforeEdge 可以配置:
false(默认值):中断后,直接计算下一个节点true:中断后,不提前计算下一个业务节点,只把nextNodeId标记为__INTERRUPTED__,恢复后才根据当前状态重新计算真正的下一个节点
配置示例:
java
CompileConfig compileConfig = CompileConfig.builder()
.saverConfig(saverConfig)
.interruptAfter("read_email")
.interruptBeforeEdge(true)
.store(new MemoryStore())
.build();
如果有【邮件读取】、【邮件分类】两个节点,当配置了【邮件读取】配置了执行后中断时,并开启了 interruptBeforeEdge = true ,下一个节点设置为(__INTERRUPTED__),而不是【邮件分类】:

【邮件读取】执行完成后会进行中断,在恢复执行时,会再将下一个节点设置为本身应该执行的节点(实时计算):
