本地化 RAG 智能问答系统:FAISS + Ollama + Flask

8 阅读5分钟

本地化 RAG 智能问答系统:FAISS + Ollama + Flask

基于 FAISS + Ollama + Flask,不依赖任何云服务,在本地搭建一套带主题分类、权限管理的检索增强生成问答系统。


为什么做这个项目

市面上大多数 RAG(Retrieval-Augmented Generation)教程停留在 Jupyter Notebook 阶段——在 Notebook 里跑通 demo,然后呢?实际落地需要解决的问题远不止"把文档向量化":

  1. 向量数据库怎么选型? Milvus 等方案依赖 Docker,但很多场景没有容器环境。
  2. Embedding 维度不匹配怎么办? 不同模型的输出维度不同,硬编码容易踩坑。
  3. 语义检索的盲区? 纯向量检索对精确关键词(型号、术语)不够敏感。
  4. 文档管理怎么持久化? 上传记录丢了,下次启动就不知道库里有什么。
  5. 谁来上传,谁来提问? 没有权限控制的 RAG 系统无法投入实际使用。
  6. 文档多了怎么分类? 用户需要知道"这个系统里有什么"。

这个项目逐一解决了上述问题。下面是完整的技术拆解。


技术架构

┌──────────────────────────────────────────────────────┐
│                    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 系统从骨架到可用的过程。核心经验:

  1. FAISS 比 Milvus 更适合轻量场景 —— 无需 Docker,嵌入式存储,备份就是一个目录
  2. 混合检索比纯语义检索更可靠 —— BM25 补足了向量检索对精确关键词的盲区
  3. Embedding 维度可以通过 Matryoshka 表示灵活控制 —— 不用换模型也能对齐索引
  4. 权限和分类不是"锦上添花" —— 没有它们,RAG 系统只能是 demo

项目代码已开源在 GitHub(此处替换为你的仓库链接)