RAG中的块大小与重叠:步长、令牌限制及拼接点
如何为RAG将长书分割成块:选择块的大小与重叠比例,理解步长概念,确保不超过嵌入令牌限制,存储元数据,并在实际分割中查看重叠情况。
以古腾堡计划中的本杰明·富兰克林自传为例。即便上下文窗口能够容纳整本书,将其全部粘贴到提示词中也是浪费资源。像“为何富兰克林希望英国保留加拿大”这样的问题,其答案很可能就在两三段文字中,而剩余的页面会占用过多上下文空间,导致相关证据被淹没。针对这种情况就有了检索增强生成(RAG)技术——系统会定位与问题相关的段落,仅将这些内容传递给语言模型。书籍仍保存在存储中,由检索功能筛选出所需证据。
嵌入向量与文本被分割的原因
RAG首先将文本片段转换为嵌入向量,即意义的数值表示。即使表述不同,含义相近的段落也会在高维向量空间中彼此靠近。当有问题出现时,它也会被转换为嵌入向量,系统会找到最接近的已存储向量,进而返回对应的源文本段落。向量用于定位文本,而模型则读取这些文本。
一个嵌入向量无法表示整本书的内容。如果用一个向量来概括富兰克林的生平,那么他波士顿的童年经历、印刷工作、电学实验、《穷理查德》以及他在议会前的证词都会被混为一谈,无法准确描述任何一项内容。因此,在进行嵌入处理之前必须先对文本进行分块。
尺寸、重叠度与步长
一个合理的起点是:
DEFAULT_CHUNK_SIZE = 800 # characters
DEFAULT_CHUNK_OVERLAP = 150 # characters
人们常常将它们视为两个互不相关的旋钮。实际上它们共同决定了同一件事——即窗口每步向前移动的距离:
stride = chunk_size − chunk_overlap
= 800 − 150
= 650 characters
该步长指的是每个新片段所添加的新鲜文本量。
应将片段大小视为上限而非目标值。分割器更倾向于在自然的断点处进行切割,无论是段落、行、句子还是单词,因此要求800个字符时,生成的片段可能是640、731或恰好800个字符。
重叠部分是指相邻片段共有的文本,它起着实际作用。如果没有重叠,切割后可能会留下类似这样的孤立片段:
"…but left school at the age of ten."
单看这一行内容,信息量其实很少。但通过适当重叠处理,后续的文本片段能够保留足够的上下文,从而保持可读性并便于检索。这对于长句子以及紧密关联的论点尤为重要。富兰克林关于加拿大的信中就有一系列长句,这些句子可能跨越段落边界,导致某个片段缺少结尾,而下一个片段又缺少开头。适度的重叠处理可以确保两个片段之间的关联得以保留。
这样的代价就是内容重复。包含相似文字的相邻片段可能会被一起检索出来,这是正常现象。当重复内容挤占了更重要的证据时,采用注重多样性的方法,如最大边际相关性算法(MMR),就能起到帮助作用。
每个存储的文本片段包含什么
每个文本片段都包含四个字段:
id
document
embedding
metadata
一条示例记录:
id 7c2a1e90-4b11-4d3e-9f08-12a6c0e84b21
document "His boyhood in Boston was a stern beginning of the habit
of hard work and rigid economy which marked the man. For
a year he went to the Latin Grammar School on School
Street, but left off at the age of ten to help his father
in making soap and candles."
embedding 384 floats — [-0.0412, 0.0187, 0.0621, -0.0094, 0.0330, …]
norm = 1.0
metadata {
book: "Franklin's Autobiography",
source: "https://www.gutenberg.org/cache/epub/36151/pg36151-images.html",
gutenberg_id: 36151,
page_label: "5",
chunk_index: 2
}
嵌入向量用于搜索,而原文则用于阅读、引用和标注。元数据同样需要妥善处理。如果版本中印有如 [Pg 5] 这样的页码标记,应保留它们,这样引用就能指向读者在其副本中能找到的页面。
在建立索引之前,应剔除价值较低的内容:标题页、模板文字、许可协议、重复的页眉以及大量脚注。这些内容会导致嵌入向量质量下降,进而影响检索效果。
标准化向量
嵌入向量通常会进行 L2 标准化处理,即将每个向量的长度调整为 1:
‖v‖ = √(v₁² + v₂² + … + vₙ²)
余弦相似度通过以下方式比较两个向量:
cos θ = (a · b) / (‖a‖ ‖b‖)
当两者都具有单位长度时,它就简化为普通的点积:
If ‖a‖ = ‖b‖ = 1:
cos θ = a · b
因此,排名反映了两种含义指向同一方向的相似程度,而原始向量的大小并不起作用,这使得检索既快速又有意义。
标记而非字符
以字符为单位的设置可能会掩盖一个硬性限制,因为嵌入模型计算的是标记数。all-MiniLM-L6-v2模型最多可处理256个标记。较长的文本片段可能会在没有任何警告的情况下被截断。数据库中仍保存着完整文本,但其嵌入仅覆盖开头部分,而被截断的尾部则难以或无法被检索到。一个谨慎的估算为:
maximum safe chunk size in characters
≈ model token limit × 4
对于一个包含256个标记的模型,其输出长度约为1,000个字符。若每个标记对应4个字符,则仍有余量,因为对话、缩写、标点以及格式化文本的标记密度通常高于散文。
较新的模型能够处理更长的输入,但分块处理依然重要。更大的窗口并不能解决检索问题,因为过大的分块会将不相关的主题混合成一个模糊的向量。
流程详解
首先,对加载的页面进行分块:
from rag_qa.gutenberg import load_pages
from rag_qa.chunk import chunk_documents
from rag_qa.config import load_settings
s = load_settings()
print(s.summary())
# {
# 'chunk_size_chars': 800,
# 'chunk_overlap_chars': 150,
# 'stride_chars': 650, # size − overlap; this sets chunk count
# 'model': 'all-MiniLM-L6-v2',
# 'model_max_tokens': 256, # hard cap; overflow is silent
# 'embedding_dims': 384,
# 'max_safe_chunk_chars': 1024, # 256 × 4
# 'book': "Franklin's Autobiography",
# 'gutenberg_id': 36151,
# }
pages = load_pages()
chunks = chunk_documents(pages, s.chunk_size, s.chunk_overlap)
print(len(pages), len(chunks), s.stride)
其次,检查每个数据块是否超出模型的令牌限制:
from sentence_transformers import SentenceTransformer
from rag_qa.tokens import check_chunk
m = SentenceTransformer("all-MiniLM-L6-v2")
print(m.max_seq_length) # 256
for c in chunks:
r = check_chunk(c)
if r.truncated:
print("silent truncate:", r.n_chars, "chars /", r.n_tokens, "tokens")
该脚本会输出模型的最大长度,然后仅列出那些会被静默裁剪的片段。常规运行时只会显示该限制值:
256
当所有内容都适配时,你只能看到这些内容。如果有部分内容超出范围,就会出现额外的行:
256
silent truncate: 1820 chars / 412 tokens
silent truncate: 960 chars / 301 tokens
如何解读这些行内容:
第三,查看最终存储的内容:
from rag_qa.store import ingest, open_store
store = open_store()
ingest(chunks, store)
row = store.get(limit=1)
print(row["ids"][0])
print(row["documents"][0][:200])
print(len(row["embeddings"][0]), row["metadatas"][0])
# 384 floats, norm 1.0, metadata.page_label == printed [Pg N]
在数据被处理后,该代码片段会查看其中的一部分并输出三行内容:
doc-12-p3-c0
She left school at the age of ten. The next sentence continues on the same page…
384 {'page_label': '3', 'source': 'notes.pdf'}
第四,查询它:
from rag_qa.retrieve import search, search_mmr
q = "why did Franklin want Britain to keep Canada"
for hit in search(q, k=5):
print(f"{hit['score']:.3f} p.{hit['metadata']['page_label']} {hit['document'][:120]}")
# overlap makes near-duplicate hits; MMR trades a little score for diversity
for hit in search_mmr(q, k=5):
print(hit["metadata"]["page_label"], hit["score"])
部分结果示例:
0.812 p.7 I have long been of opinion that the foundations of the future grandeur and stability of the British empire lie in America
0.781 p.7 they are, nevertheless, broad and strong enough to support the greatest political structure that human wisdom ever yet
0.744 p.7 I am, therefore, by no means for restoring Canada. If we keep it all the country from the St. Lawrence to the Mississippi
0.691 p.8 I left England about the end of August, 1762, in company with ten sail of merchant ships
0.640 p.6 In this Autobiography Franklin tells of his own life to the year 1757, when he went to England
最常见的几个匹配项往往来自同一页面。这就是重叠现象,因为同一个句子中相邻的片段都会相互匹配。
小样本中的重叠情况
从书的引言中选取一段约190个字符的短文,描述作者在波士顿的童年经历:早年的勤奋节俭习惯、在School Street上的拉丁文文法学校就读的一年,以及十岁时的离校。将这段文字以60个字符为片段大小进行分割。
在没有重叠的情况下,分割器会恰好在限制位置进行切割:
[0] 59 His boyhood in Boston was a stern beginning of the habit of
[1] 55 hard work and rigid economy which marked the man. For a
[2] 58 year he went to the Latin Grammar School on School Street,
[3] 31 but left off at the age of ten.
第0个片段以“the habit of”结尾,第1个片段从“hard work”开始,两者之间没有关联。短语“habit of hard work”从未完整地出现在任何一个片段中。如果分割发生在句子中间,每个向量只能编码半个意思,因此需要完整句子的查询就无法很好地匹配这两半内容。
当重叠长度为15个字符时:
[0] 59 His boyhood in Boston was a stern beginning of the habit of
[1] 57 ⟦the habit of⟧ hard work and rigid economy which marked the
[2] 55 ⟦marked the⟧ man. For a year he went to the Latin Grammar
[3] 58 ⟦Latin Grammar⟧ School on School Street, but left off at the
[4] 22 ⟦off at the⟧ age of ten.
下方的标记显示了哪些文本是从前一个部分重复而来的:
⟦…⟧ = text repeated from the previous chunk
没有任何一个片段的长度达到60个字符,这再次表明该长度限制仅是一种上限。跳过重叠部分虽然能得到更小的索引且不会出现重复匹配,但会被切掉的所有内容都会丢失。保留重叠部分是为了确保这些连接处完整,而非为了增加额外的上下文。
四项任务及对应的工具
RAG包含四项任务:加载文本、分割文本、嵌入向量,以及存储和检索这些向量。并没有某个单一的库就能称为“RAG”。用于文本分割与任务协调的工具包括:
嵌入模型:
常见的向量数据库则构成了完整的解决方案。