Spring AI Alibaba 1.x 系列【53】Interrupts 中断机制:动态中断

文章目录

  • [1. 概述](#1. 概述)
  • [2. InterruptableAction 接口](#2. InterruptableAction 接口)
  • [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 文章智能审核工作流

业务流程

  1. 开始 → 文章提交 → AI 内容审核(支持执行前、执行后双中断

2. AI 审核完成后进入人工审核环节:

  • 支持执行前中断 :配置 skipAiReview=true 可跳过 A 审核,直接进入人工审核
  • 支持执行后中断AI 检测到高风险内容(评分 < 30 分)自动中断流程,等待人工复核确认
  1. 人工审核节点(支持执行前中断):

    • 执行前中断:进入人工审核节点即刻暂停流程,等待审核人员录入审核结果与审批意见
  2. 最终审核确认(支持执行前、执行后双中断):

    • 执行前中断:AI 自动审批通过但综合评分低于阈值(< 90 分),触发中断要求人工二次确认
    • 执行后中断:整体审核流程完成后触发中断,展示最终审核结果,等待人工发布确认操作
  3. 人工确认发布 → 流程正常结束


节点中断能力汇总:

流程节点 支持中断类型 中断触发规则
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();
相关推荐
Raink老师1 小时前
【AI面试临阵磨枪-56】大模型服务部署:Docker、K8s、GPU 调度、推理加速
人工智能·面试·kubernetes·ai 面试
云上码厂1 小时前
NeurIPS 研讨会资料:用机器学习应对气候变化
人工智能
科技小花1 小时前
2026 年度生成式引擎优化(GEO)标杆产品:百分点科技 Generforce 的差异化路径
大数据·人工智能·科技·geo·ai搜索
安心联-车辆监控管理系统1 小时前
车载主动安全ADAS/DSM技术原理、业务应用与平台接入方案
人工智能·安全
用户40189933422841 小时前
第 9 章 Skills 生态
人工智能
网安情报局1 小时前
AI大模型解析:安全赛道大模型的合规稳定之选
人工智能·安全
用户298698530141 小时前
Java 操作 Word 文档:数学公式与符号的插入方法
java·后端
见青..1 小时前
JAVA安全靶场环境搭建
java·web安全·靶场·java安全
Android出海1 小时前
2026年Codex新手教程:安装、使用与自动化实战指南
人工智能·ai·chatgpt·自动化·脚本·codex·自动化脚本