新闻详情

新闻详情

首页 / 资讯中心 / 详情

AI对话系统前后端分离搭建教程:SSE流式输出与源码实战

发布时间:2026/10/2 18:46:34来源:尧图网络
AI对话系统前后端分离搭建教程:SSE流式输出与源码实战
简介这是一套面向开发者与AI应用爱好者的对话系统网站源码基于自然语言处理技术实现人机交互可结合上下文进行多轮对话并支持撰写邮件、视频脚本、文案、翻译、代码及论文等任务同时预留了对接GPT、阿里云、腾讯云等主流大模型服务的接口适合用于搭建自有AI对话站点或二次开发学习。资源包共1612个文件以783个js脚本、192个html页面、120个css样式、118个vue组件及39个php后端文件为主另含json配置、scss样式、md说明与sql建库脚本等后端运行环境为PHP7.4搭配MySQL5.6压缩包约18.79MB。目前已有1910人学习下载。整体目录结构清晰前端组件与后端接口分层明确读者可据此快速理解对话系统的请求链路、页面组织与数据表设计并在此基础上替换模型接口、调整交互逻辑完成从部署到定制的完整实践。1. 从零搭一套 AI 对话系统前后端源码怎么选、怎么跑、怎么改很多人第一次接触 AI 对话系统是被“人工智能”四个字吓住的觉得背后一定藏着大模型训练、GPU 集群、算法推导。真上手做一套能用的对话系统网站你会发现核心工作量其实落在前后端工程上前端负责会话界面、流式渲染、历史记录后端负责会话管理、模型接口转发、上下文拼装。标题里的“源码 搭建教程 前后端”说的就是这条链路。它适合两类人一类是手里有模型 API、想快速做出可演示产品的开发者另一类是想拿一个完整项目练前后端分离实战的学生或转行者。下面我按真实搭建顺序把选型、目录、接口、参数和踩坑一次讲透。2. 前后端分离架构怎么定AI 对话系统的目录与数据流2.1 为什么对话系统几乎都选前后端分离对话系统和普通 CRUD 后台最大的区别在于“流式输出”。用户发一句话模型不是一次性返回整段文本而是逐 token 吐出来。如果后端把整段结果拼完再返回用户要盯着空白页等好几秒体验直接崩。前后端分离之后前端可以用fetch的ReadableStream或EventSource边收边渲染后端只负责把模型返回的流透传出去两边职责清晰。技术栈上常见做法是前端 Vue 或 React后端 Spring Boot 或 Node.js。热搜里“springboot vue 前后端分离”“若依框架前后端分离”出现频率很高说明国内团队默认就是这套组合。我一般推荐前端 Vue3 Vite Pinia后端 Spring Boot 3 WebFlux或 Node.js Express。选 WebFlux 而不是 Spring MVC是因为流式响应在阻塞式线程模型下容易把线程池占满WebFlux 的响应式流天然适配 SSE。数据库只需要两张核心表会话表conversation和消息表message。会话表存标题、创建时间、用户标识消息表存角色user/assistant、内容、token 数、所属会话。不要一上来就上向量库除非你要做知识库检索那是第二阶段的事。2.2 一套能跑的最小目录结构下面是我常用的目录骨架前端后端分开接口层单独抽出来方便后面替换模型供应商。ai-chat/ ├── frontend/ # Vue3 前端 │ ├── src/ │ │ ├── api/chat.js # 封装 SSE 请求 │ │ ├── stores/session.js # Pinia 会话状态 │ │ ├── views/Chat.vue # 对话主界面 │ │ └── components/ │ │ ├── MessageList.vue │ │ └── InputBox.vue │ └── vite.config.js # 配置 /api 代理到后端 ├── backend/ # Spring Boot 后端 │ ├── src/main/java/com/example/chat/ │ │ ├── controller/ChatController.java │ │ ├── service/ModelService.java │ │ ├── service/ConversationService.java │ │ └── config/WebConfig.java # 跨域与超时 │ └── src/main/resources/ │ └── application.yml # 模型地址、密钥、超时 └── docker-compose.yml # 一键起 MySQL 后端 前端这个结构的关键点是api/chat.js和ModelService.java两个文件它们分别承担前端流式接收和后端流式转发。把这两个文件写对整套系统就活了。目录里没有多余的东西新手照着建不会迷路熟手也能一眼看出扩展点在哪。2.3 数据流一条消息从输入框到屏幕的完整路径用户在输入框敲字回车前端做三件事把用户消息 push 进本地消息列表、生成一个空的 assistant 消息占位、发起 SSE 请求。请求带上conversationId和content后端收到后先落库用户消息再调用模型接口把模型返回的流逐段写回响应。后端写回时要注意响应头Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive。少一个浏览器可能就把流缓冲起来用户看到的就是“一次性蹦出来”。前端接收时用TextDecoder解码按\n\n切分事件块取出data:后面的内容追加到占位消息上。这条链路里最容易出问题的是“上下文拼装”。每次请求不能只发当前这句话要把该会话的历史消息按顺序带上但要注意 token 上限。常见做法是保留最近 N 轮或者按 token 数从后往前截断。我一般设 N10超过就丢弃最早的对话并在系统提示里说明“早期对话已省略”避免模型答非所问。3. 后端接口怎么写流式转发、会话落库与参数配置3.1 用 Spring Boot WebFlux 写一个 SSE 转发接口后端核心就一个接口接收用户消息返回流式响应。下面是最小可用版本用 WebFlux 的Flux做流式返回。RestController RequestMapping(/api/chat) public class ChatController { Autowired private ModelService modelService; Autowired private ConversationService conversationService; PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString stream(RequestBody ChatRequest req) { // 1. 落库用户消息 conversationService.saveUserMessage(req.getConversationId(), req.getContent()); // 2. 拼装历史上下文 ListMessage history conversationService.loadRecent(req.getConversationId(), 10); // 3. 调用模型并流式返回 return modelService.chatStream(history) .map(chunk - ServerSentEvent.builder(chunk).build()) .doOnNext(chunk - conversationService.appendAssistantChunk( req.getConversationId(), chunk.data())) .doOnComplete(() - conversationService.finishAssistantMessage( req.getConversationId())); } }逻辑说明produces指定 SSE 类型Spring 会自动加响应头。loadRecent取最近 10 条消息这是上下文窗口的第一道闸。doOnNext每收到一段就追加到数据库这样即使连接中断已生成的内容也不会丢。doOnComplete标记该条 assistant 消息结束方便前端区分“正在生成”和“已完成”。参数说明ChatRequest至少包含conversationId和content两个字段。loadRecent的第二个参数是轮数不是消息条数一轮包含一问一答。如果你用的模型上下文窗口小把这个值降到 5窗口大可以提到 20但要注意响应延迟会上升。3.2 模型调用的三个必调参数不管后面接的是哪家模型有三个参数必须显式设置否则默认值往往不适合对话场景。参数作用对话场景建议值调错的后果temperature控制随机性0.7太高答非所问太低回答死板max_tokens单次回复上限1024太小回答被截断太大浪费额度stream是否流式true设 false 前端要等整段生成temperature 是最玄学的一个。做客服问答调到 0.3做创意写作调到 0.9通用对话 0.7 比较稳。max_tokens 要和前端渲染配合如果前端没做“继续生成”按钮就设大一点避免回答到一半断掉。stream 必须为 true否则前面写的 SSE 接口就白费了。3.3 会话落库与 token 计数消息表建议加一个token_count字段。不是为了炫技是为了做两件事一是统计用户消耗二是决定上下文截断点。token 计数不用自己写分词器大多数模型接口返回的响应里带usage字段直接取来存即可。流式响应里 usage 通常在最后一个 chunk 才出现所以要在doOnComplete里回填。CREATE TABLE message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_id BIGINT NOT NULL, role VARCHAR(16) NOT NULL, content TEXT, token_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_conv (conversation_id, created_at) );索引建在conversation_id和created_at上因为查历史永远是“按会话 按时间排序”。不要只建主键索引消息一多查询会明显变慢。content用 TEXT 而不是 VARCHAR因为模型回复长度不可控VARCHAR 容易截断报错。4. 前端怎么接流SSE 解析、状态管理与渲染优化4.1 用 fetch 的 ReadableStream 接收 SSE很多人第一反应是用EventSource但它只支持 GET 请求而对话消息通常用 POST 发。所以更通用的做法是用fetch加ReadableStream手动解析。// src/api/chat.js export async function chatStream(conversationId, content, onChunk, onDone) { const resp await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ conversationId, content }) }); const reader resp.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // SSE 事件以 \n\n 分隔 const parts buffer.split(\n\n); buffer parts.pop(); // 最后一段可能不完整留到下次 for (const part of parts) { const line part.replace(/^data:\s*/, ); if (line) onChunk(line); } } onDone(); }逻辑说明decoder.decode的{ stream: true }很关键它保证多字节字符不会被从中间切断。buffer用来处理“一个事件块还没收完”的情况parts.pop()把不完整的尾段留到下一轮。onChunk每收到一段就更新界面onDone做收尾。参数说明conversationId为 null 时表示新会话后端应创建新会话并返回 id。实际项目里我会让后端在第一个 chunk 里带上conversationId前端收到后更新本地状态避免刷新页面后找不到会话。4.2 Pinia 管理会话状态三个容易写错的地方状态管理看起来简单但对话系统有三个坑。第一消息列表要用reactive数组不要用ref包数组再整体替换否则流式追加时 Vue 的响应式追踪会失效界面不更新。第二正在生成的消息要单独标记streaming: true渲染时显示光标动画完成后置为 false。第三切换会话时要 abort 掉上一个未完成的请求否则两个会话的流会串在一起。// src/stores/session.js import { defineStore } from pinia; import { ref } from vue; export const useSessionStore defineStore(session, () { const messages ref([]); const currentId ref(null); let controller null; function appendChunk(text) { const last messages.value[messages.value.length - 1]; if (last last.role assistant) { last.content text; // 直接改属性保持响应式 } } function abort() { if (controller) controller.abort(); } return { messages, currentId, appendChunk, abort }; });逻辑说明appendChunk直接修改最后一个消息对象的content属性而不是替换整个数组这样 Vue 能精确追踪变化。controller是AbortController实例切换会话时调用abort()中断请求。参数说明messages里每条消息结构建议为{ role, content, streaming, tokenCount }。streaming用于渲染光标tokenCount用于显示消耗没有也不影响功能。4.3 渲染优化长对话列表怎么不卡对话超过 50 条之后如果每条消息都做 Markdown 渲染滚动会明显掉帧。两个手段一是虚拟滚动只渲染可视区域内的消息二是流式过程中用纯文本流结束后再转 Markdown。第二条尤其有效因为流式阶段每来一个 chunk 就重新解析 Markdown 是性能杀手。我一般会在MessageList.vue里判断message.streaming为 true 时用pre直接显示文本为 false 时才走 Markdown 组件。这样既保证生成过程流畅又保证最终展示效果。虚拟滚动可以用vue-virtual-scroller但要注意消息高度不固定需要开启动态高度模式。5. 避坑与排查搭建 AI 对话系统最常见的 5 个翻车点5.1 流式响应变成一次性返回现象前端等了很久然后整段回答突然出现没有逐字效果。原因通常是后端响应被缓冲了。可能是 Nginx 开了proxy_buffering on也可能是 Spring 的过滤器把响应包了一层。解决Nginx 配置里加proxy_buffering off;和proxy_cache off;后端确认没有额外的ResponseBodyAdvice拦截流式接口。另外检查响应头是否真的带了text/event-stream少了浏览器也会缓冲。5.2 中文乱码或半个字现象流式输出里偶尔出现乱码或者某个汉字显示成问号。原因是TextDecoder没有用流式模式多字节字符被从中间切断。解决decoder.decode(value, { stream: true })这个参数必须加。后端侧确认响应编码是 UTF-8produces里可以显式写charsetUTF-8。如果用的是 Node.js 后端注意res.write的 chunk 边界也可能切断字符必要时在服务端做缓冲。5.3 上下文越聊越慢、越聊越贵现象同一个会话聊到二三十轮后响应明显变慢费用也上去了。原因是每次请求都把全部历史发给了模型。解决在loadRecent里做截断按轮数或 token 数限制。更精细的做法是保留系统提示 最近 N 轮 最早一轮保留初始设定中间的直接丢弃。如果业务需要长期记忆那是知识库检索的范畴不要靠无限堆上下文解决。5.4 切换会话后消息串台现象在会话 A 生成到一半时切到会话 B结果 A 的内容跑到了 B 的列表里。原因是流式回调里没有校验当前会话 id。解决在appendChunk前判断currentId是否等于请求发起时的会话 id不等就直接丢弃。同时切换会话时调用abort()中断请求。这个坑很隐蔽因为不切换时永远不出现一上线多用户操作就暴露。5.5 部署后接口 404 或跨域现象本地跑得好好的部署到服务器后前端请求 404 或报 CORS 错误。原因是前后端分离部署时前端静态资源和后端接口不在同一个域。解决开发环境用 Vite 的server.proxy代理生产环境用 Nginx 把/api转发到后端端口。跨域问题优先用 Nginx 同源转发解决而不是在后端无脑加CrossOrigin后者在带凭证的请求下会失效。热搜里“tomcat 部署前后端分离项目”“windows 上用 jenkins 部署前后端分离项目”说的就是这类部署问题核心都是把静态资源和接口路径理清楚。6. 进阶技巧给对话系统加一个可验证的“模型切换”层一套对话系统做完最值钱的扩展点不是界面美化而是模型切换层。因为模型迭代太快今天用的接口明天可能就换了如果把模型调用写死在ModelService里每次换模型都要改业务代码。我的习惯是抽一个ModelProvider接口用配置决定加载哪个实现。public interface ModelProvider { FluxString chatStream(ListMessage history); } Component ConditionalOnProperty(name model.provider, havingValue openai) public class OpenAiProvider implements ModelProvider { /* ... */ } Component ConditionalOnProperty(name model.provider, havingValue local) public class LocalProvider implements ModelProvider { /* ... */ }配置里写model.provideropenai就加载对应实现换模型只改一行配置。这个设计的验证方法很简单起两个实现用同一个问题分别请求对比首 token 延迟和完整响应时间。首 token 延迟决定用户感知的“快慢”完整响应时间决定成本。我一般会记录这两个指标做成一个简单的对比表换模型时用数据说话而不是凭感觉。指标含义采集方式关注阈值首 token 延迟用户看到第一个字的等待时间请求发出到第一个 chunk 到达小于 1.5 秒完整响应时间整段回答生成完毕请求发出到流结束视长度而定token 消耗单次对话成本响应 usage 字段按预算控制最后说一个我踩过的坑不要在没有超时控制的情况下上线。模型接口偶尔会卡住不返回也不报错前端就一直转圈。后端必须设连接超时和读超时我一般设连接 5 秒、读 60 秒超时后主动结束流并给前端一个“响应超时请重试”的提示。这个后悔药希望你提前吃。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

