给前端 Agent 批量执行加并发池时,我会先把队列和结果账本拆开
最近在做一个前端 AI 工作台,用户会一次性选择多份文档,让 Agent 批量跑摘要、字段抽取、RAG 检索和格式校验。早期我把任务数组直接 map 成一组 async 调用,小样本能跑,真实项目里很快出现请求打满、接口限流、取消后旧任务回写、单个失败拖乱整批状态的问题。

原创示意图:把 Queue、Limit、Signal、Ledger 和 Trace 拆成五个可排查环节,批量任务的成功、等待、取消和失败都能落到账本。 来源:Codex image generation
问题背景
MDN 对 Promise.allSettled() 的说明里提到,它会在输入 promise 全部 settled 后返回每一项结果,适合多个异步任务互不依赖且需要知道每个结果的场景。p-limit 的 README 把它定位为限制 promise 返回函数并发数量的工具,并提供 activeCount、pendingCount、clearQueue 等队列状态。放到前端 Agent 里,我会把并发池和结果账本成对设计:前者控制同时跑多少任务,后者记录每个任务最终发生了什么。
踩坑和关键难点
第一个坑是批量执行只有一个外层 promise,UI 很难说明哪些成功、哪些失败、哪些还在等。第二个坑是取消只停网络,排队任务和本地待写状态仍会继续推进。第三个坑是进度来源混乱,列表项、汇总条和日志面板各算一份状态,最后会出现进度完成但旧结果仍在回写的情况。
解决思路
我把批量执行拆成五层:task queue 保存 taskId、输入引用和优先级;concurrency limiter 按工具类型和上游限流设置并发数;abort registry 为 batch 和 task 保存 AbortController;result ledger 只追加 queued、running、fulfilled、rejected、cancelled 事件;trace panel 把 batchId、taskId、attempt 和错误原因串到同一条排障链路里。
任务进入队列先写 queued,拿到并发名额后写 running,请求完成后写 fulfilled 或 rejected,用户取消时终止 signal,并把未开始任务标记为 cancelled。进度条、结果列表和错误摘要都从 ledger 计算出来,避免多份状态各算各的。
关键步骤
第一步是定义稳定任务协议。每个任务必须有 taskId、batchId、inputHash、toolName 和 attempt。不要用数组下标对齐结果,重试和排序都会让下标失去意义。
第二步是把并发限制放在入口层。可以用 p-limit 这类小工具,也可以写项目内 limiter。重点是暴露 running、pending 和 concurrency,方便解释某个任务为什么还没开始。
第三步是取消分两段做。正在运行的任务接收 AbortSignal,等待队列里的任务通过 clearQueue 或本地状态标记取消。已经 settled 的任务不回滚,只在 ledger 里追加诊断事件。
第四步是用 allSettled 收尾,不把单个失败扩散成整批失败。批次完成时生成 report,包含成功数、失败数、取消数、首个错误和最慢任务。
第五步是补观测字段。OpenTelemetry JavaScript 文档建议用 span 表达一次工作,并在异常时记录 exception 和错误状态。前端侧可以轻量记录 batch span 和 task span,保留 batchId、taskId、toolName、duration、status 和 errorName。
可复用经验
批量 Agent 执行要先定事实源:queue 决定谁等待,limiter 决定谁运行,signal 决定谁停止,ledger 决定用户看到什么,trace 决定问题怎么查。这个结构同样适合批量 RAG 文档处理、批量表单校验、Electron 本地任务面板和自动化工具审计台。
上线前我还会做三组小压测:十个任务、五十个任务、取消后立即重跑。观察点不复杂,只看并发数是否稳定、取消任务是否还会写 UI、失败报告是否能定位到单个 taskId。只要这三项过关,后续再加优先级、暂停恢复和失败重试,就不会把基础状态模型推翻。
还有一个细节是失败复跑。重跑时不要复用上一轮的运行状态,只复用输入引用和 taskId 映射,重新生成 attempt。这样历史报告能保留,新的执行也能独立计数,用户回看时不会把上一轮失败和这一轮成功混在一起。
这一步也能提醒团队及时收窄默认并发,别把调试阶段的宽松设置带到正式环境。