网易首页 > 网易号 > 正文 申请入驻

60%用户流失只因8秒无响应:AI集成如何用流式输出起死回生

0
分享至

我差点把一个糟糕的 AI 集成发布上线。功能本身在技术上是通的:模型能正确回答问题,界面干净利落,内部演示时所有人都点头认可。可一旦换成真实用户,48 小时内,产品经理就在 Slack 上问我:为什么 60% 的会话以“没有收到任何响应”结束? 问题不在 API,也不在服务器。问题只是八秒钟的安静。 用户发出消息后,页面没有任何动静,用户以为请求失败了,于是关掉标签页。随后响应确实到了,但已经没人看。我们构建的是一个功能没有问题、体验却完全坏掉的 AI 产品。而修复方案并不是重构架构,也不是做性能优化,而是“流式输出”:模型一产生 token 就立刻发给浏览器,而不是等完整响应生成后一次性批量返回。 这个错误,我不止在一家公司的代码里见到过。非流式版本写起来更简单,测试也能通过,开发阶段没人发现问题,因为开发者在等待时耐心很好,而真实用户没有这种耐心。 这篇教程会带你完整实现流式版本:用 Node.js 后端配合 Server-Sent Events(SSE),把 token 一边生成一边推送到浏览器;加入会话管理,让模型在多轮对话中拥有记忆;并正确处理中止场景,避免为没人读取的 token 付费。 完整代码在:github.com/ziaongit/nodejs-openai-streaming。 没有 OpenAI key?我用 Groq 跑完了整篇文章的测试。Groq 免费,兼容 OpenAI SDK 的调用格式,申请 key 大约两分钟。仓库里已经预置了 `USE_GROQ=true` 开关。把 `.env.example` 复制为 `.env`,填上你的 Groq key,就能直接跑起来。我使用的是 `llama-3.3-70b-versatile` 模型,输出质量足够好,开发调试时几乎察觉不到差别。 ## 前提条件 - Node.js v18.11 或更高版本 - 一个 OpenAI API key,或者 Groq API key - 对 Express 和 async/await 足够熟悉 ## 先看架构,再写代码 不要急着写代码。先理解一次流式请求的完整路径。 当用户在浏览器里发出一条消息,前端需要先建立一个 SSE 连接。SSE 是单向事件流,特别适合 AI 生成场景:服务器不断把数据推给浏览器,浏览器只需要监听事件并更新界面。 这里不需要 WebSocket。AI 回复本质上是“服务器到浏览器”的持续输出,而用户在生成过程中很少需要向服务器额外发送数据。使用 Express 处理一条 SSE 路由,就能完成整套交互。 在后端,Node.js 收到用户消息后,把它追加到当前会话的历史记录里,然后带着完整的历史消息调用 OpenAI 或 Groq 的 chat completions 接口,并打开 `stream: true`。模型返回的不是一个完整 JSON,而是一串流式的 chunk。每个 chunk 包含一小段增量文本。服务器拿到这段文本,立刻通过 SSE 写入响应流,浏览器就能把文字逐个显示出来。 最后,当流结束时,服务器关闭 SSE 连接。如果用户中途关闭页面,Node.js 会触发连接关闭事件,这时必须主动终止上游 API 调用,避免继续计费。 ## 为什么非流式实现会毁掉体验 非流式实现看起来没什么问题。你调用 API,等待完整结果,然后一次性返回给前端。代码更少,测试更稳定,延迟虽然高,但 demo 里没人介意。 真实用户不是这样。他们看到的不是“正在生成中”,而是“页面死了”。人类对等待的感知是非线性的,超过两三秒没有反馈,就会开始怀疑。八秒没有任何视觉变化,足够让用户关闭页面重新开始,甚至彻底放弃产品。 AI 生成一个完整回答可能耗时数秒到十几秒。如果把这个时间全部压在用户面前,不管后端多么准确,产品都会被感知为不可用。流式输出的价值,不是降低总延迟,而是把一个大延迟拆成无数个小反馈。第一个 token 很快出现,后面每个字都持续证明系统还活着。产品从“卡死”变成“正在写”,这是体验层面根本性的差异。 ## 关键实现:SSE 路由 首先初始化 Express,创建一条 GET 路由,路径可以是 `/api/chat/stream`。 ``` app.get('/api/chat/stream', async (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); res.flushHeaders(); }); ``` 注意:SSE 响应不能设置 `Content-Length`,因为响应长度未知,它会持续到流结束。 接下来,把用户消息和历史会话一起发给模型。 ``` const messages = buildMessages(sessionId, userMessage); const stream = await openai.chat.completions.create({ model: 'llama-3.3-70b-versatile', messages, stream: true, }); ``` 当 `stream` 为 `true` 时,返回的不是普通响应,而是一个可异步迭代的流。我们可以遍历每个 chunk,把增量文本提取出来,立即写入 SSE 响应。 ``` for await (const chunk of stream) { const text = chunk.choices[0]?.delta?.content || ''; if (text) { res.write(`data: ${JSON.stringify({ text })}\n\n`); } } res.end(); ``` 浏览器端收到的是标准 SSE 格式:以 `data:` 开头,后面跟 JSON,两个换行符结束一条事件。前端可以使用 `EventSource` 或者 `fetch` 配合 `ReadableStream` 来读取。 ## 会话管理:让模型记住上下文 模型本身是无状态的。每一轮请求都只看到你传进去的 messages。要想让多轮对话连贯,就必须把历史消息保存起来。 最简单的做法是按会话 ID 在内存中存一个消息数组。 ``` const sessions = new Map(); function getSession(sessionId) { if (!sessions.has(sessionId)) { sessions.set(sessionId, []); } return sessions.get(sessionId); } ``` 用户每发一条消息,就把用户输入推入会话历史,模型完成输出后,再把助理回复也推入历史。下次请求时,把这个历史数组完整传给模型。 ``` const history = getSession(sessionId); history.push({ role: 'user', content: userMessage }); // 调用模型后 history.push({ role: 'assistant', content: fullReply }); ``` 生产环境中,可以用 Redis 或数据库替代 Map,但核心逻辑不变:AI 的“记忆”就是请求里携带的上下文。 ## 中止处理:不要为没人读的内容买单 这是最容易被忽略的问题。用户如果中途关闭页面,后端仍然会继续等待模型生成完整结果,并继续消耗 token。这等同于一边开着水龙头,一边没人接水。 Node.js 中可以通过 `req.on('close')` 监听客户端断开事件。一旦断开,立即用 `AbortController` 中止上游请求。 ``` const controller = new AbortController(); req.on('close', () =>


