背景 / 现象
2026 年初,我们上线了一套基于 Agent 的智能工单处理系统,用于自动解析用户提交的工单内容,调用 RAG 检索相关知识,并由多个子 Agent 协同完成分类、优先级判定、执行建议生成等任务。系统初期运行平稳,但在一次知识库大规模更新后,出现大量工单"卡在中间状态"的现象:前端显示"处理中",但实际任务已停止推进,无错误日志,也无超时告警。
这类"静默阻塞"问题在初期被误判为偶发性网络抖动,直到一周内积压工单突破 500 条,才触发人工排查。现象集中在任务编排层------子任务 A 完成后,任务 B 未启动,但系统未标记失败,也未重试,形成"黑盒卡点"。
问题拆解
我们将整个链路拆解为四个核心模块:
- 任务调度器(Scheduler):负责接收工单,生成 DAG 执行计划,并触发首个子任务。
- 状态管理器(State Manager):维护每个工单及其子任务的状态流转(如 pending → running → completed/failed)。
- 执行器集群(Executor Cluster):实际运行子任务的 Worker 节点,通过消息队列接收任务。
- 补偿控制器(Compensation Controller):监控任务超时与状态异常,触发重试或回滚。
通过日志与状态快照对比,发现以下异常模式:
- 子任务 A 执行成功,状态已更新为 completed;
- 调度器未收到 A 完成事件,或收到但未触发 B;
- 状态管理器中 B 仍为 pending,且无任何超时或错误记录;
- 补偿控制器因缺乏"中间状态"定义,未介入处理。
核心原因
1. 状态机设计未覆盖"中间态"
原状态机仅定义了任务的终态(completed/failed),未定义"等待依赖完成"这一中间态。当任务 A 完成但事件未送达调度器时,任务 B 处于 pending,系统误认为"尚未开始",而非"已就绪但未触发"。这种设计导致补偿机制无法识别"应触发而未触发"的异常。
2. 事件驱动链路缺乏强一致性保障
任务完成事件通过消息队列异步传递,但未实现"至少一次"投递与"幂等消费"。在消息积压或消费者重启期间,事件丢失或重复消费,导致状态不一致。更严重的是,调度器未对未触发任务进行周期性扫描,形成"静默盲区"。
3. 补偿机制仅关注"失败",忽略"停滞"
现有补偿逻辑仅对 failed 状态任务进行重试,对 pending 状态任务无处理逻辑。即使任务已超时 24 小时,只要状态未变,系统便视为"正常等待"。这种设计假设所有 pending 任务最终都会被触发,忽略了调度器自身故障的可能性。
4. 模块职责边界模糊,状态变更无审计追踪
状态管理器同时承担状态存储与变更通知职责,但未记录"谁在何时将状态从 X 改为 Y"。当出现不一致时,无法定位是调度器未发事件,还是执行器未上报,还是状态管理器自身异常。
实现方案
1. 引入"就绪态"与"阻塞态"中间状态
重构状态机,新增 ready 和 blocked 状态:
ready:所有前置任务已完成,等待调度器触发;blocked:因依赖未满足或系统异常,无法继续推进。
状态流转逻辑调整为:
pending → running → completed/failed
↘ ready → running
↘ blocked → (人工介入或自动恢复)
2. 实现调度器周期性扫描与触发补偿
在调度器中增加"就绪任务扫描器"(Ready Task Scanner),每 5 分钟扫描所有处于 ready 状态的任务,检查其前置依赖是否真正完成。若依赖已完成但任务未触发,则立即重新入队。
同时,设置 ready 状态超时阈值(如 10 分钟),超时后自动标记为 blocked,并触发告警。
3. 强化事件链路的可靠性
- 消息队列启用持久化与 ACK 机制,确保事件不丢失;
- 调度器实现幂等消费,避免重复触发;
- 执行器上报任务完成时,附带任务 ID 与时间戳,调度器校验后更新状态。
4. 分层状态审计与追踪
在状态管理器中引入变更日志表,记录每次状态变更的:
- 变更前状态
- 变更后状态
- 触发方(调度器/执行器/补偿器)
- 时间戳
- 上下文快照(如依赖任务 ID 列表)
该日志用于故障排查与状态回溯,同时支持通过 API 查询任意任务的完整生命周期。
5. 补偿控制器升级为"状态感知型"
补偿控制器不再仅监控 failed 任务,而是基于状态机定义,对以下情况自动干预:
ready状态超时 → 重试触发;blocked状态持续超阈值 → 通知人工并尝试自动恢复;pending状态无前置依赖但长时间未启动 → 标记为异常并告警。
风险与边界
1. 中间态引入可能增加状态机复杂度
新增状态需严格定义触发条件与回滚路径,避免状态爆炸。我们通过有限状态机(FSM)建模工具验证所有流转路径,确保无死锁或不可达状态。
2. 周期性扫描带来性能开销
扫描任务数量大时可能影响调度器性能。解决方案:
- 按任务创建时间分片扫描;
- 使用 Redis 缓存 ready 任务 ID 列表,减少数据库查询;
- 扫描频率根据负载动态调整(低峰期 1 分钟,高峰期 10 分钟)。
3. 补偿逻辑可能引发重复执行
若任务 B 已触发但未上报完成,补偿器再次触发会导致重复执行。我们通过任务执行幂等性设计(如任务 ID 唯一键约束)与执行前状态校验规避此风险。
4. 人工介入边界需明确
并非所有 blocked 任务都需人工处理。我们定义:
- 系统级阻塞(如消息队列宕机)→ 自动恢复;
- 业务级阻塞(如依赖任务逻辑错误)→ 通知业务方;
- 未知阻塞 → 升级至运维团队。
总结
本次故障暴露了 AI 任务编排系统在状态建模与补偿机制上的深层缺陷。传统 CRUD 思维下的状态管理无法应对复杂 DAG 执行场景,必须引入"中间态"概念与分层补偿策略。通过明确模块职责(调度器负责触发、状态管理器负责审计、补偿器负责兜底),我们构建了可观测、可恢复的任务编排架构。
核心经验:
- 状态机设计必须覆盖"应发生但未发生"的场景;
- 事件驱动系统需实现端到端可靠性;
- 补偿机制应基于状态语义,而非仅依赖错误码;
- 模块边界清晰是故障隔离与快速定位的前提。
该方案上线后,静默阻塞问题归零,任务平均处理时长下降 37%,系统可观测性显著提升。
技术补丁包
-
中间态状态机设计 原理:在传统 pending/running/completed 基础上引入 ready 和 blocked 状态,明确任务在依赖满足后"待触发"的语义。 设计动机:解决"任务已就绪但调度器未响应"的静默阻塞问题。 边界条件:需确保所有状态流转路径均被覆盖,避免死锁;ready 状态必须设置超时阈值。 落地建议:使用状态机库(如 XState)建模,并通过单元测试验证所有流转路径。
-
就绪任务周期性扫描机制 原理:调度器定时扫描所有 ready 状态任务,检查其前置依赖是否完成,若完成则重新入队触发。 设计动机:弥补事件丢失或延迟导致的触发失败。 边界条件:扫描频率需根据系统负载动态调整,避免高频扫描影响性能。 落地建议:使用 Redis 缓存 ready 任务 ID,结合分片扫描降低数据库压力。
-
事件链路幂等与持久化保障 原理:消息队列启用 ACK 与持久化,调度器实现幂等消费,执行器上报时附带唯一任务 ID。 设计动机:防止事件丢失或重复消费导致状态不一致。 边界条件:需处理网络分区场景下的脑裂问题,避免双主调度。 落地建议:在任务表中增加
last_triggered_at字段,调度前校验是否已被触发。 -
状态变更审计日志 原理:记录每次状态变更的上下文,包括触发方、时间戳、前后状态与依赖快照。 设计动机:提供故障排查的可追溯性,支持状态回溯与根因分析。 边界条件:日志量可能较大,需设置保留策略与索引优化。 落地建议:使用独立日志表,并通过异步写入避免阻塞主流程。
-
分层补偿策略 原理:根据任务状态语义定义补偿动作,如 ready 超时重试、blocked 告警、pending 无依赖则标记异常。 设计动机:将"停滞"视为可修复异常,而非静默忽略。 边界条件:补偿动作需幂等,避免重复执行引发副作用。 落地建议:补偿控制器与调度器解耦,通过事件驱动方式触发,确保职责单一。