AI 全栈学习之旅 - Week2:从零搭建一个可部署的 AI 聊天应用

前言

在 Week1 中,我们完成了 Python 基础语法、FastAPI 入门和 DeepSeek API 的调用,实现了一个能在终端聊天的 AI 助手。

Week2 的目标是:把这个终端聊天工具,变成一个真正的 Web 应用

具体来说,Week2 要完成以下几件事:

  1. 搭建 FastAPI 后端框架,实现用户认证和多会话管理
  2. 使用 Vue 3 开发前端界面,实现组件化架构
  3. 集成 WebSocket 实现实时通信
  4. 使用 Docker 容器化部署

一、技术选型

层面 技术 选择理由
后端框架 FastAPI 高性能、异步支持、自动生成 API 文档
数据库 SQLite + SQLAlchemy 轻量级、无需额外安装数据库服务
认证 JWT 无状态、适合前后端分离
前端框架 Vue 3 + TypeScript 组合式 API、类型安全
构建工具 Vite 极快的冷启动和热更新
实时通信 WebSocket 双向通信、低延迟
容器化 Docker + Docker Compose 一键部署、环境一致性

二、项目架构

bash 复制代码
week2/
├── backend/                      # 后端代码
│   ├── main.py                  # FastAPI 主程序
│   ├── database.py              # 数据库模型
│   ├── auth.py                  # 认证逻辑
│   ├── dependencies.py          # 依赖注入
│   ├── migrate_add_pinned.py    # 数据库迁移脚本
│   ├── requirements.txt         # Python 依赖
│   ├── .env                     # 环境变量(需自行创建)
│   └── chat.db                  # SQLite 数据库(自动生成)
│
├── frontend/                    # 前端代码
│   ├── src/
│   │   ├── components/          # Vue 组件
│   │   │   ├── LoginForm.vue    # 登录/注册表单
│   │   │   ├── Sidebar.vue      # 侧边栏
│   │   │   ├── ChatArea.vue     # 聊天主区域
│   │   │   ├── MessageList.vue  # 消息列表
│   │   │   ├── ChatInput.vue    # 输入框
│   │   │   └── __tests__/       # 组件测试
│   │   ├── App.vue              # 根组件
│   │   └── main.ts              # 入口文件
│   ├── package.json             # 依赖配置
│   └── vite.config.ts           # Vite 配置
│
├── Dockerfile.backend           # 后端 Docker 配置
├── Dockerfile.frontend          # 前端 Docker 配置
├── docker-compose.yml           # Docker Compose 配置
├── nginx.conf                   # Nginx 配置
└── README.md                    # 项目文档

三、核心功能实现

3.1 用户认证系统

用户认证是 Web 应用的基础。我选择了 JWT(JSON Web Token)方案,因为它天然适合前后端分离架构。

认证流程:

  1. 用户注册 → 后端返回 JWT Token
  2. 用户登录 → 后端验证密码 → 返回 JWT Token
  3. 前端将 Token 存储在 localStorage
  4. 每次请求在 Header 中携带 Token
  5. 后端通过依赖注入验证 Token

关键代码(auth.py):

python 复制代码
from datetime import datetime, timedelta
from jose import jwt
from passlib.context import CryptContext

SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24  # 24小时

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)):
    # 解析 Token → 查询用户 → 返回用户对象
    ...

遇到的坑:

  • 前端请求 /api/sessions 时返回 401,原因是 get_current_user 函数中的 Depends(get_db) 写成了 Depends(get_db())(多了括号),导致数据库会话未正确注入。
  • 修复后,所有接口都通过 current_user: User = Depends(get_current_user) 进行鉴权保护。

3.2 多会话管理

会话模型(database.py):

ini 复制代码
# 会话表
class Session(Base):
    __tablename__ = "sessions"

    id = Column(String(36), primary_key=True)
    name = Column(String(100),default="新对话")
    user_id = Column(Integer, ForeignKey("users.id"))
    pinned = Column(Integer, default=0)  # 0=未置顶,1=已置顶
    created_at = Column(DateTime, default=datetime.now)
    updated_at = Column(DateTime, default=datetime.now)

前端会话列表组件(Sidebar.vue):

  • 显示所有会话
  • 支持搜索过滤
  • 支持置顶(星标)
  • 支持双击重命名
  • 支持删除

消息持久化:

每条消息都保存在数据库中,刷新页面后通过 GET /api/history?session_id=${sessionId} 接口重新加载,确保数据不丢失。

