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

线上排查了三天才搞定这个:用户网络一抖,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、重连、去重,省心不少。

相关推荐
anyup1 小时前
来简单聊聊鸿蒙开发,万元奖金的事~
前端·华为·harmonyos
北凉温华1 小时前
Univer 在线表格模块使用说明
前端
lichenyang4531 小时前
WebRuntimePage 拆分:从大页面到运行时控制器
前端
竹林8182 小时前
从报错到跑通:我用 @solana/web3.js 开发 Solana 钱包连接踩过的三个坑
前端
MariaH2 小时前
Node中操作MySQL
前端
还有多久拿退休金2 小时前
一个 var 让整个团队加班到凌晨——JS 闭包的那些暗坑
前端·javascript
weedsfly2 小时前
用了 React/Vue 之后,这些 DOM 操作的坑你踩过几个?
前端·javascript
Asize2 小时前
Ajax 入门:从 JSON 序列化到 XMLHttpRequest
前端·javascript·前端框架
林希_Rachel_傻希希2 小时前
react hooks速通笔记
前端