A12第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

中断、重试与恢复

中断、重试与恢复 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

实现「真正」的停止——从按钮一路断到上游模型连接;把失败分类成可解释的重试;处理断网、上游超时与半截消息的保存。

问题场景

你给聊天加了「停止」按钮:点击后界面停了,但服务端日志里模型还在继续生成、token 照扣;你又加了「重试」,结果同一条用户消息在会话里出现两次。停止没停到根上,重试重到了库里。

核心解释

停止是一条 abort 链,断在中间等于没断:

// 前端:一层 AbortController 传导到服务端
const controller = new AbortController();
fetch("/api/ai/chat", { method: "POST", body, signal: controller.signal }).catch(
  (e) => {
    if (e.name === "AbortError") markCancelled(); // 主动停止不是失败
  }
);
// 服务端:把客户端断开传导给上游(本站 app/api/ai/chat/route.ts 的真实写法)
const upstreamController = new AbortController();
req.signal.addEventListener("abort", () => upstreamController.abort());
await streamChatCompletion({ messages, signal: upstreamController.signal }, cb);
// streamChatCompletion 把 signal 传给上游 fetch(lib/ai/adapter.ts):
// 客户端一断,模型连接即断,生成停止、不再计费

失败要分类,重试才有解释。本站 AiError 的分类与处置:

类别 症状 给用户的解释与动作
unreachable 连不上上游 「网络或服务不可达」+ 重试按钮
rate_limit 上游 429 「请求太频繁,稍等再试」+ 重试按钮
quota 上游 402 「额度不足」,不重试,去检查配置
not_configured 401/403 或缺配置 「检查 AI 配置」,跳设置
cancelled 用户主动停 静默,保留半截内容
function describeError(category: string): { text: string; canRetry: boolean } {
  const map: Record<string, { text: string; canRetry: boolean }> = {
    unreachable: { text: "网络或服务暂时不可达,可重试", canRetry: true },
    rate_limit: { text: "请求太频繁,稍等再试", canRetry: true },
    quota: { text: "额度不足,请检查 AI 配置", canRetry: false },
    not_configured: { text: "AI 服务未配置,请先填好密钥", canRetry: false },
    cancelled: { text: "已停止", canRetry: false },
  };
  return map[category] ?? { text: "出了点问题,可重试", canRetry: true };
}

部分消息保存:已收到的半截文本不是垃圾。本站的处理是——正文非空就落库,status 标 cancelled 或 error;只有一字未出才删除占位。重试必须幂等:retry 请求不带 message,服务端不重复写用户消息,只清掉最后一条未完成的 assistant 再重新生成。

示例拆解

重试的服务端分支(本站 route.ts 的简化):

if (message && !retry) {
  insertUserMessage(convId, message);   // 正常发送才写用户消息
} else if (retry && !message) {
  deleteUnfinishedAssistant(convId);    // 只清「最后一条用户消息之后」的残留回复
}
// 之后重新走一次生成流程,占位消息重新建立

关键点:

  • 清残留要按「最后一条用户消息之后」定位,不能一刀切删整个会话的 assistant 消息。
  • 重试按钮只在 canRetry 的错误态出现;cancelled 不给「重试」(那是重新生成,语义不同)。
  • 超时是断网之外的另一种「不可达」:给上游调用加 AbortSignal.timeout(本站连接测试用 20 秒),TimeoutError 归类到 unreachable。
  • 判断主动停止用 e.name === "AbortError",它和 TimeoutError 都继承自 abort 机制,但语义完全不同,先分流再进错误分类。

动手任务

  1. 实现停止按钮:点停止后在 Network 面板确认请求变 canceled,服务端控制台确认生成循环立刻退出。
  2. DevTools 切 Offline 复现断网:确认错误分类是 unreachable、界面给出重试按钮;恢复网络后点重试,确认成功且用户消息没有写两条。
  3. 在流式输出一半时点停止,刷新页面,观察半截消息的显示与状态(它的落库收尾在 A13 课完成)。

常见错误

  • 只 abort 前端 fetch:模型继续生成继续扣费,「停止」只是把眼睛蒙上。必须传导到上游 fetch 的 signal。
  • 把 AbortError 一律当失败弹红:用户主动停止报错误,吓人且没意义。
  • 重试把用户消息再写一遍:聊天记录出现重复问题,发给模型的历史也重复。
  • 无差别重试:quota、not_configured 也挂重试按钮,点了只会再失败一次,用户更困惑。

检查与复盘

自查清单:

  • 能画出 abort 链:按钮 → fetch → req.signal → 上游 fetch
  • 每类错误都有对应文案与动作,cancelled 静默且保留半截内容
  • 重试幂等:同一问题在库里只有一条

参考资料

检查题共 3 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1故障定位较难

bug 现场:聊天页加了「停止」按钮,点击后前端 fetch 抛出 AbortError,界面正常停止。但服务端日志显示模型仍在继续生成,token 照常计费,几十秒后上游才自然结束。指出根因与修法。

// 前端
const controller = new AbortController();
fetch("/api/ai/chat", { method: "POST", body, signal: controller.signal });

// 服务端 route.ts
const upstream = await fetch(`${baseUrl}/chat/completions`, {
  method: "POST",
  body: JSON.stringify({ model, messages, stream: true }), // 没有 signal
});
Q2多选中等(多选,全部选对才算正确)

本站把上游失败分类成 AiError 类别,界面按类别给出文案与动作。下列分类与处置正确的有?(多选)

Q3故障定位中等

bug 现场:断网导致生成失败,用户点「重试」成功。但会话里同一条用户消息出现了两条——发给模型的历史也随之重复。重试当前的实现如下。指出问题与修法。

// 前端重试:原样重发
fetch("/api/ai/chat", {
  method: "POST",
  body: JSON.stringify({ message: lastUserMessage }),
});

// 服务端
if (message) insertUserMessage(convId, message);

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...