接需求那周我以为最难的是调模型,结果模型那头稳得很,反倒是前端这条 SSE 流式通道,三天里给我整出了四种姿势的"挂"。写出来给同样要把 AI 对话接进现有项目的同行避避坑。
先把结论摆前面,方便你对号入座:SSE 接 AI 流式,前端要自己扛的三件事——token 过期了怎么不中断地续上、被限流(429)了怎么退而不是死、用户切走又切回来连接怎么收尾。 这三件后端基本不管,全是前端的活。
第一坑:token 过期,流到一半断给你看
我们鉴权是短时 JWT,15 分钟过期。本地联调根本测不出来——谁联调会盯着一个回答看 15 分钟。上线第二天客服转来一条:"AI 说着说着就不说了。"
复现出来才明白:EventSource 这玩意儿不能带自定义请求头,token 只能塞 URL 上。连接建立那一刻 token 还有效,可流式回答拖长了,过期就触发服务端断流,前端只收到一个 onerror,连个像样的提示都没有。
我后来干脆不用原生 EventSource 了,换 fetch + ReadableStream 自己读:
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ messages }),
signal: ctrl.signal,
});
const reader = resp.body.getReader();
// 自己按 \n\n 切 SSE 帧、解析 data:
带 header 的问题解决了。token 过期我再加一层:请求前先看 exp,快过期(剩不到 60s)就先静默刷一次再发起流。半成品回答也不丢——刷完 token 用已有上下文续一个请求接着吐。
第二坑:429 不是错误,是"等一下"
压测一上来就给我 429。一开始我把它和 500 一样处理,弹个"服务异常",难看。
其实限流是正常的反压信号。我改成读 Retry-After 头做退避重连,指数加抖动,最多三次;超了才降级成"当前访问的人有点多,已为你排队"。这条提示文案上线后,相关工单直接没了——用户要的不是不限流,是知道"在排队,不是坏了"。
第三坑:用户切走,连接还在烧
移动端 H5 尤其明显。用户切到后台,SSE 连接不会自己断,服务端还在吐字、还在计费。我加了 visibilitychange 监听,切后台超过 30 秒就 abort,回来再续。这一刀下去,无效请求量掉了大概两成。
一个我到现在没解决干净的小毛病
弱网下 fetch 流偶尔会卡住既不报错也不继续,浏览器底层超时摸不准。我现在靠一个"多少秒没收到新 chunk 就主动 abort 重连"的看门狗兜着,能用,但谈不上优雅。有更稳的法子求评论区赐教。
说回模型本身——我没自建任何推理服务,模型和 API 直接走的讯飞 MaaS 那套现成接口,前端只管把这条流式通道伺候明白,模型那头按 token 调用就行,省了一整套部署的心。
前端接 AI,难的从来不在"调通",在"调通之后那些边界状态"。你们接流式都踩过哪些奇葩的断连?评论区交换下战损报告 👇