跳到主要内容
← 返回文章列表
面试题-AI相关2026/08/23约 8 分钟读完

Node.js BFF 中 AI 请求的取消、超时与流式错误处理

围绕“Vue 前端 → Node.js BFF → 上游模型”的完整请求链路,分析停止生成为什么没有真正取消模型请求,以及流式响应开始后如何传递错误、管理任务状态和释放资源。

Node.js BFF 中 AI 请求的取消、超时与流式错误处理
文章目录 18

题目信息

  • 领域:Node.js、、AI 请求链路

  • 难度:L1 基础认知(初级)

  • 题型:场景题

  • 预计用时:10–15 分钟

  • 考察方向:理解浏览器、BFF 和上游模型之间的请求生命周期,以及用户取消、服务端超时和上游失败的职责边界。

面试题

一个 Vue 3 AI 对话页面通过以下链路获取流式回答:

Vue 前端
  -> Node.js BFF
  -> 上游模型 API

用户点击“停止生成”时,前端调用了 .abort()。页面不再显示新内容,但服务端日志显示:

  • BFF 仍在继续接收模型生成的数据。

  • 模型直到生成完成才停止计费。

  • 用户关闭页面后,BFF 偶尔还会尝试向已经关闭的连接写入数据。

请回答:

  1. 为什么前端调用 abort() 后,上游模型请求仍可能继续执行?

  2. 如何设计从前端到 BFF,再到模型 API 的完整取消链路?

  3. 以下三种情况应该如何区分和处理?

    • 用户主动停止生成

    • BFF 达到30秒超时

    • 上游模型返回错误

  4. 连接关闭后,BFF 还需要清理哪些资源或状态?

我的回答

  1. 前端执行 abort 后,只会停止前端操作,并未通知后端停止请求大模型继续输出。

  2. 前端执行 abort 后,将当前任务 ID 发送给后端;后端根据任务 ID 停止上游模型请求,然后将取消结果通知前端。

  3. 设置任务状态。用户主动停止时,将任务设置为 cancel 状态,并执行取消链路。BFF 超时时进行有限次数重试,仍然超时则向前端返回超时错误,前端停止任务并显示错误信息。上游模型返回错误时采用类似处理。

  4. 连接关闭后,BFF 根据任务 ID 清理与上游模型的请求链路,并将任务设置为已完成。

面试点评

  • 总分:52 / 100

  • 正确性:22 / 40

  • 原理理解:9 / 20

  • 工程应用:10 / 20

  • 边界与权衡:4 / 10

  • 面试表达:7 / 10

正确点

  • 识别到只停止前端 UI 不足以停止模型生成。

  • 知道取消需要贯穿前端、BFF 和上游模型。

  • 提出了任务 ID,可以用于定位服务端生成任务。

  • 能区分用户取消、超时和上游错误三类结果。

  • 知道连接关闭后需要清理上游请求。

遗漏点

  • BFF 应监听客户端连接断开,并将取消信号主动传给上游请求。

  • 任务 ID 取消接口适合跨请求任务,但不应成为唯一的取消机制。

  • 没有考虑流式响应已经开始后,HTTP 状态码通常不能再修改。

  • 没有完整说明需要清理的资源:

    • 超时计时器

    • 事件监听器

    • 上游流读取器

    • AbortController

    • 任务注册表

    • 尚未释放的流和连接

  • 向前端写入数据前,需要确认响应连接是否仍然可用。

错误点

1. abort() 并非只影响前端

前端对 fetch 调用 abort() 后,浏览器通常会中断浏览器到 BFF 的请求连接。

问题在于,BFF 发起的上游模型请求是另一个请求。除非 BFF监听客户端断开,并主动取消上游请求,否则模型生成仍可能继续。

2. 超时不能默认自动重试

模型生成通常不是普通的幂等查询。自动重试可能导致:

  • 重复生成

  • 重复计费

  • 前端收到重复内容

  • 多个生成任务同时执行

只有在尚未输出内容、错误可以恢复,并且有重试次数、总时间和成本限制时,才考虑有限重试。

3. 取消不能设置成“已完成”

任务至少应该区分:

completed   // 正常完成
cancelled   // 用户主动取消
timed_out   // BFF 超时
failed      // 上游或内部错误

