知识库构建工具与 LangChain 操作ChromaDB
之前的课程学习了 ChromaDB 基础操作和 LangChain 集成。今天深入底层向量化方案对比 和各方案的完整代码流程,并掌握重排序(Reranker)的用法与作用。
一、回顾重排序(Reranker)--- 用法与作用
1. 为什么要重排序?
粗排(bi-encoder) 阶段------向量检索速度快但精度有限。因为它把问题和文档各自独立编码为向量,仅靠余弦相似度打分,丢失了 token 级别的交互信息。
精排(cross-encoder) 阶段------将问题与每个候选文档配对为 (question, doc),一起输入 Transformer,通过自注意力机制让问题与文档的 token 充分交互,输出精准的相关性分数。
RAG 标准流程中的位置 :用户问题 → 向量检索(粗排 top-100) → Reranker(精排 top-5) → 注入 LLM 上下文 → 生成回复
2. 粗排 vs 精排对比
| 阶段 | 模型类型 | 原理 | 速度 | 精度 | 处理量级 |
|---|---|---|---|---|---|
| 粗排 | bi-encoder(双编码器) | 各自独立编码→余弦相似度 | 快(毫秒级) | 中等 | 百万级 |
| 精排 | cross-encoder(交叉编码器) | 拼接后 Transformer 自注意力 | 慢(秒级) | 高 🔥 | 百级 |
3. 代码用法
python
from FlagEmbedding import FlagReranker
# 加载重排序模型
reranker = FlagReranker(
model_name_or_path=r'D:/TechWorkPy/models/bge-reranker-large',
use_fp16=True,
)
# 将问题与粗排召回的文档配对
reranker_input = [(question, doc) for doc in retriever_result]
# 计算相关性得分(得分越高越相关 🔥)
scores = reranker.compute_score(reranker_input)
print(f"重排序得分:{scores}")
# 按得分降序排列,取精排后的 top-3
scored_docs = list(zip(retriever_result, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
final_docs = [doc for doc, _ in scored_docs][:3]
关键点:
FlagReranker加载 cross-encoder 模型(BGE-Reranker 系列)compute_score()接收[(q, d1), (q, d2), ...]配对列表,返回每个对的得分- 重排序发生在粗排之后、注入 LLM 之前,是 RAG 最强的提质手段
- cross-encoder 速度快不了,所以只对粗排召回的少量候选(如 top-100)做精排
二、Sentence-Transformers 写代码流程
Sentence-Transformers 封装了微调后的 Transformer 模型,几行代码即可获得高质量的句子向量,是学术和工业的首选。
写代码流程:
① 准备数据 → ② 加载模型 → ③ 向量化 → ④ 计算相似度 → ⑤ 排序取 top-k
python
from sentence_transformers import SentenceTransformer
# ---------- ① 准备数据 ----------
documents = [
"RAG(检索增强生成)是一种利用外部知识库来增强大模型回答能力的技术。",
"FlagEmbedding 是一个由北京智源人工智能研究院开发的文本嵌入模型。",
"BGE 模型在 Massive Text Embedding Benchmark (MTEB) 排行榜上取得了优异的成绩。",
"苹果公司由史蒂夫·乔布斯、史蒂夫·沃兹尼亚克和罗恩·韦恩于 1976 年创立。",
"深度学习是机器学习的一个分支,它基于深层神经网络。",
"熊猫是中国的国宝,主要栖息地是四川、陕西和甘肃的山区。",
]
question = "什么是rag?"
# ---------- ② 加载模型 ----------
model = SentenceTransformer(
model_name_or_path=r'D:\TechWorkPy\models\bge-base-zh-v1.5',
device="cuda",
)
# ---------- ③ 向量化(normalize_embeddings 归一化)----------
embeddings_1 = model.encode(documents, normalize_embeddings=True) # (6, 768)
embeddings_2 = model.encode([question], normalize_embeddings=True) # (1, 768)
# ---------- ④ 计算相似度(归一化后点积 = 余弦相似度)----------
similarity = embeddings_1 @ embeddings_2.T # shape: (6, 1)
# ---------- ⑤ 排序取 top-k ----------
scores = [{index: round(item[0], 2)} for index, item in enumerate(similarity.tolist())]
scores.sort(key=lambda x: list(x.values())[0], reverse=True)
print("检索结果(top-2):")
for item in scores[:2]:
for idx in item.keys():
print(f" {documents[idx]}")
关键点:
normalize_embeddings=True将向量归一化到单位长度,归一化后点积 = 余弦相似度- 矩阵乘法
@一次性计算所有相似度(比逐条循环快得多) - Sentence-Transformers 还封装了
util.semantic_search()便捷方法
模型选择:
| 场景 | 推荐模型 | 说明 |
|---|---|---|
| 通用语义相似度 | paraphrase-multilingual-MiniLM-L12-v2 |
多语言,平衡速度和性能 |
| 中文语义搜索/RAG | BAAI/bge-base-zh-v1.5 🔥 |
中文首选,检索优化 |
| 英语语义搜索 | msmarco-distilbert-base-v4 |
专为检索微调 |
三、Transformers 库加载 --- 手动池化
HuggingFace Transformers 是通用模型加载库,需要手动实现池化策略来获取句子向量。灵活但代码量更多。
写代码流程:
① 准备数据 → ② 加载分词器+模型 → ③ 定义向量化函数(CLS池化) → ④ 计算相似度 → ⑤ 排序取 top-k
python
from transformers import AutoTokenizer, AutoModel
import torch
# ---------- ① 准备数据 ----------
documents = [
"RAG(检索增强生成)是一种利用外部知识库来增强大模型回答能力的技术。",
"FlagEmbedding 是一个由北京智源人工智能研究院开发的文本嵌入模型。",
"BGE 模型在 MTEB 排行榜上取得了优异的成绩。",
"深度学习是机器学习的一个分支,它基于深层神经网络。",
]
question = "什么是rag?"
# ---------- ② 加载分词器和模型 ----------
tokenizer = AutoTokenizer.from_pretrained(
pretrained_model_name_or_path=r'D:\TechWorkPy\models\bge-base-zh-v1.5',
)
model = AutoModel.from_pretrained(
pretrained_model_name_or_path=r'D:\TechWorkPy\models\bge-base-zh-v1.5',
)
model.eval() # 切换到评估模式
# ---------- ③ 定义文本向量化函数(CLS 池化)----------
def get_embeddings(texts):
encoded_input = tokenizer(texts, padding=True, truncation=True, return_tensors='pt')
with torch.no_grad():
model_output = model(**encoded_input)
sentence_embeddings = model_output[0][:, 0] # 取 [CLS] token
return torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1)
# ---------- ④ 计算相似度 ----------
docs_embeddings = get_embeddings(documents)
question_embeddings = get_embeddings([question])
similarity = torch.matmul(docs_embeddings, question_embeddings.T)
# ---------- ⑤ 排序取 top-k ----------
scores = [{index: round(item[0], 2)} for index, item in enumerate(similarity.tolist())]
scores.sort(key=lambda x: list(x.values())[0], reverse=True)
print("检索结果:")
for item in scores:
for idx in item.keys():
print(f" {documents[idx]}")
关键点:
- 获取句子向量需要池化策略 (Pooling):
- CLS 池化 :取
[CLS]位置的输出向量(如上例) - Mean 池化:对所有 token 输出取平均
- Max 池化:对所有 token 输出取最大值
- CLS 池化 :取
- BGE 模型推荐 CLS 池化 + L2 归一化
model.eval()关闭训练相关操作,确保推理结果稳定- Transformers 方案灵活但繁琐,生产环境推荐 Sentence-Transformers
四、LangChain RAG 完整写代码流程
这是今天的核心实战------手动搭建 LCEL 管道实现 RAG 流程,并集成 Reranker 重排序。
写代码流程:
① 声明 LLM → ② 准备数据/分割 → ③ 创建向量化模型 → ④ 存入向量数据库 → ⑤ 创建检索器 → ⑥ 定义提示词模板 → ⑦ 定义重排序函数 → ⑧ 构建 Chain → ⑨ 执行问答
① 声明 LLM
python
import os
from langchain_openai import ChatOpenAI
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,
)
② 准备数据与分割
python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
# 方式一:从文件加载并分割
loader = TextLoader(
r"D:\TechWorkPy\workspace\stu_rag\datasets\华清远见.txt",
encoding='utf-8'
)
data = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100, # 每块最大字符数
chunk_overlap=20, # 重叠字符数(保留上下文连贯性)
length_function=len,
separators=["\n\n",",", "?", "!", "\n"], # 分割优先级
)
chunks = text_splitter.split_documents(data)
# 方式二:直接准备文本 → 转为 Document 对象
# documents = [Document(page_content=chunk) for chunk in text_chunks]
| 参数 | 说明 | 建议值 |
|---|---|---|
chunk_size |
每块最大字符数 | 100~500 |
chunk_overlap |
块之间重叠字符数 | chunk_size 的 10%~20% |
separators |
分割优先级列表 | 中文优先 ["\n\n", "。", ",", " "] |
③-④ 创建向量化模型 + 存入向量数据库
python
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
EmbeddingModel = HuggingFaceEmbeddings(
model_name=r"D:\TechWorkPy\models\paraphrase-multilingual-MiniLM-L12-v2",
model_kwargs={
"device": "cuda",
"local_files_only": True,
},
)
# 存入向量数据库(一步到位:分割 → 向量化 → 存储)
vector = Chroma.from_documents(
documents=chunks,
embedding=EmbeddingModel,
persist_directory=r"D:\TechWorkPy\workspace\stu_rag\06langchain操作chromaDB\chroma",
collection_name="hqyj",
collection_metadata={"hnsw:space": "cosine"},
)
⑤ 创建检索器
python
# 若已存在持久化数据,直接加载
# vector = Chroma(
# persist_directory=r"....chroma",
# collection_name="hqyj",
# embedding_function=EmbeddingModel,
# )
retriever = vector.as_retriever(search_kwargs={"k": 6}) # 粗排召回 6 条
⑥ 定义提示词模板和问题
python
from langchain_core.prompts import PromptTemplate
# 问答内容
question = "华清远见有多少个部门"
template = """
你是一个基于知识库的AI助手。请根据RAG检索内容回答用户问题。
规则:
- 仅基于提供的知识回答,不使用外部知识补充。
- 检索内容不足时,说明信息不足,不要猜测。
- 优先提炼关键答案,避免冗长解释。
- 输出结果时,不允许输出"根据提供的参考资料"这样的内容。
参考资料:
{context}
问题:
{question}
答案:
"""
prompt = PromptTemplate(
template=template,
input_variables=["context", "question"],
)
⑦ 定义重排序函数 输出重排序内容
python
from FlagEmbedding import FlagReranker
reranker = FlagReranker(
model_name_or_path=r'D:/TechWorkPy/models/bge-reranker-large',
use_fp16=True,
)
def reranker_func(data):
print("开始进行重排序...")
print(data)
question = data["question"]
docs = data["context"]
# 没有检索到文档,无需重排序
if not docs:
return data
# 将问题与每个召回文档配对,计算重排序得分
reranker_input = [(question, doc.page_content) for doc in docs]
scores = reranker.compute_score(reranker_input)
# 将文档和得分配对,按得分降序排序
scored_docs = list(zip(docs, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
# 返回排序后的文档
data["context"] = [doc for doc, _ in scored_docs][:3]
print("重排序后的结果:")
for doc in data["context"]:
print(doc.page_content)
return data
⑧-⑨ 构建 Chain + 执行问答
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough, RunnableLambda
# 调试:打印召回结果
def print_recall(docs):
print("检索到的文档内容:")
for doc in docs:
print(f" {doc.page_content}")
return docs # ⚠️ 必须有返回值,否则 Chain 断流
# 🏗️ 构建 LCEL 管道(搭积木)
qa_chain = (
RunnableParallel({
"context": retriever | RunnableLambda(print_recall),
# 路径1:检索 + 打印
"question": RunnablePassthrough(),
# 路径2:原样传递问题
})
| RunnableLambda(reranker_func) # 🔥 重排序精排
| prompt # 拼接提示词
| LLM # 大模型生成
| StrOutputParser() # AIMessage → 纯文本
)
result = qa_chain.invoke(question)
print("\n")
print(f"LLM回复: {result}")
LCEL 管道核心组件:
| 组件 | 作用 | 说明 |
|---|---|---|
RunnableParallel |
并行执行多个路径 | 同时获取检索上下文 + 传递问题 |
RunnablePassthrough() |
透明传递输入 | 不做任何修改,直接传给下一环节 |
RunnableLambda(func) |
将普通函数包装为 Chain 组件 | ⚠️ 函数必须有返回值,否则断流 |
PromptTemplate |
模板化提示词 | 用 {context} 和 {question} 占位 |
StrOutputParser() |
提取纯文本 | 将 AIMessage 转为字符串 |
| ` | ` (pipe) | 管道运算符串联组件 |
五、三种方案对比总结
| 方案 | 代码量 | 精度 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| Sentence-Transformers | 少 | 高 | 中 | 学术/工业快速实验 |
| Transformers | 多 | 中 | 高 | 需精细控制模型行为 |
| LangChain + ChromaDB | 中 | 高(+Reranker 更高) | 高 | 生产级 RAG 系统 🔥 |
核心要点:
- 重排序(Reranker) 是 RAG 最强提质手段,cross-encoder 精度远高于 bi-encoder
- 粗排(向量检索) 负责快------从百万级文档中快速筛出候选
- 精排(Reranker) 负责准------对少量候选做精细化排序,注入 LLM 之前把关
- LangChain LCEL 管道的核心是"搭积木":每个组件实现
Runnable接口,通过|串联 - ⚠️ 最重要的原则 :建库和检索用的 Embedding 模型必须是同一个
RunnableLambda包装的函数必须有返回值,否则管道断流
最终思考:最近流程都越来越进入 RAG 生产环境的标准模板了。无论搭建什么领域的知识库问答系统,核心步骤都一样:数据准备 → 向量化存储 → 粗排检索 → 精排重排序 → Prompt 拼接 → LLM 生成。理解这个流程,就能搭建任意领域的 RAG 应用