前言:
我在使用Vue3开发一个chatgpt工具类网站的时候,翻阅了不少博客和github上的一些相关项目,都没能找到适合Vue3去实现stream的流式数据处理。经过踩坑,最终实现了适用直接调chatgpt接口的方法以及改为调用Python后端接口的方法。
背景:
默认情况下,当用户从 OpenAI 请求完成时,会生成整个完成,然后再通过单个响应发回,这样可能会造成等待响应时间过长。
解决:
“流式传输”,需在调用聊天完成或完成端点时设置 stream=True,这将返回一个对象,该对象将响应作为仅数据服务器发送的事件流回。
参数说明:
-
messages: 必须是对象数组
类型 作用 system 设置chatgpt的角色 user 用户输入的内容 assistant chatgpt返回的内容
- system 设定角色助手,使得下文对话走这条线路
- assistant充当历史记录,达到多轮对话,需将用户所问、AI所答数据存储,实现上下文功能,代价是消耗更多的tokens
- temperature
类型 | 默认值 | 取值范围 | 是否必填 |
---|---|---|---|
浮点数 | 1 | 0 - 2 | 否 |
- 随着temperature的取值越大,其输出结果更加随机(较低能集中稳定输出字节,但较高能有意想不到的性能创意)
- top_p
类型 | 默认值 | 取值范围 | 是否必填 |
---|---|---|---|
浮点数 | 1 | 0 - 1 | 否 |
- top_p用于预测可能,值越小时,其输出结果会更加肯定,响应性能会相对快,但值越大时,输出的结果可能会更贴近用户需求
How to stream completions?
API文档:/docs/api-reference/chat/create
有关实例代码:openai-cookbook/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub
有关网站示例:
/?_r=66017600068&_s=share
相信你们早就阅读了上面的文档,但还是很迷茫,感觉无从下手...下面说说我的踩坑经历:
我在网上搜索到的信息是,需要一些流式处理库,我就问chatgpt,它给我推荐了以下几种
-
RxJS:是一个响应式编程库,支持流式处理和异步操作。
-
:也是一个响应式编程库,提供了一个功能强大的事件流模型,可以用来处理异步事件。
-
:是一个基于流的函数式编程库,提供了广泛的流操作和管道组合功能。
-
的stream模块:是一个流式处理库,提供了流处理的核心功能。可以通过其定义自己的流转换器和消费者函数。
-
lodash-fp:是一个功能强大的函数式编程库,提供了一整套函数式的操作和工具,可以用来方便快捷地进行流处理。
我没走这条路,我重新查询了一波,网上的意思是,可以利用WebSocket方式或SSE的方式去实现长连接,但我都没采纳,最终使用的是fetch去实现请求即可,不用将问题复杂化哈哈哈
- 适用直接调chatgpt的接口
//
import { CHATGPT_API_URL } from '@/common/'
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: ({
model: 'gpt-3.5-turbo',
stream: true,
messages: messages
})
});
if (!) {
throw new Error(`HTTP error! status: ${}`);
}
const reader = ();
let decoder = new TextDecoder();
let resultData = '';
while (true) {
const { done, value } = await ();
if (done) break;
resultData += (value);
while (('\n')) {
const messageIndex = ('\n');
const message = (0, messageIndex);
resultData = (messageIndex + 1);
if (('data: ')) {
const jsonMessage = ((5));
if (('[DONE]')) {
break
}
const createdID =
yield {
content: [0]?.delta?.content || '',
role: "assistant",
id: createdID
};
}
}
}
}
以上是利用迭代器的写法去实现流式输出,我上面的字符串其实是chatgpt响应输出的数据,例如:
{"id":"chatcmpl-7B48ttLhb1iR4JoaCzElQTvxyAgsw","object":"","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()
= dialogId
// 获取聊天机器人的回复
for await (const result of getChatgpt_Multurn_qa()) {
// 如果返回的结果 ID 与当前对话 ID 相同,则将聊天机器人的回复拼接到当前对话中
if ( === ) {
const index = (item => === )
const dialog = [index]
+=
} else {
=
({
content: ,
role: "assistant",
id: ,
timestamp: ()
})
({
role: "assistant",
content:
})
}
}
上面代码比较关键的点就是条件的判断 --- === ,到这一步就可以实现chatgpt的流式输出啦,响应速度是非常快的!!!
补充:
1. list 是用户角色和AI角色的对话数组,可以传递给子组件去遍历渲染不同角色的聊天,在文章尾部将展示实现Markdown代码块的步骤
2. message 是将user以及assistant的所有历史记录push进去,是实现多轮对话的关键
- 改为调用后端Python的接口,先看看后端哥哥@ToTensor写给前端的文档
# 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": "",
"created": 1684246457,
"model": "gpt-3.5-turbo-0301",
"choices": [
{
"delta": {
"role": "assistant"
},
"index": 0,
"finish_reason": null
}
]
}
{
"id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
"object": "",
"created": 1684246457,
"model": "gpt-3.5-turbo-0301",
"choices": [
{
"delta": {
"content": "你"
},
"index": 0,
"finish_reason": null
}
]
}
{
"id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
"object": "",
"created": 1684246457,
"model": "gpt-3.5-turbo-0301",
"choices": [
{
"delta": {
"content": "好"
},
"index": 0,
"finish_reason": null
}
]
}
{
"id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
"object": "",
"created": 1684246457,
"model": "gpt-3.5-turbo-0301",
"choices": [
{
"delta": {
"content": "!"
},
"index": 0,
"finish_reason": null
}
]
}
{
"id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
"object": "",
"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/'
// 流式输出接口
export async function* getChatgpt_Multurn_qa(messages, onStreamDone) {
const response = await fetch(_BASE_API_URL + `你的url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: ({
messages: messages
})
})
if (!) {
throw new Error(`HTTP error! status: ${}`)
}
const reader = ()
let result = ''
let done = false
while (!done) {
const { value, done: streamDone } = await ()
if (value) {
const decoder = new TextDecoder()
result += (value)
const lines = ('\n')
result = ()
for (const line of lines) {
try {
const json = (line)
if ( && > 0) {
const content = [0].
if (content) {
yield { id: , content }
}
}
if ( && [0].finish_reason === 'stop') {
done = true
onStreamDone()
break
}
} catch (e) {
(e)
}
}
}
if (streamDone) {
done = true;
}
}
}
上面代码多了个onStreamDone参数,是我需要利用它处理响应完成的逻辑,没有这个需求的伙伴可以适当删改,接下来再看看父组件如何获取数据吧
// vue父组件
for await (const result of getChatgpt_Multurn_qa(, onStreamDone)) {
if ( === null) {
= ;
}
if ( === ) {
const index = (item => === );
const dialog = [index];
+= ;
} else {
= ;
({
content: || '',
role: "assistant",
id: ,
timestamp: ()
});
({
role: "assistant",
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 ().sort((a, b) => - )
})
说明:
通过
computed
创建了一个名为sortedList
的计算属性,该属性返回一个已排序的list
数组副本。在排序过程中,使用了slice
方法创建了一个数组副本,以避免直接修改原始数组。排序方式为按照每个数组元素的timestamp
属性升序排序。在模板中遍历循环
sortedList
的内容就能实现用户和ai对话啦
- 选择合适的Markdown 编辑器组件库
介绍一下md-editor-v3
官网:MdEditorV3 Documentation ()
github地址:imzbf/md-editor-v3: Markdown editor for vue3, developed in jsx and typescript, dark theme、beautify content by prettier、render articles directly、paste or clip the picture and upload it... ()
文档说明:MdEditorV3 Documentation ()
它提供了一些基础的 Markdown 编辑功能,如加粗、斜体、标题、无序列表、有序列表、引用、代码块等。除此之外,它还支持上传图片、撤销/重做、全屏等功能。md-editor-v3 的优点是易于使用、易于扩展,并且提供了一些定制化的选项。但是我只是想实现代码块,故解构出MdPreview
使用:
// 模板中
<MdPreview
:showCodeRowNumber="true" // 显示行号
:modelValue=""
/>
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/'
效果图: