零门槛 AI 应用开发(一):RAG 技术从入门到实战
文章目录
RAG
简介
RAG(Retrieval-Augmented Generation),即检索生成增强,是目前最主流的 LLM 落地架构之一。
RAG 核心思想
在生成答案前,先从外部知识库检索相关信息片段,将其作为"参考资料"传递给 LLM,引导模型基于真实、精确的外部数据生成回答。
核心价值与作用
- 解决 知识滞后与过时 的问题
- LLM 的训练数据通常都有时间截至点(例如,训练于 2024 年的大模型无法知晓 2025 年及之后的事件);
- RAG 通过引入外部知识库,可以实时更新信息,让模型掌握最新的知识。
- 解决 事实性错误与幻觉 的问题
- LLM 有时会生成看似合理但与事实不符的内容,这种行为即"幻觉";
- RAG 通过强制模型基于检索到可信来源进行回答,大大降低了产生幻觉的可能,提升了答案的准确性与可信度。
- 解决 成本高昂与数据隐私 的问题
- 通过微调(Fine-tuning)更新模型知识成本高、周期长;
- 若非本地部署,还存在将私有数据暴露给第三方模型提供商的风险;
- RAG 架构无需重新训练模型(更新知识),直接使用外部最新知识;
- 既降低了成本,又能将私有知识库保留在本地,避免直接暴露给外部。
RAG 系统处理全流程

一句话总结:先把知识"向量化存好",再用"关键词检索 + 语义检索"一起找内容,最后交给大模型生成答案。
— — 阶段一:离线索引构建 — —
索引构建是 RAG 系统的离线阶段,目标是将原始知识文档转化为可供检索的向量索引。它包含四个核心步骤:文档清洗、分块策略、向量化、索引存储与元数据管理。
文档清洗
简介
- 去除无关格式(HTML 标签、Markdown 符号、多余空白)
- 统一大小写(英文场景)
- 可选:敏感信息过滤、特殊字符处理
文档清洗代码示例
import re
def clean_document(text: str) -> str:
"""清洗文档格式"""
# 去除HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 去除Markdown符号
text = re.sub(r'[#*`\[\]]', '', text)
# 去除多余空白
text = re.sub(r'\s+', ' ', text).strip()
return text
分块策略(Chunking)
简介
决定检索的粒度,直接影响召回质量。
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 固定长度分块 | 按 token / 字符数切分,简单高效 | 通用、对结构要求不高 |
| 语义分块 | 按句子、段落边界切分(用 NLTK / spaCy) | 需要保持语义完整性 |
| 递归分块 | 先按大粒度切,超限则递归再切 | 文档长度差异大 |
| 重叠分块 | 相邻块保留部分重叠内容 | 避免语义边界信息丢失 |
- 块大小建议:通常 200~500 token(兼顾检索精度与模型上下文)
- 元数据保留:记录块来源文档、章节、序号等

分块代码示例
from typing import List, Dict
def chunk_text(text: str, chunk_size: int = 200, overlap: int = 50) -> List[Dict]:
"""固定长度分块(带重叠)"""
chunks = []
start = 0
chunk_id = 0
while start < len(text):
end = start + chunk_size
chunk_text = text[start:end]
chunks.append({
"chunk_id": chunk_id,
"content": chunk_text,
"start_pos": start,
"end_pos": end
})
start += chunk_size - overlap # 滑动窗口
chunk_id += 1
return chunks
Embedding 向量化
概念
Embedding 向量化是将文本(词语、句子、段落等)转换为固定长度的稠密浮点数向量的过程。这些向量在语义空间中的位置可以表示文本的内在含义——语义越接近的文本,其向量在空间中也越靠近。在 RAG 系统中,Embedding 模型负责将用户查询和知识库文档映射到同一向量空间,从而实现基于语义相似度的检索,让系统不仅能找到关键词匹配的内容,还能理解"买水果"与"选购苹果、香蕉"这类同义表达。

