切后台回来断线续接:静默超 20 秒就立即探活
做 AI 对话小程序时,有个场景几乎每天都会遇到:AI 正在流式输出,用户切出去回个微信、或者点开图片预览一下,回来发现输出停了,页面还显示“生成中”,要等很久才报错,或者干脆卡住不动。这篇文章记录一下我们对话页是怎么处理“切后台回来断线续接”的:核心就三招——切走时记时间、回来先探活、续上后接着拼字。
问题出在哪:连接“半开”了
先说一个容易误解的点:手机切后台,网络连接不一定立刻断。很多时候 TCP 连接表面上还在,但中间某个环节已经把它丢了,服务器发来的数据到不了手机,手机也不知道对方没了——这就叫“半开连接”。页面还以为自己连着,于是一直等,等不到数据也不报错,用户看到的就是输出停在半截。
所以不能只靠“等报错”,得主动判断:我有多久没收到数据了?
我们的服务端大约每 16 秒发一次心跳——可能是一个空事件或注释行,作用只有一个:证明“我还活着”。客户端每次收到任何数据就重置计时器。心跳正常,说明连接是活的;心跳停了,说明连接可能已经半开了。
第一招:onHide 记时间,onShow 回来探活
小程序页面有两个生命周期钩子,正好对应“切走”和“回来”:
onHide:切到后台时,记下当前时间;onShow:切回前台时,如果_stream还在(说明上一轮生成还没结束),就调checkAlive(20000)主动探活,问一句“你还活着吗”。
判断标准是静默是否超过 20 秒:服务端心跳约 16 秒一次,20 秒没收到任何数据,基本可以判定连接已经半开,不用再等,直接续接。20 秒这个数是留了余量的——比一次心跳间隔稍长,避免心跳稍微抖动一下就误判。
这里有个细节值得一提:预览图片、选素材也会触发 onHide / onShow。但那时心跳是正常的,回来一探活发现连接没事,checkAlive 不会打断连接,该干嘛干嘛。这个判断是按“静默时长”做的,不是按“切过后台”做的,所以短暂切出去不会误伤正在进行的生成。
第二招:看门狗兜底,断网时给用户一个说法
探活解决的是“切后台回来”这个场景,断网本身还有一层兜底:客户端有个看门狗定时器,一直在轮询“距离上次收到数据过去多久了”。超过阈值(比如 30 秒),就判定连接已死,走重连流程。
重连时页面会显示状态条“网络波动,正在重连”,后面还带一个 attempt 计数,告诉用户这是在第几次尝试。收到正常事件的第一时间就把这个状态清掉——onAgentEvent 的第一行就是清 reconnecting,靠的还是“收到任何数据就说明活着”这个判断。
这两层是互补的:checkAlive 是回来时主动问一句,不等看门狗慢慢发现;看门狗是全程盯着,兜住那些没有切后台、纯粹是网络抖动的情况。
第三招:续上后接着拼,不丢字
续接本身不难,难的是“续上之后不丢字、不重复”。
我们的 AI 输出是追加式的:服务端不攒全文,而是一点一点推送事件,前端每收到一小段 delta 就往 AI 气泡里追加显示,这就是看到的“打字机效果”。中途断网时,已经显示的字保留在消息文本里;重连后服务端从断点继续发 delta,前端继续往后追加。
具体到实现,收到的 delta 先攒进 _pendingText,再由 flushDeltas 拼到消息的 text 后面。追加式设计的好处是:续接不需要“把整段重发一遍再比对”,只要从断点继续往后拼,前面已经显示的内容一动不动,既不会丢字,也不会出现重复段落。
打开历史会话时也是同一套思路:如果 runs 里有 status === 'RUNNING' 的轮次,直接调 resumeAgentRun(runId, 0, handlers) 续上这个 run 继续收流,用的还是同一组 handlers——收事件、分发、收尾、报错、重连提示,一个都不用换。
小结
切后台断线续接,表面上是个“重连”功能,实际上是三个判断拼起来的:
- 什么时候探活:切回来时看静默时长,超 20 秒(一次心跳约 16 秒)就立即探活,不等看门狗;
- 怎么让用户知道:重连时显示“网络波动,正在重连”加 attempt 计数,收到数据立刻清除;
- 续上后怎么办:delta 追加式拼字,断点续发,不丢字也不重复。
三个数字可以当作参考:心跳约 16 秒、探活阈值 20 秒、看门狗阈值约 30 秒。它们分工不同:心跳是“活着的证明”,20 秒是“回来时立刻判断的线”,30 秒是“全程兜底的线”。做这类功能,与其纠结重连写得多优雅,不如先把“怎么知道它死了”想清楚——判断准了,重连只是顺手的事。
- 点赞
- 收藏
- 关注作者
评论(0)