流式响应断了,前端怎么自动重连续传

线上排查了三天才搞定这个:用户网络一抖,AI 正在打字打到一半就卡住,前端转圈圈,再也不动了。SSE 这玩意儿默认是不会自己续上的。下面是我最后落地的那套自动重连 + 断点续传方案,纯前端能做的部分。

先认清 SSE 的脾气

浏览器原生 EventSource 其实自带重连------断了会按 retry 间隔重发请求。但问题有俩:第一,它只支持 GET,AI 对话要 POST 一大坨 messages,用不了;第二,就算用了,它重连是从头开始,不知道你已经收到哪儿了,模型会把答案从头再吐一遍。

所以大部分 AI 应用是用 fetch + ReadableStream 自己手撸的。手撸就得自己管重连。

核心思路:记住已收到的位置

每一帧带一个递增的序号,前端记下最后收到的序号。断线重连时把这个序号回传给后端,后端从那之后接着发。

后端每帧大概长这样:

css 复制代码
data: {"seq": 42, "delta": "我们"}

data: {"seq": 43, "delta": "可以"}

前端收流的主循环:

ini 复制代码
async function streamOnce(url, body, lastSeq, onDelta, signal) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...body, resumeFrom: lastSeq }),
    signal,
  });
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buf = '';
  while (true) {
    const { value, done } = await reader.read();
    if (done) return { finished: true };
    buf += decoder.decode(value, { stream: true });
    let idx;
    while ((idx = buf.indexOf('\n\n')) !== -1) {
      const chunk = buf.slice(0, idx).trim();
      buf = buf.slice(idx + 2);
      if (!chunk.startsWith('data:')) continue;
      const payload = JSON.parse(chunk.slice(5).trim());
      onDelta(payload);   // 这里更新 lastSeq
    }
  }
}

注意 decode(value, { stream: true })------中文是多字节的,一帧可能切在一个汉字中间,不带 stream: true 会出现乱码方块。我就是被这个坑了半天,以为是后端编码问题。

外层包一个带退避的重连循环

javascript 复制代码
async function chatWithRetry(url, body, onDelta) {
  let lastSeq = -1;
  let attempt = 0;
  const ac = new AbortController();

  while (attempt < 6) {
    try {
      const r = await streamOnce(url, body, lastSeq, (p) => {
        if (p.seq <= lastSeq) return;   // 去重:重连后可能重发
        lastSeq = p.seq;
        onDelta(p.delta);
      }, ac.signal);
      if (r.finished) return;           // 正常收完,退出
    } catch (e) {
      if (ac.signal.aborted) return;    // 用户主动取消
      attempt++;
      const wait = Math.min(1000 * 2 ** attempt, 15000);
      await new Promise(r => setTimeout(r, wait + Math.random() * 300));
    }
  }
  throw new Error('重连多次仍失败');
}

几个我踩过坑后加上的细节:

  • 去重那行 if (p.seq <= lastSeq) return 不能省。后端续传的边界经常会多发一两帧重叠的,不去重屏幕上就重复一段。
  • 退避加随机抖动,不然一批用户同时断网会在同一秒一起重连,把后端打挂。
  • AbortController 一定要透传,用户切走页面或点停止,得能真正掐断 fetch,否则后台还在偷偷收流烧 token。

一个没法纯前端解决的坑

续传能不能真的「接着说」,取决于后端肯不肯把生成到一半的结果缓存住、并支持 resumeFrom。如果后端是无状态转发,断了就是断了,前端再聪明也只能整段重发。我们后端为此专门加了一层短时缓存,按会话 ID 存最近 N 秒的 delta。

说实话这块后端逻辑挺烦的,我们后来没自己扛。直接用讯飞 Agent发布出来的对话 API------它是 MaaS,模型和流式生成都在它那边,我们没自建算力,断线续传这种状态管理交给它的服务端,前端只管认 seq、重连、去重,省心不少。

相关推荐
前端snow4 小时前
ai agent --- 实现 openclaw 定时间效果
前端
码云之上4 小时前
换模型之后,Chatbot 为什么要自己做 compact?
前端·agent·前端工程化
星栈4 小时前
自定义 UI-UX-Pro-Max 的 CSV 知识库,把 AI 生成的后台拉回行业该有的样子
前端·agent·weui
研☆香4 小时前
简单的图片上传 删除 预览
前端
ITresearchGuest4 小时前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·人工智能·信息可视化
慧一居士6 小时前
Vite项目中使用Less步骤,详细使用示例
前端·css
花间相见6 小时前
【LangChain组件03】—— LangChain Agents 执行与状态:工作流程与状态管理实战
java·前端·langchain
kyriewen6 小时前
面试官让我用 AI 重构一个 8 年陈的 React 组件——他说他不看代码,只看我会不会拆
前端·javascript·面试
计算机魔术师6 小时前
700吉瓦「幽灵用电」:美国AI数据中心的抢电闹剧正在失控
前端
IT_陈寒6 小时前
JavaScript数组排序踩的坑,差点让我加班到凌晨
前端·人工智能·后端