AI 全栈学习之旅 -Week 5:RAG 知识库问答系统:从零到生产级部署的全栈实践总结

这一周,我把前几周的聊天应用继续往前推了一步:不再只是"和模型对话",而是做成一个支持用户注册登录、上传 PDF、构建个人知识库、基于检索增强生成进行问答,并最终通过域名 https://qishuixian.com/ask/ 对外提供服务的完整项目。

这篇文章不是单纯介绍功能,而是一次比较真实的项目复盘。我会重点写清楚三件事:

  • 这个 RAG 项目到底解决了什么问题
  • 我是怎么把它从本地功能做成线上系统的
  • 部署时真正踩了哪些坑,又是怎么排查出来的

一、项目目标

Week 5 的核心目标可以概括成一句话:

让每个用户都能上传自己的 PDF,并基于自己的文件内容进行问答。

相比前几周的普通聊天应用,这一版项目新增了几个关键能力:

  • 用户注册、登录、鉴权
  • PDF 上传、展示、删除
  • 文档切分、向量化、写入 ChromaDB
  • 提问时默认检索当前用户的全部文件
  • 删除文件时同步删除对应向量
  • 通过 Docker、Nginx 和域名完成线上部署

最终效果是,用户访问:

text 复制代码
https://qishuixian.com/ask/

登录后上传自己的 PDF,就可以针对这些文件直接提问,得到带知识库上下文的回答。

二、技术选型

这次项目继续沿用了"前后端分离 + 轻量部署"的思路。

前端

  • Vue 3
  • TypeScript
  • Element Plus
  • Pinia
  • Marked + Highlight.js
  • Vite

前端主要负责登录态管理、文件上传交互、消息展示和流式输出渲染。

后端

  • FastAPI
  • SQLAlchemy + SQLite
  • JWT
  • ChromaDB
  • LangChain
  • HuggingFace Embeddings
  • DeepSeek API

后端负责鉴权、文件管理、PDF 解析、向量入库、检索、拼接上下文以及调用大模型生成回答。

部署

  • Docker
  • Docker Compose
  • Nginx
  • 腾讯云 CVM

这个组合的好处是足够直接:本地能跑,打镜像后线上也能跑,适合当前这个单机项目。

三、系统是怎么工作的

整个系统可以理解成一条很清晰的数据流:

text 复制代码
用户上传 PDF
-> 后端保存原文件
-> PyPDFLoader 解析文本
-> RecursiveCharacterTextSplitter 切分
-> BAAI/bge-small-zh-v1.5 生成向量
-> ChromaDB 持久化
-> 用户提问
-> 按当前 user_id 检索相关片段
-> 拼接上下文
-> 调用 DeepSeek 生成回答
-> 前端流式展示结果

这里最关键的设计,不是"能检索",而是"按用户隔离检索"。

因为这个项目不是公共知识库,而是"每个人自己的知识库"。所以在向量入库时,我给每个文档分片都写入了这些元数据:

  • user_id
  • file_id
  • filename
  • chunk_index

这样在检索时,就可以只查当前用户自己的文件;在删除时,也可以按 user_id + file_id 精确删除对应向量,避免删错别人的数据。

四、核心功能拆解

1. 用户登录与认证

项目采用 JWT 做无状态认证。用户登录成功后,后端签发 token,前端在后续请求中通过 Authorization: Bearer <token> 携带。

密码没有使用明文存储,而是使用 pbkdf2_sha256 哈希。这里我还踩过一个小坑:项目早期尝试过 bcrypt 方案,但在实际环境里出现过兼容问题,最终改成了 passlibpbkdf2_sha256,稳定性更好。 核心实现如下:

python 复制代码
from passlib.context import CryptContext

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

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

其中:

  • hash_password() 用于在用户注册时对原始密码进行哈希处理。
  • verify_password() 用于在用户登录时校验输入密码是否与数据库中的哈希值匹配。

这种方式的优点是无需手动管理盐值和底层加密细节,同时保留了较好的安全性和可维护性。

2. 文件上传与知识库构建

上传接口做了三层约束:

  • 只允许 PDF
  • 限制大小
  • 上传成功后立刻进入解析和向量化流程

这里最大的变化,是左侧区域不再是"会话列表",而是"文件管理区"。用户可以清楚看到自己当前已经上传了哪些文件,也可以直接删除。

上传完成后,后端会:

  1. 保存文件到 /app/uploads
  2. 写数据库记录
  3. 解析 PDF
  4. 切分文本
  5. 生成 embedding
  6. 写入 ChromaDB

3. RAG 问答

用户提问时,系统默认检索"当前用户上传的全部文件",而不是要求用户先手动指定某个文件。

这种设计更像真正的个人知识库产品,也降低了交互成本。

