本地化 RAG 智能问答系统:FAISS + Ollama + Flask
基于 FAISS + Ollama + Flask,不依赖任何云服务,在本地搭建一套带主题分类、权限管理的检索增强生成问答系统。
为什么做这个项目
市面上大多数 RAG(Retrieval-Augmented Generation)教程停留在 Jupyter Notebook 阶段——在 Notebook 里跑通 demo,然后呢?实际落地需要解决的问题远不止"把文档向量化":
- 向量数据库怎么选型? Milvus 等方案依赖 Docker,但很多场景没有容器环境。
- Embedding 维度不匹配怎么办? 不同模型的输出维度不同,硬编码容易踩坑。
- 语义检索的盲区? 纯向量检索对精确关键词(型号、术语)不够敏感。
- 文档管理怎么持久化? 上传记录丢了,下次启动就不知道库里有什么。
- 谁来上传,谁来提问? 没有权限控制的 RAG 系统无法投入实际使用。
- 文档多了怎么分类? 用户需要知道"这个系统里有什么"。
这个项目逐一解决了上述问题。下面是完整的技术拆解。
技术架构
┌──────────────────────────────────────────────────────┐
│ Flask Web 应用 │
│ ┌────────────┐ ┌────────────┐ ┌─────────────────┐ │
│ │ 登录认证 │ │ 文档管理 │ │ 智能对话 │ │
│ │ (Session) │ │ (管理员) │ │ (所有用户) │ │
│ └────────────┘ └─────┬──────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────┐ │
│ │ RAGSystem (rag.py) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 混合检索器 │ │ │
│ │ │ BM25 + FAISS Ensemble │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────▼───────────┐ │ │
│ │ │ Ollama LLM (qwen2.5) │ │ │
│ │ └───────────────────────┘ │ │
│ └──────────────────────────────┘ │
│ │ │
├────────────────────────┼──────────────────────────────┤
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Ollama (本地) │ │
│ │ Embedding: qwen3-embedding:0.6b │ │
│ │ LLM: qwen2.5:7b │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ FAISS 本地索引 (faiss_index/) │ │
│ │ 上传记录 (upload_records.json) │ │
│ │ 主题定义 (categories.json) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
核心实现
1. 用 FAISS 替换 Milvus —— 零依赖的向量存储
Milvus 需要 Docker 服务,而 FAISS 是嵌入式库,pip install faiss-cpu 即可使用,无需任何外部服务。
# vector_store.py
from langchain_community.vectorstores import FAISS
class MilvusVectorStore: # 类名保持兼容
def __init__(self):
self.embedding_model = get_embedding_model()
index_file = os.path.join(DB_PATH, "index.faiss")
if os.path.exists(index_file):
# 加载已有索引
self.vector_store = FAISS.load_local(
DB_PATH, self.embedding_model, allow_dangerous_deserialization=True
)
else:
# 初始化空索引
self.vector_store = FAISS.from_texts(
[""], self.embedding_model, metadatas=[{"source": "init"}]
)
self.save()
关键点:通过检查 index.faiss 文件是否存在来判断是否需要重建索引,避免 FAISS 在空目录上加载失败。
2. 解决 Embedding 维度不匹配
Ollama 的 qwen3-embedding:0.6b 默认输出 4096 维向量,但我们的 FAISS 索引按 1024 维创建。Ollama 原生支持 dimensions 参数(Matryoshka 表示学习),可以在推理时截取低维表示:
# embedding.py
class OllamaEmbeddings(Embeddings):
def __init__(self):
self.dimensions = EMBEDDING_CONFIG["dimension"] # 1024
self.client = Client(host=self.base_url)
def embed_documents(self, texts: list[str]) -> list[list[float]]:
return self.client.embed(
self.model, texts, dimensions=self.dimensions
)["embeddings"]
这样无需重新训练模型,直接通过 dimensions 参数控制输出维度。
3. 混合检索:BM25 + FAISS
纯语义检索的盲区:当用户搜索 "QW-3000 规格参数" 时,向量检索可能匹配到 "设备选型指南" 而漏掉包含 "QW-3000" 的精确文档。
解决方案是 EnsembleRetriever,将 BM25(关键词匹配)和 FAISS(语义匹配)的结果按权重融合:
# vector_store.py
def as_hybrid_retriever(self):
faiss_retriever = self.vector_store.as_retriever(
search_kwargs={"k": 5}
)
texts = self._build_bm25_texts()
if texts:
bm25_retriever = BM25Retriever.from_texts(texts)
bm25_retriever.k = 5
return EnsembleRetriever(
retrievers=[bm25_retriever, faiss_retriever],
weights=[0.4, 0.6] # BM25 占 40%,FAISS 占 60%
)
return faiss_retriever
融合算法采用 RRF(Reciprocal Rank Fusion) :
score(doc) = 0.4 / (rank_bm25 + 60) + 0.6 / (rank_faiss + 60)
两个检索器都排名靠前的文档获得最高分。权重 0.4/0.6 表明我们更信任语义检索,但 BM25 作为关键补充。
4. 文档管理:去重 + 持久化
重复上传处理:同一文件再次上传时,先删除旧数据再添加新数据,避免向量库中出现重复片段。
def add_documents(self, documents, filename="unknown", category="other"):
existing = next((r for r in self.upload_records
if r["filename"] == filename), None)
if existing:
self._remove_by_filename(filename) # 重建 FAISS 索引,排除旧文档
self.upload_records.remove(existing)
# ... 添加新文档
持久化:上传记录保存为 faiss_index/upload_records.json,每次启动时自动加载。FAISS 索引通过 save_local() / load_local() 持久化。
5. 权限控制:管理员 vs 普通用户
基于 Flask Session 的轻量级认证,无需数据库:
| 路由 | 权限 | 功能 |
|---|---|---|
/login | 公开 | 用户登录 |
/upload | 管理员 | 上传文档 |
/categories (POST) | 管理员 | 添加主题 |
/clear | 管理员 | 清空数据 |
/ | 已登录 | 对话界面 |
/query | 已登录 | RAG 查询 |
密码通过 Werkzeug 的 check_password_hash 验证存储:
# users.json 中的密码是 generate_password_hash 生成的哈希值
if not user or not check_password_hash(user['password'], password):
return jsonify({"success": False, "error": "用户名或密码错误"})
6. 主题分类系统
用户上传文档时必须选择主题分类,让知识库有清晰的结构。
- 预设主题:技术文档、产品手册、常见问题、政策法规、其他
- 自定义主题:管理员通过模态框添加,支持自定义名称、颜色、图标、描述
- 标签云:对话面板顶部展示所有主题及其文档数量,点击可筛选
- 强制选择:未选择主题时,上传按钮和拖拽区域自动禁用
7. 三栏 Dashboard UI
┌─────────┬──────────────────────────┬──────────────┐
│ 侧边栏 │ 主内容区 │ 参考来源面板 │
│ │ │ │
│ 💬 对话 │ 主题标签云 [全部][技术]...│ 来源 1 │
│ 📄 文档 │ │ 文档A.pdf │
│ │ 对话消息区域... │ 来源 2 │
│ 👤 用户 │ │ 文档B.pdf │
│ 🚪 退出 │ [ 输入框 ] [发送] │ │
└─────────┴──────────────────────────┴──────────────┘
- 侧边栏可折叠(响应式)
- 非管理员自动隐藏文档管理入口
- 右侧参考面板实时显示回答的来源文档
项目结构
myrag/
├── app.py # Flask 主应用(路由、认证)
├── rag.py # RAG 系统核心(LLM Chain)
├── vector_store.py # FAISS 向量存储(含混合检索)
├── embedding.py # Ollama Embedding 封装
├── config.py # 集中配置
├── main.py # CLI 演示脚本
├── requirements.txt # 依赖清单
├── users.json # 用户数据库(已 hash)
├── categories.json # 主题定义
├── templates/
│ ├── index.html # Dashboard 主页面
│ └── login.html # 登录页面
└── static/
├── css/style.css
└── js/main.js
如何部署
# 1. 确保 Ollama 已安装并运行
ollama pull qwen2.5:7b
ollama pull qwen3-embedding:0.6b
# 2. 安装依赖
pip install -r requirements.txt
# 3. 启动应用
python app.py
# 4. 浏览器访问 http://localhost:5000
默认账号:
- 管理员:
admin/admin123 - 普通用户:
user/user123
总结
这个项目展示了一个完整的本地化 RAG 系统从骨架到可用的过程。核心经验:
- FAISS 比 Milvus 更适合轻量场景 —— 无需 Docker,嵌入式存储,备份就是一个目录
- 混合检索比纯语义检索更可靠 —— BM25 补足了向量检索对精确关键词的盲区
- Embedding 维度可以通过 Matryoshka 表示灵活控制 —— 不用换模型也能对齐索引
- 权限和分类不是"锦上添花" —— 没有它们,RAG 系统只能是 demo
项目代码已开源在 GitHub(此处替换为你的仓库链接)