現在有很多框架實現了流式渲染,我在這裏例舉幾個:
React框架
● Ant-Design-X
Vue框架
● Element-Plus-X
● MateChat (PC/H5雙端兼容)
H5移動端
● ChatUI-React
這裏以MateChat框架做示例,MateChat是一個獨立的AI對話組件,不與其它UI框架關聯,可直接引入項目使用。
一般的流式渲染分兩種模塊
1、深度思考
2、正文回答
我們拿到SSE接口返回的數據後,如果有深度思考,數據裏一般會有一個<think>標籤,標籤內的就是深度思考的內容,一般組件是支持識別深度思考標記的,無需我們過多處理。
let content = '<think>深度思考的內容</think>正文回覆,今天天氣不錯'
我們現在做一個真實的流式渲染示例
1、SSE消息回覆
SSE回來的消息是序列化過的,需要反序列化處理
onmessage(event: any) {
try {
// 反序列化
let data = JSON.parse(event.data);
// 完整的文本以分塊的方式輸出
flowAddText(data.answer, data.messageId);
} catch (err) {
console.log('反序列化出錯', err);
}
},
const flowAddText = async (text: string, messageId: string) => {
// 拿到倒數第一條信息
const lastChat = chatList.value.at(-1);
// 添加流式文本
lastChat.content += text;
// 添加messageId
lastChat.messageId = messageId;
// 最後一條加載中false,因為開始回覆了
lastChat.loading = false;
// 未完整回覆完畢
lastChat.contentLoading = false;
};
在這裏要注意一點,頁面展示的對話氣泡需要我們手動添加,我們現在用的MateChat框架,字段以框架説明為準,但最終消息都是要添加到chatList中做渲染的,我這裏封裝了一個hook,基本上AI對話所需的狀態都能覆蓋了,你可以根據實際情況做修改
import AIavatar from '@/assets/image/AI-avatar.png';
import { v4 as uuidv4 } from 'uuid';
/**
* AI對話通用方法
* @returns {startChat}
*/
export const useChatMethods = () => {
/**
* 添加自身對話氣泡
* @param {string} text 自身對話內容
* @returns 氣泡元信息
*/
const addSelfChat = (text: string) => {
return {
id: uuidv4(),
avatarConfig: {
imgSrc: 'https://tdesign.gtimg.com/site/avatar.jpg'
}, // 頭像
from: 'user', // 用户角色
content: text, // 內容
loading: false // 加載中
};
};
/**
* 添加AI對話氣泡
* @param {string} answer ai對話內容
* @param {boolean} loading 是否加載中
* @param {boolean} contentEnd 回答結束
* @param {boolean} feedback 點贊狀態
* @param {boolean} messageId 消息id
* @returns 氣泡元信息
*/
const addAIChat = (answer: string, loading: boolean, contentEnd: boolean, feedback: any, messageId?: string) => {
return {
id: uuidv4(),
messageId,
avatarConfig: {
imgSrc: AIavatar
}, // 頭像
feedback: {
rating: !feedback ? null : feedback.rating
}, // 點贊狀態
from: 'assistant', // 角色
content: answer, // 內容
loading, // 加載中
contentEnd, // 回答結束
expand: true // 默認展開思考
};
};
return {
addSelfChat,
addAIChat
};
};
在頁面中使用的流程
1、用户發送消息,添加自身氣泡
2、添加AI氣泡,AI氣泡狀態loading中,然後建立SSE連接
3、SSE的onmessage開始返回消息,AI氣泡loading停止,開始流式輸出
4、SSE返回完畢,重置所有你自定義的標記和置空所有連接。
const onSend = (text: string) => {
// 先添加自身氣泡
let myBubble = useChatMethods().addSelfChat(text);
chatList.value.push(myBubble);
// 然後添加建立SSE連接
startSSE();
}
const startSSE = () => {
// 連接前可以先清空所有狀態,避免重複連接
resetSSE(); // 你的自定義清空函數
// 添加AI氣泡
let myBubble = useChatMethods().addAIChat('', true, false, null, '');
chatList.value.push(myBubble);
// 建立SSE連接,你自己的邏輯......
// fetchEventSource()
}
下面是頁面結構,每個內容都用氣泡包裹,McBubble有一個默認插槽,可以自定義氣泡內的內容
<div class="chat-content">
<template v-for="(msg, index) in chatList" :key="msg.id">
<!-- 用户對話 -->
<McBubble
v-if="msg.from == 'user'"
:variant="'bordered'"
:content="msg.content"
:align="'right'"
:avatar-config="msg.avatarConfig">
</McBubble>
<!-- ai對話 -->
<McBubble
v-else
:loading="msg.loading"
:avatar-config="msg.avatarConfig"
:variant="'bordered'">
<!-- 深度思考的小框 -->
<div
v-if="msg.content.includes('<think>')"
class="think-toggle-btn"
@click="msg.expand = !msg.expand">
<LoadingIcon
class="think-loading"
v-if="!msg.content.includes('</think>') && !msg.contentEnd" />
<SvgIcon
:name="'think-end'"
:size="20"
v-else />
<span
class="think-text">
{{ isThinkEnd(msg.content) ? '已深度思考' : msg.contentEnd ? '已暫停思考' : '思考中...'}}
</span>
<i :class="['arrow', msg.expand ? 'icon-chevron-up-2' : 'icon-chevron-down-2']"></i>
</div>
<!-- McMarkdown渲染 -->
<McMarkdownCard :enable-think="true" :content="msg.content" theme="light"></McMarkdownCard>
</McBubble>
</template>
</div>