为什么需要向量化
- 计算机无法直接理解人类的自然语言,但擅长处理数值计算。向量化的本质就是将文本转换为计算机能够运算的数值形式,从而让机器可以"理解"和比较文本之间的语义关系。
- 在 RAG 系统(检索增强生成)中,向量化主要解决了以下几个关键问题:
- 实现语义搜索,而非仅关键词匹配:传统方式(BM25、TF-IDF)只能找到包含完全相同词语的文档;向量化后通过嵌入模型将查询和文档映射到语义空间中相近的位置,计算向量相似度就能召回语义相关的文档。
- 让计算机能够"计算相似度":转化为固定长度的稠密向量后,就可以通过数学运算(如点积、欧氏距离)快速比较两段文本的相似程度。
- 支持大规模高效检索:一旦文档被向量化,我们可以预先构建向量索引(如 HNSW、IVF),实现毫秒级的海量相似度搜索。
- 捕捉深层的语义特征:好的嵌入模型能够学习到同义词、上下位词等。
- 融合多模态信息:向量化不仅可以处理文本,还能处理图像、音频等。
向量化代码示例(分块)
from sentence_transformers import SentenceTransformer
from typing import List, Dict
def encode_chunks(chunks: List[Dict], model_name: str = 'BAAI/bge-large-zh-v1.5') -> List[Dict]:
"""将分块文本向量化"""
model = SentenceTransformer(model_name)
texts = [c["content"] for c in chunks]
embeddings = model.encode(texts, batch_size=8, show_progress_bar=True)
for i, chunk in enumerate(chunks):
chunk["embedding"] = embeddings[i]
return chunks
索引存储
索引类型
- 向量存储:使用 Milvus、FAISS、Pinecone 等向量数据库
- 索引类型:
- 暴力索引(Flat):精确但慢,适合 <1 万条
- 近似索引(HNSW、IVF):大规模快速检索(常用)
- 元数据存储:原始文本、发布时间、来源等,用于检索后过滤或展示
- 映射关系:向量 ID ↔ 元数据 ↔ 原始分块文本
增量更新(可选)
- 新增文档:增量 embedding → 插入索引
- 更新文档:删除旧向量 → 插入新向量
- 删除文档:按 ID 删除
索引存储代码示例(Milvus)
# pip install pymilvus
def store_to_milvus(chunks: List[Dict], collection_name: str = "knowledge_base"):
"""存储向量到Milvus"""
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility
# 连接Milvus
connections.connect(host="localhost", port="19530")
# 定义 schema(BGE模型输出维度为 1024)
fields = [
FieldSchema(name="chunk_id", dtype=DataType.INT64),
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024),
]
schema = CollectionSchema(fields=fields, description="RAG Knowledge Base")
# 创建或获取 collection
if utility.has_collection(collection_name):
collection = Collection(collection_name)
collection.drop()
collection = Collection(name=collection_name, schema=schema)
# 插入数据
data = [
[c["chunk_id"] for c in chunks],
[c["content"] for c in chunks],
[c["embedding"].tolist() for c in chunks]
]
collection.insert(data)
collection.flush()
# 创建索引
index_params = {"index_type": "HNSW", "params": {"M": 16, "efConstruction": 200}}
collection.create_index(field_name="embedding", index_params=index_params)
collection.load()
return collection
端到端索引构建流程
def build_index_from_documents(raw_documents: List[str]) -> None:
"""从原始文档构建索引"""
# 1. 清洗
cleaned = [clean_document(doc) for doc in raw_documents]
full_text = " ".join(cleaned)
# 2. 分块
chunks = chunk_text(full_text, chunk_size=200, overlap=50)
print(f"分块完成,共 {len(chunks)} 个块")
# 3. 向量化
chunks = encode_chunks(chunks)
print(f"向量化完成,维度: {chunks[0]['embedding'].shape}")
# 4. 存储
collection = store_to_milvus(chunks)
print(f"存储完成,Milvus collection: {collection.name}")
print("索引构建完成!")
# 示例运行
if __name__ == "__main__":
docs = [
"<h1>RAG技术介绍</h1>\nRAG是检索增强生成技术。",
"Embedding向量化为文本提供语义表示。",
"大模型需要向量数据库支持检索。"
]
build_index_from_documents(docs)
— — 阶段二:在线检索与生成 — —
用户查询预处理
为什么需要分词
- 构建语义基础:准确的分词是后续所有 NLP 任务(如词性标注、实体识别)的基石;
- 提升检索与召回精度:错误分词会导致专有名词被切断,生成的向量无法准确表征原义,从而显著降低搜索引擎的召回率;
- 优化大模型生成质量:高质量分词有助于精准筛选和组织上下文信息,减少噪声干扰,使 LLM 的生成内容更聚焦于核心语义。
jieba 中文分词(实践)
与英文不同,中文词语之间没有固定的分隔符(如空格),因此需要依靠分词工具进行划分。 Jieba 是 Python 中最流行的中文分词工具,它以简单易用、高效和灵活著称,支持多种分词模式、用户自定义词典和关键词提取,结合了基于前缀词典的高效扫描与隐马尔可夫模型(HMM)机器学习算法,能够精准处理复杂的中文文本结构。
分词模式
Jieba 提供了三种主要的分词模式:
- 精确模式
- 特点:旨在将文本切分成最精确的词语,无冗余结果;
- 优点:准确率极高,文本分析的首选模式;
- 缺点:无法处理所有歧义问题,对未登录词识别较弱;

