大模型项目:RAG项目实战与FlagEmbedding模型

ChromaDB 实战操作

向量数据库是 RAG 流程中的记忆层 。之前学习了 ChromaDB 基础连接与集合操作,今天深入学习文档增删查操作 + 两个完整 RAG 综合案例 + 底层向量化检索与重排序原理


1. 向集合中添加文档

add() 是 ChromaDB 中用于向集合添加文档的核心方法,数据可来源于知识库中的 txt、pdf、word、excel、json 等各类文件。

python 复制代码
import chromadb

# 获取连接对象
vector = chromadb.PersistentClient("./chroma")
# 获取集合对象
collection = vector.get_collection(name='test01')

# 准备测试数据
docs = ["小明喜欢苹果", "小红喜欢香蕉", "小刚喜欢橙子"]
# 调用 add 方法添加文档
collection.add(
    documents=docs,
    ids=[f"doc{i}" for i in range(len(docs))],
    metadatas=[{"hobby": "like_fruit"} for _ in range(len(docs))],
)
print("文档添加成功")

关键点:

  • add() 方法没有返回值,只需传入数据即可
  • documents:文本内容列表,后期来源于知识库中加载并分割后的数据
  • ids:每条数据的唯一标识 ,必须唯一且不重复,通常以 doc1, doc2, ... 格式命名
  • metadatas:元数据列表,与文档一一对应,可用于后续的 where 过滤检索
  • 三个列表的长度必须一致:len(ids) == len(documents) == len(metadatas)

collection.add() 参数详解:

参数名 类型 是否必需 说明
ids List[str] 唯一标识每条数据的字符串 ID 列表,必须唯一且不重复
documents List[str] 文本内容列表。若不提供 embeddings,ChromaDB 会自动使用创建集合时指定的嵌入模型生成向量
metadatas List[Dict] 与每个文档关联的元数据字典列表,用于后续的 where 过滤检索
embeddings List[List[float]] 预计算的向量嵌入列表,若提供则跳过自动向量化步骤

重要原则 :创建集合时如果已指定了 embedding_function,则无需手动传入向量,ChromaDB 会自动使用该模型将 documents 转为向量存储。


2. 文档查询 --- get 方法

get() 方法用于获取集合中的文档内容,支持按 ID、按元数据(where)、按文档内容(where_document)进行过滤查询。注意:get() 做精确查找,不是语义相似度检索。

python 复制代码
import chromadb

vector = chromadb.PersistentClient("./chroma")
collection = vector.get_collection(name='test01')

# 查询所有数据(指定返回内容)
results = collection.get(include=["embeddings", "metadatas", "documents"])
print(f"所有数据:{results}")

# 获取指定 id 的文档
doc1_result = collection.get(ids=['doc1'])
print(f"doc1数据:{doc1_result}")

# 按元数据过滤
like_fruit_result = collection.get(where={'hobby': 'like_fruit'})
print(f"like_fruit数据:{like_fruit_result}")

# 按文档内容包含关键词过滤
apple_result = collection.get(where_document={"$contains": "苹果"})
print(f"包含苹果数据:{apple_result}")

collection.get() 参数详解:

参数名 类型 说明
ids List[str] 要获取的文档 ID 列表(可选)
where Dict 元数据过滤条件,如 {"hobby": "like_fruit"}
where_document Dict 文档内容过滤,如 {"$contains": "苹果"}
include List[str] 指定返回哪些信息:"documents""metadatas""embeddings"

3. 相似度检索 --- query 方法

query() 是 ChromaDB 的核心方法:将查询文本转为向量后,与集合中所有文档向量计算余弦相似度,返回最相似的 top-k 条结果。

python 复制代码
result = collection.query(
    query_texts=["橘子"],
    n_results=1,
    include=["metadatas", "documents", "distances"],
)

关键点:

  • query_texts:查询文本列表,ChromaDB 自动使用集合的嵌入模型将其转为向量
  • n_results:返回最相似的前 N 条结果,默认为 10
  • include=["distances"] 获取相似度距离分数,数值越小表示越相似
  • 返回结果是一个字典,包含 idsdistancesmetadatasdocuments 等键

where 过滤 :检索时可加元数据过滤,支持 $eq$gt$lt 等操作符(过滤结构化字段)。多条件组合用 {"$and": [{"chapter": {"$lt": 10}}, {"topic": {"$eq": "机器学习"}}]}where_document 按文档内容关键词过滤,支持 $contains(过滤非结构化文本)。


