使用 fetch 请求 openai stream 响应时,内容偶尔会被“切断”

2023-07-03 14:27:33 +08:00
 s609926202
const response = await fetch(...);
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (!done) {
    const { value, done: readerDone } = await reader.read();
    if (value) {
    	const char = decoder.decode(value);
        console.log(char);
    }
}

代码如上,有时候打印出来的 char 为:

data: {"id":"chatcmpl-7Y79egENb17GOU20IaW5KgJJhbf4M","object":"chat.completion.chunk","created":1688365010,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-7Y79egEN

图示: https://i.imgur.com/P1YQs4q.png

也就是从"id"中间被切断了,导致内容少 1 到 2 个字。

请问有啥可改进的方法吗?

2144 次点击
所在节点    Node.js
10 条回复
Opportunity
2023-07-03 14:30:44 +08:00
为啥不直接用 EventSource 读,要自己手写这玩意?非要手写的话可以去参考下 EventSource 的 polyfill 怎么实现的。
s609926202
2023-07-03 14:41:12 +08:00
@Opportunity #1 不会。现在都是还是网上东拼西凑来的。。
Erroad
2023-07-03 14:44:28 +08:00
当服务器端向客户端发送一段 HTTP 流( HTTP Streaming )时,数据是以块( chunks )的形式发送的,而不是一次性发送全部。在浏览器环境中,我们可以使用 Fetch API 的流( stream )读取器读取到这些数据。

这是一个基本的例子:

```javascript
fetch('/your-http-streaming-url')
.then(response => {
const reader = response.body.getReader();
const stream = new ReadableStream({
start(controller) {
function push() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
push();
})
.catch(error => {
console.error(error);
controller.error(error);
})
}
push();
}
});

return new Response(stream, { headers: { "Content-Type": "text/html" } });
})
.then(response => response.text())
.then(result => {
console.log(result);
})
.catch(err => {
console.error(err);
});
```

这个示例做了以下事情:

1. 使用 `fetch` API 获取数据流。
2. 创建一个流读取器( stream reader )读取响应主体。
3. 创建一个新的 `ReadableStream`,在它的 `start` 函数中读取数据,并通过 `controller.enqueue` 方法将数据加入队列中。
4. 如果读取过程中出现错误,使用 `controller.error` 将错误信息发送出去。
5. 当数据全部读取完毕,关闭控制器 `controller.close`。
6. 最后,获取到的数据通过 `Response.text()` 转化为文本格式,并输出。

注意,上述示例仅适用于文本数据流,如果你需要处理的是二进制数据流,可能需要进行适当的调整。例如,你可能需要使用 `Response.blob()` 代替 `Response.text()`。

chatGPT 的回答
zhuisui
2023-07-03 14:45:18 +08:00
你好像没有正确处理 done
s609926202
2023-07-03 14:52:45 +08:00
@zhuisui #4 在循环体中处理的

```
if (choices.finish_reason === 'stop' || choices.finish_reason === 'function_call') {
done = true;
break;
}
```
mmdsun
2023-07-03 17:06:21 +08:00
@Opportunity
EventSource 只能 url 吧,我看 openAi 接口都是 POST 有 request body 的,EventSource 没法用。

curl https://api.openai.com/v1/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-3.5-turbo",
"prompt": "Say this is a test",
"max_tokens": 7,
"steam": true,
"temperature": 0
}'
yowot0088
2023-07-03 20:44:06 +08:00
我的解决方法是,先判断一个 chunk 里最后的 data: 是否为一个合法的 json ,如果不是,则将下一次最开始接收到的字符串与前一次的非法 json 拼接,可以完美解决
yowot0088
2023-07-03 20:45:44 +08:00
附上我做的 ws api 的源码

```js
wss.on('connection', ws => {
let isConnected = true

ws.on('message', async e => {
let message = JSON.parse(e.toString())
if(message.type == 'conversation') {
let es = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + 'YOUR_OPENAI_API_KEY'
},
method: 'POST',
body: JSON.stringify({
model: message.data.model,
messages: message.data.messages,
stream: true
})
})

const reader = es.body.pipeThrough(new TextDecoderStream()).getReader()

let errObj = ''

while(true) {
if(!isConnected) {
process.stdout.write('\n')
break
}
const res = await reader.read()
if(res.done) {
break
}
let chunk = res.value
chunk = chunk.replace(/data: /g, '').split('\n')

chunk.map(item => {
if(item != '[DONE]' && item != '' && item != undefined) {
let json

try {
if(errObj != '') {
item = errObj + item
errObj = ''
}

json = JSON.parse(item)

if(json.choices[0].delta.content == undefined) return
ws.send(JSON.stringify({
type: 'conversation',
data: {
type: 'continue',
text: json.choices[0].delta.content
}
}))
process.stdout.write(json.choices[0].delta.content)
}catch {
errObj = item
return
}

}else if(item == '[DONE]') {
ws.send(JSON.stringify({
type: 'conversation',
data: {
type: 'done',
text: null
}
}))
process.stdout.write('\n')
}
})
}
}
})

ws.onclose = () => {
isConnected = false
}
})
```
MEIerer
2023-07-04 09:20:11 +08:00
我发现原生 fetch 在手机端直连 gpt 的接口时一点数据都出不来,但在 pc 端就没问题,这是为什么?
s609926202
2023-07-04 09:31:56 +08:00
@yowot0088 #8 这倒是一个解决方法。不过我改用 '@fortaine/fetch-event-source' 库了,效果比手写好些。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/953666

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX