前端对接生成式AI接口(流式传输)问题汇总_request with the provided id has already finished
文章目录
- 前端实现对话流问题总结
前端实现对话流问题总结
流式数据传输问题
后台逻辑为一个模块的一大段数据返回一个流,例如一些表格信息、图表信息等都是一个流返回所有数据
后台Response Headers问题
起初后台设置的相应头为application/stream+json
这是一种非标准的流式传输格式,导致控制台无法使用EventStream
进行调试,不方便定位问题,后续修改为text/event-stream
(被大多数现代浏览器原生支持)实现
大量数据分段接收问题
application/stream+json
同时由于每次流传输的是完整一个模块的内容,数据量较大,客户端接收时会切分成多次读取(注:服务端其实是完整发送的),那么在使用application/stream+json
时不会对一个完整的流进行标注,例如返回的数据可能为
// 完整数据\"{\"type\":\"update\",\"value\":1},{\"type\":\"update\",\"value\":2},{\"type\":\"update\",\"value\":3}\"// 实际接收- fisrt stream\"{\"type\":\"update\",\"value\":1},{\"ty- second streampe\":\"update\",\"value\":2},{\"type\":\"update\",\"value\":3}\"
那么要如何判断我多次接收到的数据是不是一个完整模块,这里我采取的方案为判断是否返回了一个完整的JSON
const reader = res.getReader()let stringData = \'\'reader.read().then(function processText({ done, value }) { if(done) { return } const text = new TextDecoder().decode(value) if(text) { stringData += text if(isValidJSON(stringData)) { console.log(JSON.parse(stringData)) // Do something stringData = \'\' } } return reader.read().then(processText)}function isValidJSON(data) { try { JSON.parse(data); return true; // 如果解析成功,说明是有效的JSON } catch (e) { return false; // 如果解析失败,说明不是有效的JSON }}
text/event-stream
使用text/event-stream
格式,因为服务端发送的一个完整模块会在开头使用data:
标注,不需要再额外判断
// 完整数据\"data:{\"type\":\"update\",\"value\":1},{\"type\":\"update\",\"value\":2},{\"type\":\"update\",\"value\":3}\"// 实际接收- fisrt stream\"data:{\"type\":\"update\",\"value\":1},{\"ty- second streampe\":\"update\",\"value\":2},{\"type\":\"update\",\"value\":3}\"
实现可以变换为
const reader = res.getReader()let stringData = \'\'reader.read().then(function processText({ done, value }) { if(done) { return } const text = new TextDecoder().decode(value) if(text) { // 一个data:到下一个data:之间的值为一个完整JSON if(text.startsWith(\'data:\')) { if(stringData) { console.log(JSON.parse(stringData)) // Do something } stringData = decodedChunk.substring(5) } else { jsonString += decodedChunk } } return reader.read().then(processText)}
多个流时间戳(Time)相同导致被合并的问题
在联调接口时发现有多个流合并在同一次返回中,观察接口EventStream发现当数据生成非常快时(一般是命中了大模型的缓存),流式数据的时间戳会完全相同,导致数据会在统一批次返回,形成这种返回结果。
- fisrt stream\"data:{\"type\":\"update\",\"value\":1},{\"type\":\"update\",\"value\":2}\"\"data:{\"type\":\"update\",\"value\":3}\"
这里的处理方式为让后端在发送每条数据时添加一个毫秒级的延迟,确保每个流的时间戳不同。
中止对话问题
其实没什么难度,就是使用reader.cancel()
,但有一个点要注意,创建reader
的前提是已经建立好了连接,因此在建立连接的这段过程要么不显示中止按钮、要么实现取消发送,我这里采用不显示中止按钮
// 流式传输返回请求头即为建立连接async function send(){let isConnected = false await getData() isConnected = true}// 在发送完成的done状态也修改为isConnected为false表示结束
复制问题
实现复制功能要注意的一点是,因为浏览器的安全策略,navigator.clipboard
需要在https
或本地调试这种安全上下文中才有,所以我们的服务如果采用的是http
协议,要实现适配,适配方案为创建DOM,获取DOM内容
const giveCopy = (text) => { if (text) { if (navigator.clipboard && window.isSecureContext) { console.log(\'复制成功\') return navigator.clipboard.writeText(text) } else { let textArea = document.createElement(\'textarea\') textArea.value = text textArea.style.position = \'absolute\' textArea.style.opacity = 0 textArea.style.left = \'-9999px\' textArea.style.top = \'-9999px\' document.body.appendChild(textArea) textArea.focus() textArea.select() return new Promise((res, rej) => { document.execCommand(\'copy\') ? res(console.log(\'复制成功\')) : rej() textArea.remove() }) } }}
部署上线问题
Nginx缓冲导致无法正常流式返回数据
流式传输为Server-Sent Events (SSE) ,当采用Nginx代理时,由于其默认的缓存和缓冲机制,会出现无法正常流式返回数据的问题
在一些浏览器可能会返回
Request with the provided ID has already finished loading
因此需要配置Nginx
proxy_buffering off; # 禁用Nginx缓冲proxy_cache off; # 禁用缓存
使用axios的1.7.0以上版本兼容fetch导致旧版浏览器报错问题
项目中起先使用的axios兼容的fetch请求实现流式传输
export const api = (data) => { return request({ url: url, method: \'post\', data, responseType: \'stream\', adapter: \'fetch\' })}
然而在部署到版本较旧的浏览器时产生报错typeError: r is not async iterable
,最后还是使用了fetch请求实现,解决该问题