这一周,我把前几周的聊天应用继续往前推了一步:不再只是"和模型对话",而是做成一个支持用户注册登录、上传 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_idfile_idfilenamechunk_index
这样在检索时,就可以只查当前用户自己的文件;在删除时,也可以按 user_id + file_id 精确删除对应向量,避免删错别人的数据。
四、核心功能拆解
1. 用户登录与认证
项目采用 JWT 做无状态认证。用户登录成功后,后端签发 token,前端在后续请求中通过 Authorization: Bearer <token> 携带。 
密码没有使用明文存储,而是使用 pbkdf2_sha256 哈希。这里我还踩过一个小坑:项目早期尝试过 bcrypt 方案,但在实际环境里出现过兼容问题,最终改成了 passlib 的 pbkdf2_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
- 限制大小
- 上传成功后立刻进入解析和向量化流程
这里最大的变化,是左侧区域不再是"会话列表",而是"文件管理区"。用户可以清楚看到自己当前已经上传了哪些文件,也可以直接删除。
上传完成后,后端会:
- 保存文件到
/app/uploads - 写数据库记录
- 解析 PDF
- 切分文本
- 生成 embedding
- 写入 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-frontendask-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 传输中断
后端镜像里有 torch、transformers、sentence-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 一直 unhealthy,ask-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 的远端地址。
这一步真正缺的不是"缓存文件",而是离线模式。
最终的解决方式是:
- 本地预下载模型缓存
- 上传
hf-cache - 在
docker-compose.prod.yml中挂载缓存目录 - 设置:
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/