- 适用:词频统计、RAG 系统文本向量化。
- 全模式
- 特点:全切分扫描,词汇组合覆盖较全;
- 优点:分词速度快,对未登录词友好;
- 缺点:结果冗余度太高,包含过多无效分词;
- 适用:快速词汇匹配、数据的探索性分析(例词典编写);
- 搜索引擎模式
- 特点:精确模式输出的最优切分+长词再切分;
- 优点:兼顾准确性与召回率,能够同时支持精确匹配和部分前缀/子词检索;
- 缺点:相比精确模式,输出词数增多(但比全模式冗余少),分词速度略慢于全模式(需额外计算长词的细分),仍可能产生少量噪声(如过度切分常见组合);
- 适用:搜索引擎索引构建、细粒度文本检索(如用户输入"北京大学"既能匹配"北京大学",也能匹配"北京"或"大学")、推荐系统;
模式对比
import jieba
user_input = "我来到北京清华大学"
# 精确匹配
seg_list = list(jieba.cut(user_input, cut_all=False))
print("精确模式: " + "/ ".join(seg_list))
# 全模式
seg_list = list(jieba.cut(user_input, cut_all=True))
print("全模式: " + "/ ".join(list(seg_list)))
# 搜索引擎模式
seg_list = list(jieba.cut_for_search(user_input))
print("搜索引擎模式: " + "/ ".join(list(seg_list)))
- 精确模式:我/ 来到/ 北京/ 清华大学
- 全模式:我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
- 搜索引擎模式:我/ 来到/ 北京/ 清华/ 华大/ 大学/ 清华大学
高级功能
添加自定义词
import jieba
# 精确模式示例
user_input = "雨下整夜,是一首很美的歌"
seg_list = list(jieba.cut(user_input))
print("/ ".join(seg_list))
# 输出:雨下/ 整夜/ ,/ 是/ 一首/ 很/ 美/ 的/ 歌
# 添加自定义词
jieba.add_word("雨下整夜")
seg_list = list(jieba.cut(user_input))
print("/ ".join(seg_list))
# 输出:雨下整夜/ ,/ 是/ 一首/ 美的/ 歌
自定义词典
import jieba
import os
# 精确模式示例
user_input = "我来到隐世修所很开心,这里比森寒冬港的环境好很多"
seg_list = list(jieba.cut(user_input))
print("/ ".join(seg_list))
# 输出:我/ 来到/ 隐世修/ 所/ 很/ 开心/ ,/ 这里/ 比森寒/ 冬港/ 的/ 环境/ 好/ 很多
script_path = os.path.dirname(os.path.abspath(__file__))
user_dict_path = os.path.join(script_path, "userdict.txt")
# 加载用户自定义词典,内容:隐世修所 10 ns,森寒冬港 10 ns
jieba.load_userdict(user_dict_path)
seg_list = list(jieba.cut(user_input))
print("/ ".join(seg_list))
# 输出:我/ 来到/ 隐世修所/ 很/ 开心/ ,/ 这里/ 比/ 森寒冬港/ 的/ 环境/ 好/ 很多
关键词提取
jieba 提供了两种无监督的关键词提取算法:TF-IDF 和 TextRank。
import jieba.analyse as analyse
user_input = "我来到隐世修所很开心,这里比森寒冬港的环境好很多"
# 基于TF-IDF的关键词提取
keywords = analyse.extract_tags(user_input, topK=5, withWeight=True)
print("TF-IDF关键词:")
for keyword in keywords:
print(keyword)
# 基于TextRank的关键词提取
keywords = analyse.textrank(user_input, topK=5, withWeight=True)
print("TextRank关键词:")
for keyword in keywords:
print(keyword)
停用词清洗
文本中频繁出现,但本身不携带过多实际语义的字符,它们的存在往往会干扰核心信息的提取。
为什么要清洗:停用词清洗的目的是去噪提效——去掉那些"高频无用"的词,让算法集中精力于真正有含义的内容。
- 减少数据噪音,提升检索精度:避免大量停用词稀释核心词语的权重,加强模型对文本主题的理解;
- 降低计算成本,提升检索速度:显著降低文本特征维度,减少存储和内存占用;
- 降低大模型Token消耗:避免大量不相关的停用词被计入模型输入总数。
停用词表
- 哈工大停用词表(hit_stopwords.txt):NLP 领域最经典、应用最广泛的中文停用词表之一;
- 百度停用词表(baidu_stopwords.txt):基于海量网络搜索日志整理,覆盖大量网络流行虚词;
- 川大机器智能实验室词库(scu_stopwords.txt):针对特定领域语境优化,包含更多语境相关的停用词;
- 通用中文停用词表(cn_stopwords.txt):通用集合,适合大多数常规的文本处理场景。
过滤演示(分词+停用词清洗)
import jieba
import os
# 定义加载停用词表的函数
def load_stopwords(file_path):
stopwords = set() # 使用集合存储,查找速度快,复杂度为O(1)
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
word = line.strip() # 去除行首行尾的空白字符
if word: # 确保不添加空行
stopwords.add(word)
except FileNotFoundError:
print(f"文件 {file_path} 未找到")
except Exception as e:
print(f"读取文件 {file_path} 时发生错误: {e}")
return stopwords
# 定义清洗函数
def clean_text(text, stopwords):
"""分词并去除停用词"""
print(f"原始文本: {text}")
words = jieba.lcut(text)
print(f"分词结果: {words}")
filtered_words = [word for word in words if word not in stopwords]
print(f"去除停用词后的结果: {filtered_words}")
return ' '.join(filtered_words)
if __name__ == "__main__":
script_path = os.path.dirname(os.path.abspath(__file__))
stopwords_file = os.path.join(script_path, "hit_stopwords.txt") # 停用词文件路径
stopwords = load_stopwords(stopwords_file)
user_input = "RAG的优点是什么?"
cleaned_text = clean_text(user_input, stopwords)
print(f"清洗后的文本: {cleaned_text}")
# 输出结果
# 分词结果: ['RAG', '的', '优点', '是', '什么', '?']
# 去除停用词后的结果: ['RAG', '优点']
# 清洗后的文本: RAG 优点
分词+清洗的预处理差异
在 RAG 架构中,混合检索结合了稠密检索(向量语义检索)和稀疏检索(如 BM25)。这两种检索方式对输入文本的预处理需求存在显著差异:
| 检索类型 | 预处理需求 | 原因 |
|---|---|---|
| 稠密检索(向量语义检索) | 通常无需分词和停用词清洗 | 向量模型能够捕捉词语的上下文语义关系,保留原始文本特征往往更能精确表达查询的完整语义意图 |
| 稀疏检索(BM-25 等) | 需要分词、停用词去除等清洗操作 | 此类方法主要依赖关键词匹配和词频统计,精简文本能减少噪声,更精准地匹配文档中的关键词 |
因此,在混合检索架构中,预处理主要服务于稀疏检索组件。预处理生成的"干净"文本被输入 BM-25 等稀疏检索器,而原始或仅轻微处理的文本则直接用于向量语义检索,以实现各自的最佳性能。
稠密检索(向量检索)
原理
- 将查询(问题)和文档(参考资料)映射到同一个向量空间,通过计算向量间的距离来衡量相关性。
- 流程如下:

