外观
RAG
RAG,全称Retrieval-Augmented Generation,即检索增强生成。
过去的模型都是基于已有的数据训练的,没有实时性,无法回答最新的问题,也没有能力回答企业私有的知识。
为了处理私有知识,我们可以利用它们,通过开源大模型进行二次微调,也可以通过大模型搭建 RAG 系统。
流程

有两个主要步骤:语义搜索和生成输出。在语义搜索步骤中,从知识库中找到与要回答的查询最相关的部分内容。然后,在生成步骤中,将使用这些内容来生成响应。
完整的RAG应用流程主要包含两个阶段:
数据准备阶段:数据提取——>文本分割——>向量化(embedding)——>数据入库 应用阶段:用户提问——>数据检索(召回)——>注入Prompt——>LLM生成答案
数据切分
检索增强生成(RAG)的性能在很大程度上取决于其检索模块的质量,而文本分块(Chunking)是决定检索质量的关键前置步骤。粗暴或不恰当的分块会导致信息丢失、上下文割裂、检索效率低下等问题,严重影响 RAG 系统的最终表现。本文系统性地探讨了文本分块的核心目标与挑战,从克服上下文窗口限制、提高检索精度、维护上下文完整性等角度阐述了其重要性。接着,详细介绍了固定大小、基于句子、递归字符、基于文档结构等基础分块策略,并给出了结合 Markdown 文档结构的混合分块策略的 Python 实现。此外,文章还展望了语义分块、分层分块、句子窗口检索等高级策略,为读者提供了更广阔的技术视野。掌握精妙的文本分块技术,是告别“粗暴切分”,打造高效、精准 RAG 应用的关键一步。
固定大小分块
这是最简单、最“懒人”的方法。直接按照固定的字符数(Character Count)或 Token 数(Token Count)来切割文本。为了缓解在边界处强行切断语义的问题,通常会设置一个“重叠”(Overlap)大小。重叠部分意味着每个块的末尾会与下一个块的开头有一段重复的内容。
核心思想:设定一个 chunk_size(如 500 个字符)和一个 chunk_overlap(如 50 个字符)。从文本开头取 chunk_size 个字符作为第一个块,然后下一次从 start_index + chunk_size - chunk_overlap 的位置开始取下一个块,依此类推。
# 概念性示例 (非直接运行代码)
def fixed_size_chunking(text, chunk_size, chunk_overlap):
chunks = []
start_index = 0
while start_index < len(text):
end_index = start_index + chunk_size
chunks.append(text[start_index:end_index])
start_index += chunk_size - chunk_overlap
if start_index >= len(text): # 避免因 overlap 超出
break
return chunks
# 假设 text 是你的长文本
# chunks = fixed_size_chunking(text, 500, 50)基于句子的分块
这种策略试图尊重语言的自然边界——句子。它首先使用句子分割算法(如基于标点符号 .?!,或使用 NLP 库如 NLTK, SpaCy)将文本分割成独立的句子,然后将一个或多个连续的句子组合成一个 Chunk,使其大小接近目标范围。
核心思想:先切分成句子,再合并句子成块。可以简单地每个句子是一个块,也可以设定一个目标块大小,将连续的句子合并,直到接近该大小。同样可以引入句子级别的重叠(如一个块包含第 1-3 句,下一个块包含第 3-5 句)。
# 概念性示例 (使用简单的标点分割)
import re
def sentence_chunking(text, max_chunk_sentences=3):
sentences = re.split(r'(?<=[.?!])\s+', text) # 简单按标点分割
sentences = [s for s in sentences if s] # 去除空字符串
chunks = []
current_chunk_sentences = []
for sentence in sentences:
current_chunk_sentences.append(sentence)
if len(current_chunk_sentences) >= max_chunk_sentences:
chunks.append(" ".join(current_chunk_sentences))
current_chunk_sentences = [] # 开始新块
if current_chunk_sentences: # 处理剩余句子
chunks.append(" ".join(current_chunk_sentences))
return chunks
# chunks = sentence_chunking(text, 3) # 每块最多包含3个句子基于文档结构分块
这种策略利用文档本身的结构信息进行分割,例如 HTML 的 <div>, <p>, <li> 标签,Markdown 的标题 #, ##, 列表 -, *,或者 JSON/YAML 的层级结构。
核心思想:解析文档的结构树或特定标记,基于这些结构元素来定义 Chunks。例如,每个 <p> 标签内容作为一个 Chunk,或者每个 Markdown 的二级标题下的所有内容作为一个 Chunk。
向量数据库设计
知识库分类
一个知识库一个Collection
首先,一个知识库一个 Collection。
例如:
python_docs
chunk1
chunk2
ai_docs
chunk1
chunk2优点:
- 检索速度快。
- 不需要过滤。
- 索引独立。
- 可以使用不同 Embedding 模型。
- 可以设置不同索引参数。
缺点:
- Collection 数量会很多。
- 管理麻烦。
- 跨知识库搜索困难。
适合:
- 少量知识库。
- 不同业务系统。
- 大型企业。
一个Collection,用一个字段区分
字段:
id
vector
text
kb_id
category
doc_id| id | kb_id | text |
|---|---|---|
| 1 | python | 列表是…… |
| 2 | ai | Transformer…… |
| 3 | blog | Nuxt…… |
字段设计
必须字段
| 字段 | 类型 | 作用 |
|---|---|---|
| id | INT64/VARCHAR | 主键 |
| vector | FLOAT_VECTOR | 向量 |
| text | VARCHAR/TEXT | Chunk 内容 |
文档定位字段
因为 Chunk 必须知道自己来自哪里。
| 字段 | 作用 |
|---|---|
| knowledge_id | 知识库ID |
| document_id | 文档ID |
| chunk_index | 第几个 Chunk |
数据向量化
数据向量化(Embedding)是 RAG 中将“文本”转换为“可计算语义空间表示”的过程,本质是把非结构化文本映射为高维稠密向量(Dense Vector)。
向量的语义相似性通常通过余弦相似度或内积来度量,因此“语义接近”的文本在向量空间中距离更近。
Embedding 模型
在实际工程中,一般不会自己训练 embedding,而是直接使用预训练模型,例如:
- BGE 系列(BGE-M3 / BGE-Large-EN)
- text-embedding-3(OpenAI)
- E5 系列(intfloat)
- multilingual-e5(多语言)
核心流程:
text -> tokenizer -> transformer encoder -> pooling -> vector输出:
text: "列表是Python中的基础数据结构"
vector: [0.012, -0.98, 0.33, ...]代码示例:
from sentence_transformers import SentenceTransformer
# 加载 embedding 模型
model = SentenceTransformer("BAAI/bge-m3")
texts = [
"列表是Python中的基础数据结构",
"Transformer 是一种注意力机制模型",
"Nuxt 是一个 Vue 框架"
]
# 生成向量
embeddings = model.encode(
texts,
normalize_embeddings=True # 建议归一化,便于 cosine 相似度
)
print(embeddings.shape) # (3, dim)数据入库
向量化后的数据需要存入向量数据库,用于后续检索(ANN Approximate Nearest Neighbor)。
常见方案:
- Milvus(推荐,生产级)
- FAISS(本地轻量)
- pgvector(PostgreSQL扩展)
Milvus 存储示例:
from pymilvus import MilvusClient
# 连接本地 Milvus Lite / 服务端
client = MilvusClient(uri="milvus_demo.db")
collection_name = "rag_chunks"
# 插入数据结构
data = [
{
"id": 1,
"vector": embeddings[0].tolist(),
"text": "列表是Python中的基础数据结构",
"knowledge_id": "python",
"document_id": "doc_001",
"chunk_index": 0
},
{
"id": 2,
"vector": embeddings[1].tolist(),
"text": "Transformer 是一种注意力机制模型",
"knowledge_id": "ai",
"document_id": "doc_002",
"chunk_index": 0
}
]
client.insert(
collection_name=collection_name,
data=data
)数据检索 + rerank
数据检索
数据检索主要是靠向量相似度来实现的,通常使用余弦相似度或内积来度量向量之间的相似度。
代码示例:
import numpy as np
def retrieve(query: str, top_k: int = 3):
# 1. query embedding
query_vec = model.encode(query).tolist()
# 2. 向量搜索
res = client.search(
collection_name="docs",
data=[query_vec],
limit=top_k,
search_params={
"metric_type": "COSINE",
"params": {"nprobe": 10}
},
output_fields=["text"]
)
# 3. 解析结果
results = []
for hit in res[0]:
results.append({
"id": hit["id"],
"score": hit["distance"],
"text": hit["entity"]["text"]
})
return results
# 示例
query = "什么是向量数据库"
results = retrieve(query)
for r in results:
print(r)Rerank 重排
Rerank(重排序)是 RAG 检索流程中的第二阶段排序机制。
向量检索本质是:用 embedding 做“语义近似匹配”
问题是:
- 相似 ≠ 真正相关
- TopK 中可能混入“语义接近但答案无关”的内容
- 向量距离是“粗粒度排序”
示例:
from sentence_transformers import CrossEncoder
# rerank模型
model = CrossEncoder("BAAI/bge-reranker-base")
query = "什么是RAG"
docs = [
"RAG是检索增强生成技术,用于提升大模型准确性",
"向量数据库用于存储embedding",
"苹果是一种水果"
]
# 1. 构造 query-doc pairs
pairs = [(query, doc) for doc in docs]
# 2. 计算相关性分数
scores = model.predict(pairs)
# 3. 排序
ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
for doc, score in ranked:
print(score, doc)词嵌入模型
BGE-m3 模型
语义向量模型(Embedding Model)是语言模型生态体系中的重要组成部分,这一技术被广泛应用于搜索(Search)、问答(QA)、大语言模型检索增强(RAG)等应用场景之中。智源 BGE(BAAI General Embedding)模型自去年8月发布后广受好评,被开源爱好者集成至 LangChain、Llama_index 等项目,全球下载量已达713万次。
BGE-M3是首个集多语言(Multi-Linguality)、多粒度(Multi-Granularity)、多功能(Multi-Functionality)三大技术特征于一体的语义向量模型,极大提升了语义向量模型在现实世界的可用性。目前,BGE-M3已向社区全面开源并支持免费商用许可。
- BGE-M3支持超过100种语言的语义表示及检索任务,多语言、跨语言能力全面领先(Multi-Lingual)
- BGE-M3最高支持8192长度的输入文本,高效实现句子、段落、篇章、文档等不同粒度的检索任务(Multi-Granularity)
- BGE-M3同时集成了稠密检索、稀疏检索、多向量检索三大能力,一站式支撑不同语义检索场景(Multi-Functionality)
RAG 的 Query 改写
在RAG(检索增强生成)流程中,第一步通常是对用户的提问(query)进行改写。这是因为用户提问的方式与他们期望的答案之间可能存在差距。由于每个用户的提问方式可能千差万别,因此对问题进行改写可以帮助系统更好地理解问题并返回更相关的答案,从而提升RAG系统的鲁棒性和扩展性。
用户提出的问题通常存在两类问题:
- 信息不完整:用户的提问没有表达清楚所有的关键信息。
- 噪声问题:提问中可能包含了与答案无关的内容。
信息不完整
历史信息扩写
在对话中,前后文是相互关联的。如果仅凭当前的query进行检索,可能会导致召回精度大幅下降,因为query中往往缺少重要的上下文信息。以下是一个具体的例子:
用户: 华为meta70手机的性能怎么样?
系统: 华为meta70手机搭载了强大的处理器和先进的摄像系统,性能表现非常优秀。
用户: 与上一代相比,它有哪些改进?
系统: 华为meta70相较于meta60在处理器性能、摄像头优化和电池续航方面都有显著提升。
用户: 摄像头方面具体改进了什么?
--改写前: 摄像头方面具体改进了什么?
--改写后: 华为meta70手机的摄像头相比meta60有哪些具体改进?
关键词扩写
用户在搜索时常常输入的关键词较为简短,并且缺乏足够的上下文信息,这会影响语义检索(向量检索)的效果,导致召回的相关性较低。因此,需要对用户的原始关键词进行扩展和丰富。
用户输入: “机器学习 实践”
改写后的 Query: “机器学习在实际应用中的案例有哪些?哪些工具和方法适用于机器学习实践?
伪答案改写
伪答案改写通过在原始查询中加入一种假设性答案,来增强查询的语义丰富性,从而提高检索或回应的精准度。伪答案并非真实的答案,而是一个设想的内容,用于帮助系统更好地理解并检索相关信息。
用户输入: “如何提高企业的市场竞争力?”
改写后的 Query: “如何提高企业的市场竞争力?比如通过创新产品、优化营销策略或提升客户服务等手段。”
伪答案目的:通过提供假设性的提升方式,丰富查询的语义信息,从而增强系统在应对复杂问题时的检索能力。
缩写词改写
用户在查询时常常使用缩写,而许多相关文档通常会使用完整的术语,因此需要对缩写进行扩展,以便更好地匹配相关内容。
用户输入: “VR 技术在教育中的应用”
改写后的 Query: “虚拟现实(Virtual Reality)技术在教育中的应用有哪些?可以举一些实际的应用案例吗?”
噪声问题
一般去噪改写
通过去除查询中的无关成分(如多余的修饰语、模糊表达或不相关的背景信息),简化并优化查询,使其更加精确和可操作。这种方法有助于提高检索的准确性和效率。
用户输入: “我最近在准备面试,但对于算法的理解还不太够,能推荐一些有效的学习资源吗?”
改写后的 Query: “有哪些有效的学习资源可以帮助提高算法理解?”
分析:去除与问题无关的背景信息 “我最近在准备面试,但对于算法的理解还不太够”。 直接提取核心意图 “帮助提高算法 理解”。
关键词改写
这是一种专注于提取核心关键词并去除噪声的查询重写方法。通过识别查询中的关键内容,并排除冗余信息(如停用词、语气词和多余的描述),使查询更加简洁明了,从而提高检索效率和准确性。该方法特别适用于关键词检索召回,如 BM25 检索算法。
用户输入:“关于 Java 中的线程池,常见的实现方式有哪些?”
改写后:“Java 线程池 常见实现方式”
子查询改写
当查询涉及对比多个实体时,可能会产生相互干扰的情况。对比类查询通常包含多个元素,这些元素如果直接放在一个查询中,可能会导致信息重叠,影响检索的准确性。为了避免干扰,可以将对比类查询拆分成多个独立的查询,每个查询聚焦于其中一个实体,这样可以减少信息混淆,获得更准确的结果。
用户输入:“C++ 和 Go 哪个更适合做系统编程?”
拆分后的查询:“C++ 适合做系统编程的优点有哪些?”;“Go 适合做系统编程的优点有哪些?”
拆分原因:直接对比 C++ 和 Go 的优劣,可能使得系统无法有效地提取每种语言的特点。拆分后,系统可以分别检索 C++ 和 Go 在系统编程中的优点,避免信息混乱。
Prompt 示例
以下是一个开源rag系统,问题改写的示例,仅供参考:
您是查询扩展方面的专家,能够生成问题的释义。
我无法直接使用用户的问题从知识库中检索相关信息。
您需要通过多种方式扩展或释义用户的问题,例如使用同义词/短语、完整地写出缩写、添加一些额外的描述或解释、改变表达方式、将原始问题翻译成另一种语言(英语/中文)等。
并返回 5 个版本的问题,其中一个来自翻译。
只需列出问题。不需要其他单词。
评估(RAGAS)
当我们为某个真实线上系统开发了检索增强生成 (RAG) 应用,那么在此应用正式上线提供服务前,我们需要评估RAG 的表现到底是怎样的。如果发现现有的 RAG 效果不够理想,可能需要一些新的 RAG 算法流程来改进。在这之前,就需要对 RAG 流程进行评估,得到评估指标,然后才能进行自动化对比,观察改进的流程是否真的有效。
RAGAS (Retrieval Augmented Generation Assessment) 我们一般称为 Automated Evaluation of Retrieval Augmented Generation,即检索增强生成的自动评估。Ragas是一个大模型评测框架,可以评估检索增强生成(RAG)的效果,帮助分析模型的输出,了解模型在给定任务上的表现。Github地址: https://github.com/explodinggradients/ragas。
RAGAS的评估主要基于两个方向:检索部分和生成部分,那么针对不同的部分评估的指标也会有所区分。接下来,我们将分别介绍RAGAS评估框架所需的数据,评估指标,实际用例等。
数据说明
最开始的 RAGAs 在评估数据集时,不必依赖人工标注的标准答案,而是通过底层的大语言模型 (LLM) 来进行评估。所以只需要一个带有问题-答案对的评估数据集(QA 对),如:https://huggingface.co/datasets/m-ric/huggingface_doc的具体字段:
- question:作为 RAG 管道输入的用户查询,输入。
- answer:从 RAG 管道生成的答案,输出。
- contexts:从用于回答question外部知识源中检索的上下文。
- ground_truths:question的基本事实答案。这是唯一人工注释的信息。
评估指标