这些状态对应不同的前端提示、日志和重试策略。

推荐答案与原理解析

1. 为什么前端停止后,上游模型仍可能继续生成

前端的 AbortController 控制的是浏览器发起的请求。

调用 abort() 后,浏览器通常会中断到 BFF 的连接,但 BFF 向模型服务发起的是另一个独立请求。浏览器不会自动获得或控制 BFF 中的上游 AbortController。

因此,完整的取消需要由 BFF 主动传播:

前端调用 abort
-> 浏览器中断与 BFF 的连接
-> BFF 检测客户端断开
-> BFF 调用服务端 AbortController
-> 通过 signal 取消上游模型请求
-> 停止读取流并释放资源

前提是模型 SDK 或上游接口支持取消。即使本地停止读取,上游服务是否立即停止生成和计费,仍取决于具体供应商的实现。

2. 任务 ID 取消接口的作用

除了监听连接断开,还可以使用生成任务 ID 提供独立取消接口:

POST /api/generations/:taskId/cancel

它适用于:

  • 后台运行的生成任务

  • 页面刷新或重新连接后的取消

  • 跨设备任务管理

  • 客户端连接已经更换的情况

连接断开传播可以作为实时请求的主要机制,任务 ID 取消接口作为补充。

3. 三类终止状态的处理

用户主动取消

  • 取消 BFF 的上游模型请求。

  • 停止读取和转发流。

  • 将任务设置为 cancelled。

  • 前端保留已经生成的部分内容。

  • 不显示系统错误提示。

BFF 超时

  • BFF 主动取消上游请求。

  • 将任务设置为 timed_out。

  • 响应尚未开始时,可以返回对应 HTTP 错误。

  • 流式响应已经开始时,通过流内错误事件通知前端。

  • 只有错误可恢复且尚未输出内容时,才考虑有限重试。

上游模型错误

  • 将任务设置为 failed。

  • 记录内部错误和上游请求信息。

  • 向前端返回经过脱敏的业务错误。

  • 根据错误类型决定是否允许用户重试。

  • 不应把上游密钥、内部堆栈等敏感信息返回前端。

4. 连接关闭后的资源清理

BFF 至少需要清理:

  • 上游请求和流读取器

  • 超时计时器

  • 客户端断开事件监听器

  • 服务端 AbortController

  • 任务注册表

  • 缓冲区和临时状态

  • 正在等待的异步处理

还需要避免继续向已经关闭的响应对象写入数据。

递进追问与补答

追问一:流式响应开始后发生上游错误

追问:

BFF 已经向前端发送了三个流式文本片段,此时上游模型突然返回错误。

请回答:

  1. BFF 还能把 HTTP 状态码改成 502 吗?

  2. 应该如何把错误通知前端?

  3. 这种情况下是否适合自动重试?为什么?

我的补答:

  1. BFF 不能修改状态码。流式响应启动后,HTTP 状态码无法改变。

  2. 通过状态将错误返回前端,前端中止请求并释放相关资源。

  3. 不能自动重试。此时已经发送了三个流式片段,如果重试可能会重新发送。只有在流式传输尚未开始时报错,才适合考虑重试。

补答评分: 76 / 100

补答点评:

核心判断正确:

  • 响应头或正文发送后,不能再将 HTTP 状态码改为 502。

  • 已经输出部分内容后,自动重试可能造成内容重复和重复计费。

  • 只有流尚未开始时,才可能考虑重试。

但“通过状态返回”不够具体。流式协议需要定义业务错误事件。

如果使用 SSE,可以发送:

event: error
data: {"code":"UPSTREAM_FAILED","message":"生成中断","retryable":false}

如果使用 NDJSON,可以发送:

{"type":"error","code":"UPSTREAM_FAILED","retryable":false}

前端收到后应该:

  • 停止继续读取流。

  • 将任务设置为 failed。

  • 保留已经收到的部分内容。

  • 展示“回答生成中断”。

  • 根据 retryable 决定是否展示重新生成入口。

  • 释放 reader、timer 和 controller。

如果连接已经彻底断开,BFF 无法通过原连接通知前端。这时需要持久化任务终态,前端重新连接后通过状态查询或恢复接口获取结果。

