phase 5 part 3: mongodb, elasticsearch hybrid search, data-pipeline, nodejs BFF topics with verified examples
This commit is contained in:
1 parent
9eeb10aede
commit
075872e31e
27 files changed
+2405
-52
No files matched your search
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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 保持薄,只做协议适配和横切逻辑。”
|
||||
Reference in new issue
Block a user