检索到的多个文档片段会被拼接成上下文,然后和用户问题一起发给大模型。前端则通过流式方式展示回答,让用户不必一直等到整段文本生成完才看到结果。

4. 删除文件时同步删除向量

这是我觉得这周非常重要的一点。

很多初学 RAG 的实现里,删除文件只是删掉业务数据库里的记录,或者删掉磁盘上的 PDF。但如果不删除向量库里的对应分片,那么系统后续检索时仍然可能命中那些"看似已经删除"的内容。

所以这个项目里,删除文件是三件事一起做:

  • 删数据库记录
  • 删原始 PDF
  • 删 ChromaDB 中对应向量

这样用户看到"文件已删除"时,系统内部也真的不会再用到它。

五、为什么要用 /ask/ 子路径部署

我的域名 qishuixian.com 上已经有别的内容,所以这次没有把项目直接挂在根路径,而是放在:

text 复制代码
https://qishuixian.com/ask/

这件事看起来只是"多了一个前缀",实际上它会影响三层配置:

  • Vite 打包基路径
  • 前端容器内 Nginx
  • 宿主机 Nginx

如果三层里有一层没对齐,就很容易出现:

  • 页面能打开但静态资源 404
  • 刷新页面 404
  • /ask/ 返回 404
  • 前端请求接口路径错乱

这次项目里,前端最终固定使用:

env 复制代码
VITE_BASE_URL=/ask/

容器内 Nginx 负责提供 /ask/ 静态资源,宿主机 Nginx 再把域名下的 /ask/ 代理到前端容器的 8081 端口。

六、线上部署方案

最终线上部署采用的是双容器结构:

  • ask-frontend
  • ask-backend

对应端口如下:

  • 前端宿主机端口:8081
  • 后端宿主机端口:8001

浏览器的访问链路是:

text 复制代码
浏览器
-> https://qishuixian.com/ask/
-> 宿主机 Nginx
-> 127.0.0.1:8081
-> ask-frontend

接口调用链路是:

