上一篇介绍了RAG的由来、角色与作用和工作流程https://juejin.cn/spost/7618380654976466971
今天通过代码详细讲一讲RAG的工作流程
学习目标:
- 掌握分块策略
- 理解向量Embedding的作用与实现原理
Naive RAG遵循一个传统的处理流程,包括索引Indexing、检索Retrieval和生成Generation三个阶段 。
索引阶段(Indexing)都做什么工作?
流程是:原始文档 → 文档解析 → 文档分块 → 向量化 → 存入向量库。
- 文档分块(Chunking) 是索引阶段的核心子步骤。作用就是把长文档切成短文本块(chunk),才能向量化、检索。
- Embedding 是「分块」和「检索」之间的核心桥梁。
1. 文档分块
- 分块策略:
- 按照句子来切分
- 按照字符数来切分
- 按固定字符数 结合overlapping window
- 递归方法 RecursiveCharacterTextSplitter
1.1 按照句子来切分
以完整句子为最小分割单元,按「句号 / 问号 / 感叹号 / 换行」等句子结束符拆分文本,保证每个分块都是完整的语义句子。
- 关键特点
- 优点:语义完整性最高,不会把一句话切两半;适合法律、合同、学术论文等对句子完整性要求高的场景。
- 缺点:句子长度差异大(比如有的句子只有 10 个字,有的长句有 200 字),可能导致分块大小不均;极短 / 极长句子会影响后续向量化效果。
import re
text = "检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合检索和生成技术的模型。它通过引用外部知识库的信息来生成答案或内容,具有较强的可解释性和定制能力,适用于问答系统、文档生成、智能助手等多个自然语言处理任务中。RAG模型的优势在于通用性强、可实现即时的知识更新,以及通过端到端评估方法提供更高效和精准的信息服务.RAG 包含三个主要过程:检索、增强和生成.通过这一过程,RAG模型能够在各种自然语言处理任务中发挥作用,如问答系统、文档生成和自动摘要、智能助手和虚拟代理、信息检索以及知识图谱填充等。同时,RAG模型具有及时更新、解释性强、高度定制能力、安全隐私管理以及减少训练成本等优点。与微调相比,RAG是通用的,适用于多种任务,并且能够实现即时的知识更新而无需重新训练模型。整个 RAG 系统由两个核心模块组成:检索器和生成器。检索器从数据存储中搜索相关信息,生成器生成所需内容."
# 正则表达式匹配中文句子结束的标点符号
sentences = re.split(r'(。|?|!|....)', text)
# 重新组合句子和结尾的标点符号
chunks = []
for sentence, punctuation in zip(sentences[::2], sentences[1::2]):
chunks.append(sentence + (punctuation if punctuation else ''))
print(chunks)
for i, chunk in enumerate(chunks):
print(f"块 {i + 1}: {len(chunk)}: {chunk}")
1.2 按照字符数来切分
完全按固定字符数量硬切分文本,不考虑语义边界(比如切到一半的句子、单词),是最简单粗暴的切分方式。
- 关键特点
- 优点:实现最简单、速度最快;分块大小高度统一,便于控制向量库存储成本。
- 缺点:严重破坏语义完整性(比如把「我喜欢吃苹果」切成「我喜欢」和「吃苹果」);丢失上下文关联。
text = "检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合检索和生成技术的模型。它通过引用外部知识库的信息来生成答案或内容,具有较强的可解释性和定制能力,适用于问答系统、文档生成、智能助手等多个自然语言处理任务中。RAG模型的优势在于通用性强、可实现即时的知识更新,以及通过端到端评估方法提供更高效和精准的信息服务.RAG 包含三个主要过程:检索、增强和生成.通过这一过程,RAG模型能够在各种自然语言处理任务中发挥作用,如问答系统、文档生成和自动摘要、智能助手和虚拟代理、信息检索以及知识图谱填充等。同时,RAG模型具有及时更新、解释性强、高度定制能力、安全隐私管理以及减少训练成本等优点。与微调相比,RAG是通用的,适用于多种任务,并且能够实现即时的知识更新而无需重新训练模型。整个 RAG 系统由两个核心模块组成:检索器和生成器。检索器从数据存储中搜索相关信息,生成器生成所需内容."
def split_by_fixed_char_count(text, count):
chunks = []
for i in range(0, len(text), count):
chunks.append(text[i:i + count])
return chunks
# 假设我们按照每100个字符来切分文本
chunks = split_by_fixed_char_count(text, 100)
for i, chunk in enumerate(chunks):
print(f"块 {i + 1}: {len(chunk)}: {chunk}")
1.3 按固定字符数 结合overlapping window
在「固定字符数切分」的基础上,增加重叠窗口(Overlap) —— 后一个分块会复用前一个分块末尾的 N 个字符,既保证分块大小可控,又减少语义断裂。
-关键参数
- chunk_size:每个分块的最大字符数(比如 500)
- chunk_overlap:相邻分块的重叠字符数(比如 50)
- 关键特点
- 优点:兼顾「分块大小统一」和「语义连续性」;重叠部分能弥补硬切分的语义断裂问题,是工业界最常用的基础策略。
- 缺点:仍可能切断完整句子 / 段落(只是概率降低);重叠会增加少量冗余数据。
text = "检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合检索和生成技术的模型。它通过引用外部知识库的信息来生成答案或内容,具有较强的可解释性和定制能力,适用于问答系统、文档生成、智能助手等多个自然语言处理任务中。RAG模型的优势在于通用性强、可实现即时的知识更新,以及通过端到端评估方法提供更高效和精准的信息服务.RAG 包含三个主要过程:检索、增强和生成.通过这一过程,RAG模型能够在各种自然语言处理任务中发挥作用,如问答系统、文档生成和自动摘要、智能助手和虚拟代理、信息检索以及知识图谱填充等。同时,RAG模型具有及时更新、解释性强、高度定制能力、安全隐私管理以及减少训练成本等优点。与微调相比,RAG是通用的,适用于多种任务,并且能够实现即时的知识更新而无需重新训练模型。整个 RAG 系统由两个核心模块组成:检索器和生成器。检索器从数据存储中搜索相关信息,生成器生成所需内容."
def sliding_window_chunks(text, chunk_size, stride):
chunks = []
for i in range(0, len(text), stride):
chunks.append(text[i:i + chunk_size])
return chunks
chunks = sliding_window_chunks(text, 100, 50) # 100个字符的块,步长为50
for i, chunk in enumerate(chunks):
print(f"块 {i + 1}: {len(chunk)}: {chunk}")
1.4 递归方法(RecursiveCharacterTextSplitter)
这是「智能切分」的核心策略,也是 LangChain 等框架的默认推荐策略,逻辑是:
先按**大粒度分隔符**(段落符、换行符)切分;
如果切出的分块超过`chunk_size`,再按**中粒度分隔符**(句子符:。!?)切分;
如果还超,最后按**小粒度分隔符**(逗号、空格)切分;
若仍超,才按字符硬切分;
全程可搭配`overlap`保证语义连续。
- 关键特点
-
优点:最大化保证语义完整性(优先按自然语义边界切分);适配各种长度的文本;兼顾效率和效果。
-
缺点:实现比前几种稍复杂;切分速度略慢于纯字符切分(但可忽略)。
-
from langchain.text_splitter import RecursiveCharacterTextSplitter
text = """
检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合检索和生成技术的模型。它通过引用外部知识库的信息来生成答案或内容,具有较强的可解释性和定制能力,适用于问答系统、文档生成、智能助手等多个自然语言处理任务中。RAG模型的优势在于通用性强、可实现即时的知识更新,以及通过端到端评估方法提供更高效和精准的信息服务.RAG 包含三个主要过程:检索、增强和生成.通过这一过程,RAG模型能够在各种自然语言处理任务中发挥作用,如问答系统、文档生成和自动摘要、智能助手和虚拟代理、信息检索以及知识图谱填充等。同时,RAG模型具有及时更新、解释性强、高度定制能力、安全隐私管理以及减少训练成本等优点。与微调相比,RAG是通用的,适用于多种任务,并且能够实现即时的知识更新而无需重新训练模型。整个 RAG 系统由两个核心模块组成:检索器和生成器。检索器从数据存储中搜索相关信息,生成器生成所需内容.
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=50,
chunk_overlap=10,
length_function=len,
)
chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
print(f"块 {i + 1}: {len(chunk)}: {chunk}")
模块下载 pip install langchain
RecursiveCharacterTextSplitter 是一个用于将文本分割成较小块的工具。 它特别适用于需要递归地按字符拆分文本的场景,例如处理超长文档或嵌套结构的文本 chunk_size = 分割长度 chunk_overlap = 重叠长度 length_function = 固定写法(用于定义如何计算每个文本片段的长度)
2.向量与Embedding
2.1 向量
在数学中,向量(也称为欧几里得向量、几何向量),指具有大小(magnitude)和方向的量。它可以形象化地表示为带箭头的线段。箭头所指:代表向量的方向;线段长度:代表向量的大小。
- 将文本转成一组浮点数:每个下标
i,对应一个维度 - 整个数组对应一个
n维空间的一个点,即文本向量又叫 Embeddings - 向量之间可以计算距离,距离远近对应语义相似度大小
- 在计算机中我们用数组来表述向量
import numpy as np
v = np.array([3, 4]) # 数组表示向量
magnitude = np.linalg.norm(v) # 计算大小
unit_vector = v / magnitude # 计算单位向量(方向)
angle = np.arctan2(v[1], v[0]) * 180 / np.pi # 角度(度)
print(f"大小: {magnitude}")
print(f"单位向量 (方向): {unit_vector}")
print(f"与x轴角度: {angle:.1f}°")
2.2 Embedding
Embedding(嵌入) 本质是:把文本、图片、音频等「非结构化信息」,转换成计算机能理解的、低维度的数字向量(一串数字) ,且这个向量能反映原始信息的语义 / 特征。
- 举个例子(一看就懂)
- 原始文本:
猫→ Embedding 向量:[0.12, -0.34, 0.56, 0.78, ...] - 原始文本:
狗→ Embedding 向量:[0.11, -0.33, 0.55, 0.77, ...] - 原始文本:
汽车→ Embedding 向量:[0.89, 0.21, -0.45, -0.67, ...]
- 原始文本:
核心特点:语义越相似,向量越接近(猫和狗的向量几乎重合,猫和汽车的向量差很远)。
具体作用拆解
- 把文本块变成可计算的向量:计算机无法直接比较「文本块 A」和「问题 B」的语义相似度,但能计算两个向量的余弦相似度;
- 实现语义检索:不是关键词匹配(比如搜「猫咪」找不到「小猫」),而是语义匹配(「猫咪」和「小猫」的向量接近,能精准召回);
- 降维优化效率:原始文本是高维离散数据,Embedding 转换成低维连续向量,大幅降低向量库存储和检索的成本。
2.3 向量间的相似度计算
常用的相似度计算方法包括:
- 余弦相似度Cosine:计算两个向量之间的夹角余弦值,只关注「方向是否一致」,不关注向量长度(完美适配 Embedding 语义匹配)。
- 计算公式: 两个向量的乘积 / 两个向量范数的乘积 A×B / ∥A∥ × ∥B∥
- 范数: l1(绝对值), l2(勾股定理), l无穷
- 欧式距离L2:计算两个向量在多维空间中的直线距离,距离越小,相似度越高。
- 点积:直接计算两个向量的点积(对应维度相乘后求和),仅当向量已归一化(模长 = 1)时,等价于余弦相似度。。
# 第一步:准备两个 Embedding 向量(模拟 RAG 中的「问题向量」和「文档块向量」)
# 向量1:用户问题「宠物猫的喂养方法」
vec_question = [0.12, 0.34, -0.21, 0.56, 0.09]
# 向量2:文档块「猫咪每天需要喂2次,主食选猫粮」(高相似)
vec_chunk_similar = [0.11, 0.33, -0.20, 0.55, 0.08]
# 向量3:文档块「汽车保养需要定期换机油」(低相似)
vec_chunk_dissimilar = [0.89, -0.45, 0.67, -0.32, 0.78]
# 第二步:导入计算库
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# 第三步:定义计算函数
def calculate_similarity():
# 1. 余弦相似度(RAG 核心)
cos_sim_similar = cosine_similarity([vec_question], [vec_chunk_similar])[0][0]
cos_sim_dissimilar = cosine_similarity([vec_question], [vec_chunk_dissimilar])[0][0]
# 2. 欧式距离(值越小越相似)
euclid_similar = np.linalg.norm(np.array(vec_question) - np.array(vec_chunk_similar))
euclid_dissimilar = np.linalg.norm(np.array(vec_question) - np.array(vec_chunk_dissimilar))
# 3. 点积相似度(先归一化再计算,等价于余弦)
# 归一化向量(模长=1)
vec_q_norm = vec_question / np.linalg.norm(vec_question)
vec_c_norm = vec_chunk_similar / np.linalg.norm(vec_chunk_similar)
dot_sim = np.dot(vec_q_norm, vec_c_norm)
# 输出结果
print("===== 余弦相似度(越接近1越相似) =====")
print(f"问题 vs 相似文档块:{cos_sim_similar:.4f}")
print(f"问题 vs 不相似文档块:{cos_sim_dissimilar:.4f}")
print("\n===== 欧式距离(值越小越相似) =====")
print(f"问题 vs 相似文档块:{euclid_similar:.4f}")
print(f"问题 vs 不相似文档块:{euclid_dissimilar:.4f}")
print("\n===== 点积相似度(归一化后等价于余弦) =====")
print(f"问题 vs 相似文档块:{dot_sim:.4f}")
# 运行计算
calculate_similarity()
总结:
- 余弦相似度:RAG 场景首选,关注向量方向,不受长度影响,适配语义匹配;
- 欧式距离:直观但受长度影响,仅作辅助参考;
- 点积相似度:归一化后等价于余弦,计算更快,适合高性能场景。