Spring AI Alibaba 1.x 系列【77】执行取消

文章目录

  • [1. 概述](#1. 概述)
    • [1.1 执行取消](#1.1 执行取消)
    • [1.2 官方文档](#1.2 官方文档)
    • [1.3 当前版本不符](#1.3 当前版本不符)
    • [1.3 取消策略(本模块封装)](#1.3 取消策略(本模块封装))
    • [1.4 核心思路](#1.4 核心思路)
    • [1.5 四种模式](#1.5 四种模式)
    • [1.6 模块结构](#1.6 模块结构)
  • [2. 核心实现](#2. 核心实现)
    • [2.1 AgentExecutionManager ------ 统一执行管理器](#2.1 AgentExecutionManager —— 统一执行管理器)
    • [2.2 ReactAgent 同步](#2.2 ReactAgent 同步)
    • [2.3 ReactAgent 流式](#2.3 ReactAgent 流式)
    • [2.4 StateGraph 同步](#2.4 StateGraph 同步)
    • [2.5 StateGraph 流式](#2.5 StateGraph 流式)
  • [3. API 参考](#3. API 参考)
    • [3.1 启动任务](#3.1 启动任务)
    • [3.2 取消 & 状态](#3.2 取消 & 状态)
    • [3.3 状态值定义](#3.3 状态值定义)
  • [4. 取消机制](#4. 取消机制)
    • [4.1 两种取消策略](#4.1 两种取消策略)
    • [4.2 四层防御体系](#4.2 四层防御体系)
  • [5. 前端集成示例](#5. 前端集成示例)
  • [6. 最佳实践](#6. 最佳实践)

1. 概述

1.1 执行取消

AI 工作流中,图执行取消Graph Execution Cancelled)是指主动停止正在运行的任务流,不再继续往后跑。

AI 节点(大模型、向量库、图片生成)均消耗算力、TokenAPI 额度,在以下场景中需要引入取消支持:

  • 用户输入错误 :发送后发现 query 有问题,不想等结果
  • 需求变更:用户改变主意,不再需要当前任务的输出
  • 不想继续等待:长耗时任务,用户不愿等完
  • 图逻辑异常:无限循环或死循环,需要强制终止

特别是长耗时流程,一般都需要支持:

  • 超时自动终止:设置最大执行时长,超时自动取消,防止单任务长期独占资源
  • 用户主动终止:点击取消按钮,停止当前任务

例如豆包中的"停止运行"按钮:

1.2 官方文档

Spring AI Alibaba Graph 内置了完善的图执行取消机制 ,针对长耗时工作流场景十分实用。该能力底层基于 java-async-generator 库的取消特性实现,调用图的 stream 方法执行任务时,会返回流对象,可主动终止图执行。

核心取消方法 cancel(boolean mayInterruptIfRunning) 根据入参分为两种模式:

  1. 立即取消cancel(true)):中断当前执行线程,终止流程
  2. 优雅取消cancel(false)):执行完当前节点,不再启动下一个节点

根据流的消费方式不同,取消逻辑与表现存在差异。

1.3 当前版本不符

CompiledGraph.stream() 返回 Reactor Flux<NodeOutput>,底层基于 Reactor (非 java-async-generator),通过订阅管理原生支持取消:

java 复制代码
// graph.stream() 返回 Flux,可通过 Disposable 取消
Disposable disposable = graph.stream(inputs, config).subscribe();
disposable.dispose();  // 取消订阅,停止发射数据

源码验证(CompiledGraph.java:567-568):

java 复制代码
public Flux<NodeOutput> stream(Map<String, Object> inputs, RunnableConfig config) {
    return streamFromInitialNode(stateCreate(inputs), config);
}
// invoke() 内部也是调 stream()...block(),同步阻塞,无法中途取消
public Optional<OverAllState> invoke(OverAllState state, RunnableConfig config) {
    return Optional.ofNullable(
        streamFromInitialNode(state, config).last().map(NodeOutput::state).block());
}

CompiledGraph.invoke() 内部是 stream().last().block()同步阻塞 ,框架没有提供 cancel()stop() 方法。

核实结论:

官网说明 核实 实际情况
"底层基于 java-async-generator" ❌ 不属实 graph-core 无此依赖,底层是 Reactor Flux
"核心取消方法 cancel(boolean)" ❌ 有误导 CompiledGraph 没有 cancel() 方法;cancel(true/false)CompletableFutureAPI
"stream() 返回流对象可主动终止" ✅ 属实 FluxDisposable.dispose() 即可取消
"内置了完善的图执行取消机制" ⚠️ 部分 流式有(Reactor 原生),同步没有

1.3 取消策略(本模块封装)

鉴于框架只在流式场景有原生取消,本模块统一封装了两种取消策略:

策略 实现 行为 适用场景
立即取消 CompletableFuture.cancel(true) + Thread.interrupt() 中断当前线程,立即终止 用户点击取消、死循环
优雅取消 CompletableFuture.cancel(false) 不中断线程,等当前节点完成 超时自动终止、拿到部分结果

1.4 核心思路

把阻塞调用放到异步线程中,对外暴露可取消的 FutureFlux

复制代码
用户点击取消
      │
      ▼
POST /cancel/abort  ──→  AgentExecutionManager.cancel(threadId)
                              │
                              ├── DefaultCancellationToken.cancel()   通知工具层
                              ├── CompletableFuture.cancel(true)     立即取消 / cancel(false) 优雅取消
                              └── Thread.interrupt()                  兜底硬终止

1.5 四种模式

本模块覆盖 ReactAgentStateGraph同步流式两种模式下的取消:

组件 模式 启动端点 取消机制
ReactAgent 同步 POST /cancel/agent/sync/start Future.cancel(true) + Thread.interrupt()
ReactAgent 流式 GET /cancel/agent/stream/start (SSE) Flux 取消订阅 + Thread.interrupt()
StateGraph 同步 POST /cancel/graph/sync/start Future.cancel(true) + Thread.interrupt()
StateGraph 流式 GET /cancel/graph/stream/start (SSE) Flux.doOnCancel()Disposable.dispose()

1.6 模块结构

复制代码
spring-ai-alibaba-cancel/
├── pom.xml
├── src/main/resources/application.yml          # 端口 8089,DashScope qwen-plus
└── src/main/java/com/example/cancel/
    ├── CancelDemoApplication.java              # 启动类
    ├── config/
    │   ├── CancelDemoConfig.java               # ReactAgent + StateGraph Bean
    │   └── GraphCompileConfig.java             # StateGraph → CompiledGraph
    ├── manager/
    │   └── AgentExecutionManager.java          # 核心:线程池 + Future + CancelToken
    └── controller/
        └── CancelController.java               # 6 个 REST 端点

2. 核心实现

2.1 AgentExecutionManager ------ 统一执行管理器

管理器内部维护三张并发 Map 追踪每个任务的状态:

java 复制代码
// threadId → 同步任务 Future
ConcurrentHashMap<String, CompletableFuture<?>> syncFutures;
// threadId → 执行线程引用(硬终止用)
ConcurrentHashMap<String, Thread> runningThreads;
// threadId → 取消令牌(通知工具层)
ConcurrentHashMap<String, DefaultCancellationToken> cancelTokens;

完整源码:

java 复制代码
package com.example.cancel.manager;

import com.alibaba.cloud.ai.graph.CompiledGraph;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.agent.tool.DefaultCancellationToken;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 统一执行管理器 ------ ReactAgent / StateGraph × 同步 / 流式 = 4 种模式
 */
@Component
public class AgentExecutionManager {

    private static final Logger logger = LoggerFactory.getLogger(AgentExecutionManager.class);

    private final ReactAgent agent;
    private final CompiledGraph graph;

    // ==================== 取消基础设施 ====================

    // threadId → 同步任务 Future
    private final ConcurrentHashMap<String, CompletableFuture<?>> syncFutures = new ConcurrentHashMap<>();
    // threadId → 执行线程(硬终止)
    private final ConcurrentHashMap<String, Thread> runningThreads = new ConcurrentHashMap<>();
    // threadId → 取消令牌
    private final ConcurrentHashMap<String, DefaultCancellationToken> cancelTokens = new ConcurrentHashMap<>();

    public AgentExecutionManager(
            @Qualifier("cancelAgent") ReactAgent agent,
            @Qualifier("cancelCompiledGraph") CompiledGraph graph) {
        this.agent = agent;
        this.graph = graph;
    }

    // ==================== 1. ReactAgent 同步 ====================

    /**
     * 启动 ReactAgent 同步任务(后台线程执行,返回 Future)
     */
    public CompletableFuture<String> startAgentSync(String query, String threadId) {
        DefaultCancellationToken token = new DefaultCancellationToken();
        cancelTokens.put(threadId, token);

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            runningThreads.put(threadId, Thread.currentThread());
            try {
                RunnableConfig config = RunnableConfig.builder().threadId(threadId).build();
                logger.info("[Agent-Sync] 开始 threadId={}", threadId);
                String result = agent.call(query, config).getText();
                if (token.isCancelled() || Thread.currentThread().isInterrupted()) {
                    return "[已取消] 任务被用户终止";
                }
                logger.info("[Agent-Sync] 完成 threadId={}", threadId);
                return result;
            } catch (GraphRunnerException e) {
                logger.error("[Agent-Sync] 异常 threadId={}", threadId, e);
                return "执行出错: " + e.getMessage();
            } catch (Exception e) {
                if (isCancelled(threadId)) return "[已取消] 任务被用户终止";
                throw new RuntimeException(e);
            } finally {
                cleanup(threadId);
            }
        });

        syncFutures.put(threadId, future);
        return future;
    }

    // ==================== 2. ReactAgent 流式 ====================

    /**
     * 启动 ReactAgent 流式任务(SSE 推送,每 200ms 发射一次中间状态)
     */
    public Flux<String> startAgentStream(String query, String threadId) {
        DefaultCancellationToken token = new DefaultCancellationToken();
        cancelTokens.put(threadId, token);

        return Mono.fromCallable(() -> {
                    runningThreads.put(threadId, Thread.currentThread());
                    RunnableConfig config = RunnableConfig.builder().threadId(threadId).build();
                    logger.info("[Agent-Stream] 开始 threadId={}", threadId);
                    return agent.call(query, config).getText();
                })
                .subscribeOn(Schedulers.boundedElastic())
                // 将一次性结果拆分为逐字发射(模拟流式效果)
                .flatMapMany(text -> Flux.fromArray(text.split("(?<=\\G.{10})"))
                        .delayElements(Duration.ofMillis(50))
                        .concatWith(Mono.just("\n\n--- 执行完毕 ---")))
                .doOnCancel(() -> {
                    logger.info("[Agent-Stream] 被取消 threadId={}", threadId);
                    cancelToken(threadId);
                    interruptThread(threadId);
                })
                .doOnTerminate(() -> cleanup(threadId))
                .doOnError(e -> {
                    logger.error("[Agent-Stream] 出错 threadId={}", threadId, e);
                    cleanup(threadId);
                });
    }

    // ==================== 3. StateGraph 同步 ====================

    /**
     * 启动 StateGraph 同步任务
     */
    public CompletableFuture<String> startGraphSync(String query, String threadId) {
        DefaultCancellationToken token = new DefaultCancellationToken();
        cancelTokens.put(threadId, token);

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            runningThreads.put(threadId, Thread.currentThread());
            try {
                RunnableConfig config = RunnableConfig.builder().threadId(threadId).build();
                Map<String, Object> inputs = Map.of("input", query);
                logger.info("[Graph-Sync] 开始 threadId={}", threadId);
                Optional<OverAllState> result = graph.invoke(inputs, config);
                if (isCancelled(threadId)) return "[已取消] 任务被用户终止";
                String output = result.flatMap(s -> s.value("result"))
                        .map(Object::toString)
                        .orElse("无结果");
                logger.info("[Graph-Sync] 完成 threadId={}", threadId);
                return output;
            } catch (Exception e) {
                if (isCancelled(threadId)) return "[已取消] 任务被用户终止";
                logger.error("[Graph-Sync] 异常 threadId={}", threadId, e);
                return "执行出错: " + e.getMessage();
            } finally {
                cleanup(threadId);
            }
        });

        syncFutures.put(threadId, future);
        return future;
    }

    // ==================== 4. StateGraph 流式 ====================

    /**
     * 启动 StateGraph 流式任务(原生 graph.stream(),取消即 dispose)
     */
    public Flux<String> startGraphStream(String query, String threadId) {
        RunnableConfig config = RunnableConfig.builder().threadId(threadId).build();
        Map<String, Object> inputs = Map.of("input", query);

        DefaultCancellationToken token = new DefaultCancellationToken();
        cancelTokens.put(threadId, token);

        return graph.stream(inputs, config)
                .doOnSubscribe(sub ->
                        logger.info("[Graph-Stream] 开始 threadId={}", threadId))
                .map(node -> {
                    var data = node.state().data();
                    Object result = data.get("result");
                    return result != null ? result.toString() : "处理中...";
                })
                .doOnCancel(() -> {
                    logger.info("[Graph-Stream] 被取消 threadId={}", threadId);
                    cancelToken(threadId);
                })
                .doOnComplete(() -> {
                    logger.info("[Graph-Stream] 完成 threadId={}", threadId);
                    cleanup(threadId);
                })
                .doOnError(e -> {
                    logger.error("[Graph-Stream] 出错 threadId={}", threadId, e);
                    cleanup(threadId);
                });
    }

    // ==================== 取消操作 ====================

    /**
     * 取消同步任务(硬终止)
     */
    public boolean cancel(String threadId) {
        cancelToken(threadId);

        CompletableFuture<?> future = syncFutures.remove(threadId);
        if (future != null && !future.isDone()) {
            boolean result = future.cancel(true);
            logger.info("[Cancel] Future.cancel(true) threadId={} result={}", threadId, result);
            return result;
        }

        return interruptThread(threadId);
    }

    // ==================== 状态查询 ====================

    public String getStatus(String threadId) {
        CompletableFuture<?> future = syncFutures.get(threadId);
        if (future != null) {
            if (future.isCancelled()) return "cancelled";
            if (future.isDone()) return future.isCompletedExceptionally() ? "failed" : "completed";
            return "running";
        }

        DefaultCancellationToken token = cancelTokens.get(threadId);
        if (token != null) {
            if (token.isCancelled()) return "cancelled";
            return "streaming";
        }

        return "not_found";
    }

    // ==================== 内部方法 ====================

    private void cancelToken(String threadId) {
        DefaultCancellationToken token = cancelTokens.remove(threadId);
        if (token != null) token.cancel();
    }

    private boolean interruptThread(String threadId) {
        Thread thread = runningThreads.remove(threadId);
        if (thread != null && thread.isAlive()) {
            thread.interrupt();
            logger.info("[Cancel] Thread.interrupt() threadId={}", threadId);
            return true;
        }
        return false;
    }

    private boolean isCancelled(String threadId) {
        DefaultCancellationToken token = cancelTokens.get(threadId);
        return (token != null && token.isCancelled())
                || Thread.currentThread().isInterrupted();
    }

    private void cleanup(String threadId) {
        runningThreads.remove(threadId);
        cancelTokens.remove(threadId);
        syncFutures.remove(threadId);
    }
}

2.2 ReactAgent 同步

agent.call() 包装在 CompletableFuture.supplyAsync() 中:

java 复制代码
public CompletableFuture<String> startAgentSync(String query, String threadId) {
    DefaultCancellationToken token = new DefaultCancellationToken();
    cancelTokens.put(threadId, token);

    return CompletableFuture.supplyAsync(() -> {
        runningThreads.put(threadId, Thread.currentThread());
        try {
            String result = agent.call(query, config).getText();
            if (token.isCancelled() || Thread.currentThread().isInterrupted()) {
                return "[已取消]";
            }
            return result;
        } finally {
            cleanup(threadId);
        }
    });
}

取消时执行三层终止:

java 复制代码
public boolean cancel(String threadId) {
    cancelToken(threadId);                               // 1. 通知工具层
    syncFutures.remove(threadId).cancel(true);          // 2. 立即取消 Future
    return interruptThread(threadId);                    // 3. 中断线程兜底
}

2.3 ReactAgent 流式

agent.call() 放入 Mono.fromCallable() 并在弹性线程池上执行,前端关闭 EventSource 时自动触发 doOnCancel

java 复制代码
public Flux<String> startAgentStream(String query, String threadId) {
    return Mono.fromCallable(() -> agent.call(query, config).getText())
            .subscribeOn(Schedulers.boundedElastic())
            .flatMapMany(text -> Flux.fromArray(text.split("(?<=\\G.{10})"))
                    .delayElements(Duration.ofMillis(50)))
            .doOnCancel(() -> {
                cancelToken(threadId);
                interruptThread(threadId);
            })
            .doOnTerminate(() -> cleanup(threadId));
}

2.4 StateGraph 同步

graph.invoke() 放入异步线程,与 ReactAgent 同步模式机制一致:

java 复制代码
public CompletableFuture<String> startGraphSync(String query, String threadId) {
    return CompletableFuture.supplyAsync(() -> {
        Map<String, Object> inputs = Map.of("input", query);
        Optional<OverAllState> result = graph.invoke(inputs, config);
        return result.flatMap(s -> s.value("result"))
                .map(Object::toString).orElse("无结果");
    });
}

2.5 StateGraph 流式

直接使用 graph.stream(),返回 Reactor Flux,通过订阅管理(Disposable.dispose())即可取消,Reactor 负责线程调度和资源回收:

java 复制代码
public Flux<String> startGraphStream(String query, String threadId) {
    return graph.stream(inputs, config)
            .map(node -> node.state().data().get("result").toString())
            .doOnCancel(() -> cancelToken(threadId))
            .doOnComplete(() -> cleanup(threadId));
}

3. API 参考

3.1 启动任务

bash 复制代码
# ReactAgent 同步
curl -X POST "http://localhost:8089/cancel/agent/sync/start?query=详细介绍&threadId=u1"
# → {"mode":"agent-sync","status":"started","threadId":"u1"}

# ReactAgent 流式(SSE)
curl "http://localhost:8089/cancel/agent/stream/start?query=详细介绍&threadId=u1"

# StateGraph 同步
curl -X POST "http://localhost:8089/cancel/graph/sync/start?query=详细介绍&threadId=u1"

# StateGraph 流式(SSE)
curl "http://localhost:8089/cancel/graph/stream/start?query=详细介绍&threadId=u1"

3.2 取消 & 状态

bash 复制代码
# 通用取消(对所有模式生效)
curl -X POST "http://localhost:8089/cancel/abort?threadId=u1"
# → {"threadId":"u1","cancelled":true,"message":"任务已中止"}

# 查询状态
curl "http://localhost:8089/cancel/status?threadId=u1"
# → {"threadId":"u1","status":"cancelled"}

3.3 状态值定义

状态 含义
running 同步任务正在执行中
streaming 流式任务正在进行中
completed 任务已正常完成
cancelled 任务已被用户取消
failed 任务执行异常
not_found 未找到该 threadId 的任务

4. 取消机制

4.1 两种取消策略

对应框架内置的 cancel(boolean) 方法:

策略 对应方法 行为 适用场景
立即取消 cancel(true) 中断当前线程,立即终止 用户主动点击取消、死循环
优雅取消 cancel(false) 完成当前节点后不再继续 超时自动终止、任务切换
java 复制代码
// 立即取消(用户按钮)
public boolean abort(String threadId) {
    return syncFutures.remove(threadId).cancel(true);
}

// 优雅取消(超时自动触发)
public boolean gracefulStop(String threadId) {
    return syncFutures.remove(threadId).cancel(false);
}

4.2 四层防御体系

复制代码
┌──────────────────────────────────────────────────┐
│                   取消层次                          │
│                                                    │
│  第 1 层:DefaultCancellationToken.cancel()         │
│  └── 通知 CancellableAsyncToolCallback             │
│      工具内部 token.throwIfCancelled() 自主停止     │
│                                                    │
│  第 2 层:CompletableFuture.cancel(true/false)      │
│  └── true: 立即中断线程                            │
│      false: 等当前节点完成,不启动新节点             │
│                                                    │
│  第 3 层:Thread.interrupt()                        │
│  └── 兜底硬终止,阻塞方法抛出 InterruptedException  │
│                                                    │
│  第 4 层(仅流式):Flux 取消订阅                   │
│  └── Disposable.dispose() / EventSource.close()    │
│      java-async-generator 回收线程资源               │
└──────────────────────────────────────────────────┘

5. 前端集成示例

javascript 复制代码
async function startTask(mode, query) {
    const threadId = 'user-' + Date.now();

    if (mode === 'stream') {
        // 流式模式:EventSource
        const es = new EventSource(
            `/cancel/graph/stream/start?query=${query}&threadId=${threadId}`);
        es.onmessage = e => console.log(e.data);
        // 用户点击取消 → 关闭连接
        document.getElementById('cancelBtn').onclick = () => es.close();
    } else {
        // 同步模式:后台启动
        await fetch(`/cancel/agent/sync/start?query=${query}&threadId=${threadId}`,
            { method: 'POST' });
        // 用户点击取消 → 立即终止
        document.getElementById('cancelBtn').onclick = () =>
            fetch(`/cancel/abort?threadId=${threadId}`, { method: 'POST' });
    }
}

6. 最佳实践

  1. 用户取消用 cancel(true):立即终止,释放资源,用户感知更灵敏
  2. 超时取消用 cancel(false):让当前节点完成,拿到部分结果,避免完全白跑
  3. 同步模式用 POST 启动 + POST 取消 :启动返回 threadId,前端定时轮询 /cancel/status
  4. 流式模式用 SSE + EventSource.close() :浏览器原生关闭即触发 doOnCancel
  5. 不同任务用不同 threadId:每个任务独立管理,互不干扰
  6. 工具层配合 CancellationToken :耗时工具实现 CancellableAsyncToolCallback,定期检查 token.throwIfCancelled()

相关推荐
Teacher.chenchong1 小时前
AI-Agent2.0 科研全链路实战营:LLM+NotebookLM + 自动化编程 + 文献管理 + 论文写作,搭建本地科研智能体
人工智能·自动化
weberCd2 小时前
ChatGPT 实用技巧总结(国内)
人工智能·chatgpt
醇氧2 小时前
【Linux】Java 服务生产级部署指南:实现常驻后台、开机自启与系统服务化管理
java·开发语言
我爱cope2 小时前
【Agent智能体26 | 多智能体-多智能体工作流】
人工智能·设计模式·语言模型·职场和发展
JAVA面经实录9172 小时前
Netty 全套系统化学习文档(零基础到高阶面试完整版)
java·后端
weixin_523185322 小时前
Java面试高频题:Integer缓存机制与 equals、== 区别
java·缓存·面试
吴佳浩2 小时前
炸裂!!!给 codeX 装上本地大脑:cc-switch_Ollama 接入全记录
人工智能·rust·openai
一拳小和尚LXY2 小时前
AI 模型 API 对接方案对比:统一网关 vs 直连厂商,2026 年选型指南
人工智能
Bode_20022 小时前
共创经济实现路径
人工智能·制造·供应链