别让用户替你等:vibe 项目的后台任务与队列实战
AI 写代码有个默认偏见:所有逻辑都塞进一次 HTTP 请求里。发邮件、调大模型、批量导入——用户对着转圈等 30 秒,然后超时 500。本文讲 vibe 项目什么时候必须把工作扔进后台、队列怎么选型(Inngest / Trigger.dev / BullMQ / pg-boss)、幂等性与重试怎么做,以及让 Agent 帮你接好队列的任务拆分模板。

Agent 的默认思维:把所有事塞进一次请求里
让 Agent 写一个「用户注册后发欢迎邮件」的功能,它大概率这么干:在注册接口里 await sendEmail(),等邮件发完再返回。你在本地测试:邮件 800 毫秒发出,页面跳转,岁月静好。上线第一周,邮件服务商偶发延迟 8 秒——用户点完注册按钮,转圈 8 秒,然后 Vercel 的 10 秒函数超时直接 500。用户以为注册失败,又注册一遍,收到两封欢迎邮件,还顺手给你留了条「这网站有 bug」的反馈。
这不是 Agent 的错,这是它的默认思维:它写的代码只考虑「逻辑正确」,不考虑「时间」。HTTP 请求有超时(Serverless 平台通常 10~60 秒),用户有耐心上限(超过 3 秒就开始烦躁),第三方 API 有波动——这三个现实叠加,任何「在请求里顺手做点慢事」的写法,上线后都是定时炸弹。
解法就一句话:凡是用户不需要立刻看到结果的工作,全部扔进后台异步执行。请求只负责「接单」——把任务写进队列立刻返回 200;真正干活的 worker 在后台慢慢处理。这是后端工程里最古老也最有效的模式之一,而 vibe coder 恰恰最容易跳过它,因为 Agent 永远不会主动提。
本文给你一套完整的后台任务心智模型:哪些场景必须异步、队列方案怎么选、幂等性和重试怎么做、定时任务有哪些坑,最后给一个可以直接贴给 Agent 的任务拆分模板。
哪些场景必须异步:一张自查表
判断标准很简单:这个操作如果慢到 3 秒以上,用户会不会觉得网站坏了?会,就异步。下面是 vibe 项目里最高频的六个场景,对号入座:
一,调用大模型。生成一段文案 10 秒、生成一份报告 2 分钟——LLM 调用是 vibe 项目里最常见的慢操作。正确姿势是:请求创建一条「生成任务」记录并返回任务 ID,前端轮询或走 WebSocket 拿进度,worker 在后台流式生成、写库。注意这和上一轮性能指南里讲的「流式输出」不矛盾:流式解决的是「已经决定同步等」的体感,队列解决的是「根本不该同步等」的架构问题。超过 30 秒的生成任务,老老实实用队列。
二,发邮件 / 短信 / 推送。第三方服务偶发延迟是常态,而且你根本控制不了。注册欢迎信、订单通知、密码重置——全部进队列,失败了自动重试,用户无感。
三,文件和图片处理。用户上传一张图,你要压缩、转格式、生成缩略图、打水印、传 CDN——串行做完 5~10 秒。进队列后用户上传完立刻能继续操作,处理完再通知。
四,批量操作。导入 5000 行 CSV、批量打标签、数据迁移——这些动辄几分钟,绝对不能放在请求里。队列 + 进度条(已处理 1200/5000)是标准答案。
五,定时任务。每天凌晨算账单、每小时同步一次第三方数据、每周发一次周报——cron 调度的本质就是后台任务。
六,Webhook 接收。支付平台的回调要求你 5 秒内返回 200,否则它会重试轰炸你。正确姿势:收到回调先验签、入库、回 200,真正的业务处理(发货、记账)扔进队列慢慢做。
记住一句话:请求是「接单员」,worker 才是「干活的人」。接单员的 KPI 是快(毫秒级返回),干活的人的 KPI 是稳(失败重试、最终完成)。
选型:从 setTimeout 到专业队列
方案按复杂度排个序,vibe 项目按阶段选,别一上来就上 Kubernetes 那套。
第一档:平台原生能力(个人项目 / MVP)。Vercel Cron、Cloudflare Workers 的 Cron Triggers、Supabase 的 pg_cron——定时任务直接用平台的,不用自己搭。缺点是只能做定时触发,做不了「用户触发一个慢任务」的场景。
第二档:Serverless 友好的托管队列(推荐大多数 vibe 项目)。Inngest 和 Trigger.dev 是这个赛道的两个代表:没有常驻进程,用函数即服务的方式跑任务,写起来像写普通函数(step.run("send-email", ...)),重试、并发控制、定时调度、可观测面板开箱即用。Inngest 的免费额度对个人项目很慷慨,Trigger.dev 的 dashboard 做得尤其直观。选型建议:已经在 Vercel/Netlify 上、想要零运维,闭眼选这档。代价是任务执行有平台限制(单步超时、按量计费),超大规模再考虑自建。
第三档:Redis 队列(有一定后端经验)。BullMQ(Node 生态事实标准)配合一个 Redis 实例(Upstash 的 serverless Redis 免运维),延迟队列、优先级、限流、重复任务去重都很成熟。适合任务量上来、需要精细控制的场景。成本是一个 Redis + 你自己要保证 worker 进程活着(可以用 Railway/Fly.io 跑一个常驻 worker)。
第四档:Postgres 即队列(已经在用 Postgres 且想零新增依赖)。pg-boss 直接拿你的 Postgres 当队列,省掉 Redis。性能不如 Redis 方案,但「少一个组件」对独立开发者是实实在在的幸福。Supabase 用户也可以考虑 pgmq 扩展。注意:别自己手写「jobs 表 + setInterval 轮询」——并发、锁、重试、死信这些坑,pg-boss 都替你踩过了。
一句话总结:MVP 用 Inngest/Trigger.dev,用户量起来换 BullMQ+Redis,极简主义用 pg-boss。三种我都见过跑得很好的 vibe 项目,选哪个都不丢人,迟迟不选、把慢逻辑堆在请求里才丢人。
幂等性:重试不会搞砸你的数据
上了队列,第一个要补的概念就是幂等性(idempotency):同一个任务被执行两次,结果和执行一次一样。为什么?因为队列一定会重试——worker 崩了、超时了、部署重启了,任务都会被重新投递。「至少执行一次」是队列的默认语义,「恰好一次」在分布式系统里几乎不存在,所以你的任务处理函数必须自己保证重复执行是安全的。
反例:扣款任务里 balance -= 100,重试一次就扣了 200。正例:给每个任务一个业务唯一键(订单号、userId:2026-10-08 这种),处理前先查「这个键处理过没有」,处理过直接跳过。数据库层面给唯一键加唯一索引,并发重复投递时靠数据库兜底——这是成本最低、最可靠的幂等方案。
三个实操规则:一,任务载荷只带 ID 不带对象。队列里放 { orderId: "123" },worker 执行时再查库拿最新状态。放完整对象会有「数据过期」问题:任务排了 10 分钟才执行,对象里的状态早变了。二,状态机显式化。订单 pending → processing → done/failed,worker 只处理处于预期状态的任务,重复投递看到 done 直接返回。三,外部调用做去重。调支付、发短信这种「重试会产生真实副作用」的操作,用服务商提供的幂等键(Stripe 的 Idempotency-Key 就是干这个的),别指望「应该不会重试两次」。
让 Agent 写 worker 代码时,把这三条写进需求里——它默认写出的 worker 几乎一定没有幂等保护,你不提它不加。
可靠性三件套:重试、死信、告警
任务进队列只是开始,可靠性靠三件套撑起来。
重试策略:指数退避 + 上限。第一次失败 1 分钟后重试,第二次 5 分钟,第三次 30 分钟……而不是每 10 秒无脑重试——无脑重试会在下游服务故障时变成 DDoS 帮凶。重试次数设上限(比如 5~10 次),超过上限就别再试了,承认失败比无限重试体面。
死信队列(DLQ):重试耗尽还没成功的任务,别丢,进死信队列躺着。DLQ 是你的「待人工处理」收件箱:每天看一眼,是数据问题就修数据重放,是 bug 就修代码重放。没有 DLQ 的队列,失败任务要么无限重试烧钱,要么静默丢失——两种都是生产事故。
告警:队列深度(积压多少任务)、失败率、worker 心跳,三个指标配告警。Inngest/Trigger.dev 自带 dashboard 和通知;自建方案就接 Sentry + 一个简单的 cron 检查脚本。vibe 项目的常见死法:worker 进程挂了三天没人知道,队列里躺了两万个任务——一个「worker 5 分钟没心跳就发短信」的告警能救命。
还有一个 Agent 时代特有的坑:任务超时设置。LLM 调用任务的超时要按「模型最慢响应 × 2」设,别用默认的 30 秒;批量任务按单条耗时 × 数量估算。超时设太短,任务没做完就被杀、然后重试、再被杀——无限循环烧你的 API 账单。
定时任务的三个隐形坑
cron 看似简单,坑全在细节里。
坑一:时区。cron 表达式里的 0 9 * * * 是 UTC 9 点还是北京时间 9 点?云平台的 cron 默认多半是 UTC,你的「每天早上 9 点发日报」会变成北京时间下午 5 点。写 cron 第一件事:确认时区,显式指定。
坑二:重叠执行。每小时跑一次的数据同步,某次因为数据量大跑了 70 分钟——下一个周期的任务又启动了,两个任务同时跑,互相覆盖、重复处理。用分布式锁(Redis SET NX、Postgres advisory lock)保证同一任务同时只有一个实例在跑;Inngest/Trigger.dev 有内置的并发控制,直接开。
坑三:静默失败。cron 任务失败了,没人看日志,三个月后才发现账单没算。给每个定时任务加「成功心跳」:跑完写一条记录(或调 Healthchecks.io 这类服务),超过预期时间没心跳就告警。反向思维:不告警的 cron 等于没跑。
实战:让 Agent 帮你接好队列的任务模板
直接复制给 Agent 用。把方括号里的内容换成你的项目信息:
给 [项目名] 接入后台任务队列,方案用 [Inngest / Trigger.dev / BullMQ+Upstash Redis]。需求:
1. 用户触发 [慢操作,如 AI 生成报告] 时,API 只创建任务记录(含 idempotencyKey=[业务唯一键规则])并返回任务 ID,不做实际处理;
2. worker 里按 idempotencyKey 做幂等检查:处理前查库,已处理直接返回;任务载荷只带 ID,执行时重新查库;
3. 重试策略:指数退避(1min/5min/30min),最多 8 次,耗尽后进死信队列并记 Sentry;
4. 状态机:[pending → processing → done/failed],非法状态转换直接抛错;
5. 前端用轮询(3 秒一次)查任务状态,done 后展示结果,failed 显示可重试按钮;
6. 超时设置为 [按最慢情况估算,如 10 分钟],不要用默认值。
约束:不要把业务逻辑写在 API 路由里;worker 函数必须可独立测试;所有第三方调用走环境变量。
这个模板的价值在于把本章所有的坑(幂等、重试、状态机、超时、载荷设计)一次性翻译成 Agent 能执行的指令。你会发现,加上模板前后,Agent 输出的 worker 代码质量是两个物种。
上线前检查清单
□ 所有超过 3 秒的操作都已异步化,API 只接单不干活
□ 队列方案已选定(Inngest/Trigger.dev/BullMQ/pg-boss 四选一),不是手写轮询
□ 每个任务有业务唯一键 + 数据库唯一索引,重复投递安全
□ 任务载荷只带 ID,worker 执行时重新查库
□ 重试策略是指数退避 + 次数上限,不是固定间隔无脑重试
□ 死信队列已配置,每天有人看一眼
□ 队列深度、失败率、worker 心跳三项告警已接
□ 定时任务显式指定时区,加了分布式锁和成功心跳
□ 超时按最慢情况估算,不是默认值
□ Webhook 接收 5 秒内返回,先入库再慢慢处理
最后说一句:后台任务是 vibe 项目从「玩具」到「产品」的分水岭之一。demo 可以全同步——反正只有你一个人点;但只要有 10 个真实用户,第一个超时 500 就会教你做人。与其事后救火,不如在 Agent 写下第一个 await sendEmail() 时,就问它一句:「这个操作超过 3 秒怎么办?」
相关文章

公网上的每个接口都会在某个深夜被超预期调用。这篇实战为一人团队搭建限流体系:算法选型(滑动窗口 vs 令牌桶)、四层防御、AI 接口烧钱专项防护、配额设计、429 响应规范、误伤排查,最后附上线检查清单。

每个 vibe 项目都会经历同一个黑色幽默时刻:网站白屏了,朋友比你的监控先告诉你。这篇实战为一人团队搭建完整错误监控体系:5 分钟 Sentry 最小闭环、错误边界、上报上下文设计、后端结构化日志、AI 调用专项防护、告警分级降噪,最后附上线检查清单。

每个 vibe 项目迟早需要定时任务:每日数据同步、过期订单清理、账单对账、定时报告。AI 给你的第一个版本通常是 setInterval——开发够用,生产必死。这篇实战给出四种跑法的选型地图(应用内/Vercel Cron/GitHub Actions/Cloudflare),cron 表达式速查与时区坑,幂等性、防重叠分布式锁、失败重试与告警、可观测性 run log,以及 cron 接口的鉴权,最后附上线清单。