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 条结果,默认为 10include=["distances"]获取相似度距离分数,数值越小表示越相似- 返回结果是一个字典,包含
ids、distances、metadatas、documents等键
where 过滤 :检索时可加元数据过滤,支持
$eq、$gt、$lt等操作符(过滤结构化字段)。多条件组合用{"$and": [{"chapter": {"$lt": 10}}, {"topic": {"$eq": "机器学习"}}]}。where_document 按文档内容关键词过滤,支持$contains(过滤非结构化文本)。
4. 综合案例 01 --- LangChain RetrievalQA 链
注意 :LangChain 封装的 Chroma API(
langchain_chroma)和原生chromadbAPI 不一样,不要混淆。需安装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,实现更灵活的控制。使用 RunnableParallel、RunnablePassthrough、RunnableLambda、StrOutputParser 等组件自定义 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)与原生chromadbAPI 不同,注意区分 - 入库和检索用的 Embedding 模型必须一致,否则向量空间不匹配,检索无效
- 放入 Chain 的普通函数需
RunnableLambda包装且必须有返回值 - 粗排(bi-encoder)快但精度有限,精排(cross-encoder/reranker)慢但精度高,两者配合使用
- RAG 完整流程:知识库构建 → 向量检索 → Reranker 精排 → 注入 LLM → 生成回答