优点
- 语义理解强:能捕捉同义词、上下文和隐含语义,不依赖字面匹配;
- 匹配更智能:例如"如何修自行车"可匹配到"单车维修指南";
- 抗噪声:对拼写错误、同义替换等有一定鲁棒性;
- 支持多模态:可将文本、图像等映射到同一空间进行跨模态检索。
缺点
- 计算成本高:训练和推理需要GPU资源,尤其是大规模索引。
- 领域依赖性强:在未见过的专业领域可能性能下降明显。
- 可解释性差:难以解释为什么某篇文档被召回(黑箱特性)。
- 数据需求大:需要大量高质量的标注数据来训练模型。
稀疏检索(BM25 检索)
原理
- 基于词袋模型和倒排索引,用精确的词项匹配及统计权重(如 BM25)计算查询与文档的相关性。
- 词袋表示:将文本表示为高维稀疏向量,每一维对应词典中的一个词,值通常是词频(TF)、TF-IDF 或 BM25 权重;
- 倒排索引:预先建立"词 → 包含该词的文档列表"的映射,实现快速定位。
- 流程如下:

优点
- 检索速度快:依靠倒排索引,无需遍历所有文档。
- 精确匹配能力强:对专业术语、编号等精确词召回准确。
- 可解释性高:高亮匹配词,清晰说明为什么文档被召回。
- 资源消耗低:只需要 CPU 和少量内存,无需 GPU。
- 成熟稳定:几十年的工业级实践,易于调优(如调整 BM25 的 k1、b 参数)。
缺点
- 语义鸿沟:无法理解同义词、近义词(如"车"与"汽车"无法匹配)。
- 词形敏感:对拼写错误、时态、词形变化无力。
- 稀疏向量维度爆炸:词典大小动辄几十万维,但实际有效计算仍借助倒排索引。
- 忽略词序和语境:"猫追老鼠"与"老鼠追猫"会被视为相同(若只考虑词袋)。
- 长尾查询召回弱:罕见词或组合可能出现零匹配。
混合检索
稠密检索 vs 稀疏检索
| 维度 | 稠密检索(向量检索) | 稀疏检索(BM25) |
|---|---|---|
| 匹配方式 | 语义相似度 | 关键词精确匹配 |
| 同义词处理 | 能匹配(语义理解) | 无法匹配(依赖字面) |
| 计算资源 | 需要GPU | 仅需CPU |
| 检索速度 | 较快(借助向量索引) | 快(倒排索引) |
| 可解释性 | 较低(黑箱) | 高(可显示匹配词) |
| 领域适应性 | 依赖训练数据 | 无需训练,通用 |
| 适用场景 | 语义理解需求高的查询 | 专业术语、精确匹配需求 |
为什么需要混合检索
混合检索通过融合稠密检索的语义理解能力和稀疏检索的关键词匹配能力,实现"语义+精确"互补,是当下高质量检索系统(尤其是RAG)的标准方案。
混合检索的优点
- 召回率更高:两类检索的漏检文档往往不重叠,合并后可覆盖更多相关结果;
- 鲁棒性强:单一方法失效时(如查询中包含罕见词且为同义词),另一方法可能仍有效;
- 平衡精准与语义:既保留关键词的精确性,又利用语义泛化能力。
结果融合(以 RRF 为例)
在混合检索中,结果融合是指将来自不同检索通路(如稀疏检索的 BM25 结果、稠密检索的向量相似度结果)返回的文档列表合并成一个统一的、按相关性排序的最终列表的过程。
常用融合方法
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RRF(倒数排名融合) | 只使用文档在各结果列表中的排名,忽略原始分数。 | 无需分数归一化;对离群值鲁棒;简单高效 | 未能利用原始分数信息 |
| 加权求和 | 先将不同检索器的分数归一化到相同范围(如 0~1),然后加权相加。 | 可以调节每个检索器的权重 | 归一化方式敏感;权重需手动或学习得到 |
| 学习排序(LTR) | 使用机器学习模型(如 RankNet, LambdaMART)从标注数据中学习融合规则。 | 理论上最佳,能挖掘复杂模式 | 需要大量标注数据,训练成本高 |
RRF(Reciprocal Rank Fusion,倒数排名融合) 是一种简单且高效的结果融合方法,仅利用不同检索器返回的文档排名,无需对原始分数进行归一化,即可合并多个排名列表。
RRF 原理
- 公式:

