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

08-前后端分离改造-把Chatchat嵌入你的业务系统

前后端分离改造:把 Chatchat 嵌入你的业务系统

标签:前后端分离 | FastAPI | Vue | React | 系统集成 | 二次开发


一、Streamlit 的"天花板"

Chatchat 默认的前端是用Streamlit做的,开发速度快、代码量少,但用久了你会发现它的局限:

  • 样式难定制:想换个主题色?改个字体?很费劲
  • 交互受限:复杂的组件交互实现不了
  • 状态管理弱:多页面状态共享麻烦
  • 部署方式单一:只能以独立服务运行
  • 难集成:想嵌入现有系统?基本不可能

如果你要把 Chatchat 集成到公司的业务系统里,Streamlit 肯定不够用。

今天这篇,咱们就做前后端分离改造——保留 Chatchat 强大的后端能力,把前端换成现代化的 Vue/React 应用。


二、改造思路

2.1 现状分析

当前架构(耦合): ┌─────────────────────────────────┐ │ Streamlit WebUI │ │ (前端页面 + 直接调后端 API) │ ├─────────────────────────────────┤ │ FastAPI 后端 │ │ (API 路由 + 业务逻辑) │ └─────────────────────────────────┘ 目标架构(分离): ┌──────────────┐ HTTP API ┌──────────────┐ │ Vue/React │ ←──────────────→ │ FastAPI │ │ 前端应用 │ (OpenAI 兼容) │ 后端 │ │ │ │ (保留) │ └──────────────┘ └──────────────┘ ↓ ↓ 独立部署 独立部署 Nginx/CDN Docker/K8s

2.2 改造范围

模块改造方式工作量
FastAPI 后端保留,暴露标准 API
Streamlit WebUI替换为 Vue/React
用户体系对接现有系统
部署方式前后端分别部署

好消息:Chatchat 的后端 API 设计得不错,兼容 OpenAI 格式,前端替换成本不高。


三、后端 API 梳理

3.1 核心 API 列表

Chatchat 提供的 API 可以分为几类:

对话类:

接口方法说明
/chat/chatPOST通用对话(LLM / 知识库 / Agent)
/chat/chat/completionsPOSTOpenAI 兼容接口
/chat/feedbackPOST对话反馈评分

知识库类:

接口方法说明
/knowledge_base/listGET列出所有知识库
/knowledge_base/createPOST创建知识库
/knowledge_base/deletePOST删除知识库
/knowledge_base/update_infoPOST更新知识库信息
/knowledge_base/upload_docsPOST上传文档
/knowledge_base/delete_docsPOST删除文档
/knowledge_base/recreate_vector_storePOST重建向量库

工具类:

接口方法说明
/toolsGET列出可用工具

3.2 OpenAI 兼容接口详解

最重要的接口是/chat/chat/completions,它完全兼容 OpenAI 的格式:

# 请求示例POST/chat/chat/completions{"model":"qwen2-instruct","messages":[{"role":"system","content":"你是一个助手"},{"role":"user","content":"公司年假政策是什么?"}],"stream":true,"temperature":0.7,//Chatchat 特有参数(通过 extra_body)"extra_body":{"knowledge_base_name":"company-docs","tools":["search_internet"]}}
# 响应示例(非流式){"id":"chatcmpl-xxx","object":"chat.completion","created":1234567890,"model":"qwen2-instruct","choices":[{"index":0,"message":{"role":"assistant","content":"根据公司文档,年假政策如下..."},"finish_reason":"stop"}]}

3.3 流式响应(SSE)

# 流式请求POST/chat/chat/completions{"model":"qwen2-instruct","messages":[{"role":"user","content":"你好"}],"stream":true}# 流式响应(SSE 格式)data:{"choices":[{"delta":{"content":"你"}}]}data:{"choices":[{"delta":{"content":"好"}}]}data:{"choices":[{"delta":{"content":"!"}}]}data:[DONE]

四、前端开发实战(Vue3)

4.1 项目结构

chatchat-frontend/ ├── src/ │ ├── api/ # API 封装 │ │ ├── chat.js # 对话相关 │ │ ├── kb.js # 知识库相关 │ │ └── request.js # axios 配置 │ ├── components/ # 组件 │ │ ├── ChatBox.vue # 对话组件 │ │ ├── MessageItem.vue │ │ └── SourceDocs.vue │ ├── views/ # 页面 │ │ ├── ChatView.vue │ │ ├── KBManageView.vue │ │ └── SettingsView.vue │ ├── stores/ # Pinia 状态管理 │ │ └── chat.js │ └── App.vue ├── package.json └── vite.config.js

4.2 API 封装

