当前位置: 首页 > news >正文

前后端流式交互的几种方式

        最近在用AI做练手项目,做的最多的就属聊天助手了,是的,用聊天助手做聊天助手,我做了个很多个版本,有基于electron的,有基于pyqt5的,还有基于web的,web的有用flask框架的,还有用fastapi的,尝试下来,发现做聊天助手要解决的无非下面几个问题,1. 流式交互。2. markdown格式解析。markdown解析有现成的库,我看代码都差不多,无非是把AI返回的回答用markdown解析模块转换一下变成html,再把html显示出来。当然本地客户端的写法和web可能不太一样,因为像pyqt5的控件本身就支持markdown格式,所以压根就不用转,只不过显示的样式没有web的好看,所以,想要好看的话,最好是基于html的界面。然后流式交互我发现就有点意思了,这里面涉及到很多方式,下面对这些方式做一下总结。

        1. electron。electron前端是html+css+js这些,然后,后端是node,其实也是js,但是像这种封装度比较高的框架,前后端的交互往往都是写好的,基本就是发消息,动态交互的话,依赖已经写好的包,electron一般有三个主要文件,一个main.js,主要是后端的逻辑,然后有个preload.js,负责前后端的绑定,还有一个是render.js,主要负责前端的逻辑,然后electron的流式交互主要是通过event实现的,每当大模型返回一点内容,就使用event对象向一个消息接收函数发送,具体代码如下

ipcMain.handle('api-query', async (event, prompt) => {try {const stream = await client.chat.completions.create({messages: [{ role: 'user', content: prompt }],model: 'deepseek-ai/DeepSeek-V3-0324',stream: true,})let fullResponse = '';for await (const chunk of stream) {var content = chunk.choices[0]?.delta?.content || '';//content = content.replace(/\s/g, '');fullResponse += content;event.sender.send('stream-chunk', content);console.log(content);}return fullResponse;} catch (error) {console.error('API Error:', error);throw { message: `API req failed: ${error.message}` };}
});

这里是main.js里的一段代码,是负责响应页面的发送消息按钮的消息的,也就是说,页面点击发送,后台就会调用这个函数,我们可以看到,当用户点击发送自己的问题,那么后台就开始接收用户的问题,prompt,同时进来的还有个event参数,后面当遍历模型的回复时,每当收到一个回复,就调用event.sender.send(),像界面的一个函数,发送一条内容,然后再看下绑定代码preload.js

contextBridge.exposeInMainWorld('electronAPI', {marked: marked,sendQuery: (prompt) => ipcRenderer.invoke('api-query', prompt),receiveStreamChunk: (callback) => ipcRenderer.on('stream-chunk', (event, chunk) => callback(chunk))
});

event对象是向stream-chunk函数发送的消息,这里看起来比较绕,首先,receiveStreamChunk是前端的一个函数,然后这个函数的参数还是个函数,当收到名字为stream-chunk的消息时,调用callback函数处理这个消息。再看下前端的receiveStreamChunk函数

window.electronAPI.receiveStreamChunk((chunk) => {if(currentResponseDiv) {buffer += chunk || '';content = marked.parse(buffer)currentResponseDiv.innerHTML = content;chatContainer.scrollTop = chatContainer.scrollHeight;}});

当然这里面没什么特别的,只是用了匿名函数的写法,其实把函数单独摘出来也是一样的道理,这里可能是为了方便,所以js里面由于好多参数都是回调函数,所以会比较绕,用法也基本是固定的了,只是需要一定的适应,至于说为什么非要再套一层,这个就是js的特点了,js里面全是异步,只要涉及到异步,就要套个回调函数,而不是直接传参。

        2. pyqt5的桌面程序。其实桌面程序大体的编程逻辑都有些相似,上面说了electron,其实pyqt5也是差不多的,pyqt5主要通过信号和槽的机制在不同线程中传递消息,信号其实就是给消息起个名字,防止混乱,槽就是接收消息的函数,因为桌面程序界面本来就占一个线程,而调用大模型时比较耗时,如果直接在界面类里写就会阻塞界面导致界面一卡一卡的,所以,一般情况下,比较耗时的操作需要单独开一个线程,然后通过信号和槽和界面通信,这样就不会阻塞界面,或者在每次收到回复的后面加一句QApplication.processEvents()  # 更新UI,这个方法也行,但是不如用线程优雅,或者如果代码较多的话,还是建议单独写一个线程。

        用pyqt5的话除了使用现成的控件以外,还有一种是使用web控件,就是界面直接用html+css+js编写,这种方式就跟electron几乎是一致的,只不过用的后端语言不同,前后端的通信模式也很类似,只不过python不强调异步,所以感觉上理解上要比js好懂。看下代码

class Worker(QThread):def __init__(self, api_client,config,bridge,user_input):super().__init__()self.api_client = api_clientself.config = configself.user_input = user_inputself.bridge = bridgeself.conversation = []def run(self):full_response = ""self.conversation.append({"role": "user", "content": self.user_input})for chunk in self.api_client.chat_completion_stream(self.conversation,model=self.config["model"],):# 收集流式内容self.bridge.sendMessage(chunk)

Worker类主要负责调用大模型api,获取回答,然后发送给界面,api_client是调用大模型的api,config主要是调用大模型的一些参数配置,比如模型名称,温度,最大token数等,唯一需要注意就是bridge了,这个bridge就是python和前端js通信的桥梁,其实和electron的preload.js做的事差不多,就是绑定前端和后端的函数和消息,看下bridge的代码

class Bridge(QObject):messageReceived = pyqtSignal(str)messageSent = pyqtSignal(str)@pyqtSlot(str)def handleMessage(self, message):print("message",  message)#self.messageSent.emit(f"{1}")self.messageReceived.emit(message)@pyqtSlot(str)def sendMessage(self, message):self.messageSent.emit(message)

它是一个类,这个类是必须的,只要前端用浏览器控件,你想要和前端js通信,都要写这么一个类,类的名字其实无所谓,叫什么都行,最主要的是里面的信号和函数,负责接收和发送消息,跟前端进行通信,xxx.emit(xx)就是发送消息。再看下前端怎么接收消息的

window.pyObj.messageSent.connect(function(message) {if (current_msg_div) {all_content += message || '';html_content = marked.parse(all_content);if (all_content){current_msg_div.innerHTML = html_content;hljs.highlightAll();window.scrollTo(0, document.body.scrollHeight);}}});

熟悉pyqt5的小伙伴都知道,在pyqt5使用通过connect函数来绑定消息和消息处理函数的,在js里,因为习惯用匿名函数,所以写法上保持了js的特点,直接就在参数那写函数逻辑了,其实单独写个函数,然后在连接这写函数名也是可以的。然后这里的window.pyobj,其实就是刚刚的Bridge类的实例对象,前面有个绑定的过程

window.pyObj = channel.objects.pyObj;

然后pyqt5官方提供了一个叫qwebchannel.js的文件,它就是负责js和python通信的,细节咱就不研究了,凡是前端需要导入这个js代码。这个就是用pyqt5+webengineview实现的桌面版聊天应用,因为用到webengineview,它其实相当于给软件集成了浏览器,所以打包出来非常大,好处是可以用html写界面,可用的库是非常多的。

        3. 还有一种就是web了,前端还是html+css+js,网站由于是和服务器通信,这块的流式交互就有很多方法,我看ai实现的主要由两种,一种是用fetch+reader的方式(kimi k2),一种是用eventsource方式,这两种都是js内部实现的,不需要第三方模块,还有一个是fetcheventsource,微软的一个实现,这个和eventsource不同的是它可以用post方式请求,eventsource只支持get方式看下fetch+reader的方式代码

async function sendMessage(message) {if (!message.trim()) return;// 添加用户消息到界面addMessage(message, true);messages.push({ role: 'user', content: message });// 清空输入框并禁用发送按钮chatInput.value = '';sendButton.disabled = true;showTyping();try {const response = await fetch('http://localhost:8000/api/chat', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({messages: messages,model: modelSelect.value,temperature: 0.7,max_tokens: 65536})});if (!response.ok) {throw new Error('服务器响应错误');}const reader = response.body.getReader();const decoder = new TextDecoder();let assistantMessage = '';const assistantContentDiv = addMessage('');while (true) {const { done, value } = await reader.read();if (done) break;const chunk = decoder.decode(value);const lines = chunk.split('\n');for (const line of lines) {if (line.startsWith('data: ')) {const data = line.slice(6);if (data === '[DONE]') {messages.push({ role: 'assistant', content: assistantMessage });hideTyping();sendButton.disabled = false;return;}try {const parsed = JSON.parse(data);if (parsed.content) {assistantMessage += parsed.content;htmlContent = marked.parse(assistantMessage);//assistantContentDiv.textContent = assistantMessage;assistantContentDiv.innerHTML = htmlContent;hljs.highlightAll();chatMessages.scrollTop = chatMessages.scrollHeight;}} catch (e) {console.error('解析响应数据失败:', e);}}}}} catch (error) {console.error('发送消息失败:', error);hideTyping();showError('发送消息失败,请检查网络连接或稍后再试。');sendButton.disabled = false;}}

但是不知道什么情况,我做的这个项目,在多次问答后,前端就莫名的卡死,不知道为什么,不知道是不是这种方式有bug,后端我打印,都是没问题的,回答已经很快的返回,但是前端就是不显示,用console.log打印,也是卡。eventsource因为只能用get方式请求,需要改后端代码,就没测,fetcheventsource需要安装第三方包,所以也没测,我太懒了。。。

        好的,上面就是近期AI项目总结的几种前后端流式交互的总结,写的比较乱,有问题的小伙伴可以留言。

http://www.lryc.cn/news/609842.html

相关文章:

  • Petalinux快捷下载
  • 【笔记】ROS1|2 Turtlebot3汉堡Burger连接和远程控制【旧文转载】
  • 【SpringAI】SpringAI的介绍与简单使用
  • 算力板卡:驱动智能时代的核心引擎
  • File、IO流体系
  • 防御保护综合练习
  • 关键领域软件研发如何构建智能知识管理体系?从文档自动化到安全协同的全面升级
  • 详解Python标准库之通用操作系统服务
  • ZeroNews内网穿透安全策略深度解析:构建企业级安全连接体系
  • 【2025】想曰(yue)免费开源的文本加密软件,保障隐私安全
  • 福彩双色球第2025089期篮球号码分析
  • 竞品分析爬虫实现方案
  • 人类学家与建筑师:解析 UX 研究与项目管理的需求分析差异​
  • Opencv[一]
  • # 自动定时运行Python爬虫脚本教程(Windows任务计划程序)
  • 项目实战二:RPC
  • 17.6 超拟人大模型CharacterGLM技术解析:92.7%角色一致性+虚拟偶像互动提升300%,如何吊打GPT-4?
  • C++-异常
  • Python----大模型(量化 Quantization)
  • MySQL详解(一)
  • 从零开始的云计算生活——项目实战
  • 商标续展如果逾期了还有办法补救吗?
  • 消息系统技术文档
  • 学习嵌入式第十九天
  • 系统一个小时多次Full GC,导致系统线程停止运行,影响系统的性能,可靠性
  • 靶场(二十八)---小白心得靶场体会---Mantis
  • 前端VUE基础环境搭建
  • STM32_Hal库学习SPI
  • ctfshow:pwn85(高级ROP 64 位 Partial-RELRO)、pwn141
  • 探访WAIC2025:当AI成为双刃剑,合合信息如何破解真假难题