本教程系统性地讲解 Embedding(嵌入)、Rerank(重排序)和向量数据库三大核心概念,并通过构建一个完整的RAG(检索增强生成)知识库应用来实践这些技术。教程从理论基础出发,逐步深入到代码实现,帮助读者理解这些组件在向量RAG系统中的角色与协作方式。

前置教程

如想快速开始学习本教程,你可能需要先完成以下前置教程:

资源下载

1. 核心概念详解

1.1 Embedding(嵌入)

Embedding 是将文本、图像或其他类型的数据映射到高维向量空间中的连续向量的过程。在自然语言处理领域,Embedding 模型将一段文本转换为一组固定长度的浮点数数组(向量),使得语义相似的文本在向量空间中的距离更近。

Embedding 的工作原理:现代 Embedding 模型(如本教程示例使用的 bge-small-zh-v1.5)基于 Transformer 架构,通过大规模预训练学习语言的语义表示。模型将输入文本编码为一个稠密向量(dense vector),该向量捕获了文本的语义信息。两个文本向量的余弦相似度或欧氏距离可以作为它们语义相似度的度量。

Embedding 在向量RAG中的角色:在检索增强生成系统中,Embedding 模型负责将文档库中的文本转换为向量索引,同时将用户查询转换为向量,通过向量相似度搜索找到与查询语义最相关的文档片段

1.2 Rerank(重排序)

Rerank(重排序) 是在向量检索初步结果的基础上,使用更精确的模型对候选文档进行二次排序的过程。向量检索(如余弦相似度搜索)速度快但精度有限,Rerank 模型通过更深入地分析查询与候选文档之间的相关性,对结果进行重新排序,将最相关的结果提升到前列。

Rerank 与 Embedding 的对比

对比维度 Embedding 模型 Rerank 模型
输入 单条文本,生成向量 查询 + 文档对,计算相关性分数
输出 向量表示 相关性评分(0-1)
计算开销 低(一次编码) 高(每对计算一次)
精度 中等
适用阶段 初筛(Top-K 检索) 精排(Top-N 重排序)

为什么需要 Rerank:向量检索通常返回 Top-50 或 Top-100 候选结果,其中可能包含与查询部分相关但不精确匹配的文档。Rerank 模型对这些候选结果进行精细化的相关性判断,将精确匹配的结果排在前面,过滤掉不相关的噪声,从而提高最终提供给 LLM 的上下文质量。

1.3 向量数据库

向量数据库 是专门设计用于存储和检索向量数据的数据管理系统。与传统数据库基于精确匹配或关键字索引不同,向量数据库以向量之间的相似度(如余弦相似度、欧氏距离、内积)作为检索依据,支持高效的近似最近邻(ANN,Approximate Nearest Neighbor)搜索。

ChromaDB 是本教程使用的向量数据库,它具有以下特点:

  • 轻量级,可嵌入式运行,无需单独部署数据库服务器
  • 提供持久化存储,支持增量添加文档
  • 简单的 API 接口,适合快速原型开发

向量数据库的核心操作

  1. 向量入库:将文档分块后,通过 Embedding 模型转换为向量,存入数据库并建立索引。
  2. 相似度搜索:给定查询向量,在索引中快速查找最相似的 Top-K 个向量。
  3. 元数据过滤:结合文档的元数据(如来源、日期、类别)进行条件过滤后再搜索。
  4. 集合管理:创建和管理不同的知识库集合(Collection),每个集合可包含独立的文档和索引。

主流向量数据库对比

特性 ChromaDB FAISS Milvus Qdrant
部署方式 嵌入式/Python 库形式 分布式服务 服务化
适用场景 原型开发/小规模 高性能检索 大规模生产 中等规模生产
学习成本
持久化 支持 需自行实现 内建支持 内建支持
Python API 原生支持 支持 支持 支持

2. 环境配置

2.1 模型文件准备

本教程使用以下两个本地模型文件(需前往本文头部资源下载预先下载到指定路径):

模型 用途 预期路径
BAAI/bge-small-zh-v1.5 Embedding 向量生成 C:\models\bge-small-zh-v1.5
BAAI/bge-reranker-base 结果重排序 C:\models\bge-reranker-base

请确保目录结构与下图一致, Rerank 模型同理:

模型文件准备示意图

bge-small-zh-v1.5 模型体积小、推理速度快,适合作为 Embedding 编码器;bge-reranker-base 精度更高,用于对检索结果进行精细排序。

2.2 Python 虚拟环境配置

推荐使用 Conda 创建独立的虚拟环境:

