長時間工作的進度與 AI 回答串流
匯出、匯入、轉檔與 AI 工作:跟著實際工作前進的進度條、一完成就出現的結果,以及邊寫邊出現的回答。
匯出、影片轉檔、產生報表、匯入資料和 AI 工作,可能要十秒到十分鐘。一個沒有任何回饋的轉圈圈,會讓人重新整理、再按一次,或乾脆放棄。顯示真實的進度,並在結果準備好的那一刻呈現,就能把人留在頁面上。AI 功能也一樣適用:即使工作在背景執行,也能把助理的回答邊寫邊串流出來。
使用者得到什麼
- 跟著實際工作前進的進度條:「正在處理 10,000 列中的第 3,200 列」。
- 工作一完成,下載連結或結果就出現,即使在另一個分頁。
- AI 的回答一個字一個字出現,不必等很久才一次出現。
- 可以放心離開頁面:完成時會通知他們。
用到的功能
- 使用者驗證與
sendToUser,工作的更新只送給啟動它的人,而且送到每個分頁。 - 私人頻道(
private-job-<id>),用在多人一起追蹤同一個工作時。 - 任何語言的 worker:Node.js、PHP、Python、Go、Ruby、Java 和 .NET 的官方 Pusher 伺服器函式庫都能從佇列 worker 發布。
- 推播通知,在使用者關掉 App 後告訴他們長時間的工作已完成。
步驟 1:啟動工作,取得 ID
app.post('/exports', requireLogin, async (req, res) => {
const job = await queue.add('export', { userId: req.user.id, filters: req.body });
res.status(202).json({ jobId: job.id });
});步驟 2:從 worker 回報進度
在有意義的階段回報進度,不要每一列都回報:
worker.process('export', async (job) => {
const user = String(job.data.userId);
const rows = await db.orders.query(job.data.filters);
for (let i = 0; i < rows.length; i += 1000) {
await writeChunk(rows.slice(i, i + 1000));
await pusher.sendToUser(user, 'job-progress', { jobId: job.id, done: Math.min(i + 1000, rows.length), total: rows.length });
}
const url = await uploadAndSign();
await pusher.sendToUser(user, 'job-done', { jobId: job.id, url });
});用 pusher 套件的 Python worker 寫法相同:
pusher_client.send_to_user(str(user_id), "job-progress", {"jobId": job_id, "done": done, "total": total})步驟 3:在瀏覽器裡顯示
使用者在連線上登入後(請看即時站內通知):
const { jobId } = await fetch('/exports', { method: 'POST', body: form }).then((r) => r.json());
pusher.user.bind('job-progress', (p) => {
if (p.jobId === jobId) progress.set(p.done / p.total);
});
pusher.user.bind('job-done', (p) => {
if (p.jobId === jobId) showDownload(p.url);
});串流 AI 回答
當 AI 模型在背景工作裡撰寫回答時,把輸出分成小批轉給使用者,這樣一個很長的回答是幾十則訊息,而不是幾千則:
let buffer = '';
let last = Date.now();
for await (const chunk of llm.stream(prompt)) {
buffer += chunk.text;
if (Date.now() - last > 150 || buffer.length > 2000) {
await pusher.sendToUser(user, 'ai-delta', { id: answerId, text: buffer });
buffer = '';
last = Date.now();
}
}
await pusher.sendToUser(user, 'ai-done', { id: answerId, text: buffer });pusher.user.bind('ai-delta', ({ id, text }) => answers.get(id).append(text));
pusher.user.bind('ai-done', ({ id, text }) => answers.get(id).append(text).finish());也把完整的回答存在你的伺服器上,重新整理後才還在。
小提醒
- 每次更新都是一則訊息。每 1–5% 或每 150 毫秒更新一次就很順,一個工作也只用幾十則訊息。
- 在啟動工作之前先訂閱,或在頁面打開時查詢一次工作狀態,就不會漏掉在頁面連線之前送出的更新。
- 如果使用者已經關掉分頁,工作完成時照推播通知指南發送推播。