当前位置:   article > 正文

Vue3实现chatgpt的流式输出_vue chatgpt

vue chatgpt

前言:

我在使用Vue3开发一个chatgpt工具类网站的时候,翻阅了不少博客和github上的一些相关项目,都没能找到适合Vue3去实现stream的流式数据处理。经过踩坑,最终实现了适用直接调chatgpt接口的方法以及改为调用Python后端接口的方法。

背景:

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

解决:

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

参数说明:

  • messages: 必须是对象数组

    类型作用
    system设置chatgpt的角色
    user用户输入的内容
    assistantchatgpt返回的内容
  • system 设定角色助手,使得下文对话走这条线路
  • assistant充当历史记录,达到多轮对话,需将用户所问、AI所答数据存储,实现上下文功能,代价是消耗更多的tokens
  • temperature
类型默认值取值范围是否必填
浮点数10 - 2
  • 随着temperature的取值越大,其输出结果更加随机(较低能集中稳定输出字节,但较高能有意想不到的性能创意)
  • top_p
类型默认值取值范围是否必填
浮点数10 - 1
  • top_p用于预测可能,值越小时,其输出结果会更加肯定,响应性能会相对快,但值越大时,输出的结果可能会更贴近用户需求

How to stream completions?

API文档:https://platform.openai.com/docs/api-reference/chat/create

有关实例代码:openai-cookbook/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub

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

  1. RxJS:是一个响应式编程库,支持流式处理和异步操作。

  2. Bacon.js:也是一个响应式编程库,提供了一个功能强大的事件流模型,可以用来处理异步事件。

  3. Highland.js:是一个基于流的函数式编程库,提供了广泛的流操作和管道组合功能。

  4. Node.js的stream模块:是一个流式处理库,提供了流处理的核心功能。可以通过其定义自己的流转换器和消费者函数。

  5. lodash-fp:是一个功能强大的函数式编程库,提供了一整套函数式的操作和工具,可以用来方便快捷地进行流处理。

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

  • 适用直接调chatgpt的接口
  1. // gpt.js
  2. import { CHATGPT_API_URL } from '@/common/config.js'
  3. const OPENAI_API_KEY = '你的接口'
  4. // TODO 适用直接调chatgpt接口
  5. export async function* getChatgpt_Multurn_qa(messages) {
  6. const response = await fetch(CHATGPT_API_URL + '你的url', {
  7. method: 'POST',
  8. headers: {
  9. 'Content-Type': 'application/json',
  10. 'Authorization': `Bearer ${ OPENAI_API_KEY }`
  11. },
  12. body: JSON.stringify({
  13. model: 'gpt-3.5-turbo',
  14. stream: true,
  15. messages: messages
  16. })
  17. });
  18. if (!response.ok) {
  19. throw new Error(`HTTP error! status: ${response.status}`);
  20. }
  21. const reader = response.body.getReader();
  22. let decoder = new TextDecoder();
  23. let resultData = '';
  24. while (true) {
  25. const { done, value } = await reader.read();
  26. if (done) break;
  27. resultData += decoder.decode(value);
  28. while (resultData.includes('\n')) {
  29. const messageIndex = resultData.indexOf('\n');
  30. const message = resultData.slice(0, messageIndex);
  31. resultData = resultData.slice(messageIndex + 1);
  32. if (message.startsWith('data: ')) {
  33. const jsonMessage = JSON.parse(message.substring(5));
  34. if (resultData.includes('[DONE]')) {
  35. break
  36. }
  37. const createdID = jsonMessage.created
  38. yield {
  39. content: jsonMessage.choices[0]?.delta?.content || '',
  40. role: "assistant",
  41. id: createdID
  42. };
  43. }
  44. }
  45. }
  46. }

