RAG 任务调度链路设计改动

RAG 任务调度链路设计改动

1. 任务状态设计:PENDING 的语义

  • submit_task 写入 TaskRecord 时状态设为 PENDING,因为此时任务只是"被登记、进队列",尚未被 worker 消费RUNNING 只能由真正开始执行的 worker 设置。
  • 完整状态生命周期:PENDING(提交)→ RUNNING(worker 取出执行)→ SUCCESS/FAILED(执行结束)。
  • 意义:保证"任务已被接受"和"任务已被执行"是两个可观察的独立状态,前端提交后立刻查询也能拿到明确状态,而不是空白。

2. 读写分离:CQRS 模式

  • 架构上属于 CQRS(Command Query Responsibility Segregation)
    • 写路径submit_task/resume_task)→ 驱动真实计算,需要走队列做背压和 worker 并发限流。
    • 读路径/status 查 dispatcher、/result 查 LangGraph checkpointer)→ 幂等、无副作用,不占用 worker,直接读。
  • 两个数据源分工明确:
    • status(PENDING/RUNNING/SUCCESS/FAILED)= dispatcher 层"这次调用有没有在跑"。
    • result(answer/citations/interrupts/next_nodes)= LangGraph 层"业务状态",checkpointer 是唯一权威来源,dispatcher 不镜像。

3. TaskRecord.result 字段清理

  • 原因:run_rag_chat_task 等 handler 始终返回 {},业务结果全部走 checkpointer 获取,_mark_success 写入的 result 字段从未被 get_task_snapshot 读出,属于死代码。
  • 处理:删除 result 字段,TaskHandler 返回类型语义随之收窄。

4. Callable 类型标注踩坑

  • 错误写法:Callable[[dict[str, Any], str]](只给了参数列表,漏了返回类型)。
  • 后果:typing.Callable 下标必须是 [参数列表, 返回类型] 两元素,写错会在模块导入时直接抛 TypeError,不是运行到才报错。
  • 正确写法:Callable[[dict[str, Any], str], Awaitable[bool]]

5. SUCCESS 状态语义陷阱

  • LangGraph 的 interrupt() 会让 graph.ainvoke() 正常返回(不抛异常、无特殊返回值差异),所以"图跑完"和"图被中断挂起等待人工确认"在 dispatcher 层看起来是同一回事,都会被标记为 SUCCESS
  • 风险:前端容易把 status=SUCCESS 误解为"已经有最终答案",实际还需要查 /result 里的 next_nodes/interrupts 才能判断真实进度。

6. 引入 interrupted 标记的设计取舍

  • 两种方案对比:
    • 方案 A:直接扩展 TaskStatus 枚举加 INTERRUPTED——会破坏"dispatcher 不镜像 LangGraph 状态"的原则,且 worker 无法仅凭 ainvoke() 返回值区分中断/完成,需要 handler 额外查 snapshot.next 才能判断。
    • 方案 B(采用):保留四态不变,额外挂一个独立的 interrupted: bool 字段。两个维度解耦:status 仍表示"调用是否正常结束",interrupted 表示"结束时是否停在中断点",前端可直接据此判断要不要走审批流程,减少无意义的 /result 轮询。
  • 实现关键:handler 在 ainvoke() 后主动 await graph.aget_state(config),用 bool(snapshot.next) 判断是否停在中断点,并把这个轻量控制信号(不是业务数据)透传给 dispatcher。

7. _dispatch 的角色

  • 定位:worker 循环中的路由转发层,职责是"按 task_typeself._handlers 字典找到对应 handler 并执行",与 _worker_loop(负责取任务/标记状态/超时处理)职责分离。
  • 找不到对应 handler 时抛 InvalidRequestError 兜底,避免运行时因 handler is None 直接崩溃。
  • 之所以单独拆出,是因为 asyncio.wait_for 需要一个协程整体做超时控制,拆出来也便于未来统一加日志/权限校验等横切逻辑。
  • 踩坑记录:一度把 _dispatch 改成 -> None 且遗漏 return,导致返回值丢失、bool | None 类型错误一路传导到 _worker_loop 里的 interrupted 变量。修复:显式 return await handler(payload, session_id) 并标注 -> bool
posted @ 2026-07-03 17:41  asphyxiasea  阅读(4)  评论(0)    收藏  举报