智能体项目实现AI对话流式返回效果

1、智能体项目里与AI大模型对话的时候,需要从后端的流式接口里取数据并实现打字机渲染效果。这里涉及到 Markdown 格式的渲染,所以需要配合 marked.js 实现,安装 marked.js :

npm install marked

引用:

import { marked } from 'marked';

2、调用后端流式接口,并处理获取到的数据

         // 建立连接
        createSseConnect(){
            let token = '1215421542125'
            let that = this;
                // 创建一个 AbortController 实例
                that.abortController = new AbortController();
                
                //接口请求参数
                let sendData = {
                    message: that.currSendValue,
                    modelId:'1',
                }
                // 请求配置
                const config = {
                method: "post",
                headers: {
                    "Content-Type": "application/json",
                    Accept: "text/event-stream",
                    clientid: "112121212121212121212",
                    Authorization: "Bearer " + token, // 根据实际情况获取 token
                },
                body: JSON.stringify(sendData),
                signal: that.abortController.signal, // 将 AbortController 的 signal 传递给 fetch
                };
                let url = base.url + '/zntdata/stream';
                // 发起请求
                let thinkingTime = true;
                fetch(url, config)
                .then((response) => {
                const reader = response.body.getReader();
                const decoder = new TextDecoder("utf-8");
                let buffer = "";

                that.currSendValue = ''
                
                // 处理接收到的消息
                function processMessage(message) {
                    message = message.split("data:")[0];
                    
                    const newChars = message.split("");
                    //解析深度思考
                    if (message.includes("<think>")) {
                        thinkingTime = true;
                    }
                    if (message.includes("</think>")) {
                        thinkingTime = false;
                    }
                    if (thinkingTime) {
                        that.charQueue2.push(...newChars);
                    } else {
                        that.charQueue.push(...newChars);
                    }

                    if (message.includes("[DONE]")) {
                        that.dialogId = message.substring(9)
                    }

                    // 启动打字机效果(如果尚未启动)
                    if (!that.isTyping) {
                        that.startTyping();
                    }
                    return false;
                }

                // 读取流式数据
                function readStream() {
                    reader
                    .read()
                    .then(({ done, value }) => {
                        if (done) {
                            console.log("Stream ended",value);
                            return;
                        }

                        // 解码数据并添加到缓冲区
                        buffer += decoder.decode(value);

                        // 处理完整的事件 -- 非统一处理方法,需根据业务需求和接口数据格式处理
                        while (buffer.includes("data:")) {
                            if ( buffer.includes("[DONE]")) {
                               that.dialogId = 
                            buffer.substring(buffer.indexOf('id:')+3).replaceAll('\n','')
                            }
                            const eventEndIndex = buffer.indexOf("data:");
                            let eventData = buffer.slice(0,eventEndIndex);
                            buffer = buffer.slice(eventEndIndex+5);
                            eventData = eventData.substring(0,eventData.lastIndexOf('\n\n')) 
                            const message = eventData;
                            if (eventData) {
                                if (processMessage(message)) return;
                            }
                        }

                        // 继续读取
                        readStream();
                    }).catch((err) => {
                        console.error("Stream error:", err);
                        const lastQuestionIndex = that.dialogueList.length-1
                        that.dialogueList[lastQuestionIndex].content = that.dialogueList[lastQuestionIndex].content + '出错了,暂时无法回答您的问题,请稍后再试。'
                    });
                }

                    // 开始读取流
                    readStream();
                })
            },



         // 启动打字机效果
         startTyping() {
            const that = this;
            that.isTyping = true;
            const lastQuestionIndex = that.dialogueList.length-1

            // 初始间隔时间
            let intervalTime = 30; // 初始速度为 30ms
            const minIntervalTime = 5; // 最小间隔时间,防止速度过快
            const acceleration = 0.9; // 每次加速的比例(0.9 表示每次间隔时间减少 10%)

            // 清除已有定时器
            if (that.typingInterval) clearInterval(that.typingInterval);
            // 定义打字机效果函数
            function typeCharacter() {
                if (that.charQueue.length === 0 && that.charQueue2.length === 0) {
                   clearInterval(that.typingInterval);
                   that.isTyping = false;
                   return;
                }

                if (that.charQueue2.length > 0) {
                  if (that.dialogueList[lastQuestionIndex].content == '正在思考中...') {
                    that.dialogueList[lastQuestionIndex].content = ''
                  }
                   // 取出一个字符并更新界面
                  let char = that.charQueue2.shift();
                  if (char) {
                      that.thinkText += char;
                  }

                that.dialogueList[lastQuestionIndex].thinkText = marked.parse(that.thinkText) //渲染Markdown格式
                }

                if (that.charQueue.length > 0) {
                    let char = that.charQueue.shift();
                    if (char) {
                        that.answerWithFlow += char;
                    }

                    that.dialogueList[lastQuestionIndex].content = marked.parse(that.answerWithFlow) //渲染Markdown格式

                }


                // 滚动到底部
                that.scrollToSending();

                // 加速逻辑:减少间隔时间
                intervalTime = Math.max(minIntervalTime, intervalTime * acceleration);
                clearInterval(that.typingInterval); // 清除旧的定时器
                that.typingInterval = setInterval(typeCharacter, intervalTime); // 设置新的定时器
            }

            // 启动打字机效果
            that.typingInterval = setInterval(typeCharacter, intervalTime);
         },


         //保持最后一段对话实时出现在视口最下面
         scrollToSending() {
            this.$nextTick(() => {
                if (this.$refs.dialogueBox) {
                this.$refs.dialogueBox.scrollTop =
                    this.$refs.dialogueBox.scrollHeight +
                    this.$refs.dialogueBox.offsetHeight;
                }
            });
          },