# 创建并激活虚拟环境
conda create -n rag-kb python=3.10
conda activate rag-kb

2.3 安装依赖

在项目根目录(rag-knowledge-base/)下安装依赖:

# 安装核心依赖
pip install -r requirements.txt

各依赖包的作用:

包名 用途
fastapi + uvicorn Web 服务框架和 ASGI 服务器
chromadb 向量数据库
sentence-transformers 加载并运行 Embedding 和 Rerank 模型
openai 调用兼容 OpenAI API 格式的 LLM 服务
httpx HTTP 客户端
python-multipart FastAPI 处理文件上传
numpy 数值计算基础库

2.4 智谱 API 配置

本教程使用智谱 AI 的 glm-4.7-flash 模型作为 LLM。你需要在 config.py 中填写自己的 API Key,如果你还没有 API Key,可以先注册智谱账号并获取:

# 打开 config.py,将这一行中的 "your-api-key-here" 替换为你的智谱 API Key
ZHIPU_API_KEY = "your-api-key-here"      # 请替换为你的智谱 API Key

glm-4.7-flash 是智谱 AI 提供的快速推理模型,具有较快的响应速度和较好的中文理解能力,适合作为向量RAG系统的回答生成模型。

3. 示例代码实现

完整代码请前往网盘下载:示例项目rag-kb源码下载地址

3.1 项目结构

rag-knowledge-base/
├── requirements.txt        # 依赖清单
├── config.py               # 配置文件(模型路径、API 密钥等)
├── rag_core.py             # 核心模块(Embedding、Rerank、ChromaDB 封装)
├── cli.py                  # 命令行知识库创建工具
├── main.py                 # FastAPI 应用(知识检索 API + 前端页面)
└── static/
    └── index.html          # 前端界面

3.2 配置文件(config.py)

配置文件集中管理所有可变参数,包括模型路径、API 密钥、ChromaDB 存储路径等:

# ChromaDB 持久化存储路径
CHROMA_PERSIST_DIR = "./chroma_data"

# Embedding 模型路径
EMBEDDING_MODEL_PATH = r"C:\models\bge-small-zh-v1.5"

# Rerank 模型路径
RERANK_MODEL_PATH = r"C:\models\bge-reranker-base"

# 智谱 API 配置(请替换为你的 API Key)
ZHIPU_API_KEY = "your-api-key-here"
ZHIPU_API_BASE = "https://open.bigmodel.cn/api/paas/v4"
LLM_MODEL_NAME = "glm-4.7-flash"

# 向量检索参数
VECTOR_SEARCH_TOP_K = 30
RERANK_TOP_N = 5

3.3 核心模块(rag_core.py)

核心模块封装了四项关键能力:Embedding 编码、向量数据库操作、Rerank 重排序、LLM 回答生成。下面通过核心代码片段说明各组件的实现原理。

Embedding 服务

使用 sentence-transformers 加载 bge-small-zh-v1.5 模型,将文本转换为 512 维稠密向量:

from sentence_transformers import SentenceTransformer

class EmbeddingService:
    """Embedding 编码服务"""

    def __init__(self, model_name_or_path: str):
        self.model = SentenceTransformer(model_name_or_path)

    @property
    def dimension(self) -> int:
        return self.model.get_sentence_embedding_dimension()

    def encode(self, texts: list[str]) -> list[list[float]]:
        embeddings = self.model.encode(
            texts, normalize_embeddings=True, show_progress_bar=False
        )
        return embeddings.tolist()

    def encode_query(self, query: str) -> list[float]:
        return self.encode([query])[0]

ChromaDB 向量存储

封装了知识库的创建、文档添加和相似度搜索:

import chromadb

class VectorStore:
    """ChromaDB 向量数据库封装"""

    def __init__(self, persist_dir: str, embedding_service: EmbeddingService):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.embedding_service = embedding_service

    def create_collection(self, name: str) -> chromadb.Collection:
        try:
            self.client.delete_collection(name)
        except ValueError:
            pass
        return self.client.create_collection(name)

    def add_documents(self, collection_name: str, documents: list[str],
                      metadatas: list[dict] | None = None) -> list[str]:
        collection = self.get_collection(collection_name)
        ids = [f"doc_{i}_{hash(doc) % (10**8)}" for i, doc in enumerate(documents)]
        embeddings = self.embedding_service.encode(documents)
        collection.add(
            embeddings=embeddings, documents=documents,
            metadatas=metadatas or [{"source": "unknown"} for _ in documents],
            ids=ids
        )
        return ids

    def search(self, collection_name: str, query_vector: list[float],
               top_k: int = 10) -> list[dict]:
        collection = self.get_collection(collection_name)
        results = collection.query(
            query_embeddings=[query_vector],
            n_results=min(top_k, collection.count()),
            include=["documents", "metadatas", "distances"]
        )
        hits = []
        if results["documents"] and results["documents"][0]:
            for i in range(len(results["documents"][0])):
                hits.append({
                    "id": results["ids"][0][i],
                    "text": results["documents"][0][i],
                    "metadata": results["metadatas"][0][i] if results["metadatas"] else {},
                    "score": 1 - results["distances"][0][i] if results["distances"] else 0
                })
        return hits