4. 综合案例 01 --- LangChain RetrievalQA 链

注意 :LangChain 封装的 Chroma API(langchain_chroma)和原生 chromadb API 不一样,不要混淆。需安装 langchain-chroma==1.1.0

此案例演示完整 RAG 流程:构建知识库 → 向量化存储 → 问答链实现。

python 复制代码
"""
langchain 封装 chromadb 的 API 实现流程:
    一、构造向量数据库数据内容
        1、加载数据
        2、数据分块
        3、加载向量化模型
        4、创建向量数据库然后存入数据
    二、问答 --- 外部知识库查询结果作为上下文,然后模型生成回复
        1、加载向量数据库
        2、加载大模型、向量化模型
        3、得到向量数据库对象、创建问答链
        4、生成回复
"""
import os
from langchain_classic.chains.retrieval_qa.base import RetrievalQA
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI

# 配置
vector_path = r"D:\TechWorkPy\workspace\stu_rag\04ChromaDB向量数据库\lc_chroma"
collection_name = "hqyj"
model_path = r"D:\TechWorkPy\models\paraphrase-multilingual-MiniLM-L12-v2"

llm = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    model="qwen3.7-max-preview",
    streaming=True,
)


# ① 构建向量数据库(离线操作,执行一次即可)
def build_vector_db():
    """数据加载 → 文本分割 → 向量化 → 存入向量数据库"""
    # 1. 加载数据
    loader = TextLoader(r"D:\TechWorkPy\workspace\stu_rag\datasets\华清远见.txt", encoding='utf-8')
    data = loader.load()

    # 2. 文本分割
    text_spliter = RecursiveCharacterTextSplitter(
        chunk_size=100,       # 分块大小
        chunk_overlap=20,     # 重叠大小
        length_function=len,
        separators=["\n\n", ".", "?", "!", "\n"],
    )
    chunks = text_spliter.split_documents(data)

    # 3. 加载向量化模型(本地模型,local_files_only=True)
    embedding_model = HuggingFaceEmbeddings(
        model_name=model_path,
        model_kwargs={"device": "cuda", "local_files_only": True},
    )

    # 4. 创建向量数据库并存入数据(一步到位)
    Chroma.from_documents(
        documents=chunks,
        embedding=embedding_model,
        persist_directory=vector_path,
        collection_name=collection_name,
        collection_metadata={"hnsw:space": "cosine"},
    )
    print("向量数据库创建成功")


# ② 不使用 RAG 的问答(纯 LLM)
def qa_no_rag():
    """直接让 LLM 回答,无外部知识库参考"""
    messages = [
        {"role": "system", "content": "请用一句话回复用户的问题"},
        {"role": "user", "content": "介绍华清远见cc老师"},
    ]
    rs = llm.invoke(messages)
    print(rs.content)


# ③ 使用 RAG 的问答(RetrievalQA 链)
def qa_with_rag(question):
    """RAG 问答:检索 → 增强 → 生成"""
    # 1. 加载向量化模型(必须与建库时使用的一致)
    embedding_model = HuggingFaceEmbeddings(
        model_name=model_path,
        model_kwargs={"device": "cuda", "local_files_only": True},
    )

    # 2. 加载向量数据库
    vector_db = Chroma(
        persist_directory=vector_path,
        collection_name=collection_name,
        embedding_function=embedding_model,
    )

    # 3. 创建 RetrievalQA 问答链
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        retriever=vector_db.as_retriever(search_kwargs={"k": 2}),  # 召回 top-2
        return_source_documents=True,  # 返回检索到的源文档
    )

    # 4. 执行问答
    rs = qa_chain.invoke(question)
    print(rs['result'])


if __name__ == '__main__':
    # 1. 构建向量数据库(只需执行一次)
    # build_vector_db()
    # 2. 对比测试
    # qa_no_rag()                    # 纯 LLM --- 可能产生幻觉
    qa_with_rag("介绍华清远见张三老师")  # RAG --- 基于知识库回答

关键点:

  • Chroma.from_documents() 一步完成数据入库:文档分割 → 向量化 → 存入向量数据库
  • persist_directory 指定磁盘存储路径,重启后数据不丢失
  • as_retriever(search_kwargs={"k": 2}) 将向量库转为检索器,k 控制召回数量
  • RetrievalQA.from_chain_type() 封装好的问答链,自动完成检索 → 拼接提示词 → LLM 生成
  • ⚠️ 核心原则 :入库和检索用的 Embedding 模型必须是同一个,否则向量空间不一致导致检索无效
  • return_source_documents=True 可查看 LLM 基于哪些文档片段生成的回答,便于调试

