V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
s609926202
V2EX  ›  Node.js

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

  •  1
     
  •   s609926202 · 2023-07-03 14:27:33 +08:00 · 2189 次点击
    这是一个创建于 520 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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
        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   ·   实用小工具   ·   5868 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 02:22 · PVG 10:22 · LAX 18:22 · JFK 21:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.