RRF(d) = Σ (1 / (k + ri(d))),其中i从1到m。
- d :文档
- m :检索器(或查询方式)的数量
- ri(d):文档 d 在第 i 个检索结果列表中的排名(从 1 开始)
- k :平滑常数,通常取 60(实验证明 60 效果较好,避免单个高排名文档得分过大)
最终按 RRF 降序排序。
- 示例:
假设两个检索器返回的排名:
| 文档 | BM25 | 向量排名 |
|---|---|---|
| DocA | 1 | 3 |
| DocB | 2 | 1 |
| DocC | 3 | 2 |
令 k = 60:
- DocA: 1/(60+1) + 1/(60+3) ≈ 0.0324
- DocB: 1/(60+2) + 1/(60+1) ≈ 0.0325
- DocC: 1/(60+3) + 1/(60+2) ≈ 0.0318
最终排序:DocB --> DocA --> DocC
RRF 优势
- 无需分数归一化:避免不同检索器分数尺度不统一的问题
- 对离群值鲁棒:极端高分或低分不会影响排名质量
- 简单高效:只需排名索引,计算复杂度低
- 参数不敏感:k 通常在 10–100 间效果稳定,60 为通用默认值
- 无监督:不需要训练数据
RRF 缺点
- 忽略原始分数信息:如 BM25 得分差距很大或很小,RRF 仅看排名,可能损失细节
- 所有检索器平等对待:无法根据检索器质量加权(除非手动调整排名前加权重)
- 对排名噪声敏感:如果某个检索器的排名质量很差,仍会平等贡献分数
RRF 典型应用
- 混合检索:融合 BM25(稀疏)与稠密向量的结果
- 多模态检索:本文、图像、音频等多路召回
- RAG 系统:提升长尾 query 的召回鲁棒性
- 竞赛或实战中的基线:比简单加权归一化更稳定
RERANK 重排序
Rerank 简介
在信息检索和 RAG 系统中,Rerank(重排序) 是指在初步召回(如混合检索返回 Top‑K 候选)之后,使用一个更精细、更强大的模型对这些候选文档进行重新打分与排序的过程。它的目标是把最相关的文档尽量排在更靠前的位置,从而提升最终结果的精度。
为什么需要 Rerank
- 初检快但糙:混合检索(BM25 + 向量)能快速从海量文档中召回数百/数千个候选,但排序精度有限(受限于稀疏匹配或双塔模型的表达能力)。
- 精排慢而准:用一个计算量更大但更精准的模型,对少量候选重新打分,可以显著改善 Top‑K 结果的相关性。
- 弥补语义鸿沟:双塔模型(用于稠密检索)由于将 query 和 document 独立编码,丢失了 query‑document 间的交互信息;而 Rerank 模型通常采用交叉编码器(Cross‑Encoder),能同时看 query 和 document 的全文本并做深度交互,精度更高。
主流 Rerank 模型
| 类型 | 例子 | 特点 |
|---|---|---|
| 交叉编码器 | BERT‑Cross‑Encoder, MonoBERT, DeBERTa‑v3 | 将 query 和 document 拼接后输入 Transformer,输出相关性分数。精度高,但 O(n²) 交互,不适合对所有文档直接使用。 |
| 基于 LLM 的 Rerank | RankGPT, RankLLaMA, GPT‑4 with reranking prompt | 利用大语言模型的指令跟随能力进行排序。可解释性强,但成本高、延迟大。 |
| 轻量级 Rerank | Cohere Rerank, BGE‑Reranker, MiniLM‑Cross‑Encoder | 专门训练的交叉编码器,平衡精度与速度,通常能处理几百个候选。 |
典型工作方式(以交叉编码器为例)
- 输入构造:将 [CLS] Query [SEP] Document [SEP] 拼成一个序列。
- 前向传播:通过 BERT 类模型得到 [CLS] 位置的输出向量。
- 打分:将 [CLS] 向量输入一个全连接层,输出一个 0~1 之间的相关度分数。
- 重排:按新分数降序排列文档。
代码实现
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
# pip install sentence-transformers
from sentence_transformers import CrossEncoder
# 1. 加载预训练的交叉编码器模型(轻量级,适合快速演示)
# 常见模型:'cross-encoder/ms-marco-MiniLM-L-6-v2' 或 'BAAI/bge-reranker-base'
model = CrossEncoder('BAAI/bge-reranker-base')
# 2. 模拟用户查询和初检召回的文档列表(来自混合检索)
query = "如何修理自行车"
candidate_docs = [
"单车维修教程:补胎、调刹车、换链条",
"今天天气不错,适合出去骑车",
"自行车轮胎漏气的快速处理方法",
"汽车发动机保养指南",
"山地车变速器调节技巧"
]
# 3. 构建 (query, doc) 对
pairs = [(query, doc) for doc in candidate_docs]
# 4. 使用交叉编码器模型预测每个 (query, doc) 对的相关性分数(得分越高表示越相关)
scores = model.predict(pairs)
# 5. 将文档与分数配对,按分数降序排序
ranked_results = sorted(zip(candidate_docs, scores), key=lambda x: x[1], reverse=True)
# 6. 输出重排序后的结果
print("原始排序:")
for i, doc in enumerate(candidate_docs):
print(f"{i+1}. {doc}")
print("\n重排序后:")
for i, (doc, score) in enumerate(ranked_results):
print(f"{i+1}. [得分: {score:.4f}] {doc} ")
# 原始排序:
# 1. 单车维修教程:补胎、调刹车、换链条
# 2. 今天天气不错,适合出去骑车
# 3. 自行车轮胎漏气的快速处理方法
# 4. 汽车发动机保养指南
# 5. 山地车变速器调节技巧
# 重排序后:
# 1. [得分: 0.7155] 单车维修教程:补胎、调刹车、换链条
# 2. [得分: 0.5905] 自行车轮胎漏气的快速处理方法
# 3. [得分: 0.0282] 汽车发动机保养指南
# 4. [得分: 0.0075] 山地车变速器调节技巧
# 5. [得分: 0.0023] 今天天气不错,适合出去骑车
Rerank 优点
- 精度显著提升:交叉编码器能捕捉 query‑document 间的相互影响,例如否定词、同义词的对应关系。
- 缓解过召回噪声:初检可能混入不相关文档,Rerank 可以有效过滤。
- 灵活适配:可以在不同召回通路后挂相同的 Rerank 模型。
Rerank 缺点
计算开销大:交叉编码器需要对每个 (query, doc) 对做一次完整的前向推理,无法离线预计算文档向量。因此通常只能对几百个候选重排,而非全集。
延迟增加:作为在线步骤,会增加 100ms~几秒的延迟(取决于模型大小与 GPU)。
模型训练成本:需要精标注的 query‑doc 相关性数据,领域迁移后可能需要微调。
何时必须使用 Rerank?
- 对结果精度要求极高(如医疗问答、法律检索)。
- 初检模型较薄弱(例如只用 BM25 或只用普通双塔)。
- 需要满足业务指标中的 “Top‑1 准确率”(如客服机器人直接展示第一条答案)。
与混合检索的关系
| 阶段 | 方法 | 数量级 | 侧重点 |
|---|---|---|---|
| 召回 | 混合检索(稀疏+稠密) | 几百 ~ 几千 | 高召回、宽覆盖、速度快 |
| 重排 | 交叉编码器 / LLM | 几十 ~ 几百 | 高精度、精细排序、可解释 |
最佳实践:先利用混合检索从百万级文档中召回 Top‑200,再交给 Rerank 模型精排到 Top‑10。这样兼顾了效率与准确性,是工业级 RAG 的通用架构。
大模型生成:检索结果到答案
在完成检索和重排序后,RAG 系统最后一步是将Top-K 最相关的文档块与用户原始问题一起构造成 Prompt,提交给大语言模型(LLM)生成最终答案。
Prompt 模板设计
一个典型的 RAG Prompt 通常包含三部分:
- System Prompt:定义模型角色和回答约束
- Context:拼接检索到的文档块作为参考上下文
- User Query:原始用户问题

