上海小团队的周五上线:让聊天回复经过后端逐段抵达浏览器
聊天页面不必等到整段答案生成
假设你在上海的一支小团队里,周五要交付一个客服助手。页面已经能提问,但用户点击发送后,往往要盯着空白区域等上几秒,整段回答才突然出现。你想让文字边生成边显示。
这就是流式响应:模型生成一部分,服务端便发送一部分,浏览器收到后更新页面。它主要缩短的是用户看到首段内容的等待时间,不保证整段回答更快完成,也不是把已经生成好的答案故意慢慢打出来。
自己的后端负责接住并转发回复
请求会经过三站:浏览器、你的后端、模型服务。浏览器把问题交给后端;后端携带密钥请求模型,并开启流式输出;模型陆续返回内容,后端再把内容传给浏览器。
这层后端首先保护密钥。写进网页脚本的密钥可以被访问者看到,不能靠隐藏按钮来保密。后端还应检查登录状态、限制请求频率,并决定用户可以使用哪些模型。浏览器提交的用户身份和额度信息都不能直接信任。
如果团队需要接入模型,可以考虑按量付费的 RelayVano:通过兼容 OpenAI 的 SDK,将 API 基础地址设为 https://api.relayvano.com,并在后端配置密钥。你可以注册 RelayVano 账户,用一个简短问题验证整个转发过程。
用 Fetch 读取后端持续返回的内容
对第一次实现的团队,一个实用选择是:浏览器用 Fetch 发送 POST 请求,后端用 SSE 格式返回事件。SSE 是一种文本格式,允许同一个响应连续携带多条消息。这里用 Fetch 读取响应流;浏览器原生的 EventSource 更适合 GET 请求,不能直接携带这类 POST 请求体。
先约定三类事件就够了:新增文字、生成结束、生成失败。后端把上游返回的文字增量转换成自己的事件,前端只处理这些约定。以后调整模型接口时,聊天页面通常就不必跟着修改。
- 新增文字:把这一段追加到当前回答。
- 生成结束:停止加载状态,允许用户继续提问。
- 生成失败:保留已有文字,并显示可重试提示。
网络分块不等于一条完整消息
浏览器每次读到的数据,可能只有半条事件,也可能包含好几条事件。不能每读一次就直接当成完整 JSON 解析。前端需要先用支持流式解码的 TextDecoder 处理字节,再把文本放进缓冲区,按 SSE 的空行边界取出完整事件,留下未完成的部分等下一次读取。
也不要假设每次增量都是一个字或一个词。页面只需按顺序追加文字;如果更新太频繁,可以每个动画帧合并刷新一次,减少界面抖动。
代理缓冲会让流式页面看起来卡住
后端应返回正确的事件流内容类型,并及时写出数据。如果本地能逐段显示,上线后却总是一次出现整段答案,先检查反向代理、网关和托管平台是否缓冲响应,以及是否允许长连接。某些压缩配置也可能推迟数据发送。
验证时可以让模型生成稍长的回答,观察浏览器是否分多次收到数据。仅仅在请求里设置流式开关,并不能证明每一层都在及时转发。
停止按钮和断线提示也属于交付范围
用户点击停止时,前端可以通过 AbortController 取消请求;后端还要监听连接关闭,并取消上游请求,避免页面已经离开,模型仍继续生成。具体计费情况需要以上游实际处理的用量为准。
最后,区分正常结束和意外断线:只有收到约定的结束事件,才把回答标为完成。连接中断时保留已收到的文字,提示回答可能不完整,再由用户选择重试。第一次上线先把密钥留在后端、文字逐段显示、停止能够传递、断线能够识别这四件事做好,聊天页面就有了可靠的基础。