RAG vs 纯 LLM 对比:

对比维度 纯 LLM 问答 RAG 问答
信息来源 仅靠模型训练数据 外部知识库 + 模型能力
幻觉风险 高,可能编造不存在的信息 低,有检索到的文档作为依据
实时性 训练数据截止后无法更新 知识库可随时更新,答案实时有效
私有数据 无法回答私有领域问题 支持企业私有知识库问答
实现方式 直接 llm.invoke(messages) RetrievalQA 链自动完成检索与回答

5. 综合案例 02 --- 自定义 Chain 实现 RAG

除了使用封装好的 RetrievalQA,还可以手动搭建 Chain,实现更灵活的控制。使用 RunnableParallelRunnablePassthroughRunnableLambdaStrOutputParser 等组件自定义 RAG 流程。

python 复制代码
from langchain_core.runnables import RunnableParallel, RunnablePassthrough, RunnableLambda
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser


def qa_with_rag(question):
    # 获取检索器(同上)
    retriever = vector_db.as_retriever(search_kwargs={"k": 2})

    # 定义提示词模板
    template = """
你是一个基于知识库的AI助手。请根据RAG检索内容回答用户问题。
规则:
    - 仅基于提供的知识回答,不使用外部知识补充。
    - 检索内容不足时,说明信息不足,不要猜测。
    - 优先提炼关键答案,避免冗长解释。
    - 输出结果时,不允许输出"根据提供的参考资料"这样的内容。
参考资料:
    {context}
问题:
    {question}
答案:
"""
    prompt = PromptTemplate(
        template=template,
        input_variables=["context", "question"],
    )

    # 打印召回结果(调试用)
    def print_docs(docs):
        print("检索到的文档内容:")
        for doc in docs:
            print(doc.page_content)
        return docs  # ⚠️ 必须有返回值,否则后续环节拿不到数据

    """
    构建 Chain 的核心思路:搭积木
    每个积木(组件)必须实现 Runnable 接口,通过 | 管道运算符串联
    """
    qa_chain = (
        RunnableParallel({                         # 并行执行器
            "context": retriever | RunnableLambda(print_docs),  # 检索 + 打印
            "question": RunnablePassthrough(),      # 透明传递问题,不做任何更改
        })
        | prompt                                   # 替换 {context} 和 {question}
        | llm                                      # 大模型生成回复
        | StrOutputParser()                        # AIMessage → 纯文本字符串
    )

    result = qa_chain.invoke(question)
    print(f"输出结果:{result}")

关键点:

  • 自定义 Chain 的优势:完全控制每一步的处理逻辑,可插入调试、过滤、转换等自定义环节
  • RunnableParallel:并行执行多个任务,这里同时获取检索上下文和传递用户问题
  • RunnablePassthrough()透明传递,直接将输入值传给下一环节,不做任何处理
  • RunnableLambda(print_docs):将普通函数包装为 Chain 组件(必须实现 Runnable 接口才能放入 Chain)
  • | 管道运算符:将多个组件串联,前一个的输出自动成为后一个的输入
  • StrOutputParser():将 LLM 返回的 AIMessage 对象转为纯文本字符串
  • ⚠️ RunnableLambda 包装的函数必须有返回值,否则 Chain 断流

Chain 执行流程:

复制代码
用户问题
    │
    ▼
RunnableParallel
    ├── retriever → 检索向量数据库
    │       │
    │       ▼
    │   print_docs(打印调试)
    │       │
    │       └── "context": 文档片段
    │
    └── RunnablePassthrough → "question": 原样传递
    │
    ▼
PromptTemplate(替换 {context} 和 {question} 占位符)
    │
    ▼
ChatOpenAI(LLM 基于上下文生成回复)
    │
    ▼
StrOutputParser(AIMessage → 纯文本字符串)
    │
    ▼
最终输出结果

6. FlagEmbedding 向量化模型

除了使用高层的 ChromaDB + LangChain 方案,也可以直接使用 FlagEmbedding(BGE 系列模型)进行底层向量化,理解 RAG 的底层原理。

