vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3 chatgpt的流式输出

Vue3实现chatgpt的流式输出过程

作者:临枫541

这篇文章主要介绍了Vue3实现chatgpt的流式输出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

前言

我在使用Vue3开发一个chatgpt工具类网站的时候,翻阅了不少博客和github上的一些相关项目,都没能找到适合Vue3去实现stream的流式数据处理。

经过踩坑,最终实现了适用直接调chatgpt接口的方法以及改为调用Python后端接口的方法。

背景

默认情况下,当用户从 OpenAI 请求完成时,会生成整个完成,然后再通过单个响应发回,这样可能会造成等待响应时间过长。

解决

“流式传输”,需在调用聊天完成或完成端点时设置 stream=True,这将返回一个对象,该对象将响应作为仅数据服务器发送的事件流回。

参数说明

类型作用
system设置chatgpt的角色
user用户输入的内容
assistantchatgpt返回的内容
类型默认值取值范围是否必填
浮点数10 - 2

随着temperature的取值越大,其输出结果更加随机(较低能集中稳定输出字节,但较高能有意想不到的性能创意)

类型默认值取值范围是否必填
浮点数10 - 1

top_p用于预测可能,值越小时,其输出结果会更加肯定,响应性能会相对快,但值越大时,输出的结果可能会更贴近用户需求

How to stream completions?

相信你们早就阅读了上面的文档,但还是很迷茫,感觉无从下手...下面说说我的踩坑经历:我在网上搜索到的信息是,需要一些流式处理库,我就问chatgpt,它给我推荐了以下几种

我没走这条路,我重新查询了一波,网上的意思是,可以利用WebSocket方式或SSE的方式去实现长连接,但我都没采纳,最终使用的是fetch去实现请求即可,不用将问题复杂化哈哈哈

