最近进行了AI答疑相关的需求开发,总共进行了两次版本迭代,在此做个记录并分享前端的实现过程。
需求描述
前端进行提问,接口这边使用AI进行答疑,回答内容流式输出到前端。接口的 Response Headers 的 Content-Type 为 text/event-stream; charset=utf-8
。
第一个版本
前端实现:
/** 获取答案 */
export const getAnswer = (params) => {
return fetch('https://gpt.xxxx.com/chat',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(params)
}
)
}
// 调用
getAnswer({
question: '为啥?'
})
.then(async response => {
if (!response.ok) {
throw new Error('Network response was not ok')
}
// 确保响应是可读流
if (!response.body) {
throw new Error('Response body is not available')
}
const reader = response.body.getReader()
const textDecoder = new TextDecoder()
let result = true
let output = ''
while (result) {
const { done, value } = await reader.read()
if (done) {
console.log('Stream ended')
result = false
break
}
const chunkText = textDecoder.decode(value)
output += chunkText
}
console.log('output:', output)
})
.catch(() => {
})
为什么不用 axios:开始也尝试使用过 axios,设置 responseType: 'stream'
,但并没有实现流式输出。
此版本存在的缺陷:后端将前端的提问传给 chatgpt,等带 chatgpt 输入完后才流式输出给前端…
第二个版本
后端修改了第一个版本的不合理的流式输出,采用了 SSE 模式实现,不用等待 chatgpt 回答完成才输出内容。
简单介绍下SSE
SSE:Server-Sent Events 服务器推送事件,简称 SSE,是一种服务端实时主动向浏览器推送消息的技术。
SSE 是 HTML5 中一个与通信相关的 API,主要由两部分组成:服务端与浏览器端的通信协议(HTTP 协议)及浏览器端可供 JavaScript 使用的 EventSource 对象。
从“服务端主动向浏览器实时推送消息”这一点来看,该 API 与 WebSockets API 有一些相似之处。但是,该 API 与 WebSockers API 的不同之处在于:
Server-Sent Events API | WebSockets API |
---|---|
基于 HTTP 协议 | 基于 TCP 协议 |
单工,只能服务端单向发送消息 | 全双工,可以同时发送和接收消息 |
轻量级,使用简单 | 相对复杂 |
内置断线重连和消息追踪的功能 | 不在协议范围内,需手动实现 |
文本或使用 Base64 编码和 gzip 压缩的二进制消息 | 类型广泛 |
支持自定义事件类型 | 不支持自定义事件类型 |
连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100) | 连接数无限制 |
前端实现
/** 获取答案 */
export const getAnswer = (
params: IAnswerParams,
config?: {
onopen?: (response: Response) => Promise<void>
onmessage?: (e: EventSourceMessage) => void
onerror?: (e: any) => void
onclose?: () => void
}
) => {
let p = Object.assign({}, params, {
env: envObj[import.meta.env.VITE_ENV]
})
removeAbort()
window.abortControllerAi = new AbortController()
// console.log(' --- AbortController signal --- ', window.abortControllerAi.signal)
window.abortControllerAi.signal.addEventListener('abort', () => {
console.log(' --- AbortController abort --- ')
// 用户手动中止
})
return fetchEventSource(
'https://gpt.xxxx.com/chat',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(p),
signal: window.abortControllerAi.signal,
openWhenHidden: true, // 页面不可见时不中断链接, 这个一定要设置!!!
...config
}
)
}
/** 中断ai请求 */
export const abortAiRequest = () => {
if (window.abortControllerAi) {
window.abortControllerAi.abort()
removeAbort()
}
}
function removeAbort() {
if (window.abortControllerAi) {
window.abortControllerAi.signal.removeEventListener('abort', () => {})
}
}
// 调用
let answer = ref('')
getAnswer(
{ question: '为啥?' },
{
onmessage(event) {
// console.log(event.data)
// event.data 为 [DONE] 代表结束
let value: any = {}
try {
value = JSON.parse(event.data)
} catch (e) {}
// 回答内容
let str = ''
try {
str = value.choices.map(i => i.delta.content).join('')
} catch (e) {}
answer.value += str
},
onclose() {
console.log('close')
},
onerror(err) {
console.log('err', err)
throw new Error() // 中断链接
}
}
)
这里使用了 '@microsoft/fetch-event-source'
库,以解决 EventSource
的不足(不支持自定义请求头、能使用get等)。