3、渲染数据。要保持Markdown的格式输出,不能直接使用花括号{{}}渲染数据,需要结合v-html使用。

以上就能实现逐字渲染的一个AI大模型对话需求

### UniApp 中实现 AI 聊天流交互 在构建基于 UniApp 的应用程序时,为了实现实时的逐字或逐词输出效果来增强用户体验,可以借鉴现有的 jQuery Spring Boot SSE 方案[^2]。然而,在具体实施过程中,考虑到框架特性不同,需采用适合 Vue.js 生态系统的解决方案。 #### 使用 WebSocket 或 Server-Sent Events (SSE) 对于服务器端推送消息给客户端的应用场景,WebSocket 是一种双向通信协议;而当只需要单向传输数据时,则可选用更轻量级的技术——Server-Sent Events(SSE)[^2]。这两种方法都能很好地支持流输出需求。 #### 客户端代码示例 下面是一个简单的例子展示如何利用 JavaScript 实现在 UniApp 应用程序内的逐字符渲染功能: ```javascript // 假设这是来自AI的回答字符串 let responseFromAi = "这是一个模拟的人工智能回复"; function streamResponse(containerId, text) { let container = document.getElementById(containerId); let index = 0; function typeWriter() { if (index < text.length) { container.innerHTML += text.charAt(index); index++; setTimeout(typeWriter, Math.random() * (80 - 30) + 30); // 随机延迟增加真实感 } } typeWriter(); } export default { mounted() { this.$nextTick(() => { streamResponse('chatBox', responseFromAi); }); }, }; ``` 此段脚本会在页面加载完成后自动调用 `streamResponse` 函数,并按照设定的时间间隔依次将字符追加到指定 DOM 元素内,从而达到流畅的文字滚动播放效果[^3]。 #### 后端接口设计建议 如果计划让前端能够接收到来自后端持续更新的数据包而非一次性获取全部内容的话,那么应该考虑调整 API 设计模以适应这种增量的请求/响应机制。例如,可以通过 RESTful 接口配合分页参数或是直接切换至 Websocket/SSE 进行长连接通讯。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值