HowToCook 干锅花菜实战指南:湘味家常菜的标准化做法与火候原理 2026/10/2 21:02:12

HowToCook 干锅花菜实战指南:湘味家常菜的标准化做法与火候原理

文档教程 【免费下载链接】HowToCook Programmers guide about how to cook at home. 项目地址: https://gitcode.com/GitHub_Trending/ho/HowToCook 点击查看 免费下载 干锅花菜是一道湘味家常菜,以脆嫩干香的花菜搭配焦香四溢的五花肉为核心&#xff…

阅读更多 →
SnapOtter权限管理:RBAC角色、17种细粒度权限与API密钥范围完全指南 2026/10/2 21:01:59

SnapOtter权限管理:RBAC角色、17种细粒度权限与API密钥范围完全指南

SnapOtter权限管理:RBAC角色、17种细粒度权限与API密钥范围完全指南 【免费下载链接】SnapOtter Open-source, self-hosted file-processing tool. Convert, compress, OCR, transcribe & run local AI across image, video, audio, PDF & documents, via UI, REST API…

阅读更多 →
FreeRTOS 实战教程-第一章 2026/10/2 21:01:59

FreeRTOS 实战教程-第一章

第一章 FreeRTOS 基础与 CubeIDE 配置 1.1 从裸机到多任务:为什么要用 RTOS 先回顾一下我们写过的 9 个裸机工程,它们几乎都是同一种结构 —— 超级循环(Super Loop): int main(void) {HAL_Init(); /* 各种初始化 */SystemClock_Config();MX_GPIO_Init()…

阅读更多 →
单视频三维重构与无人机蜂群协同侦察阵地态势融合技术白皮书 2026/10/2 21:01:59

单视频三维重构与无人机蜂群协同侦察阵地态势融合技术白皮书

1. 摘要现代化阵地攻防作战呈现立体化、全域化、快节奏、高对抗发展趋势,传统二维视频监控、单机侦察、空地独立感知的态势体系已无法满足全天候、无盲区、连续化的战备感知需求。地面固定视频监控存在维度单一、纵深不足、遮挡盲区泛滥、立体态势缺失等短板&#x…

阅读更多 →
《动手学深度学习》Adadelta 优化算法全解:无需学习率的自适应梯度方法(d2l-zh) 2026/10/2 21:01:59

《动手学深度学习》Adadelta 优化算法全解:无需学习率的自适应梯度方法(d2l-zh)

人工智能深度学习机器学习教程 【免费下载链接】d2l-zh 《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。 项目地址: https://gitcode.com/GitHub_Trending/d2/d2l-zh 点击查看 免费下载 Adadelta 是 Ad…

阅读更多 →
南昌市热门的老板桌源头工厂排名及服务好的定制厂家推荐汇总 鑫恒家具 2026/10/2 21:01:59

南昌市热门的老板桌源头工厂排名及服务好的定制厂家推荐汇总 鑫恒家具

南昌选老板桌不想踩坑?这篇源头工厂挑选干货值得看完开篇导语: 老板桌(班台)是办公室里最能体现企业形象的一件家具,但市面上产品鱼龙混杂:板材异味重、封边开裂、大桌进不了电梯、售后没人管……在南昌,江西省鑫恒家具有限公司(…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