Web应用全栈开发:从零构建基于Qwen3-0.6B-FP8的智能问答网站

发布时间:2026/5/17 16:42:07

Web应用全栈开发:从零构建基于Qwen3-0.6B-FP8的智能问答网站 Web应用全栈开发从零构建基于Qwen3-0.6B-FP8的智能问答网站想象一下你有一个想法想做一个自己的智能问答网站让用户能像跟真人聊天一样向一个聪明的AI助手提问。这个想法听起来很酷但一想到要同时搞定前端界面、后端服务、数据库还要集成一个复杂的AI模型是不是感觉头都大了别担心这篇文章就是为你准备的。我会带你一步步从零开始把一个完整的智能问答网站搭建起来。我们不用那些庞大到需要专业显卡的模型而是选择小巧但聪明的Qwen3-0.6B-FP8模型。它经过量化处理对硬件要求友好但理解能力和回答质量依然在线。更重要的是我们会构建一个现代Web应用该有的样子漂亮的交互界面、稳定的后端服务、用户登录、对话历史保存还有流畅的实时回答效果。无论你是想学习全栈开发还是想亲手实现一个AI应用跟着这篇实战指南走你都能收获一个可运行、可扩展的真实项目。1. 项目蓝图与核心思路在动手写代码之前我们先花几分钟看看这个网站最终会长什么样以及我们打算怎么把它做出来。这就像盖房子前先看设计图心里有谱后面才不会乱。我们的目标是构建一个“智能问答网站”。用户打开网站可以注册登录。登录后进入一个清爽的聊天界面在输入框里提问网站能像真人聊天一样一个字一个字地“流式”给出回答并且回答内容能正确显示为漂亮的Markdown格式比如加粗、列表、代码块。所有的对话记录都会被自动保存用户可以随时翻看历史。为了实现这个目标我们需要三块“积木”大脑AI模型Qwen3-0.6B-FP8。它负责理解问题并生成答案。我们选择它是因为它在保持不错能力的同时对内存和算力要求更低更容易在普通服务器上部署。后台与仓库后端 数据库我们用Python的FastAPI框架来搭建后端服务。它负责接收前端的请求调用AI“大脑”思考然后把结果返回。同时它还会连接一个数据库比如SQLite或PostgreSQL把用户信息和聊天记录妥善保存起来。门面与交互前端我们用Vue.js来构建用户看到的网页界面。它负责呈现漂亮的聊天窗口处理用户的输入和点击并和后端“悄悄”通信获取和展示AI的回答。整个流程是这样的用户在Vue前端输入问题 - Vue把问题发给FastAPI后端 - FastAPI调用Qwen模型得到答案 - FastAPI一边生成答案一边流式传回给Vue - Vue实时地把答案显示出来 - 同时FastAPI把这次对话记到数据库里。接下来我们就从搭建环境开始一步步把这些“积木”拼装起来。2. 环境搭建与模型准备工欲善其事必先利其器。我们先来把开发环境和核心的AI模型准备好。这一步做完后续的编码工作就会顺畅很多。2.1 创建项目与Python环境首先为我们的项目创建一个专属的文件夹并建立一个独立的Python环境避免包版本冲突。# 创建项目目录 mkdir smart-qa-website cd smart-qa-website # 创建Python虚拟环境假设你已安装Python3.8 python -m venv venv # 激活虚拟环境 # 在Windows上 venv\Scripts\activate # 在MacOS/Linux上 source venv/bin/activate激活后你的命令行提示符前面应该会出现(venv)字样表示你已经在这个独立环境中了。2.2 安装后端核心依赖我们的后端主要依赖FastAPI、数据库驱动以及AI模型相关的库。创建一个requirements.txt文件并安装它们。# requirements.txt fastapi0.104.1 uvicorn[standard]0.24.0 sqlalchemy2.0.23 pydantic2.5.0 pydantic-settings2.1.0 python-jose[cryptography]3.3.0 passlib[bcrypt]1.7.4 python-multipart0.0.6 httpx0.25.1 transformers4.36.0 accelerate0.25.0 torch2.1.0 sentencepiece0.1.99 # 某些模型Tokenizer需要使用pip安装pip install -r requirements.txt2.3 获取与准备Qwen3-0.6B-FP8模型Qwen3-0.6B-FP8是一个量化版本的模型我们需要先下载它。这里我们使用Hugging Face的transformers库它会自动处理缓存。我们可以写一个简单的脚本来测试模型是否能正常加载和运行。在项目根目录创建一个test_model.py文件# test_model.py from transformers import AutoModelForCausalLM, AutoTokenizer import torch # 指定模型名称。FP8量化模型可能需要从特定的仓库加载。 # 请根据实际的模型仓库地址进行替换例如 Qwen/Qwen2.5-0.5B-Instruct model_name Qwen/Qwen2.5-0.5B-Instruct # 示例请替换为正确的FP8模型路径 print(f正在加载模型和分词器: {model_name}) tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) # 注意加载量化模型可能需要指定 torch_dtype 或 load_in_8bit 等参数。 # 具体参数请查阅该模型仓库的说明。 model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.float16, # 根据模型要求调整 device_mapauto, # 自动分配设备CPU/GPU trust_remote_codeTrue ) print(模型加载成功) # 简单的推理测试 prompt 请用一句话介绍一下你自己。 inputs tokenizer(prompt, return_tensorspt).to(model.device) with torch.no_grad(): generated_ids model.generate(**inputs, max_new_tokens50) output tokenizer.decode(generated_ids[0], skip_special_tokensTrue) print(f测试提问: {prompt}) print(f模型回答: {output})运行这个脚本python test_model.py如果能看到模型成功加载并输出一句自我介绍说明模型环境基本就绪。请注意你需要将model_name替换为真实的、包含FP8量化版本的Qwen3-0.6B模型仓库地址。下载模型可能需要较长时间和一定的磁盘空间。准备工作完成我们的“大脑”已经就位。接下来开始构建它的“躯干”——后端服务。3. 构建后端服务FastAPI后端是应用的中枢它连接着前端、数据库和AI模型。我们用FastAPI来构建因为它速度快、现代并且能自动生成交互式API文档对开发非常友好。3.1 项目结构与核心配置我们先规划一下后端代码的目录结构让代码清晰易懂。smart-qa-website/ ├── backend/ │ ├── __init__.py │ ├── main.py # FastAPI应用入口 │ ├── config.py # 配置文件 │ ├── database.py # 数据库连接与模型定义 │ ├── models.py # Pydantic数据模型请求/响应体 │ ├── crud.py # 数据库增删改查操作 │ ├── auth.py # 用户认证与JWT相关 │ ├── dependencies.py # FastAPI依赖项如获取当前用户 │ └── routers/ │ ├── __init__.py │ ├── auth.py # 用户登录注册路由 │ ├── chats.py # 对话相关路由 │ └── stream.py # 流式响应路由 ├── requirements.txt └── test_model.py首先创建backend/config.py来管理配置比如数据库地址、JWT密钥等。# backend/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): # 应用元数据 app_name: str 智能问答网站API app_version: str 1.0.0 # 数据库配置 (这里先用SQLite方便演示) database_url: str sqlite:///./smart_qa.db # JWT认证相关配置 secret_key: str your-secret-key-change-in-production # 生产环境务必更改 algorithm: str HS256 access_token_expire_minutes: int 30 # 模型配置 model_name: str Qwen/Qwen2.5-0.5B-Instruct # 替换为你的模型路径 max_new_tokens: int 512 class Config: env_file .env # 可以从.env文件加载配置 settings Settings()3.2 数据库模型与连接我们使用SQLAlchemy来定义数据表和操作数据库。创建backend/database.py和backend/models.py这里是SQLAlchemy模型注意和Pydantic模型区分。# backend/database.py from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from datetime import datetime import pytz from .config import settings # 创建数据库引擎 engine create_engine(settings.database_url, connect_args{check_same_thread: False} if sqlite in settings.database_url else {}) # 创建会话工厂 SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) # 声明基类 Base declarative_base() # 定义用户表 class DBUser(Base): __tablename__ users id Column(Integer, primary_keyTrue, indexTrue) username Column(String(50), uniqueTrue, indexTrue, nullableFalse) email Column(String(100), uniqueTrue, indexTrue, nullableFalse) hashed_password Column(String(200), nullableFalse) created_at Column(DateTime, defaultlambda: datetime.now(pytz.utc)) # 关联到对话 chats relationship(DBChat, back_populatesuser) # 定义对话表 class DBChat(Base): __tablename__ chats id Column(Integer, primary_keyTrue, indexTrue) user_id Column(Integer, ForeignKey(users.id), nullableFalse) title Column(String(255), default新对话) # 对话标题可由第一条消息生成 created_at Column(DateTime, defaultlambda: datetime.now(pytz.utc)) updated_at Column(DateTime, defaultlambda: datetime.now(pytz.utc), onupdatelambda: datetime.now(pytz.utc)) # 关联到用户和消息 user relationship(DBUser, back_populateschats) messages relationship(DBMessage, back_populateschat, cascadeall, delete-orphan) # 定义消息表 class DBMessage(Base): __tablename__ messages id Column(Integer, primary_keyTrue, indexTrue) chat_id Column(Integer, ForeignKey(chats.id), nullableFalse) role Column(String(20), nullableFalse) # user 或 assistant content Column(Text, nullableFalse) created_at Column(DateTime, defaultlambda: datetime.now(pytz.utc)) # 关联到对话 chat relationship(DBChat, back_populatesmessages) # 创建所有表 def create_tables(): Base.metadata.create_all(bindengine) # 获取数据库会话的依赖函数 def get_db(): db SessionLocal() try: yield db finally: db.close()3.3 用户认证与安全用户系统是Web应用的基础。我们实现基于JWTJSON Web Token的认证。创建backend/auth.py。# backend/auth.py from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session from . import crud from .database import get_db from .config import settings # 密码加密上下文 pwd_context CryptContext(schemes[bcrypt], deprecatedauto) # OAuth2密码Bearer令牌方案用于从请求头提取token oauth2_scheme OAuth2PasswordBearer(tokenUrlapi/auth/login) def verify_password(plain_password, hashed_password): 验证密码 return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): 生成密码哈希 return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] None): 创建JWT访问令牌 to_encode data.copy() if expires_delta: expire datetime.now(timezone.utc) expires_delta else: expire datetime.now(timezone.utc) timedelta(minutessettings.access_token_expire_minutes) to_encode.update({exp: expire}) encoded_jwt jwt.encode(to_encode, settings.secret_key, algorithmsettings.algorithm) return encoded_jwt async def get_current_user(token: str Depends(oauth2_scheme), db: Session Depends(get_db)): 依赖项获取当前登录用户 credentials_exception HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail无效的认证凭证, headers{WWW-Authenticate: Bearer}, ) try: payload jwt.decode(token, settings.secret_key, algorithms[settings.algorithm]) username: str payload.get(sub) if username is None: raise credentials_exception except JWTError: raise credentials_exception user crud.get_user_by_username(db, usernameusername) if user is None: raise credentials_exception return user3.4 核心路由认证与对话现在我们来创建处理具体请求的路由。首先是认证路由 (backend/routers/auth.py)。# backend/routers/auth.py from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.orm import Session from datetime import timedelta from .. import crud, models, auth from ..database import get_db from ..config import settings router APIRouter(prefix/api/auth, tags[认证]) router.post(/register, response_modelmodels.User) def register(user_in: models.UserCreate, db: Session Depends(get_db)): 用户注册 # 检查用户名是否已存在 db_user crud.get_user_by_username(db, usernameuser_in.username) if db_user: raise HTTPException(status_code400, detail用户名已存在) # 检查邮箱是否已存在 db_user crud.get_user_by_email(db, emailuser_in.email) if db_user: raise HTTPException(status_code400, detail邮箱已注册) # 创建用户 return crud.create_user(dbdb, useruser_in) router.post(/login) def login(form_data: OAuth2PasswordRequestForm Depends(), db: Session Depends(get_db)): 用户登录返回访问令牌 user crud.authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detail用户名或密码错误, headers{WWW-Authenticate: Bearer}, ) access_token_expires timedelta(minutessettings.access_token_expire_minutes) access_token auth.create_access_token( data{sub: user.username}, expires_deltaaccess_token_expires ) return {access_token: access_token, token_type: bearer}接下来是对话相关的路由 (backend/routers/chats.py)处理创建对话、获取历史消息等。# backend/routers/chats.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from .. import crud, models from ..database import get_db from ..dependencies import get_current_user router APIRouter(prefix/api/chats, tags[对话管理]) router.get(/, response_modelList[models.Chat]) def read_chats( skip: int 0, limit: int 100, current_user: models.DBUser Depends(get_current_user), db: Session Depends(get_db) ): 获取当前用户的对话列表 chats crud.get_user_chats(db, user_idcurrent_user.id, skipskip, limitlimit) return chats router.post(/, response_modelmodels.Chat) def create_chat( current_user: models.DBUser Depends(get_current_user), db: Session Depends(get_db) ): 为用户创建一个新的空对话 return crud.create_chat(dbdb, user_idcurrent_user.id) router.get(/{chat_id}/messages, response_modelList[models.Message]) def read_messages( chat_id: int, current_user: models.DBUser Depends(get_current_user), db: Session Depends(get_db) ): 获取某个对话的所有消息 # 验证该对话是否属于当前用户 chat crud.get_chat(db, chat_idchat_id) if not chat or chat.user_id ! current_user.id: raise HTTPException(status_code404, detail对话不存在或无权限访问) return chat.messages3.5 灵魂所在流式问答接口这是整个后端最核心的部分它负责调用Qwen模型并以流式Server-Sent Events的方式将生成的答案实时推送给前端。创建backend/routers/stream.py。# backend/routers/stream.py from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session import asyncio import json from typing import AsyncGenerator from .. import crud, models from ..database import get_db from ..dependencies import get_current_user from ..config import settings from ..model_loader import get_model_and_tokenizer # 假设我们有一个加载模型的模块 router APIRouter(prefix/api/stream, tags[流式问答]) async def generate_response_stream(prompt: str, chat_id: int, user_id: int, db: Session) - AsyncGenerator[str, None]: 流式生成模型回答的核心异步生成器。 它一边调用模型生成一边将结果yield出去。 model, tokenizer get_model_and_tokenizer() # 1. 将用户消息存入数据库 user_message crud.create_message(db, chat_idchat_id, roleuser, contentprompt) # 为了演示我们简单更新对话标题可以用第一条消息的前N个字符 if len(prompt) 20: new_title prompt[:20] ... else: new_title prompt crud.update_chat_title(db, chat_idchat_id, new_titlenew_title) # 2. 构建模型输入。这里需要根据Qwen模型的对话格式来构造。 # Qwen通常使用类似 |im_start|system\n...|im_end|\n|im_start|user\n... 的格式。 # 为简化我们先只使用当前单轮对话。实际可以传入历史消息。 formatted_prompt f|im_start|user\n{prompt}|im_end|\n|im_start|assistant\n inputs tokenizer(formatted_prompt, return_tensorspt).to(model.device) # 3. 流式生成 generated_text with torch.no_grad(): # 使用generate并设置streamerTrue如果transformers版本支持或手动循环生成 # 这里演示一个简化的手动生成循环 for _ in range(settings.max_new_tokens): outputs model.generate(**inputs, max_new_tokens1, do_sampleTrue, temperature0.7) next_token_id outputs[0, -1].item() next_token tokenizer.decode([next_token_id], skip_special_tokensFalse) # 如果遇到结束符则停止 if next_token in [|im_end|, /s, tokenizer.eos_token]: break generated_text next_token # 将生成的token通过SSE格式发送 data json.dumps({content: next_token}, ensure_asciiFalse) yield fdata: {data}\n\n await asyncio.sleep(0.01) # 稍微延迟控制流的速度 # 更新输入为下一个token生成做准备 inputs tokenizer(formatted_prompt generated_text, return_tensorspt).to(model.device) # 4. 将完整的助手消息存入数据库 crud.create_message(db, chat_idchat_id, roleassistant, contentgenerated_text) router.post(/chat/{chat_id}) async def stream_chat( chat_id: int, request: models.ChatRequest, current_user: models.DBUser Depends(get_current_user), db: Session Depends(get_db) ): 流式问答端点返回Server-Sent Events # 验证chat_id属于当前用户 chat crud.get_chat(db, chat_idchat_id) if not chat or chat.user_id ! current_user.id: raise HTTPException(status_code404, detail对话不存在或无权限访问) # 返回StreamingResponse媒体类型为 text/event-stream return StreamingResponse( generate_response_stream(request.message, chat_id, current_user.id, db), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no # 禁用Nginx缓冲 } )注意上面的generate_response_stream函数是一个高度简化的示例。在实际使用中你需要根据transformers库的最新API和Qwen模型的具体生成方式来调整流式生成逻辑。可能使用TextIteratorStreamer等工具会更方便。我们还需要一个backend/model_loader.py来集中加载和管理模型避免每次请求都重复加载。# backend/model_loader.py import torch from transformers import AutoModelForCausalLM, AutoTokenizer from ..config import settings _model None _tokenizer None def get_model_and_tokenizer(): 获取全局的模型和分词器实例单例模式 global _model, _tokenizer if _model is None or _tokenizer is None: print(正在加载AI模型...) _tokenizer AutoTokenizer.from_pretrained(settings.model_name, trust_remote_codeTrue) _model AutoModelForCausalLM.from_pretrained( settings.model_name, torch_dtypetorch.float16, device_mapauto, trust_remote_codeTrue ) print(模型加载完成) return _model, _tokenizer最后在backend/main.py中组装所有部件并启动我们的FastAPI应用。# backend/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import uvicorn from .database import create_tables from .routers import auth, chats, stream # 创建数据库表 create_tables() # 创建FastAPI应用实例 app FastAPI(title智能问答网站后端API) # 配置CORS跨域资源共享允许前端访问 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], # 前端开发服务器地址根据实际情况修改 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 注册路由 app.include_router(auth.router) app.include_router(chats.router) app.include_router(stream.router) app.get(/) def read_root(): return {message: 智能问答网站后端服务已启动, version: 1.0.0} if __name__ __main__: uvicorn.run(backend.main:app, host0.0.0.0, port8000, reloadTrue)现在后端的主体部分就完成了。你可以运行python backend/main.py来启动后端服务访问http://localhost:8000/docs就能看到自动生成的交互式API文档非常方便测试。后端搭建好了是时候给它做一个好看的“门面”了。4. 构建前端界面Vue 3 Vite前端是用户直接交互的地方一个好的界面能极大提升体验。我们使用Vue 3的组合式API和Vite构建工具快速创建一个响应式、现代化的聊天界面。4.1 初始化Vue项目首先确保你安装了Node.js和npm。然后在项目根目录与backend同级创建前端项目。# 使用Vite官方模板创建Vue项目 npm create vuelatest frontend # 按照提示操作项目名就叫 frontend。 # 选择需要的特性TypeScript, Vue Router, Pinia状态管理是推荐的。 # 进入项目并安装依赖 cd frontend npm install # 安装额外需要的UI库和工具 npm install axios sse.js marked highlight.js # axios用于HTTP请求sse.js用于接收流式响应marked和highlight.js用于渲染Markdown和代码高亮。4.2 核心组件聊天界面我们创建一个主要的聊天页面组件src/views/ChatView.vue。!-- src/views/ChatView.vue -- template div classchat-container !-- 侧边栏对话历史 -- aside classsidebar div classsidebar-header h2对话历史/h2 button clickcreateNewChat classnew-chat-btn 新对话/button /div ul classchat-list li v-forchat in chats :keychat.id :class{ active: currentChatId chat.id } clickswitchChat(chat.id) {{ chat.title }} /li /ul /aside !-- 主聊天区域 -- main classmain-chat-area div classmessages-container refmessagesContainer div v-formsg in messages :keymsg.id :class[message, msg.role] div classavatar{{ msg.role user ? 你 : AI }}/div div classcontent v-htmlrenderMarkdown(msg.content)/div /div !-- 正在思考的指示器 -- div v-ifisThinking classmessage assistant thinking div classavatarAI/div div classcontent正在思考.../div /div /div !-- 输入区域 -- div classinput-area textarea v-modeluserInput keydown.enter.exact.preventsendMessage placeholder向AI助手提问... (ShiftEnter换行) rows3 :disabledisThinking /textarea button clicksendMessage :disabled!userInput.trim() || isThinking 发送 /button /div /main /div /template script setup langts import { ref, onMounted, onUpdated, nextTick } from vue import { marked } from marked import hljs from highlight.js import highlight.js/styles/github.css // 代码高亮样式 import axios from axios import { EventSourcePolyfill } from sse.js import type { Chat, Message } from /types/api // 状态定义 const chats refChat[]([]) const messages refMessage[]([]) const currentChatId refnumber | null(null) const userInput ref() const isThinking ref(false) const messagesContainer refHTMLElement() // 配置marked以支持代码高亮 marked.setOptions({ highlight: function(code, lang) { const language hljs.getLanguage(lang) ? lang : plaintext return hljs.highlight(code, { language }).value }, langPrefix: hljs language- }) // 渲染Markdown为HTML const renderMarkdown (content: string) { return marked(content) } // 滚动到消息底部 const scrollToBottom () { nextTick(() { if (messagesContainer.value) { messagesContainer.value.scrollTop messagesContainer.value.scrollHeight } }) } // 获取对话列表 const fetchChats async () { try { const token localStorage.getItem(access_token) const response await axios.get(/api/chats/, { headers: { Authorization: Bearer ${token} } }) chats.value response.data if (chats.value.length 0 !currentChatId.value) { currentChatId.value chats.value[0].id fetchMessages(chats.value[0].id) } } catch (error) { console.error(获取对话列表失败:, error) } } // 获取特定对话的消息 const fetchMessages async (chatId: number) { try { const token localStorage.getItem(access_token) const response await axios.get(/api/chats/${chatId}/messages, { headers: { Authorization: Bearer ${token} } }) messages.value response.data scrollToBottom() } catch (error) { console.error(获取消息失败:, error) } } // 创建新对话 const createNewChat async () { try { const token localStorage.getItem(access_token) const response await axios.post(/api/chats/, {}, { headers: { Authorization: Bearer ${token} } }) chats.value.unshift(response.data) // 新对话加到前面 currentChatId.value response.data.id messages.value [] userInput.value } catch (error) { console.error(创建新对话失败:, error) } } // 切换对话 const switchChat (chatId: number) { currentChatId.value chatId fetchMessages(chatId) } // 发送消息并接收流式响应 const sendMessage async () { if (!userInput.value.trim() || !currentChatId.value || isThinking.value) return const question userInput.value.trim() userInput.value isThinking.value true // 立即将用户消息添加到界面 const userMessage: Message { id: Date.now(), // 临时ID chat_id: currentChatId.value, role: user, content: question, created_at: new Date().toISOString() } messages.value.push(userMessage) scrollToBottom() // 准备接收AI的流式回答 let assistantMessageContent const assistantMessageId Date.now() 1 const assistantMessage: Message { id: assistantMessageId, chat_id: currentChatId.value, role: assistant, content: , created_at: new Date().toISOString() } messages.value.push(assistantMessage) const token localStorage.getItem(access_token) const eventSource new EventSourcePolyfill(/api/stream/chat/${currentChatId.value}, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${token} }, body: JSON.stringify({ message: question }), // 如果需要polyfill可以配置 }) eventSource.onmessage (event) { try { const data JSON.parse(event.data) assistantMessageContent data.content // 更新界面中对应消息的内容 const msgIndex messages.value.findIndex(m m.id assistantMessageId) if (msgIndex -1) { messages.value[msgIndex].content assistantMessageContent scrollToBottom() } } catch (e) { console.error(解析流数据失败:, e) } } eventSource.onerror (error) { console.error(SSE连接错误:, error) eventSource.close() isThinking.value false // 如果出错可以给一个错误提示 if (assistantMessageContent.length 0) { const msgIndex messages.value.findIndex(m m.id assistantMessageId) if (msgIndex -1) { messages.value[msgIndex].content 抱歉回答生成时出现了问题。 } } } eventSource.addEventListener(end, () { eventSource.close() isThinking.value false // 流式结束可以做一些清理或触发后续操作比如重新获取对话列表更新标题 fetchChats() }) } // 组件挂载时获取对话列表 onMounted(() { fetchChats() }) // 消息更新后滚动到底部 onUpdated(() { scrollToBottom() }) /script style scoped /* 样式部分较长这里给出关键结构具体样式可根据喜好调整 */ .chat-container { display: flex; height: 100vh; } .sidebar { width: 260px; border-right: 1px solid #e5e7eb; display: flex; flex-direction: column; } .main-chat-area { flex: 1; display: flex; flex-direction: column; } .messages-container { flex: 1; overflow-y: auto; padding: 1rem; } .message { display: flex; margin-bottom: 1.5rem; } .message.user { flex-direction: row-reverse; } .message .avatar { /* 头像样式 */ } .message .content { /* 内容区域样式 */ } .input-area { border-top: 1px solid #e5e7eb; padding: 1rem; display: flex; } .input-area textarea { flex: 1; /* 文本框样式 */ } /style4.3 状态管理与API配置我们使用Pinia进行简单的状态管理并统一配置axios。创建src/stores/auth.ts和src/utils/request.ts。// src/stores/auth.ts import { defineStore } from pinia import { ref } from vue import axios from /utils/request export const useAuthStore defineStore(auth, () { const token ref(localStorage.getItem(access_token) || ) const isAuthenticated ref(!!token.value) const login async (username: string, password: string) { const formData new FormData() formData.append(username, username) formData.append(password, password) const response await axios.post(/api/auth/login, formData) token.value response.data.access_token localStorage.setItem(access_token, token.value) isAuthenticated.value true axios.defaults.headers.common[Authorization] Bearer ${token.value} return response } const logout () { token.value isAuthenticated.value false localStorage.removeItem(access_token) delete axios.defaults.headers.common[Authorization] } return { token, isAuthenticated, login, logout } })// src/utils/request.ts import axios from axios // 创建axios实例配置基础URL和拦截器 const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || http://localhost:8000, // 从环境变量读取 timeout: 10000, }) // 请求拦截器自动添加token request.interceptors.request.use( (config) { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器处理通用错误 request.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { // 未授权跳转到登录页 localStorage.removeItem(access_token) window.location.href /login } return Promise.reject(error) } ) export default request4.4 配置与运行在frontend目录下创建.env.development文件配置开发环境的后端API地址。# .env.development VITE_API_BASE_URLhttp://localhost:8000修改vite.config.ts以配置开发服务器代理避免前端直接访问后端端口时的CORS问题虽然我们后端已配置CORS但代理更彻底。// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, })现在你可以分别启动后端和前端服务了在项目根目录启动后端python backend/main.py在frontend目录启动前端npm run dev打开浏览器访问http://localhost:5173Vite默认前端地址你应该能看到登录/注册界面需要先实现。登录后就能进入我们刚刚构建的智能聊天界面了5. 总结与展望走到这一步一个功能完整的智能问答网站骨架就已经搭建完成了。我们从前端漂亮的Vue界面到后端稳健的FastAPI服务再到数据库对对话历史的持久化最后接入了Qwen3-0.6B-FP8模型作为智能核心实现了从用户输入到AI流式输出的完整闭环。回顾整个过程最关键的几个技术点包括使用FastAPI构建高效且文档齐全的API利用SQLAlchemy优雅地操作数据库通过JWT实现安全的用户认证采用Server-Sent Events实现流畅的流式回答体验以及用Vue 3构建出响应式的现代用户界面。把这些技术组合在一起就是一个非常典型的现代全栈AI应用架构。当然这个项目目前还是一个“最小可行产品”MVP。它已经能跑起来但还有很多可以打磨和扩展的地方。比如你可以为对话历史增加搜索功能让用户能快速找到某次聊天可以引入更复杂的提示词工程让AI的回答更符合你的业务场景或者增加多模型切换的支持让用户可以选择不同风格的AI助手。在部署方面可以考虑使用Docker容器化方便地在云服务器上部署和扩展。最重要的是通过亲手实现这个项目你不仅学会了如何集成一个AI模型到Web应用中更掌握了全栈开发的完整工作流。这套方法完全可以复用到你的下一个创意项目中。技术迭代很快但构建应用的思路是相通的。希望这个实战指南能成为你探索AI应用开发的一块有用的垫脚石。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

相关新闻