特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
北京男给侄子转5万忘挂电话,听到侄子骂他,正要发火,听到秘密

北京男给侄子转5万忘挂电话,听到侄子骂他,正要发火,听到秘密

阿天爱旅行
2026-08-17 16:23:20
笑不活!中国人取外号真是没轻没重,特别是第6个

笑不活!中国人取外号真是没轻没重,特别是第6个

夜深爱杂谈
2026-08-17 21:19:30
连休3天,不调休!最新放假通知→

连休3天,不调休!最新放假通知→

山西晚报
2026-08-17 11:43:09
洛卡特利:拒绝阿森纳完全是出于内心的选择,只想去尤文

洛卡特利:拒绝阿森纳完全是出于内心的选择,只想去尤文

懂球帝
2026-08-17 21:25:11
主力资金 | 6股尾盘获大幅抢筹

主力资金 | 6股尾盘获大幅抢筹

数据宝
2026-08-17 20:37:35
中国男篮又输了!篮板落后16个,郭士强集训效果差,进世界杯没戏

中国男篮又输了!篮板落后16个,郭士强集训效果差,进世界杯没戏

侃球熊弟
2026-08-17 21:18:51
立秋后,这3种肉再贵也要吃,比猪肉养人、比鸭肉温润,每天换着吃,浑身有劲精神好!

立秋后,这3种肉再贵也要吃,比猪肉养人、比鸭肉温润,每天换着吃,浑身有劲精神好!

