文章目录
- [1. 概述](#1. 概述)
- [2. InterruptableAction 接口](#2. InterruptableAction 接口)
-
- 2.1【节点执行前】中断
- [2.2 【节点执行后】中断](#2.2 【节点执行后】中断)
- [3. InterruptableAction 实现动态中断](#3. InterruptableAction 实现动态中断)
-
- [3.1 文章提交节点](#3.1 文章提交节点)
- [3.2 AI 内容审核节点](#3.2 AI 内容审核节点)
- [3.3 人工审核节点](#3.3 人工审核节点)
- [3.4 最终审核确认节点](#3.4 最终审核确认节点)
- [3.5 组装工作流](#3.5 组装工作流)
- [3.6 对话测试](#3.6 对话测试)
-
- [3.6.1 提交文章,执行到人工审核节点](#3.6.1 提交文章,执行到人工审核节点)
- [3.6.2 提交人工审核结果,恢复执行](#3.6.2 提交人工审核结果,恢复执行)
- [3.6.3 测试结果](#3.6.3 测试结果)
- [4. 其他说明](#4. 其他说明)
-
- [4.1 interruptBeforeEdge](#4.1 interruptBeforeEdge)
- [4.2 STATE_UPDATE](#4.2 STATE_UPDATE)
1. 概述
InterruptionMetadata 模式允许节点在运行时动态决定是否需要中断,提供了最大的灵活性。节点通过实现 InterruptableAction 接口,可以在任意时刻返回 InterruptionMetadata 来中断执行。
优势:
- 灵活性强:可以在任意节点根据运行时状态决定是否中断
- 动态控制:中断逻辑由节点自身控制,不需要提前配置
- 状态感知:可以根据当前状态动态决定是否需要等待用户输入
2. InterruptableAction 接口
InterruptableAction 是流程图执行中断的标准契约 ,实现了这个接口的节点,可以在节点执行前/执行后主动暂停流程 ,专门用来做:人工审批、等待用户输入、条件暂停。
定义了流程图节点执行时的中断契约,提供两个中断钩子:
- 节点执行前中断
- 节点执行后中断
核心执行流程:
interrupt() → 节点业务逻辑 apply() → interruptAfter()
执行前中断 节点核心逻辑 执行后中断
关键参数含义:
| 参数 | 含义 |
|---|---|
nodeId |
当前要执行的节点ID(标记哪个节点要中断) |
state |
流程当前的全局状态(OverAllState,存业务数据) |
config |
运行配置(RunnableConfig,存中断标记、线程ID、审批数据) |
actionResult |
节点执行完的返回结果(仅后中断有) |
2.1【节点执行前】中断
节点的业务代码还没执行 之前调用,判断是否需要提前暂停流程,适用于节点一开始就需要人工审批、等待用户输入的场景。
java
/**
* 【节点执行前】中断判断
* <p>
* 调用时机:当前节点的业务方法 apply() 执行之前被调用
* 作用:根据节点ID、流程状态、运行配置,决定是否暂停流程
* @param nodeId 当前执行的节点ID
* @param state 流程全局状态(存储业务数据)
* @param config 运行配置(线程ID、断点ID、中断标记等)
* @return Optional<InterruptionMetadata>
* 有值:触发流程中断,返回中断元信息
* 空值:不中断,继续执行节点
*/
Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config);
);
2.2 【节点执行后】中断
节点业务代码执行完成 之后调用,默认实现是空(不中断),可以按需重写,如果中断,节点执行结果会先合并到状态 → 生成检查点 → 再暂停。适用于根据节点执行结果,决定是否需要人工审核的场景。
java
/**
* 【节点执行后】中断判断
* <p>
* 调用时机:当前节点的业务方法 apply() 执行完成后,结果合并到状态前调用
* 特点:默认实现为不中断,可根据业务重写
* 注意:如果触发中断,节点执行结果会先合并状态、生成检查点,再暂停流程
* @param nodeId 当前执行的节点ID
* @param state 流程全局状态(节点结果未合并前)
* @param actionResult 节点业务方法执行后的返回结果
* @param config 运行配置
* @return Optional<InterruptionMetadata>
* 有值:触发流程中断
* 空值:不中断,继续执行(默认)
*/
default Optional<InterruptionMetadata> interruptAfter(String nodeId, OverAllState state,
Map<String, Object> actionResult, RunnableConfig config) {
// 默认不执行中断
return Optional.empty();
}
3. InterruptableAction 实现动态中断
案例 :AI 文章智能审核工作流
业务流程:
- 开始 → 文章提交 →
AI内容审核(支持执行前、执行后双中断)
2. AI 审核完成后进入人工审核环节:
- 支持执行前中断 :配置
skipAiReview=true可跳过A审核,直接进入人工审核 - 支持执行后中断 :
AI检测到高风险内容(评分 <30分)自动中断流程,等待人工复核确认
-
人工审核节点(支持执行前中断):
- 执行前中断:进入人工审核节点即刻暂停流程,等待审核人员录入审核结果与审批意见
-
最终审核确认(支持执行前、执行后双中断):
- 执行前中断:AI 自动审批通过但综合评分低于阈值(<
90分),触发中断要求人工二次确认 - 执行后中断:整体审核流程完成后触发中断,展示最终审核结果,等待人工发布确认操作
- 执行前中断:AI 自动审批通过但综合评分低于阈值(<
-
人工确认发布 → 流程正常结束
节点中断能力汇总:
| 流程节点 | 支持中断类型 | 中断触发规则 |
|---|---|---|
| AI 内容审核 | 执行前中断 / 执行后中断 | 前中断:可配置跳过;后中断:风险评分<30分 |
| 人工审核 | 执行前中断 | 进入节点即暂停,等待人工录入审批结果 |
| 最终审核确认 | 执行前中断 / 执行后中断 | 前中断:评分<90分需二次确认;后中断:审核完成等待发布确认 |
中断类型说明:
| 中断类型 | 触发节点 | 时机 | 说明 |
|---|---|---|---|
| SKIP_AI_REVIEW | ai_content_review | 前中断 | 跳过 AI 审核直接进入人工审核 |
| HIGH_RISK_DETECTED | ai_content_review | 后中断 | AI 检测到高危内容需确认 |
| HUMAN_REVIEW_REQUIRED | human_review | 前中断 | 等待人工审核输入 |
| AUTO_APPROVE_CONFIRMATION | final_approval | 前中断 | AI 自动批准确认 |
| PUBLISH_CONFIRMATION | final_approval | 后中断 | 最终发布确认 |
3.1 文章提交节点
接收用户提交的文章内容,进行预处理和初始化审核状态。
java
public class ArticleSubmitNode implements AsyncNodeActionWithConfig {
private static final Logger log = LoggerFactory.getLogger(ArticleSubmitNode.class);
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
String articleTitle = state.value("article_title", String.class).orElse("未命名文章");
String articleContent = state.value("article_content", String.class).orElse("");
String authorId = state.value("author_id", String.class).orElse("anonymous");
log.info("Processing article submission - title: {}, content length: {}, author: {}",
articleTitle, articleContent.length(), authorId);
// 文章预处理 - 基本校验
if (articleContent.length() < 10) {
return CompletableFuture.completedFuture(Map.of(
"review_status", "REJECTED",
"rejection_reason", "文章内容过短,至少需要10个字符",
"article_id", UUID.randomUUID().toString(),
"next_node", "END"
));
}
if (articleContent.length() > 50000) {
return CompletableFuture.completedFuture(Map.of(
"review_status", "REJECTED",
"rejection_reason", "文章内容过长,最多50000个字符",
"article_id", UUID.randomUUID().toString(),
"next_node", "END"
));
}
// 初始化审核状态
Map<String, Object> result = new HashMap<>();
result.put("article_id", UUID.randomUUID().toString());
result.put("article_title", articleTitle);
result.put("article_content", articleContent);
result.put("author_id", authorId);
result.put("submit_timestamp", System.currentTimeMillis());
result.put("review_status", "SUBMITTED");
result.put("current_stage", "AI_REVIEW");
result.put("next_node", "ai_content_review");
log.info("Article submitted successfully - articleId: {}", result.get("article_id"));
return CompletableFuture.completedFuture(result);
}
}
3.2 AI 内容审核节点
实现 InterruptableAction 接口,支持:
- 前中断:审核前检查是否需要跳过
AI审核直接进入人工审核 - 后中断:审核后根据结果判断是否需要中断等待人工确认高危结果
java
public class AiContentReviewNode implements AsyncNodeActionWithConfig, InterruptableAction {
private static final Logger log = LoggerFactory.getLogger(AiContentReviewNode.class);
// 预设敏感词列表
private static final Set<String> SENSITIVE_WORDS = Set.of(
"敏感词1", "敏感词2", "违规", "违禁", "禁止发布"
);
/**
* 前中断逻辑 - 审核前判断
* 如果标记了 skip_ai_review = true,则跳过 AI 审核直接进入人工审核
*/
@Override
public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
Boolean skipAiReview = state.value("skip_ai_review", Boolean.class).orElse(false);
if (Boolean.TRUE.equals(skipAiReview)) {
log.info("Skipping AI review as requested, interrupting before execution...");
return Optional.of(InterruptionMetadata.builder(nodeId, state)
.addMetadata("interruption_type", "SKIP_AI_REVIEW")
.addMetadata("reason", "已配置跳过 AI 审核,直接进入人工审核")
.addMetadata("skip_ai_review", true)
.build());
}
return Optional.empty();
}
/**
* 后中断逻辑 - 审核后判断
* 如果 AI 审核发现高危问题(分数 < 30),则中断等待人工确认
*/
@Override
public Optional<InterruptionMetadata> interruptAfter(String nodeId, OverAllState state,
Map<String, Object> actionResult, RunnableConfig config) {
Integer aiScore = (Integer) actionResult.get("ai_score");
// 如果 AI 分数过低(高危),则中断等待人工确认
if (aiScore != null && aiScore < 30) {
log.warn("AI review detected high risk content (score: {}), interrupting after execution...", aiScore);
return Optional.of(InterruptionMetadata.builder(nodeId, state)
.addMetadata("interruption_type", "HIGH_RISK_DETECTED")
.addMetadata("reason", "AI 检测到高风险内容,需要人工确认审核结果")
.addMetadata("ai_score", aiScore)
.addMetadata("ai_issues", actionResult.get("ai_issues"))
.addMetadata("needs_human_confirmation", true)
.build());
}
return Optional.empty();
}
/**
* AI 审核核心逻辑
*/
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
String articleTitle = state.value("article_title", String.class).orElse("");
String articleContent = state.value("article_content", String.class).orElse("");
log.info("Starting AI content review for article: {}", articleTitle);
// 1. 敏感词检测
List<String> foundSensitiveWords = detectSensitiveWords(articleTitle + " " + articleContent);
// 2. 合规性检查
Map<String, Boolean> complianceChecks = runComplianceChecks(articleContent);
// 3. 计算 AI 审核分数
int aiScore = calculateAiScore(foundSensitiveWords, complianceChecks, articleContent);
// 4. 生成问题列表
List<String> aiIssues = generateAiIssues(foundSensitiveWords, complianceChecks, aiScore);
// 5. 构建审核结果
Map<String, Object> result = new HashMap<>();
result.put("ai_score", aiScore);
result.put("sensitive_words", foundSensitiveWords);
result.put("compliance_checks", complianceChecks);
result.put("ai_issues", aiIssues);
result.put("review_status", "AI_REVIEWED");
// 根据 AI 分数决定下一节点
if (aiScore >= 80) {
result.put("next_node", "final_approval");
result.put("suggested_action", "AUTO_APPROVE");
} else if (aiScore >= 50) {
result.put("next_node", "human_review");
result.put("suggested_action", "HUMAN_REVIEW");
} else {
result.put("next_node", "human_review");
result.put("suggested_action", "HUMAN_REVIEW_URGENT");
}
log.info("AI review completed - score: {}, issues: {}, next_node: {}",
aiScore, aiIssues.size(), result.get("next_node"));
return CompletableFuture.completedFuture(result);
}
private List<String> detectSensitiveWords(String content) {
List<String> found = new ArrayList<>();
String lowerContent = content.toLowerCase();
for (String word : SENSITIVE_WORDS) {
if (lowerContent.contains(word.toLowerCase())) {
found.add(word);
}
}
return found;
}
private Map<String, Boolean> runComplianceChecks(String content) {
Map<String, Boolean> checks = new HashMap<>();
checks.put("has_valid_structure", content.length() >= 50);
checks.put("no_excessive_capitalization", countCapitalLetters(content) < content.length() * 0.5);
checks.put("no_excessive_punctuation", !content.matches(".*[!?。]{5,}.*"));
checks.put("has_proper_paragraphs", content.contains("\n") || content.length() > 200);
return checks;
}
private int calculateAiScore(List<String> sensitiveWords, Map<String, Boolean> complianceChecks, String content) {
int score = 100;
// 敏感词扣分
score -= sensitiveWords.size() * 25;
// 合规性检查扣分
for (Map.Entry<String, Boolean> check : complianceChecks.entrySet()) {
if (!check.getValue()) {
score -= 10;
}
}
// 内容长度评分
if (content.length() < 100) score -= 10;
if (content.length() < 50) score -= 10;
return Math.max(0, Math.min(100, score));
}
private List<String> generateAiIssues(List<String> sensitiveWords, Map<String, Boolean> complianceChecks, int aiScore) {
List<String> issues = new ArrayList<>();
if (!sensitiveWords.isEmpty()) {
issues.add("检测到敏感词: " + String.join(", ", sensitiveWords));
}
for (Map.Entry<String, Boolean> check : complianceChecks.entrySet()) {
if (!check.getValue()) {
issues.add("合规检查失败: " + check.getKey());
}
}
if (aiScore < 50) {
issues.add("文章整体质量较低,建议人工审核");
}
if (issues.isEmpty()) {
issues.add("未发现明显问题");
}
return issues;
}
private int countCapitalLetters(String content) {
int count = 0;
for (char c : content.toCharArray()) {
if (Character.isUpperCase(c)) {
count++;
}
}
return count;
}
}
3.3 人工审核节点
实现 InterruptableAction 接口,支持前中断:检查是否已有人工审核结果,如果没有则中断等待人工输入。
java
public class HumanReviewNode implements AsyncNodeActionWithConfig, InterruptableAction {
private static final Logger log = LoggerFactory.getLogger(HumanReviewNode.class);
/**
* 前中断逻辑 - 等待人工审核
* 如果没有 human_review_result,则中断等待人工输入审核意见
*/
@Override
public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
// 检查是否已经有人工审核结果
boolean hasHumanReviewResult = state.value("human_review_result").isPresent();
if (!hasHumanReviewResult) {
log.info("Waiting for human review, interrupting execution at node: {}", nodeId);
// 构建审核摘要数据供审核人员查看
Map<String, Object> reviewSummary = buildReviewSummary(state);
return Optional.of(InterruptionMetadata.builder(nodeId, state)
.addMetadata("interruption_type", "HUMAN_REVIEW_REQUIRED")
.addMetadata("reason", "等待人工审核确认")
.addMetadata("review_summary", reviewSummary)
.addMetadata("available_actions", Map.of(
"APPROVED", "审核通过",
"REJECTED", "审核拒绝",
"NEEDS_REVISION", "需要修改后重新提交"
))
.build());
}
log.info("Human review result found, resuming execution...");
return Optional.empty();
}
/**
* 人工审核节点核心逻辑
*/
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
String humanReviewResult = state.value("human_review_result", String.class).orElse("");
String humanReviewComment = state.value("human_review_comment", String.class).orElse("");
String reviewerId = state.value("reviewer_id", String.class).orElse("system");
Integer aiScore = state.value("ai_score", Integer.class).orElse(0);
log.info("Processing human review - result: {}, reviewer: {}", humanReviewResult, reviewerId);
Map<String, Object> result = new HashMap<>();
result.put("human_review_result", humanReviewResult);
result.put("human_review_comment", humanReviewComment);
result.put("reviewer_id", reviewerId);
result.put("review_timestamp", System.currentTimeMillis());
result.put("review_status", "HUMAN_REVIEWED");
// 根据人工审核结果决定下一步
switch (humanReviewResult.toUpperCase()) {
case "APPROVED":
result.put("next_node", "final_approval");
result.put("final_status", "APPROVED");
result.put("final_conclusion", generateApprovedConclusion(aiScore, humanReviewComment));
break;
case "REJECTED":
result.put("next_node", "END");
result.put("final_status", "REJECTED");
result.put("final_conclusion", generateRejectedConclusion(humanReviewComment));
break;
case "NEEDS_REVISION":
result.put("next_node", "END");
result.put("final_status", "NEEDS_REVISION");
result.put("final_conclusion", generateRevisionConclusion(humanReviewComment));
break;
default:
// 默认进入最终审核确认
result.put("next_node", "final_approval");
result.put("final_status", "PENDING_FINAL_CHECK");
}
log.info("Human review processed - final_status: {}, next_node: {}",
result.get("final_status"), result.get("next_node"));
return CompletableFuture.completedFuture(result);
}
private Map<String, Object> buildReviewSummary(OverAllState state) {
Map<String, Object> summary = new HashMap<>();
summary.put("article_id", state.value("article_id").orElse(""));
summary.put("article_title", state.value("article_title").orElse(""));
summary.put("ai_score", state.value("ai_score").orElse(0));
summary.put("ai_issues", state.value("ai_issues").orElse("无"));
summary.put("sensitive_words", state.value("sensitive_words").orElse("无"));
summary.put("submit_timestamp", state.value("submit_timestamp").orElse(0L));
// 截断内容预览
String content = state.value("article_content", String.class).orElse("");
if (content.length() > 200) {
summary.put("content_preview", content.substring(0, 200) + "...");
} else {
summary.put("content_preview", content);
}
return summary;
}
private String generateApprovedConclusion(Integer aiScore, String comment) {
StringBuilder sb = new StringBuilder();
sb.append("文章审核通过。");
sb.append("AI 评分为 ").append(aiScore).append(" 分。");
if (comment != null && !comment.isEmpty()) {
sb.append("审核意见:").append(comment);
}
return sb.toString();
}
private String generateRejectedConclusion(String comment) {
StringBuilder sb = new StringBuilder();
sb.append("文章审核未通过,已拒绝发布。");
if (comment != null && !comment.isEmpty()) {
sb.append("拒绝原因:").append(comment);
}
return sb.toString();
}
private String generateRevisionConclusion(String comment) {
StringBuilder sb = new StringBuilder();
sb.append("文章需要修改后重新提交审核。");
if (comment != null && !comment.isEmpty()) {
sb.append("修改意见:").append(comment);
}
return sb.toString();
}
}
执行节点实现 InterruptableAction 接口, 重写 interrupt() 方法,需要中断时直接返回中断元数据,引擎执行到这个节点 → 立即暂停 → 等待人工操作,人工审批完成 → 断点续跑 → 流程继续。
在之前的邮件处理案例中,我们使用 interruptBefore 模式配置了一个人工审核节点:
java
public class HumanReviewNode implements NodeAction {
private static final Logger log = LoggerFactory.getLogger(HumanReviewNode.class);
@Override
public Map<String, Object> apply(OverAllState state) throws Exception {
EmailClassification classification = state.value("classification")
.map(v -> (EmailClassification) v)
.orElse(new EmailClassification());
// 准备审核数据
@SuppressWarnings("unchecked")
Map<String, Object> reviewData = Map.of(
"email_id", state.value("email_id").map(v -> (String) v).orElse(""),
"original_email", state.value("email_content").map(v -> (String) v).orElse(""),
"draft_response", state.value("draft_response").map(v -> (String) v).orElse(""),
"urgency", classification.getUrgency(),
"intent", classification.getIntent(),
"action", "请审核并批准/编辑此响应"
);
log.info("Waiting for human review: {}", reviewData);
// 返回审核数据和下一个节点
// 注意:在 interruptBefore 模式下,此节点在人工输入后才会执行
return Map.of(
"review_data", reviewData,
"status", "waiting_for_review",
"next_node", "send_reply"
);
}
}
3.4 最终审核确认节点
实现 InterruptableAction 接口,支持前后中断:
- 前中断:如果
AI自动批准但分数低于阈值,可中断要求二次确认 - 后中断:最终审核结果可中断要求发布确认
java
public class FinalApprovalNode implements AsyncNodeActionWithConfig, InterruptableAction {
private static final Logger log = LoggerFactory.getLogger(FinalApprovalNode.class);
private static final int AUTO_APPROVE_THRESHOLD = 90;
/**
* 前中断逻辑
* 如果是 AI 自动批准(跳过人工审核)且分数低于阈值,则中断要求人工确认
*/
@Override
public Optional<InterruptionMetadata> interrupt(String nodeId, OverAllState state, RunnableConfig config) {
// 检查是否跳过了人工审核(即没有 human_review_result)
boolean skippedHumanReview = !state.value("human_review_result").isPresent();
Integer aiScore = state.value("ai_score", Integer.class).orElse(0);
if (skippedHumanReview && aiScore < AUTO_APPROVE_THRESHOLD) {
log.info("AI auto-approve score ({}) is below threshold ({}), interrupting for human confirmation...",
aiScore, AUTO_APPROVE_THRESHOLD);
return Optional.of(InterruptionMetadata.builder(nodeId, state)
.addMetadata("interruption_type", "AUTO_APPROVE_CONFIRMATION_REQUIRED")
.addMetadata("reason", "AI 自动批准分数低于阈值,需要人工确认")
.addMetadata("ai_score", aiScore)
.addMetadata("threshold", AUTO_APPROVE_THRESHOLD)
.addMetadata("available_actions", Map.of(
"CONFIRM_AUTO_APPROVE", "确认自动批准",
"SEND_TO_HUMAN_REVIEW", "转人工审核"
))
.build());
}
// 检查是否有确认操作请求
Optional<String> finalConfirmAction = state.value("final_confirm_action", String.class);
if (finalConfirmAction.isPresent() && "SEND_TO_HUMAN_REVIEW".equals(finalConfirmAction.get())) {
// 用户选择转人工审核,修改流向
log.info("User selected to send article to human review");
return Optional.empty(); // 不中断,让 apply 处理转向逻辑
}
return Optional.empty();
}
/**
* 后中断逻辑
* 审核完成后,中断展示最终结果,等待发布确认
*/
@Override
public Optional<InterruptionMetadata> interruptAfter(String nodeId, OverAllState state,
Map<String, Object> actionResult, RunnableConfig config) {
String finalStatus = (String) actionResult.get("final_status");
// 如果是最终批准,则中断等待发布确认
if ("FINAL_APPROVED".equals(finalStatus)) {
log.info("Final approval completed, interrupting for publish confirmation...");
return Optional.of(InterruptionMetadata.builder(nodeId, state)
.addMetadata("interruption_type", "PUBLISH_CONFIRMATION")
.addMetadata("reason", "审核完成,等待发布确认")
.addMetadata("final_result", Map.of(
"article_id", actionResult.get("article_id"),
"final_status", actionResult.get("final_status"),
"final_conclusion", actionResult.get("final_conclusion")
))
.addMetadata("available_actions", Map.of(
"CONFIRM_PUBLISH", "确认发布",
"CANCEL_PUBLISH", "取消发布"
))
.build());
}
return Optional.empty();
}
/**
* 最终审核核心逻辑
*/
@Override
public CompletableFuture<Map<String, Object>> apply(OverAllState state, RunnableConfig config) {
String articleId = state.value("article_id", String.class).orElse("");
String articleTitle = state.value("article_title", String.class).orElse("");
Integer aiScore = state.value("ai_score", Integer.class).orElse(0);
String humanReviewResult = state.value("human_review_result", String.class).orElse(null);
String finalConfirmAction = state.value("final_confirm_action", String.class).orElse(null);
String publishAction = state.value("publish_action", String.class).orElse(null);
log.info("Processing final approval for article: {}", articleId);
Map<String, Object> result = new HashMap<>();
result.put("article_id", articleId);
result.put("article_title", articleTitle);
// 处理发布确认
if (publishAction != null) {
if ("CONFIRM_PUBLISH".equals(publishAction)) {
result.put("final_status", "PUBLISHED");
result.put("final_conclusion", "文章已成功发布!");
result.put("publish_timestamp", System.currentTimeMillis());
result.put("next_node", "END");
log.info("Article published: {}", articleId);
} else {
result.put("final_status", "PUBLISH_CANCELLED");
result.put("final_conclusion", "发布已取消,文章状态保持已审核通过");
result.put("next_node", "END");
log.info("Publish cancelled for article: {}", articleId);
}
return CompletableFuture.completedFuture(result);
}
// 处理转人工审核请求
if ("SEND_TO_HUMAN_REVIEW".equals(finalConfirmAction)) {
result.put("next_node", "human_review");
result.put("review_status", "SENT_TO_HUMAN_REVIEW");
result.put("redirect_message", "已转人工审核,等待审核人员处理");
log.info("Redirecting article {} to human review", articleId);
return CompletableFuture.completedFuture(result);
}
// 常规最终审核逻辑
if (humanReviewResult != null) {
// 有人工审核结果
switch (humanReviewResult.toUpperCase()) {
case "APPROVED":
result.put("final_status", "FINAL_APPROVED");
result.put("final_conclusion", generateFinalConclusion(state, true));
result.put("next_node", "END");
break;
case "REJECTED":
result.put("final_status", "FINAL_REJECTED");
result.put("final_conclusion", generateFinalConclusion(state, false));
result.put("next_node", "END");
break;
default:
result.put("final_status", humanReviewResult);
result.put("final_conclusion", "审核状态:" + humanReviewResult);
result.put("next_node", "END");
}
} else {
// AI 自动批准
if (aiScore >= AUTO_APPROVE_THRESHOLD) {
result.put("final_status", "FINAL_APPROVED");
result.put("final_conclusion", "AI 自动批准通过 (分数: " + aiScore + ")");
result.put("approved_by", "AI_AUTO");
result.put("next_node", "END");
} else {
// 理论上不会走到这里,因为前中断会拦截这种情况
result.put("final_status", "REDIRECTED_TO_HUMAN_REVIEW");
result.put("final_conclusion", "AI 分数不足,已转人工审核");
result.put("next_node", "human_review");
}
}
result.put("review_status", "COMPLETED");
result.put("completion_timestamp", System.currentTimeMillis());
log.info("Final approval completed - status: {}, articleId: {}",
result.get("final_status"), articleId);
return CompletableFuture.completedFuture(result);
}
private String generateFinalConclusion(OverAllState state, boolean approved) {
String humanComment = state.value("human_review_comment", String.class).orElse("");
Integer aiScore = state.value("ai_score", Integer.class).orElse(0);
StringBuilder sb = new StringBuilder();
if (approved) {
sb.append("文章审核通过!");
sb.append("AI 评分:").append(aiScore).append(" 分。");
if (!humanComment.isEmpty()) {
sb.append("审核意见:").append(humanComment);
}
} else {
sb.append("文章审核未通过。");
sb.append("AI 评分:").append(aiScore).append(" 分。");
if (!humanComment.isEmpty()) {
sb.append("拒绝原因:").append(humanComment);
}
}
return sb.toString();
}
}
3.5 组装工作流
AI 文章智能审核工作流配置:
java
@Bean("articleReviewGraph")
public CompiledGraph articleReviewGraph(
ObservationRegistry observationRegistry,
GraphLifecycleListener lifecycleListener) throws GraphStateException {
// 创建节点
// 注意:所有实现 AsyncNodeActionWithConfig 的节点都可以直接使用,无需包装
// InterruptableAction 接口的实现也直接兼容
var articleSubmit = new ArticleSubmitNode(); // 实现 AsyncNodeActionWithConfig
var aiContentReview = new AiContentReviewNode(); // 实现 AsyncNodeActionWithConfig + InterruptableAction
var humanReview = new HumanReviewNode(); // 实现 AsyncNodeActionWithConfig + InterruptableAction
var finalApproval = new FinalApprovalNode(); // 实现 AsyncNodeActionWithConfig + InterruptableAction
// 构建 StateGraph
StateGraph workflow = new StateGraph();
// 添加节点
workflow.addNode("article_submit", articleSubmit);
workflow.addNode("ai_content_review", aiContentReview);
workflow.addNode("human_review", humanReview);
workflow.addNode("final_approval", finalApproval);
// 添加边
workflow.addEdge(START, "article_submit");
// 简单直接边(避免 Map.of null key 问题)
workflow.addEdge("article_submit", "ai_content_review");
workflow.addEdge("ai_content_review", "human_review");
workflow.addEdge("human_review", "final_approval");
workflow.addEdge("final_approval", END);
// 配置编译参数
SaverConfig saverConfig = SaverConfig.builder()
.register(new MemorySaver())
.build();
CompileConfig compileConfig = CompileConfig.builder()
.saverConfig(saverConfig)
.store(new MemoryStore())
.build();
log.info("Article review graph compiled successfully");
// 编译工作流
return workflow.compile(compileConfig);
}
3.6 对话测试
3.6.1 提交文章,执行到人工审核节点
java
log.info("========== 测试场景 1: 正常文章 - 完整审核流程 (含中断恢复) ==========");
String threadId = "test-thread-001";
Map<String, Object> inputs = new HashMap<>();
inputs.put("article_title", "Spring AI 入门指南");
inputs.put("article_content", "Spring AI 是一个用于构建 AI 应用的框架。它提供了简洁的 API,支持多种大语言模型。本文将介绍如何使用 Spring AI 快速开发智能应用。Spring AI 提供了 ChatClient 接口,可以方便地与各种大语言模型进行交互,同时支持工具调用、向量存储等高级功能。");
inputs.put("author_id", "test_author_001");
inputs.put("skip_ai_review", false);
// ========== 第一步:开始执行,等待人工审核中断 ==========
log.info("\n--- 步骤 1: 提交文章,执行到人工审核节点 ---");
RunnableConfig config = RunnableConfig.builder()
.threadId(threadId)
.build();
List<String> executedNodes = Collections.synchronizedList(new ArrayList<>());
CountDownLatch interruptLatch = new CountDownLatch(1);
final int[] aiScore = new int[]{0};
final String[] interruptionType = new String[]{null};
Flux<NodeOutput> firstStream = articleReviewGraph.stream(inputs, config);
firstStream
.doOnNext(output -> {
if (output instanceof InterruptionMetadata) {
InterruptionMetadata metadata = (InterruptionMetadata) output;
interruptionType[0] = metadata.metadata()
.map(m -> (String) m.get("interruption_type"))
.orElse("UNKNOWN");
log.info("✅ 触发中断 - 节点: {}, 类型: {}", output.node(), interruptionType[0]);
interruptLatch.countDown();
} else {
executedNodes.add(output.node());
log.info("执行节点: {}", output.node());
if ("ai_content_review".equals(output.node())) {
output.state().value("ai_score", Integer.class)
.ifPresent(score -> {
aiScore[0] = score;
log.info("AI 审核分数: {}", score);
});
}
}
})
.doOnComplete(() -> interruptLatch.countDown())
.subscribe();
boolean interrupted = interruptLatch.await(30, TimeUnit.SECONDS);
assertTrue(interrupted, "应该在 human_review 节点触发中断");
assertEquals("HUMAN_REVIEW_REQUIRED", interruptionType[0], "应该触发人工审核中断");
log.info("已执行节点: {}\n", executedNodes);
3.6.2 提交人工审核结果,恢复执行
java
log.info("--- 步骤 2: 提交人工审核结果,恢复执行 ---");
Map<String, Object> humanReviewInputs = new HashMap<>();
humanReviewInputs.put("human_review_result", "APPROVED");
humanReviewInputs.put("human_review_comment", "文章质量良好,审核通过");
humanReviewInputs.put("reviewer_id", "reviewer_admin");
RunnableConfig resumeConfig = RunnableConfig.builder()
.threadId(threadId)
.resume()
.build();
CountDownLatch completeLatch = new CountDownLatch(1);
List<String> resumedNodes = Collections.synchronizedList(new ArrayList<>());
Flux<NodeOutput> resumeStream = articleReviewGraph.stream(humanReviewInputs, resumeConfig);
resumeStream
.doOnNext(output -> {
if (!(output instanceof InterruptionMetadata)) {
resumedNodes.add(output.node());
log.info("恢复执行节点: {}", output.node());
} else {
InterruptionMetadata metadata = (InterruptionMetadata) output;
log.info("再次触发中断: {}", metadata.metadata()
.map(m -> m.get("interruption_type")).orElse("UNKNOWN"));
}
})
.doOnComplete(() -> completeLatch.countDown())
.doOnError(error -> log.error("恢复执行错误", error))
.subscribe();
boolean completed = completeLatch.await(30, TimeUnit.SECONDS);
assertTrue(completed, "恢复执行后应该在 30 秒内完成");
log.info("\n✅ 正常文章 - 完整审核流程测试通过!");
log.info("✅ 中断检测 -> 人工审核 -> 恢复执行 整个链路工作正常!\n");
3.6.3 测试结果
第一次执行到人工审核节点时,触发中断:

恢复执行后,再次进入到【最终审核确认节点】,因为它配置了中断:

4. 其他说明
4.1 interruptBeforeEdge
InterruptableAction 【执行后中断】也时支持 interruptBeforeEdge 参数的。
4.2 STATE_UPDATE
InterruptableAction 支持恢复时传递 STATE_UPDATE 元数据进行状态更新,你传什么,就覆盖什么,默认全部替换检查点状态中的数据。
使用示例:
java
RunnableConfig resumeConfig = RunnableConfig.builder()
.threadId(threadId)
.resume()
.addMetadata(RunnableConfig.STATE_UPDATE_METADATA_KEY,Map.of("human_review_comment","文章质量良好"))
.build();