01-flagembedding向量化模型.py:使用 BGE 模型将文档批量转为向量并保存。

python 复制代码
"""
参考网站:https://github.com/FlagOpen/FlagEmbedding
选择模型:BAAI/bge-base-zh-v1.5
下载地址:https://huggingface.co/BAAI/bge-base-zh-v1.5
"""
from FlagEmbedding import FlagAutoModel
import numpy as np

# 准备测试数据
documents = [
    "FlagEmbedding 是一个由北京智源人工智能研究院开发的文本嵌入模型。",
    "BGE 模型在 MTEB 排行榜上取得了优异的成绩。",
    "RAG(检索增强生成)是一种利用外部知识库增强大模型回答能力的技术。",
    "深度学习是机器学习的一个分支,基于深层神经网络。"
]

# 加载向量化模型
embedding_model = FlagAutoModel.from_finetuned(
    # 模型路径(本地已下载)
    model_name_or_path="G:/models/bge-base-zh-v1.5",
    # 检索指令前缀 --- 告诉模型向量用于检索任务,显著提升精度
    query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:",
    # 半精度推理 --- 显存减半、速度提升
    use_fp16=True,
)

# 批量向量化文档
embedding_docs = embedding_model.encode(documents)

# 存储为 npz 文件(原文 + 向量一并保存)
np.savez(
    r"E:\workspace\feifan_two\stu_rag\datasets\flag_embedding.npz",
    documents=documents,
    embedding_docs=embedding_docs
)

关键点:

  • BAAI/bge-base-zh-v1.5:北京智源人工智能研究院(BAAI)开发的中文文本嵌入模型,在 MTEB 中文排行榜上领先
  • FlagAutoModel.from_finetuned():加载微调后的 FlagEmbedding 模型,支持 BGE、BGE-M3 等系列
  • query_instruction_for_retrieval:检索指令前缀,告诉模型"这个向量将用于检索匹配",不加指令精度会明显下降
  • use_fp16=True:半精度浮点数推理,显存占用减少约 50%,推理速度提升
  • embedding_model.encode(documents):批量向量化,自动利用 GPU 加速
  • np.savez():将原始文本和向量二进制数据打包保存到 .npz 文件,后续检索时直接加载

7. 余弦相似度检索与重排序(Reranker)

02-flagebedding向量化结果使用.py 演示了加载已向量化的数据,手动完成问题向量化 → 余弦相似度计算 → 排序 → top-k 检索的全流程。

python 复制代码
import numpy as np
from FlagEmbedding import FlagAutoModel

# 加载向量化模型(与建库时相同)
embedding_model = FlagAutoModel.from_finetuned(
    model_name_or_path="G:/models/bge-base-zh-v1.5",
    query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:",
    use_fp16=True,
)

# 加载已保存的 npz 文件
data = np.load(r"E:\workspace\feifan_two\stu_rag\datasets\flag_embedding.npz")
docs = data['documents']          # 原始文档列表
embeddings = data['embedding_docs']  # 向量化数据

# 测试问题
question = "什么是rag?"
# 问题向量化(必须用同一个模型)
question_embedding = embedding_model.encode(question)


# 定义余弦相似度计算函数
def cosine_similarity(vec1, vec2):
    """计算两个向量的余弦相似度,值越接近1越相似"""
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))


# 计算问题向量与每个文档向量的相似度
score_list = [
    {index: cosine_similarity(item, question_embedding)}
    for index, item in enumerate(embeddings)
]

# 按相似度降序排序(从高到低)
score_list.sort(key=lambda x: list(x.values())[0], reverse=True)

# 取 top-3 检索结果
top_k = 3
for index, item in enumerate(score_list[:top_k]):
    for doc_idx in item.keys():
        print(f"top-{index+1}文档:\n{docs[doc_idx]}")

余弦相似度公式详解:

cos(θ)=A⋅B∥A∥×∥B∥=∑i=1nAiBi∑i=1nAi2×∑i=1nBi2cos(\theta) = \frac{A \cdot B}{\|A\| \times \|B\|} = \frac{\sum_{i=1}^n A_i B_i}{\sqrt{\sum_{i=1}^n A_i^2} \times \sqrt{\sum_{i=1}^n B_i^2}}cos(θ)=∥A∥×∥B∥A⋅B=∑i=1nAi2 ×∑i=1nBi2 ∑i=1nAiBi

  • 值域 −1,1-1, 1−1,1,值越大表示两个向量方向越接近,语义越相似
  • np.dot(vec1, vec2):计算向量点积(分子)
  • np.linalg.norm(vec):计算向量的 L2 范数/模长(分母)