ChromaDB默认用“L2距离”来判断两个向量像不像,距离越近越像。因为我们在存向量之前已经把它们都“拉”成了单位长度(也就是做了L2归一化),所以这个距离天然就在0到2之间。为了方便理解,我们可以用 1 减 距离 把它转成一个0到1之间的分数,分数越高,表示语义越接近。

Rerank 服务

使用 bge-reranker-base 模型对向量检索的候选结果进行精细排序:

from sentence_transformers import CrossEncoder

class RerankService:
    """Rerank 重排序服务"""

    def __init__(self, model_name_or_path: str):
        self.model = CrossEncoder(model_name_or_path)

    def rerank(self, query: str, candidates: list[dict], top_n: int = 5) -> list[dict]:
        if not candidates:
            return []
        pairs = [(query, cand["text"]) for cand in candidates]
        scores = self.model.predict(pairs)
        for i, score in enumerate(scores):
            candidates[i]["rerank_score"] = float(score)
        reranked = sorted(candidates, key=lambda x: x["rerank_score"], reverse=True)
        return reranked[:top_n]

LLM 服务

封装对智谱 glm-4.7-flash 模型的 API 调用,将检索结果组织为上下文后生成回答:

from openai import OpenAI

class LLMService:
    """LLM 回答生成服务"""

    def __init__(self, api_key: str, api_base: str, model_name: str):
        self.client = OpenAI(api_key=api_key, base_url=api_base)
        self.model_name = model_name

    def generate_answer(self, query: str, context_docs: list[dict]) -> str:
        context_parts = []
        for i, doc in enumerate(context_docs):
            source = doc.get("metadata", {}).get("source", "未知来源")
            context_parts.append(f"[文档 {i+1}](来源: {source})\n{doc['text']}")
        context_text = "\n\n".join(context_parts)

        system_prompt = (
            "你是一个基于知识库的问答助手..."
        )
        user_prompt = f"## 上下文信息\n\n{context_text}\n\n## 用户问题\n\n{query}"

        response = self.client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.3,
            max_tokens=1024
        )
        return response.choices[0].message.content

完整代码请查看 rag-knowledge-base/rag_core.py

3.4 命令行知识库创建(cli.py)

CLI(命令行界面)工具负责知识库的创建和删除管理。用户通过子命令指定操作类型,工具自动完成文档分块、向量化编码和存储。

核心组件使用 argparse 的子命令(subparsers)组织操作:

import argparse
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from config import *
from rag_core import EmbeddingService, VectorStore