评估检索(context)的指标:提供了上下文相关性(context_relevancy)和上下文召回率(context_recall),这些可以衡量你的检索系统的性能,即检索的段落是否相关。
评估生成(answer)的指标:提供了忠实度(faithfulness),用以衡量生成的信息是否准确无误;以及答案相关性(answer_relevancy),用以衡量答案对问题的切题程度,即模型生成的答案是否恰当。
上下文相关性(context relevance)
作用:指的是检索到的上下文应该只包含回答问题所需的信息,这个指标旨在惩罚包含冗余信息的情况。比率越高,表示检索到的上下文与问题的相关性越强。
为了估算上下文的相关性,我们用 LLM 从上下文 C(q) 中抽取对回答问题 q 至关重要的句子 S。提取的prompt如下:
请从提供的{上下文}中提取可能有助于回答以下{问题}的相关句子。
如果没有找到相关句子,或者你认为问题无法从给定上下文中得到回答,则返回短语“信息不足”。
在提取候选句子时,你不得更改给定上下文中的句子。
然后,在RAGAS中,通过下面的公式计算相关性:
CR=total number of sentences in c(q)number of extracted sentences
上下文召回率(context recall)
作用:衡量检索到的上下文(contexts)与真实答案(ground_truths)的匹配程度。
该指标通过问题、标注答案和检索到的上下文计算,分数范围在0到1之间,得分越高表示性能更好。
要从真实答案中估计上下文召回率,需要分析真实答案中的每个声明(claim),以确定它是否可以归因于检索到的上下文。理想情况下,真实答案中的所有声明都应可归因于检索到的上下文。
假设真实答案(Reference Answer)为:
“2010年世界杯的冠军是西班牙。”
“西班牙在决赛中以1-0击败了荷兰。”
RAG检索到的上下文(Retrieved Context)为:
“西班牙在2010年世界杯的决赛中击败了荷兰。”
“2010年世界杯的冠军是西班牙,西班牙队首次赢得世界杯。”
步骤具体实施:
声明1:"2010年世界杯的冠军是西班牙。"
输入给GPT-3.5:检索到的上下文 + 声明1。
GPT-3.5检查上下文是否包含“2010年世界杯的冠军是西班牙”。
结果:GPT-3.5发现上下文包含该信息,因此该声明“召回”。
声明2:"西班牙在决赛中以1-0击败了荷兰。"
输入给GPT-3.5:检索到的上下文 + 声明2。
GPT-3.5检查上下文是否包含“西班牙在决赛中以1-0击败了荷兰”。
结果:GPT-3.5确认上下文中提到西班牙击败了荷兰,但未提到具体的比分1-0,因此该声明“未召回”。
计算召回率:总声明数:2(声明1和声明2)。
被召回的声明数:1(声明1被召回,声明2未召回)。
Context Recall=0.5
公式:
context recall=∣Number of claims in GT∣∣GT claims that can be attributed to context∣
分子:GT claims that can be attributed to context,表示在真实答案(GT)中的论断中,有多少是可以归因于检索到的上下文的。换句话说,这些论断在检索到的上下文中找到了支持或依据。
分母:Number of claims in GT 表示真实答案中论断的总数量。
忠实度(faithfulness)
作用:指答案确实是根据给定的上下文得到的。这对于避免错觉并确保检索到的上下文可以用作生成答案的依据非常重要。
如果分数低,它表明 LLM 的回应没有遵循检索到的知识,提供幻觉式答案的可能性增加。
为了估计忠实度,我们首先使用 LLM 提取一组陈述,S(a(q))。方法是使用以下提示:
给定一个问题和答案,从给定答案的每个句子中创建一个或多个陈述。
问题:[问题]
答案:[答案]
生成 S(a(q)) 后,LLM 判断每个陈述 si 是否可以从 c(q) 中推断出来。这个验证步骤使用以下提示进行:
考虑给定的上下文和以下陈述,然后确定它们是否由上下文中的信息支持。在得出结论(是/否)之前,为每个陈述提供简要解释。在最后以给定格式为每个陈述提供最终结论。不要偏离指定的格式。
陈述:[陈述 1]
...
陈述:[陈述 n]
最终的忠实度分数,F,计算为 F=|V|/|S|,其中 |V| 表示 LLM 支持的陈述数量,|S| 表示陈述的总数。
答案相关性(answer relevancy)
作用:生成的答案与查询之间的相关性。分数越高,相似性越好。
为了估计答案的相关性,我们提示 LLM 根据给定的答案生成 n 个潜在问题 qi,如下所示:
为给定答案生成一个问题。
答案:[答案]
具体步骤
- 对给定答案,提示大型语言模型(LLM)生成基于该答案可能的 n个问题 qi。
- 使用embedding模型获取所有问题的嵌入表示。
- 对于每个生成的问题 qi,计算它与原始问题 q 之间的相似度 sim(q,qi)。这里的相似度是通过计算对应嵌入之间的余弦相似度来得到的。
- 答案相关性得分(平均相似度) AR 通过以下公式计算:
公式:
AR=n1i=1∑nsim(q,qi)