秀厨娘
2026-08-16 21:53:18
印度记者吐槽国羽:陈雨菲满嘴套话,石宇奇宁可被罚款也不配合采访

印度记者吐槽国羽:陈雨菲满嘴套话,石宇奇宁可被罚款也不配合采访

杨华评论
2026-08-17 02:41:24
【刘海粟】“艺术流氓”“文化汉奸”,吹牛撒谎令周总理拍案怒斥

【刘海粟】“艺术流氓”“文化汉奸”,吹牛撒谎令周总理拍案怒斥

年之父
2026-08-07 04:05:03
戴笠身边人晚年坦言:他当年安顿胡蝶时,仅因瞥见她旗袍下摆一处褶皱,竟立刻命人腾出小院

戴笠身边人晚年坦言:他当年安顿胡蝶时,仅因瞥见她旗袍下摆一处褶皱,竟立刻命人腾出小院

磊子讲史
2026-08-15 17:48:13
罗德里和比西武我都要,德科是个好总监,巴萨找到对的人

罗德里和比西武我都要,德科是个好总监,巴萨找到对的人

米奇兔
2026-08-17 20:33:32
明日起鸿运当头!3大生肖逆风翻盘,事业暴涨聚财纳福,富贵挡不住

明日起鸿运当头!3大生肖逆风翻盘,事业暴涨聚财纳福,富贵挡不住

人閒情事
2026-08-17 10:03:48
微信公告:集体下架

微信公告:集体下架

金融界
2026-08-15 22:15:24
2024年,周海媚安葬于北京,她的墓造型格外特别,尤其是墓碑上刻着的那句话,读着读着眼眶就热了

2024年,周海媚安葬于北京,她的墓造型格外特别,尤其是墓碑上刻着的那句话,读着读着眼眶就热了

人生录
2026-08-11 00:05:09
省委书记,走进“全球最大零食店”

省委书记,走进“全球最大零食店”

中国商界杂志社
2026-08-17 14:41:11
终于出事!精神污染神作《牛来》紧急下架!到底发生了什么?

终于出事!精神污染神作《牛来》紧急下架!到底发生了什么?

魔都囡
2026-08-17 07:54:01
瑞典大满贯落幕,国乒185万奖金分配出炉:王曼昱68万,陈熠10万

瑞典大满贯落幕,国乒185万奖金分配出炉:王曼昱68万,陈熠10万

星Xin辰大海
2026-08-17 06:31:18
临时世界排名,世界第一之争更激烈,武汉夺冠小特也不足以保第一

临时世界排名,世界第一之争更激烈,武汉夺冠小特也不足以保第一

天涯远行人
2026-08-17 06:51:54
越来越多的城市,房价“鹤岗化”了

越来越多的城市,房价“鹤岗化”了

城市财经
2026-08-17 11:47:09
新一代电子茅台!iPhone Ultra首批备货十分紧张:产能远低于直板机

新一代电子茅台!iPhone Ultra首批备货十分紧张:产能远低于直板机

快科技
2026-08-17 11:08:22
2026-08-17 22:15:00
野生运营
野生运营
懂点产品,懂点AI,正在努力给平淡日子搞点新花样。
322文章数 82关注度
往期回顾 全部

科技要闻

马斯克600亿美元买条近路

头条要闻

男子开的"埃安"现离奇现象 续航低于300公里就狂掉电

头条要闻

男子开的"埃安"现离奇现象 续航低于300公里就狂掉电

体育要闻

黄蜂不要天马行空,只要稳稳地幸福?

娱乐要闻

立案风波席卷德云社多场巡演叫停!

财经要闻

爱丽家居跨界玩存储:溢价背后藏着机密案

汽车要闻

2027款艾瑞泽8 PRO 年轻人的务实之选

态度原创

教育
房产
健康
本地
公开课

教育要闻

因“不敢坐飞机”放弃211?安徽582分女孩的选择,直接把家长气哭了

房产要闻

刚刚官宣!海口房价,又跌了!

专家解答青少年脊柱侧弯七大问题

本地新闻

黄景藏用半刀泥刻瓷都魂

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版