text 复制代码
浏览器
-> /api/*
-> 前端容器内 Nginx
-> backend:8001
-> ask-backend

为了避免容器重启后数据丢失,我把几类数据都挂载到了宿主机:

  • ./data:/app/data
  • ./ask:/app/uploads
  • ./hf-cache:/root/.cache/huggingface

分别对应:

  • SQLite 数据库
  • 用户上传文件
  • Hugging Face 模型缓存

七、这次部署里最真实的几个坑

如果只看最终效果,会觉得这是一次很顺利的 Docker 部署。但实际排查过程比"写完 compose 然后 up -d"复杂得多。

坑 1:镜像太大,scp 传输中断

后端镜像里有 torchtransformerssentence-transformers 等依赖,docker save 出来的 tar 很大。最开始我直接:

bash 复制代码
scp ask-backend.tar root@<SERVER_IP>:/opt/ask/

结果多次上传到一半断开,服务器上的 tar 文件不完整,接着 docker load -i ask-backend.tar 就报了 unexpected EOF

这类问题表面上看像是 Docker 失败,实际上根因是上传没完成。

后来我改成了分片上传,才把镜像完整传到服务器。

坑 2:容器一直 unhealthy

镜像成功加载后,新的问题出现了:ask-backend 一直 unhealthyask-frontend 因为依赖后端健康检查,也起不来。

一开始看起来像是应用没监听端口,但继续查日志后发现真正原因是:

text 复制代码
Network is unreachable
Failed to connect to huggingface.co

问题出在后端启动时会初始化:

python 复制代码
HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")

而服务器无法访问 Hugging Face,所以模型拉不下来,应用根本没真正启动完成。

坑 3:明明上传了缓存,还是继续联网

我后来在本地把 BAAI/bge-small-zh-v1.5 下载到了 hf-cache,打包传到服务器,再挂载进容器。结果容器还是继续去请求 Hugging Face 的远端地址。

这一步真正缺的不是"缓存文件",而是离线模式

最终的解决方式是:

  1. 本地预下载模型缓存
  2. 上传 hf-cache
  3. docker-compose.prod.yml 中挂载缓存目录
  4. 设置:
env 复制代码
HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1

加上这两个环境变量后,后端终于不再出网拉模型,而是直接使用本地缓存。

坑 4:hf-cache 下载成功了,但本地目录却是空的

这是一个很典型但也很隐蔽的问题。

我最开始在 Windows Git Bash 里用这种方式挂载:

bash 复制代码
docker run --rm -it -v "$(pwd)/hf-cache:/root/.cache/huggingface" python:3.13-slim bash

容器里模型明明下载成功了,但退出后本地 hf-cache 目录却是空的。

最后才定位到:在 Windows Git Bash 环境下,这个挂载路径并不稳定,模型实际下载到了临时容器文件系统里,而不是宿主机目录。

最终可用的做法是直接使用 Windows 绝对路径:

bash 复制代码
docker run --rm -it -v "E:/my/ai-fullstack-journey/week5/rag_project/hf-cache:/root/.cache/huggingface" python:3.13-slim bash

这一步看起来只是路径写法不同,但它直接决定了缓存文件到底有没有真的落到本地。

坑 5:容器都正常了,浏览器访问 /ask/ 还是 404

这是这次部署里最后一个比较典型的坑。

后端健康了,前端也启动了,容器内访问 127.0.0.1:8081/ask/ 也能返回 200,但浏览器访问:

text 复制代码
https://qishuixian.com/ask/

还是直接看到 Nginx 的 404 Not Found

这时候问题就不在 Docker 里了,而在宿主机 Nginx。

最终确认是:修改了 /etc/nginx/sites-available/qishuixian 之后,没有及时执行:

bash 复制代码
sudo nginx -t
sudo systemctl reload nginx

这次也再次提醒我,排查线上问题时一定要分层看:

  • 浏览器
  • 宿主机 Nginx
  • Docker 端口
  • 容器日志

不要一看到页面打不开,就直接怀疑应用代码本身。

八、这次项目里最重要的几个经验

1. RAG 项目不只是"检索 + 大模型"

真正做成一个可用系统后会发现,RAG 项目不仅仅是模型调用流程,还包括:

  • 用户体系
  • 文件生命周期管理
  • 数据隔离
  • 部署和运维
  • 模型依赖管理

这部分工作量往往比"写一个检索 demo"大得多。

2. 删除一致性非常重要

如果用户删掉文件后,系统还能检索出旧内容,整个产品可信度会立刻下降。

所以"删除业务记录"和"删除向量数据"必须绑定在一起,这是知识库类产品的基本一致性要求。

3. 子路径部署一定要全链路统一

只改前端 base 不够,只改 Nginx 也不够。/ask/ 这种子路径部署必须让前端打包、容器 Nginx、宿主机 Nginx 三者完全一致。

4. 线上依赖不能默认认为"容器会自己下载好"

如果应用启动依赖外网模型资源,那么线上网络环境就是部署的一部分。服务器能否访问 Hugging Face,不是一个"可选问题",而是决定服务能否启动的硬条件。

这次最终走通的方案,本质上是把"在线依赖"改成"离线可复现依赖"。

九、当前结果与下一步计划

到 2026 年 8 月底,这个项目已经完成了从本地开发到线上部署的闭环:

  • ask-backend 成功启动并通过健康检查
  • ask-frontend 正常提供页面
  • https://qishuixian.com/ask/ 已可访问
  • 支持登录、上传 PDF、查看文件、删除文件、基于知识库问答

当然,这还不是终点。下一步我还想继续优化几个方向:

  • 增加 rerank,提升检索排序效果
  • 完善会话管理和历史记录体验
  • 支持更多文件格式
  • 优化构建体积和部署链路
  • 尝试接入自动化构建与发布

十、总结

如果说前几周的重点是"把功能做出来",那 Week 5 更像是真正开始面对"系统如何上线、如何稳定运行、如何处理现实环境问题"。

这次我最大的收获,不只是做出了一个 RAG 知识库,而是更清楚地理解了一个 AI 应用从 demo 走向真实可访问服务,中间到底要跨过哪些环节。

功能实现只是第一步,部署成功也不是最后一步。只有当它能被真实用户访问、能稳定运行、能正确处理文件和知识库生命周期时,这个项目才真正有了"产品"的雏形。

如果你也在做自己的 RAG 项目,我会非常建议你尽早把它放到真实部署环境里跑一次。很多问题,只有到了线上,才会第一次真正出现。


项目目录:week5/rag_project

在线地址:qishuixian.com/ask/

相关推荐
2601_9623818617 分钟前
Python办公联动:Excel数据一键自动生成PPT图表
python·数据分析·自动化·excel·ppt
东莞市永盛电气23 分钟前
三相208V变380V变压器,出海工业设备供电适配方案
python·单片机·嵌入式硬件
光影少年36 分钟前
react navite 安卓iOS 打包、签名、环境区分
前端·react native·react.js
coderCN42 分钟前
Nodejs 第三十四章 数据库(表达式和函数、子查询和连表)
前端·node.js
未若君雅裁1 小时前
上下文压缩双刃剑-Summarization与ContextEditing中间件实战
python·中间件·langchain
IT毕设实战小研1 小时前
电影数据可视化推荐系统
大数据·后端·爬虫·python·算法·信息可视化·课程设计
ssshooter1 小时前
AI 时代你不能不知道的 git worktree
前端·后端·面试
前端 贾公子2 小时前
第09章:上下文与记忆 (3)
python·langchain
观测云2 小时前
AI时代的用户访问监测:观测云带你身临其境体验用户与前端UI交互旅程
前端·可观测性·观测云·rum