以上是利用迭代器的写法去实现流式输出,我上面的字符串其实是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 相同的流数据存储到一起,才能形成一个消息的闭环,否则页面的效果会是一个字就占一个段落,你们可以去试一试

  1. // vue组件部分代码
  2. const currentDialogId = ref(null)
  3. const dialogId = uniqueId()
  4. currentDialogId.value = dialogId
  5. // 获取聊天机器人的回复
  6. for await (const result of getChatgpt_Multurn_qa(messages.value)) {
  7. // 如果返回的结果 ID 与当前对话 ID 相同,则将聊天机器人的回复拼接到当前对话中
  8. if (result.id === currentDialogId.value) {
  9. const index = list.value.findIndex(item => item.id === currentDialogId.value)
  10. const dialog = list.value[index]
  11. dialog.content += result.content
  12. } else {
  13. currentDialogId.value = result.id
  14. list.value.push({
  15. content: result.content,
  16. role: "assistant",
  17. id: result.id,
  18. timestamp: Date.now()
  19. })
  20. messages.value.push({
  21. role: "assistant",
  22. content: result.content
  23. })
  24. }
  25. }

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

补充:

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

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

  •  改为调用后端Python的接口,先看看后端哥哥@ToTensor写给前端的文档
  1. # ChatGPT流式输出接口
  2. ## 接口路径
  3. ```bash
  4. https://后端提供的url
  5. ```
  6. ## 请求方式
  7. **POST**
  8. ## 请求参数
  9. ```bash
  10. {
  11. "messages": [
  12. {
  13. "role": "user",
  14. "content": "你好"
  15. }
  16. ]
  17. }
  18. ```
  19. ## 请求参数说明
  20. ```bash
  21. messages: 消息体
  22. ```
  23. ## curl
  24. ```bash
  25. curl --location 'https://后端提供的url' \
  26. --header 'Content-Type: application/json' \
  27. --data '{
  28. "messages": [
  29. {
  30. "role": "user",
  31. "content": "你好"
  32. }
  33. ]
  34. }'
  35. ```
  36. ## 返回数据
  37. ```bash
  38. {
  39. "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
  40. "object": "chat.completion.chunk",
  41. "created": 1684246457,
  42. "model": "gpt-3.5-turbo-0301",
  43. "choices": [
  44. {
  45. "delta": {
  46. "role": "assistant"
  47. },
  48. "index": 0,
  49. "finish_reason": null
  50. }
  51. ]
  52. }
  53. {
  54. "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
  55. "object": "chat.completion.chunk",
  56. "created": 1684246457,
  57. "model": "gpt-3.5-turbo-0301",
  58. "choices": [
  59. {
  60. "delta": {
  61. "content": "你"
  62. },
  63. "index": 0,
  64. "finish_reason": null
  65. }
  66. ]
  67. }
  68. {
  69. "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
  70. "object": "chat.completion.chunk",
  71. "created": 1684246457,
  72. "model": "gpt-3.5-turbo-0301",
  73. "choices": [
  74. {
  75. "delta": {
  76. "content": "好"
  77. },
  78. "index": 0,
  79. "finish_reason": null
  80. }
  81. ]
  82. }
  83. {
  84. "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
  85. "object": "chat.completion.chunk",
  86. "created": 1684246457,
  87. "model": "gpt-3.5-turbo-0301",
  88. "choices": [
  89. {
  90. "delta": {
  91. "content": "!"
  92. },
  93. "index": 0,
  94. "finish_reason": null
  95. }
  96. ]
  97. }
  98. {
  99. "id": "chatcmpl-7GpjNUPkhPZF0MtJBqTMvW2bbWPPG",
  100. "object": "chat.completion.chunk",
  101. "created": 1684246457,
  102. "model": "gpt-3.5-turbo-0301",
  103. "choices": [
  104. {
  105. "delta": {},
  106. "index": 0,
  107. "finish_reason": "stop"
  108. }
  109. ]
  110. }
  111. ```
  112. ## 返回参数说明
  113. ```bash
  114. role = assistant, 开始输出
  115. finish_reason = stop, 输出结束
  116. finish_reason = null, 正在输出
  117. content 输出内容
  118. ```

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

  1. // TODO 改用chatgpt接口
  2. import { _BASE_API_URL } from '@/common/config.js'
  3. // 流式输出接口
  4. export async function* getChatgpt_Multurn_qa(messages, onStreamDone) {
  5. const response = await fetch(_BASE_API_URL + `你的url`, {
  6. method: 'POST',
  7. headers: {
  8. 'Content-Type': 'application/json'
  9. },
  10. body: JSON.stringify({
  11. messages: messages
  12. })
  13. })
  14. if (!response.ok) {
  15. throw new Error(`HTTP error! status: ${response.status}`)
  16. }
  17. const reader = response.body.getReader()
  18. let result = ''
  19. let done = false
  20. while (!done) {
  21. const { value, done: streamDone } = await reader.read()
  22. if (value) {
  23. const decoder = new TextDecoder()
  24. result += decoder.decode(value)
  25. const lines = result.split('\n')
  26. result = lines.pop()
  27. for (const line of lines) {
  28. try {
  29. const json = JSON.parse(line)
  30. if (json.choices && json.choices.length > 0) {
  31. const content = json.choices[0].delta.content
  32. if (content) {
  33. yield { id: json.created, content }
  34. }
  35. }
  36. if (json.choices && json.choices[0].finish_reason === 'stop') {
  37. done = true
  38. onStreamDone()
  39. break
  40. }
  41. } catch (e) {
  42. console.error(e)
  43. }
  44. }
  45. }
  46. if (streamDone) {
  47. done = true;
  48. }
  49. }
  50. }

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

  1. // vue父组件
  2. for await (const result of getChatgpt_Multurn_qa(messages.value, onStreamDone)) {
  3. if (currentConversationId.value === null) {
  4. currentConversationId.value = result.id;
  5. }
  6. if (result.id === currentConversationId.value) {
  7. const index = list.value.findIndex(item => item.id === currentConversationId.value);
  8. const dialog = list.value[index];
  9. dialog.content += result.content;
  10. } else {
  11. currentConversationId.value = result.id;
  12. list.value.push({
  13. content: result.content || '',
  14. role: "assistant",
  15. id: result.id,
  16. timestamp: Date.now()
  17. });
  18. messages.value.push({
  19. role: "assistant",
  20. content: result.content || ''
  21. });
  22. }
  23. }
  • 父子组件是如何通信的呢?
  1. // 父组件
  2. <session-box :list="list" @sent="handleSent"></session-box>

 

  1. // 子组件
  2. const props = defineProps({
  3. list: {
  4. type: Array,
  5. default: []
  6. }
  7. })
  8. const { list } = toRefs(props)
  9. const sessionList = ref(null)
  10. const sortedList = computed(() => {
  11. return list.value.slice().sort((a, b) => a.timestamp - b.timestamp)
  12. })

说明:

通过 computed 创建了一个名为 sortedList 的计算属性,该属性返回一个已排序的 list 数组副本。在排序过程中,使用了 slice 方法创建了一个数组副本,以避免直接修改原始数组。排序方式为按照每个数组元素的 timestamp 属性升序排序。

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

  •  选择合适的Markdown 编辑器组件库

介绍一下md-editor-v3 

官网:MdEditorV3 Documentation (imzbf.github.io)

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... (github.com)

文档说明:MdEditorV3 Documentation (imzbf.github.io)

 它提供了一些基础的 Markdown 编辑功能,如加粗、斜体、标题、无序列表、有序列表、引用、代码块等。除此之外,它还支持上传图片、撤销/重做、全屏等功能。md-editor-v3 的优点是易于使用、易于扩展,并且提供了一些定制化的选项。但是我只是想实现代码块,故解构出MdPreview

 使用:

  1. // 模板中
  2. <MdPreview
  3. :showCodeRowNumber="true" // 显示行号
  4. :modelValue="item.content"
  5. />
  6. import { MdPreview } from 'md-editor-v3'
  7. import 'md-editor-v3/lib/style.css'

效果图:

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号