// src/api/request.jsimportaxiosfrom'axios'constrequest=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL||'http://localhost:7861',timeout:30000,})// 请求拦截器:加 tokenrequest.interceptors.request.use(config=>{consttoken=localStorage.getItem('token')if(token){config.headers.Authorization=`Bearer${token}`}returnconfig})exportdefaultrequest
// src/api/chat.jsimportrequestfrom'./request'exportconstsendMessage=(data)=>{returnrequest.post('/chat/chat/completions',{model:data.model||'qwen2-instruct',messages:data.messages,stream:data.stream??true,temperature:data.temperature||0.7,extra_body:{knowledge_base_name:data.knowledgeBase,tools:data.tools,}})}exportconstgetKnowledgeBases=()=>{returnrequest.get('/knowledge_base/list')}

4.3 对话组件核心逻辑

<!-- src/components/ChatBox.vue --> <template> <div class="chat-box"> <!-- 消息列表 --> <div class="messages" ref="messagesRef"> <MessageItem v-for="msg in messages" :key="msg.id" :message="msg" /> </div> <!-- 输入框 --> <div class="input-area"> <el-select v-model="selectedKB" placeholder="选择知识库"> <el-option v-for="kb in knowledgeBases" :key="kb.name" :label="kb.name" :value="kb.name" /> </el-select> <el-input v-model="inputText" type="textarea" rows="3" placeholder="输入问题..." @keydown.enter.prevent="send" /> <el-button type="primary" @click="send" :loading="loading"> 发送 </el-button> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { sendMessage, getKnowledgeBases } from '@/api/chat' import MessageItem from './MessageItem.vue' const messages = ref([]) const inputText = ref('') const selectedKB = ref('') const knowledgeBases = ref([]) const loading = ref(false) // 加载知识库列表 onMounted(async () => { const res = await getKnowledgeBases() knowledgeBases.value = res.data }) // 发送消息 const send = async () => { if (!inputText.value.trim()) return const userMsg = { id: Date.now(), role: 'user', content: inputText.value } messages.value.push(userMsg) const assistantMsg = { id: Date.now() + 1, role: 'assistant', content: '', sources: [] } messages.value.push(assistantMsg) loading.value = true try { // 流式请求 const response = await fetch('/chat/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'qwen2-instruct', messages: messages.value .filter(m => m.content) .map(m => ({ role: m.role, content: m.content })), stream: true, extra_body: { knowledge_base_name: selectedKB.value } }) }) // 处理 SSE 流 const reader = response.body.getReader() const decoder = new TextDecoder() 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]') continue try { const parsed = JSON.parse(data) const content = parsed.choices?.[0]?.delta?.content || '' assistantMsg.content += content } catch (e) { // 忽略解析错误 } } } } } finally { loading.value = false } inputText.value = '' } </script>

4.4 消息展示组件(含来源标注)