03-flagebedding向量化结果使用+重排序.py :在粗排结果之上使用 cross-encoder 重排序模型 进行精排,这是 RAG 中最强提质手段

python 复制代码
from FlagEmbedding import FlagReranker

# 粗排阶段(同上)--- 取 top-3
retriever_result = []
for item in score_list[:top_k]:
    for doc_idx in item.keys():
        retriever_result.append(docs[doc_idx])

print("-------------------重排序-------------------------")
"""
重排序原理:把 question 和粗排召回的文档配对,通过 cross-encoder 模型
计算 (question, doc1)、(question, doc2)... 的相关性得分。
自注意力机制让问题和文档的每个 token 互相交互,精度远高于向量点积。
重排序发生在粗排之后、注入 LLM 上下文之前。
"""

# 加载重排序模型(cross-encoder)
reranker = FlagReranker(
    model_name_or_path=r'G:\models\bge-reranker-large_v1',
    use_fp16=True,
)

# 将问题与每个召回文档配对
reranker_input = [(question, doc) for doc in retriever_result]

# 计算相关性得分(得分越高越相关)
scores = reranker.compute_score(reranker_input)
print(f"重排序得分:{scores}")

粗排(Retrieval)vs 精排(Rerank)对比:

阶段 模型类型 原理 速度 精度 处理量级
粗排 bi-encoder(双编码器) 问题和文档各自独立编码为向量,余弦相似度算分 快(毫秒级) 中等 百万级
精排 cross-encoder(交叉编码器) 问题和文档拼接后输入 Transformer,自注意力计算相关度 慢(秒级) 🔥 百级

RAG 标准流程中的位置 :用户问题 → 向量检索(粗排 top-100)Reranker(精排 top-5) → 注入 LLM 上下文 → 生成回复。两者配合兼顾效率与精度。


8. 总结 --- Day3 知识串联
复制代码
Day3 学习路线

ChromaDB 操作            LangChain 集成               底层原理
  add() 添加文档            RetrievalQA 链             BGE 向量化模型
  get() 精确查询            自定义 Chain              余弦相似度手算
  query() 语义检索          RunnableParallel           Reranker 精排
                            RunnableLambda            cross-encoder
                            StrOutputParser

核心要点:

  • get() 做精确查找,query() 做语义相似度检索,二者分工不同
  • add()ids 必须唯一,metadatas 用于后续 where 过滤
  • LangChain 的 Chroma API(langchain_chroma)与原生 chromadb API 不同,注意区分
  • 入库和检索用的 Embedding 模型必须一致,否则向量空间不匹配,检索无效
  • 放入 Chain 的普通函数需 RunnableLambda 包装且必须有返回值
  • 粗排(bi-encoder)快但精度有限,精排(cross-encoder/reranker)慢但精度高,两者配合使用
  • RAG 完整流程:知识库构建 → 向量检索 → Reranker 精排 → 注入 LLM → 生成回答
相关推荐
蓝狐社1 小时前
宁德时代不是电池厂,是下棋的
人工智能
李燚7 小时前
RAG 流水线设计:Eino 的 Loader → Transformer → Indexer → Retriever(第60篇-E46)
人工智能·深度学习·transformer·agent·rag·aiagent·eino
码农学院7 小时前
Vue3小程序开发实战:服装鞋帽箱包行业库存同步方案
人工智能
To_OC7 小时前
跟 AI 写代码越写越乱?我靠这套「Vibe Coding」思路彻底治好了幻觉屎山
人工智能·agent·vibecoding
IT小盘7 小时前
03-大模型API不只是发送Prompt-流式输出超时重试与异常处理
人工智能·python·prompt
深圳慧闻智造技术有限公司8 小时前
机器人减速机壳体加工精度要求的四大核心维度分析
人工智能·机器人·机器人壳体加工
冬奇Lab8 小时前
AI 评测系列(07):自定义 Benchmark——从业务场景到评测集
人工智能
骏晔科技DreamLNK9 小时前
国产蓝牙模块哪个品牌好?骏晔科技 7 款主流型号横向对比
人工智能·科技
Zzz不能停9 小时前
个人博客系统系统---测试报告【笔耕云】
python·功能测试·自动化·压力测试