// gpt.js
import { CHATGPT_API_URL } from '@/common/config.js'
const OPENAI_API_KEY = '你的接口'
// TODO 适用直接调chatgpt接口
export async function* getChatgpt_Multurn_qa(messages) {
  const response = await fetch(CHATGPT_API_URL + '你的url', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${ OPENAI_API_KEY }`
    },
    body: JSON.stringify({
      model: 'gpt-3.5-turbo',
      stream: true,
      messages: messages
    })
  });

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const reader = response.body.getReader();
  let decoder = new TextDecoder();
  let resultData = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    resultData += decoder.decode(value);
    while (resultData.includes('\n')) {
      const messageIndex = resultData.indexOf('\n');
      const message = resultData.slice(0, messageIndex);
      resultData = resultData.slice(messageIndex + 1);
      if (message.startsWith('data: ')) {
        const jsonMessage = JSON.parse(message.substring(5));
        if (resultData.includes('[DONE]')) {
          break
        }
        const createdID = jsonMessage.created
        yield {
          content: jsonMessage.choices[0]?.delta?.content || '',
          role: "assistant",
          id: createdID
        };
      }
    }
  }
}

以上是利用迭代器的写法去实现流式输出,我上面的字符串其实是chatgpt响应输出的数据,例如:

{"id":"chatcmpl-7B48ttLhb1iR4JoaCzElQTvxyAgsw","object":"chat.completion.chunk","created":1682871887,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"."},"index":0,"finish_reason":null}]}

注意:

我利用迭代器需要将每一句created相同的流数据存储到一起,才能形成一个消息的闭环,否则页面的效果会是一个字就占一个段落,你们可以去试一试

// vue组件部分代码  

const currentDialogId = ref(null)

const dialogId = uniqueId()
currentDialogId.value = dialogId   

// 获取聊天机器人的回复
for await (const result of getChatgpt_Multurn_qa(messages.value)) {
  // 如果返回的结果 ID 与当前对话 ID 相同,则将聊天机器人的回复拼接到当前对话中
  if (result.id === currentDialogId.value) {
    const index = list.value.findIndex(item => item.id === currentDialogId.value)
    const dialog = list.value[index]
    dialog.content += result.content
  } else {
    currentDialogId.value = result.id
    list.value.push({
      content: result.content,
      role: "assistant",
      id: result.id,
      timestamp: Date.now()
    })
    messages.value.push({
      role: "assistant",
      content: result.content
    })
  }
}

上面代码比较关键的点就是条件的判断 ---  result.id === currentDialogId.value ,到这一步就可以实现chatgpt的流式输出啦,响应速度是非常快的!!!

补充:

1. list 是用户角色和AI角色的对话数组,可以传递给子组件去遍历渲染不同角色的聊天,在文章尾部将展示实现Markdown代码块的步骤

2. message 是将user以及assistant的所有历史记录push进去,是实现多轮对话的关键

# ChatGPT流式输出接口

## 接口路径

```bash
https://后端提供的url
```

## 请求方式

**POST**

## 请求参数


```bash
{
    "messages": [
        {
            "role": "user",
            "content": "你好"
        }
    ]
}
```

## 请求参数说明

```bash
messages: 消息体
```


## curl

```bash
curl --location 'https://后端提供的url' \
--header 'Content-Type: application/json' \
--data '{
    "messages": [
        {
            "role": "user",
            "content": "你好"
        }
    ]
}'
```


## 返回数据

```bash
{
    "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
    "object": "chat.completion.chunk",
    "created": 1684246457,
    "model": "gpt-3.5-turbo-0301",
    "choices": [
        {
            "delta": {
                "role": "assistant"
            },
            "index": 0,
            "finish_reason": null
        }
    ]
}
{
    "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
    "object": "chat.completion.chunk",
    "created": 1684246457,
    "model": "gpt-3.5-turbo-0301",
    "choices": [
        {
            "delta": {
                "content": "你"
            },
            "index": 0,
            "finish_reason": null
        }
    ]
}
{
    "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
    "object": "chat.completion.chunk",
    "created": 1684246457,
    "model": "gpt-3.5-turbo-0301",
    "choices": [
        {
            "delta": {
                "content": "好"
            },
            "index": 0,
            "finish_reason": null
        }
    ]
}
{
    "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
    "object": "chat.completion.chunk",
    "created": 1684246457,
    "model": "gpt-3.5-turbo-0301",
    "choices": [
        {
            "delta": {
                "content": "!"
            },
            "index": 0,
            "finish_reason": null
        }
    ]
}
{
    "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
    "object": "chat.completion.chunk",
    "created": 1684246457,
    "model": "gpt-3.5-turbo-0301",
    "choices": [
        {
            "delta": {},
            "index": 0,
            "finish_reason": "stop"
        }
    ]
}
```

## 返回参数说明

```bash
role = assistant, 开始输出
finish_reason = stop, 输出结束
finish_reason = null, 正在输出
content 输出内容
```

根据文档,我们只需要小小改动代码

// TODO 改用chatgpt接口
import { _BASE_API_URL } from '@/common/config.js'

// 流式输出接口
export async function* getChatgpt_Multurn_qa(messages, onStreamDone) {
  const response = await fetch(_BASE_API_URL + `你的url`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: messages
    })
  })

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }

  const reader = response.body.getReader()

  let result = ''
  let done = false

  while (!done) {
    const { value, done: streamDone } = await reader.read()

    if (value) {
      const decoder = new TextDecoder()
      result += decoder.decode(value)
      const lines = result.split('\n')
      result = lines.pop()

      for (const line of lines) {
        try {
          const json = JSON.parse(line)
          if (json.choices && json.choices.length > 0) {
            const content = json.choices[0].delta.content
            if (content) {
              yield { id: json.created, content }
            }
          }
          if (json.choices && json.choices[0].finish_reason === 'stop') {
            done = true
            onStreamDone()
            break
          }
        } catch (e) {
          console.error(e)
        }
      }
    }
    if (streamDone) {
      done = true;
    }
  }
}

上面代码多了个onStreamDone参数,是我需要利用它处理响应完成的逻辑,没有这个需求的伙伴可以适当删改,接下来再看看父组件如何获取数据吧

// vue父组件 
     for await (const result of getChatgpt_Multurn_qa(messages.value, onStreamDone)) {
        if (currentConversationId.value === null) {
          currentConversationId.value = result.id;
        }
        if (result.id === currentConversationId.value) {
          const index = list.value.findIndex(item => item.id === currentConversationId.value);
          const dialog = list.value[index];
          dialog.content += result.content;
        } else {
          currentConversationId.value = result.id;
          list.value.push({
            content: result.content || '',
            role: "assistant",
            id: result.id,
            timestamp: Date.now()
          });
          messages.value.push({
            role: "assistant",
            content: result.content || ''
          });
        }
      }
// 父组件

<session-box :list="list" @sent="handleSent"></session-box>
// 子组件
const props = defineProps({
  list: {
    type: Array,
    default: []
  }
})

const { list } = toRefs(props)

const sessionList = ref(null)

const sortedList = computed(() => {
  return list.value.slice().sort((a, b) => a.timestamp - b.timestamp)
})

说明:

通过 computed 创建了一个名为 sortedList 的计算属性,该属性返回一个已排序的 list 数组副本。

在排序过程中,使用了 slice 方法创建了一个数组副本,以避免直接修改原始数组。排序方式为按照每个数组元素的 timestamp 属性升序排序。

在模板中遍历循环sortedList的内容就能实现用户和ai对话啦

介绍一下md-editor-v3 

 它提供了一些基础的 Markdown 编辑功能,如加粗、斜体、标题、无序列表、有序列表、引用、代码块等。除此之外,它还支持上传图片、撤销/重做、全屏等功能。

md-editor-v3 的优点是易于使用、易于扩展,并且提供了一些定制化的选项。但是我只是想实现代码块,故解构出MdPreview

使用:

// 模板中
<MdPreview
   :showCodeRowNumber="true"            // 显示行号
   :modelValue="item.content"
/>


import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'

效果图:

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文