即使流尚未开始,也不是所有错误都适合重试,还需要考虑:

  • 错误是否临时可恢复

  • 是否会重复计费

  • 是否具有幂等机制

  • 是否超过次数和总超时预算

  • 遇到限流时是否需要等待

追问二:设计最小流式错误消息

追问:

为上述场景设计一个最小的流式错误消息,说明至少应该包含哪些字段,以及前端收到后如何把任务从 streaming 转换到最终状态。

我的补答:

至少包含:

  • status:状态

  • message:错误消息

  • taskId:任务或消息 ID

  • 幂等键

前端收到后通过 TextDecoder 解码,根据 SSE 的空行分隔符 \n\n 或 \r\n\r\n 提取完整事件。

补答评分: 60 / 100

补答点评:

正确方向包括:

  • 错误事件需要携带状态、消息和任务标识。

  • 使用 TextDecoder 解码流式字节。

  • SSE 通过空行区分完整事件。

  • 考虑了重试和幂等性。

但还存在以下问题:

  • taskId 是生成任务 ID,不是消息 ID。同一条消息可能对应多次生成任务。

  • 幂等键主要用于重试请求去重,不是每个错误事件都必须携带的字段。

  • 缺少 code 和 retryable 等前端决策字段。

  • 没有说明错误事件触发的任务状态迁移。

  • 没有处理网络 chunk 与 SSE 事件边界不一致的问题。

SSE 错误事件设计

一个最小错误事件可以设计为:

event: error
data: {"taskId":"gen_123","code":"UPSTREAM_FAILED","message":"模型生成中断","retryable":true}

建议字段:

type StreamError = {
  taskId: string;
  code: string;
  message: string;
  retryable: boolean;
};

如果已经通过 event: error 表示事件类型,JSON 中不一定还需要额外的 status。

SSE 增量解析

网络 chunk 不等于 SSE 事件,一次 reader.read() 可能得到:

  • 半个 SSE 事件

  • 一个完整事件

  • 多个 SSE 事件

  • 一个 UTF-8 字符的一部分

因此必须增量解码,并保留尚未形成完整事件的 buffer:

const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();

  if (done) {
    buffer += decoder.decode();
    break;
  }

  buffer += decoder.decode(value, { stream: true });

  const events = buffer.split(/\r\n\r\n|\n\n|\r\r/);
  buffer = events.pop() ?? "";

  for (const rawEvent of events) {
    handleSSEEvent(rawEvent);
  }
}

解析出错误事件后,需要先校验任务 ID:

function handleStreamError(error: StreamError) {
  if (error.taskId !== activeTask.id) {
    return; // 忽略其他任务或过期任务
  }

  activeTask.status = "failed";
  activeTask.error = error;

  // 保留已生成的部分内容
  // 清理 reader、timer 和 AbortController
  // 根据 retryable 决定是否显示重新生成入口
}

最终总结

这道题的核心是理解“取消”和“错误”必须贯穿完整链路:

前端停止显示
≠ BFF 已经停止处理
≠ 上游模型已经停止生成

需要建立以下完整流程:

客户端连接关闭
-> BFF 检测断开
-> 取消上游模型请求
-> 停止读取流
-> 写入准确任务终态
-> 清理计时器、监听器和任务资源

流式错误处理还需要区分:

响应开始前
-> 可以使用 HTTP 错误状态

响应开始后
-> 通过 SSE 或 NDJSON 错误事件

连接彻底断开
-> 持久化任务状态,等待查询或恢复

本轮回答已经掌握:

  • 流式响应开始后不能修改 HTTP 状态码。

  • 输出部分内容后不应直接自动重试。

  • SSE 需要通过空行识别完整事件。

  • 错误事件需要携带任务标识和错误信息。

仍需继续巩固:

  • 浏览器连接断开如何传播到 BFF 和上游请求。

  • 网络 chunk、UTF-8 字符与 SSE 事件边界的区别。

  • 消息 ID 与生成任务 ID 的区别。

  • 流式错误事件后的状态迁移和资源清理。

  • 模型生成重试的幂等性、成本和时间边界。

评论

0 条

评论会在审核后展示,邮箱不会公开。

正在确认登录状态

5000 / 5000