引用溯源
为了提升答案的可信度和可追溯性,RAG 系统通常会:
- 展示引用来源:在答案中标注参考了哪些文档块(如"根据文档[1]…")
- 高亮原文片段:将答案中引用自检索文档的关键句子标注来源
Token 预算管理
LLM 通常有上下文长度限制(如 4K、16K、128K tokens),需要管理输入 Token 数量:
- 块数限制:根据 Token 预算限制输入的文档块数量(如最多 5 个块)
- 截断策略:对过长文档块进行截断,保留核心内容
- 动态选择:根据文档与查询的相关性分数,选择最相关的块
主流 API 调用代码示例
OpenAI GPT 系列
# pip install openai
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
def generate_with_rag(query: str, context_docs: list[str]) -> str:
"""使用 OpenAI GPT 生成答案"""
# 构建 Context
context = "\n---\n".join([f"[{i+1}] {doc}" for i, doc in enumerate(context_docs)])
response = client.chat.completions.create(
model="gpt-4",
messages=[
{
"role": "system",
"content": "你是一个专业的知识助手。基于给定的上下文信息,准确回答用户的问题。"
},
{
"role": "user",
"content": f"Context:\n---\n{context}\n---\n\nUser: {query}"
}
],
temperature=0.7,
max_tokens=500
)
return response.choices[0].message.content
Anthropic Claude
# pip install anthropic
from anthropic import Anthropic
client = Anthropic(api_key="your-api-key")
def generate_with_rag_claude(query: str, context_docs: list[str]) -> str:
"""使用 Claude 生成答案"""
context = "\n---\n".join([f"[{i+1}] {doc}" for i, doc in enumerate(context_docs)])
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=500,
system="你是一个专业的知识助手。基于给定的上下文信息,准确回答用户的问题。",
messages=[
{
"role": "user",
"content": f"Context:\n---\n{context}\n---\n\nUser: {query}"
}
]
)
return message.content[0].text
本地模型(vLLM)
# pip install vllm
from vllm import LLM, SamplingParams
def generate_with_rag_local(query: str, context_docs: list[str], model_path: str = "path/to/local/model"):
"""使用本地 vLLM 模型生成答案"""
llm = LLM(model=model_path, tensor_parallel_size=1)
context = "\n---\n".join([f"[{i+1}] {doc}" for i, doc in enumerate(context_docs)])
prompt = f"""你是一个专业的知识助手。基于给定的上下文信息,准确回答用户的问题。
Context:
---
{context}
---
User: {query}
"""
sampling_params = SamplingParams(temperature=0.7, max_tokens=500)
outputs = llm.generate([prompt], sampling_params)
return outputs[0].outputs[0].text
总结:RAG 系统架构全景
完整流程回顾

