ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AI 停止生成设计:从进程内状态到分布式任务控制

Spring AI 停止生成设计:从进程内状态到分布式任务控制 一、停止生成的控制边界流式聊天中“停止生成”看起来只是一个按钮后端处理的却是一个持续运行的生成任务。用户点击停止后最直接的目标是让后续模型内容不再继续发送到前端因此应用需要控制当前 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 MapString, 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 IDsessionId 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 的运行状态。这时需要把任务状态放到多个实例共同访问的存储中例如 Redisai:generation:G101 → RUNNINGStop 请求可以更新为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 管理都可以沿着同一套思路继续扩展。
返回列表