使用 fetch 请求 openai stream 响应时,内容偶尔会被“切断” - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
s609926202
V2EX    Node.js

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

  •  1
     
  •   s609926202 2023-07-03 14:27:33 +08:00 2763 次点击
    这是一个创建于 841 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 个字。

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

    第 1 条附言    2023-07-03 16:44:45 +08:00

    听了@Opportunity的建议,上了 '@fortaine/fetch-event-source' 库。

    测试之后内容切断的问题应该是解决了,,但是另一个问题出现了。。。无语。

    错误信息:Uncaught Error: The error you provided does not contain a stack trace.

    代码如下:

    await fetchEventSource('xxx', { async onopen(response) { if (response.ok && response.headers.get('content-type') === EventStreamContentType) { return; // everything's good } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { // client-side errors are usually non-retriable: throw new FatalError(); } else { throw new RetriableError(); } }, onmessage(msg) { if (msg.data === '[DONE]' || finished) { return finish(); } const text = msg.data; try { const json = JSON.parse(text); const finishReason = json.finish_reason; const choices = json.choices[0]; const delta = choices.delta; if (delta.hasOwnProperty('content') && delta.content) { reply += delta.content; } else if (delta.hasOwnProperty('function_call')) { ... } else if (finishReason === 'function_call' || finishReason === 'stop') { return finish(); } } catch (e) { console.error('[Request] parse error', text, msg); } }, onclose() { finish(); }, onerror(err) { console.error('[Request] error', err); throw err; }, }); 

    再次请教改怎么改进、、、代码参考:@link https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/app/client/platforms/openai.ts

    第 2 条附言    2023-07-03 16:45:42 +08:00

    这个错误 Uncaught Error: The error you provided does not contain a stack trace. 不影响运行,但看着不顺眼、、

    10 条回复    2023-07-04 09:31:56 +08:00
    Opportunity
        1
    Opportunity  
       2023-07-03 14:30:44 +08:00
    为啥不直接用 EventSource 读,要自己手写这玩意?非要手写的话可以去参考下 EventSource 的 polyfill 怎么实现的。
    s609926202
      &nbs; 2
    s609926202  
    OP
       2023-07-03 14:41:12 +08:00
    @Opportunity #1 不会。现在都是还是网上东拼西凑来的。。
    Erroad
        3
    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
        4
    zhuisui  
       2023-07-03 14:45:18 +08:00
    你好像没有正确处理 done
    s609926202
        5
    s609926202  
    OP
       2023-07-03 14:52:45 +08:00
    @zhuisui #4 在循环体中处理的

    ```
    if (choices.finish_reason === 'stop' || choices.finish_reason === 'function_call') {
    dOne= true;
    break;
    }
    ```
    mmdsun
        6
    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
        7
    yowot0088  
       2023-07-03 20:44:06 +08:00
    我的解决方法是,先判断一个 chunk 里最后的 data: 是否为一个合法的 json ,如果不是,则将下一次最开始接收到的字符串与前一次的非法 json 拼接,可以完美解决
    yowot0088
        8
    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
        9
    MEIerer  
       2023-07-04 09:20:11 +08:00
    我发现原生 fetch 在手机端直连 gpt 的接口时一点数据都出不来,但在 pc 端就没问题,这是为什么?
    s609926202
        10
    s609926202  
    OP
       2023-07-04 09:31:56 +08:00
    @yowot0088 #8 这倒是一个解决方法。不过我改用 '@fortaine/fetch-event-source' 库了,效果比手写好些。。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3993 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 05:24 PVG 13:24 LAX 22:24 JFK 01:24
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86