ARTICLE · 1029695
33.长任务状态管理:如何让用户看到文档解析进度?
长任务状态管理:如何让用户看到文档解析进度?
码海寻道 · 大模型、智能体与 RAG 工程组件系列第 33 篇

文件上传后,用户最关心的不是后台使用了 RabbitMQ 还是 Kafka,而是“现在处理到哪一步了,什么时候可以搜索”。长任务状态管理的目标,是让后台任务可追踪、可重试、可恢复,让前端展示可信进度。
长任务状态应以 PostgreSQL 等持久化事实源为准,Redis 可以缓存进度,SSE/WebSocket 只负责实时通知。通知连接断开后,客户端仍应通过查询接口恢复最新状态,不能把进度只放在推送连接内存中。
一、不要只保存一个 processing
uploaded → queued → parsing → ocr → chunking → embedding → indexing → ready失败状态也要明确:
parse_failedocr_failedembedding_failedindex_failedcancelled只有状态足够细,系统才能知道从哪里重试,也才能给用户准确提示。
每个状态转换都应记录 job_id、attempt、worker_id、updated_at 和错误摘要,并使用乐观锁或版本号防止旧 Worker 覆盖新状态。
二、数据库状态模型
CREATE TABLE document_jobs ( job_id uuid PRIMARY KEY, document_id uuid NOT NULL, version integerNOT NULL, status text NOT NULL, current_stage text NOT NULL, progress numeric(5,2) NOT NULLDEFAULT0, attempt integerNOT NULLDEFAULT0, error_code text, error_message text, started_at timestamptz, finished_at timestamptz, updated_at timestamptz NOT NULLDEFAULT now());progress 是展示信息,status 和 current_stage 才是任务事实。不要只依赖百分比判断任务是否成功。
三、进度怎么计算?
最简单的是阶段加权:
解析 20%OCR 25%切分 15%向量化 25%索引 15%如果一个阶段包含 1000 页,可以进一步根据已处理数量计算子进度:
阶段进度 = 已完成页数 / 总页数总进度 = 已完成阶段权重 + 当前阶段权重 × 阶段进度进度不必承诺精确剩余时间。解析器、OCR 和模型调用耗时差异很大,宁可展示“正在处理第 3/5 阶段”,也不要让百分比长时间停在 99%。
四、前端如何获取状态?
轮询
前端每隔几秒请求:
GET /api/documents/{document_id}/jobs/{job_id}实现简单,适合低频后台任务。
SSE
服务端通过 Server-Sent Events 推送状态更新,适合单向进度通知:
event: progressdata: {"stage":"embedding","progress":65}WebSocket
适合需要双向实时交互的系统,但连接管理复杂度更高。无论哪种方式,数据库状态都应是最终事实来源,不能只保存在 WebSocket 内存里。
五、任务重试从哪里开始?
不同阶段的重试策略不同:
• 解析失败:检查格式后重试或转人工; • OCR 超时:切分图片、降低批次或更换引擎; • Embedding 限流:指数退避并限制并发; • 向量写入失败:使用批次 ID 重试; • 权限错误:通常不应无限重试。
建议记录 attempt、错误类型和最后一次失败时间,避免用户只能看到“处理失败”而无法判断原因。
六、如何保证状态与实际数据一致?
一个常见错误是先把状态写成 ready,再异步写向量。用户看到完成后立即搜索,却找不到内容。
更稳妥的顺序是:
解析完成 ↓Chunk 和向量批次写入成功 ↓检查索引可搜索状态 ↓写入发布版本 ↓任务状态 = ready如果某一步成功、下一步失败,应保持中间状态并允许补偿,而不是把整个任务伪装成成功。
ready 应是一个发布承诺:解析结果存在、向量写入成功、索引可搜索、权限和版本过滤已经生效。只有满足这些条件,前端才应该显示“可搜索”。
七、取消任务怎么做?
取消不是简单地把数据库状态改成 cancelled。Worker 可能正在运行,还要处理:
• 检查取消标记; • 中断后续阶段; • 清理临时文件; • 删除未发布的向量; • 释放外部模型调用; • 防止取消后的旧任务覆盖新版本状态。
任务应使用版本和 job_id 校验更新权限,旧任务不能修改新任务的状态。
八、长任务状态接口示例
{"job_id":"job-001","document_id":"doc-001","status":"running","stage":"embedding","progress":65,"processed":130,"total":200,"can_cancel":true,"updated_at":"2026-08-12T10:02:00Z","error":null}前端可以根据 status 决定按钮和提示,不能只根据 progress=100 判断完成。
九、监控长任务健康度
记录:
• 各阶段耗时; • 队列等待时间; • 当前运行任务数; • 失败率和重试率; • 卡住任务数量; • 最老任务年龄; • 每个租户的任务配额。
设置超时检测:如果任务长时间没有更新时间,就标记为 stalled,由恢复程序检查 Worker 是否仍然存活并决定重试或人工介入。
恢复程序需要区分“Worker 仍在处理”和“Worker 已经丢失心跳”。可以使用租约、心跳时间和拥有者 ID,避免两个 Worker 同时接管同一个任务;接管后仍要依靠阶段幂等保证重复执行安全。
十、上线检查清单
• 状态机有明确的合法流转; • 每个阶段可单独观测和重试; • job_id、document_id 和 version 有稳定关联; • ready 只在数据真正可检索后设置; • 前端可以轮询或订阅状态; • 取消不会被旧任务覆盖; • 错误码和提示不会泄露敏感信息; • 有卡住任务检测和恢复机制。 • SSE/WebSocket 断线后可以通过查询接口恢复; • 状态更新带版本控制,旧 Worker 不能覆盖新状态; • stalled 任务有心跳、租约和接管策略;

结语
长任务状态管理的核心不是显示一个漂亮的进度条,而是让任务拥有可验证的状态、可恢复的阶段和明确的失败边界。对于 RAG 知识库,只有当向量和发布状态都准备好后,文件才真正“处理完成”。
至此,第六篇章“缓存、文件与异步任务”全部完成。下一篇章将进入 Agent 与工作流编排,从《什么是 AI Agent?》开始。
参考资料
1. Redis 官方文档:Job Queue 2. RabbitMQ 官方文档:Work Queues 3. MDN:Server-sent events 4. PostgreSQL 官方文档:Concurrency Control
本文为“码海寻道”原创技术文章。任务状态机和重试策略应结合业务数据一致性、取消语义与故障恢复要求设计。