chatGPT流式调用实践

1,344 阅读3分钟

OPENAI_API_KEY获取渠道:(都需要替换服务端域名)

API2D—— 请求包装过的接口,订阅价格为原价的1.5倍,加拿大付费合法渠道。

AI Proxy——请求openai接口,订阅价格为原价的1.12倍,新加坡付费合法渠道,账号安全。

OhMyGPT——请求openai接口(需加代理), 按分钟使用次数收费,60次/分内免费,个人付费渠道,安全性低。

应用方向:(基本都为直接收费模式;可以考虑间接收费,通过广告,流量盈利)

  1. 出售api_key,因为openai接口收费,且需代理,所以这个在国内是有较大需求的,包括二次出售,例如: API2D
  2. 精化prompt,根据指定方向训练,来单一化提供功能,例如小红书标题,周报,月报等,例如: aitxt.app
  3. ......

关于流式调用openAI接口的尝试:

要了解chatGPT的流式获取,首先介绍一下Server-Sent Events服务器信息推送

SSE(Server-Sent Events) 的客户端 API 部署在EventSource对象上, EventSource是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream格式发送事件, 会一直保持开启直到被要求关闭。

一、在服务端openai返回的流式数据后,首先尝试了使用new EventSource()来建立持久化连接

注意点:请求头通过EventSource制定了请求头为text/event-stream,服务端也需要指定返回格式为text/event-stream

不同的model,返回的流内容不同,需要做对应解析:

text-davinci-003 返回:

data: {"id":"cmpl-7dguOmZt40yelK9VLxOattYQPutXC","object":"text_completion","created":1689694088,"choices":[{"text":"发","index":0,"logprobs":null,"finish_reason":null}],"model":"text-davinci-003"} 

gpt-3.5-turbo 返回:

data: {"id":"chatcmpl-7dgvw4NaEd67U01ZsFmXgZMjuDW3X","object":"chat.completion.chunk","created":1689694184,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"?"},"finish_reason":null}]}
// 服务端...
        // 响应头需要设置text/event-steam
        res.setHeader("Content-Type", "text/event-stream");
        res.setHeader("Cache-Control", "no-cache");
        res.setHeader("Connection", "keep-alive");     
        const gptResult = await openai.createCompletion({
            model:"text-davinci-003",
            prompt: req.query.data,
            max_tokens: 100,
            temperature: 0,
            stream: true,
        }, { responseType: 'stream' });
 
 
        gptResult.data.on('data', chunk => { //buffer流
            const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
            for (const line of lines) {
                const message = line.replace(/^data: /, '');
                if (message === '[DONE]') {
                    res.end();
                    return; // Stream finished
                }
                try {
                    const parsed = JSON.parse(message);
                    const content = parsed.choices[0].text;
                    console.log(res.write, parsed.choices[0].text);
                    res.write(`data: ${content}\n\n`); // Send SSE message to the browser client
                } catch(error) {
                    console.error('Could not JSON parse stream message', message, error);
                }
            }
        });
 
// 客户端...
useEffect(() => {
    source.onmessage = (event) => {
        setMessages((prevMessages) => [...prevMessages, event.data]);
      };
   
    source.onerror = (error) => {
        console.error('EventSource error:', error);
        source.close();
    };
    return () => {
        source?.close?.();
        setMessages([]);
    };
  }, [source]);
 
  const onBlur =async (data) => {
    const source = new EventSource('/api/event-source?data='+data.target.value);
    setSource(source);
  }

二、后来在开发中,发现new EventSource()这种长链接,有一个较大的问题,就是它只能发送get请求,没有办法发送post请求

于是尝试使用fetchEventSource, fetchEventSource本质 利用了fetch返回promise的特点以及修改请求头为text/event-stream来实现持续获取数据

并且 1. 可以通过和new AbortController()结合,来中止fetch请求 2. 接口失败默认会触发重试

  //服务端...
	  	// 响应头需要设置text/event-steam
		res.setHeader("Content-Type", "text/event-stream");
  		res.setHeader("Cache-Control", "no-cache");
  		res.setHeader("Connection", "keep-alive");	 

  		const gptResult = await openai.createChatCompletion({
            messages:req.body,
            stream: true,
            model: 'gpt-3.5-turbo',
          },  { responseType: 'stream' });
        
        gptResult.data.on('data', chunk => {
            const lines = chunk.toString().split('\n').filter(line => line.trim() !== '');
            for (const line of lines) {
                const message = line.replace(/^data: /, '');
                if (message === '[DONE]') {
                    res.end();
                    return; // Stream finished
                }
                try {
                    const parsed = JSON.parse(message);
                    const content = parsed.choices[0].delta.content || '';
                    console.log(res.write, parsed.choices[0].delta.content);
                    res.write(`data: ${content}\n\n`); // Send SSE message to the browser client
                } catch(error) {
                    console.error('Could not JSON parse stream message', message, error);
                }
            }
        });

//客户端...
		await fetchEventSource('/api/fetch-event-source', {
           	signal:updateController().signal,
            method: 'POST',
            body: JSON.stringify([{"role": "system", "content": data.target.value}]),
            headers: {
                'Content-Type': 'application/json'
            },
            onmessage:((event) => {
                setMessages((prevMessages) => [...prevMessages, event.data]);
            }),
            onerror:((error) => {
                // setAvailable(true);
                console.error('EventSource error:', error);
            }),
            onclose:((a) => {
                // setAvailable(true);
                console.log(a,23);
            })
        });        

三、测试时发现,极易出现上一轮请求未结束就开始下一轮请求

所以做出调整

// 客户端...
   const updateController=()=>{
    controllerRef.current.abort(); //中止旧的
    const controller = new AbortController(); //创建新的
    controllerRef.current  = controller;
    return controller;
  }

遇到的问题:

    1. 当使用流式获取信息时,如果不加开关,容易出现上一个请求没结束,就开始下一个请求的情况

解决方案:

  1. 若是new EventSource(url),则可直接调用 实例上的close()方法,关闭连接。
  2. 若是fetch-event-source,则有两种方式
    • 通过状态控制,在第一次请求没有停止前,禁止调用第二次请求。(有点类似现在的openai官网使用方式)

    • 通过 new AbortController().abort() 来中止进行中的异步任务,即fetch返回的promise

参考:

EventSource MDN

github.com/Azure/fetch…

zh.javascript.info/fetch-abort