本文介绍了 RAG(检索增强生成)技术的完整链路:
离线阶段(索引构建)
原始文档 → 文档清洗 → 分块策略 → Embedding向量化 → 索引存储(Milvus/FAISS等)
在线阶段(检索与生成)
用户查询 → 查询预处理(分词+停用词清洗)→ 混合检索(稠密+稀疏)→ RRF融合 → Rerank重排序 → LLM生成答案
各阶段核心组件
| 阶段 | 核心组件 | 关键技术 |
|---|---|---|
| 文档清洗 | 正则表达式、HTML解析器 | 去除噪声、保留核心内容 |
| 分块策略 | 固定长度、语义分块、重叠分块 | 块大小200-500 token |
| Embedding | BGE、OpenAI text-embedding | 语义向量表示 |
| 稠密检索 | Milvus、FAISS、HNSW索引 | 向量相似度计算 |
| 稀疏检索 | BM25、倒排索引 | 词频统计、关键词匹配 |
| 结果融合 | RRF、加权求和 | 多路召回融合 |
| Rerank | BGE-Reranker、Cross-Encoder | 交叉编码精排 |
| LLM生成 | GPT-4、Claude、本地模型 | Prompt工程、Token管理 |
RAG 系统的核心优势
- 知识时效性:无需重新训练模型,即可更新知识库
- 减少幻觉:基于真实检索结果生成,答案可溯源
- 成本可控:相比微调,维护成本更低
- 灵活性强:可混合多种检索方法,根据场景调整
RAG 将检索系统的精确性与大语言模型的生成能力完美结合,是当前 LLM 应用落地的主流架构。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)