--- id: backend.nodejs.scenario.ai-bff title: 用 Node.js 给 AI 应用做 BFF:鉴权、限流、流式转发、超时和断开处理 type: scenario domain: backend stack: [nodejs, redis, nginx, fastapi] tags: [BFF, SSE, 流式转发, 限流, 超时, request id] level: intermediate prerequisites: [backend.nodejs.express-nest] est_hours: 2 related: [backend.nodejs.interview, frontend.ssr.scenario.help-center] week: [14] --- # 用 Node.js 给 AI 应用做 BFF:鉴权、限流、流式转发、超时和断开处理 ## 业务背景与问题 前端团队维护的 AI 助手页面直接调用 Python 的 Agent 服务,问题: 1. Agent 服务要校验各业务线不同的登录态,越来越臃肿; 2. 页面需要的数据来自 3 个服务,前端串行请求,首屏慢; 3. 用户关掉页面后,Agent 仍在生成,浪费模型费用; 4. 出问题时无法串起前端、BFF、Agent 三方日志。 ## 约束 - 前端团队熟悉 TypeScript,希望自己维护这一层。 - Agent 服务由 AI 团队维护,接口为 SSE。 ## 方案设计 1. **BFF(Express 5 或 NestJS)**:统一鉴权(会话 / JWT)、按用户限流(Redis)、生成并透传 `x-request-id`。 2. **聚合接口**:首屏所需的用户信息、会话列表、配额用 `Promise.allSettled` 并行请求,各自设超时,部分失败时降级返回。 3. **流式转发**:`fetch` 上游时传入 `AbortSignal`,`res.on("close")` 时中止;首字节超时和总超时分别控制;上游出错时发送 SSE `error` 事件而不是挂起。 4. **部署**:多实例无状态,Nginx 关闭对 SSE 路由的缓冲,设置较长的读超时。 ## 关键代码 ```ts app.post("/chat", async (req, res) => { const user = await auth(req); // 失败时 throw HttpError(401) await rateLimit(`chat:${user.id}`); // 超限 throw HttpError(429) const controller = new AbortController(); res.on("close", () => controller.abort()); const timeout = setTimeout(() => controller.abort(), 120_000); // 总超时 const upstream = await fetch(`${AGENT_URL}/chat`, { method: "POST", headers: { "content-type": "application/json", "x-request-id": res.locals.requestId, "x-user-id": user.id }, body: JSON.stringify({ question: req.body.question }), signal: controller.signal, }); res.set({ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" }); try { for await (const chunk of upstream.body!) res.write(chunk); // 原样透传上游的 SSE 字节 } catch { if (!controller.signal.aborted) res.write('event: error\ndata: {"message":"生成中断"}\n\n'); } finally { clearTimeout(timeout); res.end(); } }); ``` 可运行的基础版本(含测试):[examples/express-bff](../examples/express-bff/)。 ## 取舍 | 选择 | 优点 | 缺点 | | --- | --- | --- | | **Node BFF(本方案)** | 前端团队自治、类型共享、擅长 I/O | 多一层服务和一跳网络 | | Next.js 路由处理器做 BFF | 和页面同一个项目 | 受部署形态限制(Serverless 超时) | | 由 Agent 服务直接对外 | 链路短 | 鉴权、聚合逻辑侵入 AI 服务 | ## 易错点 - 把上游的流读完再返回(`await upstream.text()`),失去流式效果。 - 没有向上游传播取消。 - `Promise.all` 中一个接口失败导致整页失败:首屏聚合用 `allSettled` + 降级。 - 在 BFF 里堆业务逻辑,最后变成“第二个后端”。 ## 面试怎么讲 > “AI 助手页面我们加了一层 Node BFF:统一鉴权和按用户限流,生成 request id 透传到 Agent 服务;首屏数据并行聚合、部分失败降级;对话接口把 Agent 的 SSE 原样透传,用户断开时通过 AbortController 中止上游,节省模型费用;上游失败时发 error 事件。BFF 保持薄,只做协议适配和横切逻辑。”