<!-- src/components/MessageItem.vue --> <template> <div :class="['message', message.role]"> <div class="avatar"> {{ message.role === 'user' ? '👤' : '🤖' }} </div> <div class="content"> <!-- Markdown 渲染 --> <div v-html="renderedContent" /> <!-- 来源文档 --> <div v-if="message.sources?.length" class="sources"> <div class="sources-title">📚 参考来源:</div> <el-collapse> <el-collapse-item v-for="(doc, idx) in message.sources" :key="idx" :title="doc.title" > <p>{{ doc.content }}</p> </el-collapse-item> </el-collapse> </div> </div> </div> </template> <script setup> import { computed } from 'vue' import MarkdownIt from 'markdown-it' const props = defineProps({ message: Object }) const md = new MarkdownIt() const renderedContent = computed(() => { return md.render(props.message.content) }) </script> <style scoped> .message { display: flex; gap: 12px; padding: 16px; margin: 8px 0; border-radius: 8px; } .message.user { background: #f0f7ff; flex-direction: row-reverse; } .message.assistant { background: #f5f5f5; } .sources { margin-top: 12px; padding-top: 12px; border-top: 1px dashed #ddd; } .sources-title { font-size: 12px; color: #666; margin-bottom: 8px; } </style>

五、用户体系对接

5.1 现状:Chatchat 无用户体系

Chatchat 默认没有用户登录、权限控制,这在企业场景下不够用。

5.2 方案:JWT + 现有用户系统

用户登录(你的业务系统) ↓ 颁发 JWT Token ↓ 前端每次请求带 Token ↓ Chatchat 后端验证 Token ↓ 根据用户身份返回对应数据

5.3 后端改造:加认证中间件

# server/middleware/auth.pyfromfastapiimportRequest,HTTPExceptionfromjoseimportjwtasyncdefauth_middleware(request:Request,call_next):# 排除公开接口ifrequest.url.pathin["/docs","/openapi.json"]:returnawaitcall_next(request)# 获取 Tokentoken=request.headers.get("Authorization","").replace("Bearer ","")ifnottoken:raiseHTTPException(status_code=401,detail="Missing token")try:# 验证 JWT(用你的密钥)payload=jwt.decode(token,SECRET_KEY,algorithms=["HS256"])request.state.user=payload# 把用户信息挂到请求上exceptjwt.JWTError:raiseHTTPException(status_code=401,detail="Invalid token")returnawaitcall_next(request)

5.4 知识库权限控制

# 根据用户返回不同的知识库列表@kb_router.get("/list")asyncdeflist_knowledge_bases(request:Request):user=request.state.user# 管理员看全部ifuser.get("role")=="admin":returnget_all_kbs()# 普通用户只看有权限的returnget_kbs_by_user(user["id"])

六、部署方案

6.1 开发环境

# 后端conda activate chatchat chatchat start-p# 只启动 API# 前端cdchatchat-frontendnpminstallnpmrun dev# Vite 热更新

6.2 生产部署

# docker-compose.ymlversion:'3'services:backend:image:chatchat-backend:latestports:-"7861:7861"environment:-CHATCHAT_ROOT=/data/chatchatvolumes:-./data:/data/chatchatfrontend:image:chatchat-frontend:latestports:-"80:80"depends_on:-backendenvironment:-VITE_API_BASE_URL=http://backend:7861

6.3 Nginx 配置

server { listen 80; server_name chatchat.yourcompany.com; # 前端静态资源 location / { root /var/www/chatchat-frontend; try_files $uri $uri/ /index.html; } # API 代理 location /api/ { proxy_pass http://localhost:7861/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

七、小结

这篇咱们完成了前后端分离改造:

✅ 分析了 Streamlit 的局限和改造必要性
✅ 梳理了 Chatchat 的核心 API(尤其是 OpenAI 兼容接口)
✅ Vue3 前端开发实战:API 封装、对话组件、SSE 流式处理
✅ 用户体系对接:JWT 认证、权限控制
✅ 生产部署方案:Docker + Nginx

改造后的收益:

维度改造前 (Streamlit)改造后 (Vue/React)
定制性完全可控
用户体验一般专业级
集成能力
维护成本
开发效率

建议:

  • 快速验证/个人使用 → Streamlit 够用
  • 企业级应用/产品化 → 必须前后端分离


你的前端技术栈是 Vue 还是 React?在集成大模型对话功能时遇到过什么坑?欢迎交流!

http://www.jsqmd.com/news/863625/

相关文章:

  • 武平县黄金回收哪家强?铭润稳居第一 - 亦辰小黄鸭
  • 泗洪县黄金回收哪家强?铭润稳居第一 - 亦辰小黄鸭
  • 镇江本地黄金回收六家老店服务周到诚信经营值得信赖 六大品牌 优选长悦 - 专业黄金回收
  • 第九篇:《软件测试中的常见误区与事实》
  • 泗水县黄金回收哪家强?铭润稳居第一 - 亦辰小黄鸭
  • 2026洗发水排行榜:不同发质都爱的5款修护洗发水 - 速递信息
  • 从数据到图形:ElGrapho数据模型与布局算法深度解析
  • 天赐范式第50天:当生活成为你每天必须照的镜子,实际上就是同行评议的反向蓝图——同时触发自审视
  • 实时API数据集成:从Yelp API到Postgres数据库的完整ETL流程
  • 从创意到分镜:用DeepSeek打造短视频一气呵成的秘密
  • REFramework游戏启动崩溃:如何高效解决注入冲突的实用解决方案
  • C# Mat对象 VS JaCoCo Win32_类:3个致命坑,谁才是代码维护的“真香“选手?
  • 泗县黄金回收哪家强?铭润稳居第一 - 亦辰小黄鸭
  • 5分钟掌握PlantUML Editor:代码驱动UML设计的终极解决方案
  • CANN/asc-devkit:uint8转uint32向量转换API
  • 如何在10分钟内搭建个人游戏串流服务器:Sunshine跨平台游戏流媒体终极指南
  • 2026年专业的精雕铸铝门制造商,精雕铸铝门定制厂家,推荐的精雕铸铝门工厂 - 品牌推广大师
  • 如何打造你的私人游戏云:Sunshine自托管串流服务器终极指南
  • 新疆市省心水电暖网络一站式:水磨沟专业的水电安装公司有哪些 - LYL仔仔
  • 第十篇:《软件测试的未来:AI测试、DevOps与测试左移》
  • CANN/asc-devkit矩阵计算优化实践
  • 现在完成时 和 现在完成进行时 区别
  • 科研绘图革命:3步让Matplotlib图表达到期刊发表标准
  • MOOTDX:Python量化投资的数据获取革命
  • 终极指南:如何在Windows上使用ViGEmBus实现完美游戏控制器模拟
  • 泗阳县黄金回收哪家强?铭润稳居第一 - 亦辰小黄鸭
  • Hotkey Detective终极指南:快速定位Windows热键冲突的智能侦探
  • 如何利用AI视频处理工具提升视频流媒体效率:Awesome Video中的机器学习应用指南
  • 3步解锁Beyond Compare 5专业版:Python密钥生成器终极指南
  • 如何用TV Bro让智能电视真正“上网“:面向新手的完整使用指南