def chunk_text(text: str, chunk_size: int = 256, overlap: int = 32) -> list[tuple[str, int]]:
    """将长文本分割为有重叠的文本块"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append((text[start:end], start))
        start += chunk_size - overlap
    return chunks


def cmd_create(args):
    """处理 create 子命令:创建知识库"""
    embedding_service = EmbeddingService(EMBEDDING_MODEL_PATH)
    vector_store = VectorStore(CHROMA_PERSIST_DIR, embedding_service)
    vector_store.create_collection(args.name)
    # 读取文件 -> 分块 -> 编码 -> 存入向量数据库
    for file_path in args.files:
        with open(file_path, "r", encoding="utf-8") as f:
            text = f.read()
        chunks = chunk_text(text, CHUNK_SIZE, CHUNK_OVERLAP)
        # 编码并存入...
    # ...


def main():
    parser = argparse.ArgumentParser(description="知识库管理工具")
    subparsers = parser.add_subparsers(dest="command", required=True, help="可用命令")

    create_parser = subparsers.add_parser("create", help="创建知识库并导入文档")
    create_parser.add_argument("--name", "-n", required=True, help="知识库名称")
    create_parser.add_argument("--files", "-f", nargs="+", required=True,
                               help="要导入的文档文件路径")

    delete_parser = subparsers.add_parser("delete", help="删除知识库")
    delete_parser.add_argument("--name", "-n", required=True, help="要删除的知识库名称")

    args = parser.parse_args()
    if args.command == "create":
        cmd_create(args)
    elif args.command == "delete":
        cmd_delete(args)


if __name__ == "__main__":
    main()

CLI 使用示例

# 创建名为 "company_handbook" 的知识库,导入两个文档
python cli.py create --name company_handbook --files handbook.txt policies.txt

# 删除知识库
python cli.py delete --name company_handbook

# 查看帮助
python cli.py --help

当前 CLI 工具仅支持 .txt 纯文本格式 文件的导入。如需支持 PDF、Word 等格式,需额外引入解析库(如 pypdfpython-docx)。

文档分块策略说明

graph LR A[原始文档] --> B[分块处理] B --> C1[块1
字符 0-256] B --> C2[块2
字符 224-480] B --> C3[块3
字符 448-704] B --> C4[...] C1 --> D[向量编码] C2 --> D C3 --> D C4 --> D D --> E[(ChromaDB 存储)] style A fill:#d6eaf8,stroke:#2980b9 style B fill:#fdebd0,stroke:#e67e22 style C1 fill:#d5f5e3,stroke:#27ae60 style C2 fill:#d5f5e3,stroke:#27ae60 style C3 fill:#d5f5e3,stroke:#27ae60 style C4 fill:#d5f5e3,stroke:#27ae60 style D fill:#ffecd6,stroke:#d35400 style E fill:#fadbd8,stroke:#c0392b

分块时采用重叠策略(overlap=32 字符),确保在切分边界附近的语义信息不会丢失。例如,一句话可能被切到两个块的分界处,重叠区域让两个块都包含该句子的完整语义。

完整代码请查看 rag-knowledge-base/cli.py

3.5 FastAPI 后端服务(main.py)

FastAPI 应用提供 RESTful API 接口和前端页面服务,串联 Embedding 编码、向量检索、Rerank 重排序和 LLM 回答生成四个阶段:

from fastapi import FastAPI, Query, HTTPException
from fastapi.responses import HTMLResponse
from config import *
from rag_core import (
    EmbeddingService, VectorStore, RerankService, LLMService,
)
import uvicorn

# FastAPI 应用初始化
app = FastAPI(
    title="RAG Knowledge Base API",
    description="基于 Embedding + Rerank + ChromaDB + LLM 的知识检索服务",
    version="1.0.0"
)

# 全局服务实例
embedding_service = EmbeddingService(EMBEDDING_MODEL_PATH)
vector_store = VectorStore(CHROMA_PERSIST_DIR, embedding_service)
rerank_service = RerankService(RERANK_MODEL_PATH)
llm_service = LLMService(ZHIPU_API_KEY, ZHIPU_API_BASE, LLM_MODEL_NAME)


@app.get("/api/collections")
def list_collections():
    """获取所有知识库名称列表"""
    return {"collections": vector_store.list_collections()}


@app.get("/api/search")
def search_knowledge_base(
    query: str = Query(..., description="用户查询"),
    collection: str = Query(..., description="知识库名称"),
    top_k: int = Query(VECTOR_SEARCH_TOP_K, description="初筛候选数"),
    top_n: int = Query(RERANK_TOP_N, description="精选数"),
):
    """在指定知识库中执行 RAG 检索流水线"""
    # 阶段1: 查询编码
    query_vector = embedding_service.encode_query(query)
    # 阶段2: 向量检索(初筛)
    candidates = vector_store.search(collection, query_vector, top_k)
    if not candidates:
        return {"query": query, "answer": "未在知识库中找到相关信息。"}
    # 阶段3: Rerank 重排序
    reranked = rerank_service.rerank(query, candidates, top_n)
    # 阶段4: LLM 回答生成
    answer = llm_service.generate_answer(query, reranked)
    return {"query": query, "candidates": reranked, "answer": answer}


@app.get("/", response_class=HTMLResponse)
def index():
    """返回前端交互页面"""
    html_path = os.path.join(os.path.dirname(__file__), "static", "index.html")
    with open(html_path, "r", encoding="utf-8") as f:
        return HTMLResponse(content=f.read())


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

API 接口说明

端点 方法 功能 参数
/api/collections GET 获取知识库列表
/api/search GET 检索知识库并生成回答 query, collection, top_k, top_n
/ GET 前端交互页面

完整代码请查看 examples/rag-knowledge-base/main.py

3.6 前端界面(static/index.html)

前端界面提供简洁的知识库搜索交互体验,使用项目主题色 #16baaa 统一设计风格。

你可以前往 rag-knowledge-base/static/index.html 查看完整代码。

4. 完整运行流程

4.1 启动后端服务

# 设置 API 密钥
# 在 config.py 中设置 ZHIPU_API_KEY 为你的 API Key

# 确保在 rag-knowledge-base/ 目录下
cd rag-knowledge-base/

# 启动 FastAPI 服务
python main.py

服务启动后,访问 http://localhost:8000 即可看到前端交互页面。

4.2 创建知识库

使用 CLI 工具管理知识库(CLI 工具独立运行,无需启动服务):

# 创建知识库,导入 txt 文档
python cli.py create --name company_handbook --files ./docs/handbook.txt ./docs/policies.txt

# 创建多个知识库
python cli.py create --name product_docs --files ./docs/product_v1.txt ./docs/product_v2.txt

# 删除知识库
python cli.py delete --name company_handbook

参数说明:

  • --name:知识库名称,用于区分不同的知识库。
  • --files:要导入的文档文件路径,支持多个文件路径;./表示当前目录下的文件或文件夹。

4.3 知识检索问答

通过前端页面完成:

  1. 打开浏览器访问 http://localhost:8000
  2. 从下拉列表中选择目标知识库
  3. 输入自然语言问题
  4. 点击"检索"按钮
  5. 查看 AI 回答和参考文档
graph LR U[用户
- 输入查询文本
- 展示回答和参考文档] F[前端界面
- GET /api/search?query=...&collection=...
- 返回检索结果 + 回答] API[FastAPI 后端] EMB[Embedding 模型
- 查询编码
- 查询向量] CHROMA[(ChromaDB
- 向量相似度搜索
Top-K=30)] RERANK[Rerank 模型
- 重排序
Top-N=5] LLM[LLM glm-4.7-flash
- 构建上下文并生成回答] U -->|输入查询| F F -->|GET 请求| API API -->|1. 调用| EMB EMB -->|2. 查询向量| CHROMA CHROMA -->|3. 候选文档列表| API RERANK -->|5. 精选文档列表| API API -->|4. 传入候选| RERANK API -->|6. 构建上下文| LLM LLM -->|7. AI 回答| API API -->|8. 返回结果+回答| F F -->|展示| U style U fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style F fill:#e8f4f8,stroke:#2980b9 style API fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style EMB fill:#d6eaf8,stroke:#2980b9 style CHROMA fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style RERANK fill:#fadbd8,stroke:#c0392b style LLM fill:#ffecd6,stroke:#d35400,stroke-width:2px

最终效果

知识检索问答示意图

5. 总结

5.1 核心内容回顾

  • Embedding:将文本转换为语义向量的技术,是向量检索的基础。bge-small-zh-v1.5 提供轻量级的中文 Embedding 能力。
  • Rerank:对向量检索结果进行二次排序,提高最终结果的精确度。bge-reranker-base 以 CrossEncoder 架构实现更精准的相关性判断。
  • 向量数据库(ChromaDB):专为向量相似度搜索设计的轻量级数据库,支持持久化存储和元数据过滤。
  • 两阶段检索策略:向量检索快速初筛(Top-30),Rerank 精排精选(Top-5),在效率和精度之间取得平衡。

5.2 常见问题与解答

问:Embedding 模型和 Rerank 模型可以共用同一个模型吗?

不能。Embedding 模型使用 Bi-Encoder 架构,为每条文本独立生成向量,效率高。Rerank 模型使用 CrossEncoder 架构,需要对查询和每条候选文档进行联合编码,精度高但计算量大。两者在向量RAG流水线中扮演不同角色,不能互换。

问:ChromaDB 与其他向量数据库相比有什么优缺点?

优点:无需单独部署、API 简洁、Python 原生支持、适合学习和原型验证。缺点:大规模生产场景下性能和功能不如 Milvus、Qdrant 等专业向量数据库。

问:如何选择 Top-K 和 Top-N 的值?

Top-K(初筛数)取决于文档库规模和 Embedding 模型的召回能力,通常在 20-100 之间。Top-N(精选数)取决于 LLM 的上下文窗口大小和回答质量要求,通常为 3-10。如果 Rerank 模型质量高,可以容忍较小的 Top-K。

问:文档分块的合适大小是多少?

取决于具体场景:事实性问答(如产品规格)使用 128-256 字符的小块;概念性问答(如政策条文)使用 512-1024 字符的大块。本教程使用 256 字符的块大小和 32 字符的重叠,适合一般性文档。

问:如何将这个系统扩展到更大规模的文档库?

可以从三个方向扩展:一是使用更高效的向量数据库(如 Milvus);二是引入全文检索作为补充(混合检索);三是增加文档预处理 pipeline,包括格式解析(PDF、Word)、表格提取、段落重排等。