
一、停止生成的控制边界
流式聊天中,“停止生成”看起来只是一个按钮,后端处理的却是一个持续运行的生成任务。用户点击停止后,最直接的目标是让后续模型内容不再继续发送到前端,因此应用需要控制当前 Reactive 流的生命周期。
这里首先要区分两个层次。第一层是应用侧停止输出,即后端终止当前 Flux,让后续 chunk 不再继续向客户端传递;第二层是模型 Provider 是否真正停止远端推理。前者可以由应用控制,后者还取决于 HTTP 客户端、模型 SDK 和 Provider 对取消请求的支持。
因此,一个可靠的停止功能首先应该保证应用侧任务能够被准确定位和取消。
二、单一状态变量的适用范围
最小 Demo 中,可以直接使用一个 boolean 表示当前是否继续生成:
// 标记当前是否继续生成
boolean generating = true;
模型持续输出时检查该变量,用户点击停止后将其改成 false。对于只有一个用户、一个任务的实验环境,这已经能够完成基本功能。
问题在多用户环境中立即出现。Web 服务可能同时处理多个生成请求,全局 boolean 无法区分当前状态属于哪一个用户或哪一次会话。用户 A 点击 Stop 后,如果直接修改全局变量,用户 B 正在进行的生成任务也可能受到影响。
因此,停止状态必须从“全局状态”升级为“任务关联状态”。
三、会话级状态与并发安全
最直接的改进,是把生成状态与 Session ID 关联:
// 每个会话独立维护生成状态
sessionA → true
sessionB → true
sessionC → true
开始生成时写入状态,停止时删除状态。课程中可以使用 ConcurrentHashMap 保存这类数据:
// 线程安全的会话级状态容器
private final Map<String, Boolean> generating =
new ConcurrentHashMap<>();
生成开始时:
// 生成开始时标记为运行中
generating.put(sessionId, true);
用户停止时:
// 用户停止时移除状态
generating.remove(sessionId);
读取状态时:
generating.getOrDefault(sessionId, false);
这里使用 ConcurrentHashMap 的原因,是 Web 请求存在并发读写。普通 HashMap 不适合作为多个线程共享修改的运行状态容器。
停止后直接 remove,也比长期保存 false 更合理。已经结束的任务不再具有业务价值,如果每个历史 Session 都永久保留一个状态值,容器会持续增长。
四、Reactive 流的终止与资源清理
有了任务状态以后,就可以把它与 Reactor Flux 结合。takeWhile 会在元素继续向下游传递之前执行 Predicate,只要条件成立就继续,一旦条件变为 false,当前流就停止继续发送。
例如:
return modelFlux
.takeWhile(chunk ->
generating.getOrDefault(sessionId, false))
.doFinally(signal ->
generating.remove(sessionId));
这种结构中,状态负责表达“任务是否仍然有效”,takeWhile 负责根据状态决定是否继续输出。
资源清理同样重要。生成任务可能通过正常完成、异常、用户取消等多种方式结束。如果只在某一种路径中 remove,就可能遗留失效状态。doFinally 可以集中处理流终止后的清理,使临时状态的生命周期与生成任务保持一致。
五、Generation ID 的任务级控制
使用 Session ID 已经能够解决多用户问题,但它仍然属于会话级标识。一个 Session 中可能连续发生多次请求,也可能出现重新生成、重试甚至并行生成。如果停止状态只绑定 Session ID,粒度仍然过粗。
更准确的方案,是为每一次实际生成任务创建独立的 Generation ID:
sessionId = S001
generationId = G101
generationId = G102
运行状态改为:
G101 → RUNNING
G102 → RUNNING
用户停止 G101 时,只影响对应生成任务,G102 可以继续执行。Session ID 继续表示业务会话,Generation ID 专门表示一次运行中的生成任务。
因此,系统复杂度提升以后,可以进一步形成:
User
↓
Session
↓
Request
↓
Generation
停止生成应该尽量绑定到实际需要被控制的 Generation,而不是整个 Session。
六、多实例部署中的共享状态
ConcurrentHashMap 只能解决单个 JVM 内部的并发。如果 AI Service 部署多个实例,新的问题会再次出现。
假设 G101 实际运行在实例 A,而用户点击 Stop 后,请求被 Gateway 分配到实例 B。实例 B 的本地 Map 中并没有 G101,因此它无法修改实例 A 的运行状态。
这时需要把任务状态放到多个实例共同访问的存储中,例如 Redis:
ai:generation:G101 → RUNNING
Stop 请求可以更新为 STOPPED,或者根据设计直接删除,同时设置 TTL,避免异常任务永久残留。
Redis 在这里解决的是“多个服务实例共享任务状态”。它并不会自动持有真正运行中的 Flux。实际生成任务仍然存在于某个具体 JVM 中,因此系统规模继续增大后,还可以增加消息通知机制,让收到 Stop 请求的实例通知真正持有 G101 的执行节点完成取消。
七、生成任务的状态模型
系统进一步工程化以后,单纯的 RUNNING 和 STOPPED 可能仍然不够。一次生成任务还可能经历创建、运行、请求停止、正常完成、执行失败和取消等阶段:
CREATED
↓
RUNNING
↓
STOP_REQUESTED
↓
CANCELLED
或者:
RUNNING → COMPLETED
RUNNING → FAILED
显式状态模型能够更准确地处理竞态。例如用户点击 Stop 的同时模型刚好自然结束,或者前端连续发送两次停止请求,都需要系统明确当前任务已经处于什么状态。
此时 Stop 接口也应该尽量设计成幂等操作。任务已经完成时再次 Stop,可以直接返回当前终态,而不是产生新的异常。
八、总结
停止生成的设计实际上是一条非常完整的工程演进路径。单用户 Demo 可以使用 boolean;进入多用户后,需要把状态与 Session 关联;并发读写要求线程安全容器;同一个 Session 出现多个生成任务后,需要进一步引入 Generation ID;服务进行多实例部署以后,本地 Map 又需要演化为 Redis 等共享状态机制。
整个过程体现的重点并不是 Redis 或 Reactor API 本身,而是状态粒度和部署环境不断变化以后,原有方案会在哪个位置失效。
一个稳定的设计原则是:运行状态应该绑定到真正需要控制的任务对象,临时状态应该跟随任务生命周期创建和清理,部署范围扩大以后再选择对应范围的共享机制。 只要这三个边界明确,停止生成、任务取消和后续的 Agent Run 管理都可以沿着同一套思路继续扩展。
网硕互联帮助中心


评论前必须登录!
注册