文章目录
- [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 节点(大模型、向量库、图片生成)均消耗算力、Token、API 额度,在以下场景中需要引入取消支持:
- 用户输入错误 :发送后发现
query有问题,不想等结果 - 需求变更:用户改变主意,不再需要当前任务的输出
- 不想继续等待:长耗时任务,用户不愿等完
- 图逻辑异常:无限循环或死循环,需要强制终止
特别是长耗时流程,一般都需要支持:
- 超时自动终止:设置最大执行时长,超时自动取消,防止单任务长期独占资源
- 用户主动终止:点击取消按钮,停止当前任务
例如豆包中的"停止运行"按钮:

1.2 官方文档
Spring AI Alibaba Graph 内置了完善的图执行取消机制 ,针对长耗时工作流场景十分实用。该能力底层基于 java-async-generator 库的取消特性实现,调用图的 stream 方法执行任务时,会返回流对象,可主动终止图执行。
核心取消方法 cancel(boolean mayInterruptIfRunning) 根据入参分为两种模式:
- 立即取消 (
cancel(true)):中断当前执行线程,终止流程 - 优雅取消 (
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) 是 CompletableFuture 的 API |
| "stream() 返回流对象可主动终止" | ✅ 属实 | Flux → Disposable.dispose() 即可取消 |
| "内置了完善的图执行取消机制" | ⚠️ 部分 | 流式有(Reactor 原生),同步没有 |
1.3 取消策略(本模块封装)
鉴于框架只在流式场景有原生取消,本模块统一封装了两种取消策略:
| 策略 | 实现 | 行为 | 适用场景 |
|---|---|---|---|
| 立即取消 | CompletableFuture.cancel(true) + Thread.interrupt() |
中断当前线程,立即终止 | 用户点击取消、死循环 |
| 优雅取消 | CompletableFuture.cancel(false) |
不中断线程,等当前节点完成 | 超时自动终止、拿到部分结果 |
1.4 核心思路
把阻塞调用放到异步线程中,对外暴露可取消的
Future或Flux。
用户点击取消
│
▼
POST /cancel/abort ──→ AgentExecutionManager.cancel(threadId)
│
├── DefaultCancellationToken.cancel() 通知工具层
├── CompletableFuture.cancel(true) 立即取消 / cancel(false) 优雅取消
└── Thread.interrupt() 兜底硬终止
1.5 四种模式
本模块覆盖 ReactAgent 和 StateGraph 在同步 和流式两种模式下的取消:
| 组件 | 模式 | 启动端点 | 取消机制 |
|---|---|---|---|
| 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. 最佳实践
- 用户取消用
cancel(true):立即终止,释放资源,用户感知更灵敏 - 超时取消用
cancel(false):让当前节点完成,拿到部分结果,避免完全白跑 - 同步模式用
POST启动 +POST取消 :启动返回threadId,前端定时轮询/cancel/status - 流式模式用
SSE+EventSource.close():浏览器原生关闭即触发doOnCancel - 不同任务用不同
threadId:每个任务独立管理,互不干扰 - 工具层配合
CancellationToken:耗时工具实现CancellableAsyncToolCallback,定期检查token.throwIfCancelled()