3.3 流式响应(SSE)

AI 回复通常需要几秒钟,如果等全部生成完再返回,用户体验很差。流式响应可以让用户边生成边看到内容

实现原理:

  1. 前端通过 POST 请求发送消息
  2. 后端使用 StreamingResponse 返回事件流
  3. 前端使用 EventSourcefetchReadableStream 逐步读取
  4. 每收到一段内容,就更新页面上的消息

关键代码(后端main.py):

ini 复制代码
# ---- 新增的流式接口 ----
@app.post("/chat/stream")
async def chat_stream(
    request: ChatRequest,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    try:
        messages = [{"role": "system", "content": "你是一个友善的AI助手。"}]
        for msg in request.history:
            messages.append(msg)
        messages.append({"role": "user", "content": request.message})

        # 调用 DeepSeek 流式 API
        stream = client.chat.completions.create(
            model="deepseek-v4-flash",
            messages=messages,
            stream=True
        )

        # 定义一个生成器,逐块产生 SSE 格式的数据
        async def generate():
            full_reply = ""  # 用于累积完整回复
            for chunk in stream:
                if chunk.choices[0].delta.content is not None:
                    content = chunk.choices[0].delta.content
                    full_reply += content
                    # SSE 格式:data: <json>\n\n
                    yield f"data: {json.dumps({'content': content})}\n\n"
           
            # 使用注入的 db 会话
            db.add(ChatMessage(role="user", content=request.message, session_id=request.session_id))
            db.add(ChatMessage(role="assistant", content=full_reply, session_id=request.session_id))
            await db.commit()
            
            # 发送结束标志
            yield "data: [DONE]\n\n"

        return StreamingResponse(
            generate(),
            media_type="text/event-stream"
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
    

前端处理(ChatArea.vue):

php 复制代码
const response = await fetch('/api/chat/stream', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${props.token}`
      },
      body: JSON.stringify({
        message: text,
        history: [],
        session_id: props.currentSessionId || 'default'
      }),
      signal: controller.value.signal
    })

3.4 WebSocket 实时通信

除了流式响应,我还需要 WebSocket 来处理更复杂的实时场景(如多用户在线状态、消息广播等)。

WebSocket 连接管理:

python 复制代码
python
class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []
    
    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)
    
    async def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)
    
    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

Token 验证:

WebSocket 连接时通过 URL 参数传递 Token:

bash 复制代码
纯文本
ws://localhost:8000/ws?token=eyJhbGciOiJIUzI1NiIs...

3.5 前端组件化

为了提高代码的可维护性和复用性,我将前端拆分为 5 个核心组件:

组件 职责
LoginForm.vue 登录/注册表单
Sidebar.vue 会话列表、搜索、置顶
ChatArea.vue 聊天主区域,协调消息列表和输入框
MessageList.vue 消息渲染(Markdown)
ChatInput.vue 输入框、发送按钮、停止生成

四、Docker 容器化部署

4.1 环境准备

部署的第一步是安装 Docker Desktop。但我的 Windows 10 版本是 18363,不满足 Docker 的要求(需要 22H2 以上)。

升级过程:

  1. 使用微软官方升级工具升级到 Windows 10 22H2
  2. 安装 WSL2(Windows Subsystem for Linux)
  3. 安装 Docker Desktop

升级耗时: ​ 约 1 小时(下载 + 安装 + 多次重启)

4.2 Docker 配置

后端 Dockerfile:

sql 复制代码
dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

前端 Dockerfile(多阶段构建):

sql 复制代码
dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

docker-compose.yml:

yaml 复制代码
yaml
services:
  backend:
    build:
      context: .
      dockerfile: Dockerfile.backend
    ports:
      - "8000:8000"
    volumes:
      - ./backend/chat.db:/app/chat.db

  frontend:
    build:
      context: .
      dockerfile: Dockerfile.frontend
    ports:
      - "80:80"
    depends_on:
      - backend

4.3 踩坑记录

坑1:Nginx proxy_pass 末尾斜杠

bash 复制代码
nginx
# ❌ 错误写法
proxy_pass http://backend:8000/;  # 末尾带斜杠

# ✅ 正确写法
proxy_pass http://backend:8000;   # 末尾不带斜杠

带斜杠会导致路径被重写,例如 /api/register 变成 /register,导致 405 错误。

五、测试

5.1 后端测试

使用 pytest + httpx 编写 API 测试:

python 复制代码
python
@pytest.mark.asyncio
async def test_register_and_login():
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
        # 注册
        resp = await ac.post("/register", data={"username": "testuser", "password": "test123"})
        assert resp.status_code == 200
        assert "access_token" in resp.json()
        
        # 登录
        resp = await ac.post("/token", data={"username": "testuser", "password": "test123"})
        assert resp.status_code == 200

5.2 前端测试

使用 Vitest + @vue/test-utils 编写组件测试:

scss 复制代码
typescript
describe('LoginForm', () => {
  it('renders correctly', () => {
    const wrapper = mount(LoginForm)
    expect(wrapper.find('h1').text()).toBe('🤖 AI 助手')
    expect(wrapper.find('.auth-submit').text()).toBe('登录')
  })
})

六、项目成果

功能清单

  • ✅ 用户注册/登录(JWT 认证)
  • ✅ 多会话管理(创建、切换、删除、搜索、置顶、重命名)
  • ✅ 流式 AI 响应(SSE)
  • ✅ Markdown 渲染
  • ✅ 消息持久化(刷新不丢失)
  • ✅ 会话导出(JSON / Markdown)
  • ✅ WebSocket 实时通信
  • ✅ Docker 容器化部署

技术亮点

  1. 前后端分离架构 --- RESTful API + Vue SPA
  2. 异步编程 --- FastAPI async/await + SQLAlchemy async
  3. 实时通信 --- WebSocket + SSE 流式响应
  4. 安全认证 --- JWT Token + 密码哈希
  5. 组件化设计 --- Vue 3 Composition API
  6. 容器化部署 --- Docker + Docker Compose + Nginx

项目截图


七、遇到的问题与解决方案

问题 解决方案
Windows 版本过低无法安装 Docker 升级到 Windows 10 22H2
WSL2 安装失败 手动下载内核更新包安装
前端请求后端 404 检查 Nginx proxy_pass 配置
前端请求后端 405 修复 proxy_pass 末尾斜杠问题
前端测试 document is not defined 配置 vitest.config.ts 的 environment: 'jsdom'
刷新页面会话丢失 实现消息持久化到数据库
WebSocket 连接失败 URL 参数传递 Token 进行验证

八、下周计划

Week3 的目标是进一步提升前端体验:

  1. UI 框架集成 --- 引入 Element Plus 或 Naive UI,美化界面
  2. 前端路由与状态管理 --- Vue Router + Pinia
  3. 高级功能 --- 消息编辑/删除、文件上传、主题切换
  4. 性能优化 --- 虚拟滚动、懒加载、代码分割

九、总结

Week2 是强度很大的一周,从后端 API 设计到前端组件开发,再到 Docker 容器化部署,几乎覆盖了全栈开发的完整流程。

最大的收获不是学会了某个具体的技术,而是建立了 "全栈思维" ------知道一个 Web 应用从前端到后端、从开发到部署的完整链路是怎样的。

最大的教训是 "环境配置是第一生产力" ------在 Windows 上配置 Docker 花了整整一天时间,如果一开始就升级系统,后面会顺利很多。

期待 Week3 的前端深度开发!


附录:项目地址

相关推荐
aixingpan2 小时前
aixingpan.cn API开发文档:api_docs_trichart_natal_solararc_transit2接口指南
前端·php
Ayayoyo2 小时前
公平随机转盘的前端实现:Web Crypto API、拒绝采样与加权抽取
前端
程序员黑豆2 小时前
鸿蒙应用开发:@Link 装饰器实现父子组件双向同步
前端·后端·harmonyos
前端炒粉2 小时前
简易实现ssr
开发语言·前端·javascript
顶级自由人3 小时前
【前端菜鸟的补课01】Zod 与 PostgreSQL 全栈数据工程教学
前端·后端·程序员
swipe3 小时前
11|(前端转全栈)购物车不能只存在前端:用户维度数据如何在后端落库
前端·后端·全栈
Chengbei115 小时前
云安全漏洞挖掘SKILL、一站式云漏洞挖掘工具,支持S3爆破、IMDS探测、K8s检测与AK/SK权限利用
前端·人工智能·网络安全·云原生·容器·kubernetes·系统安全
不简说5 小时前
JS 代码技巧 vol.9 — 20 个设计模式在真实项目里的应用
前端·javascript·github
CoderLiu5 小时前
Agent 工程的下一层:为什么「把流程写成图」还不够,还需要 Graph Engineering
前端·人工智能·后端