在检索增强生成(RAG)这种技术中,文档嵌入的质量对于检索准确性至关重要。虽然大家更关注的是文档分割策略,但一个关键的问题往往被忽视:如何创建能够保留文档整体语境的嵌入,即使在单独处理这些片段的时候。
对人类而言,一份文档内容丰富,充满了交织的想法、定义和引用。当我们为AI处理而拆分文档时,实际上是在切断这些联系,将一个整体的故事拆解成孤立的片段,这些片段往往忽略了整体的图景,无法呈现大局。这可能导致检索失败,相关性下降,以及RAG系统的性能降低。
下面这张图清楚地展示了:当我们把维基百科上的这段文章分成几个部分时,这些短语如‘its’和‘the city’实际上指的是这座城市柏林,但一个嵌入模型对此却毫无所知,因此这些分块的向量表示就会完全不对头了!
图片来自:https://jina.ai/news/late-chunking-in-long-context-embedding-models
这个问题的解决方案在于两种先进的嵌入策略:Late Chunking 和 Contextual Retrieval。这两种策略都提供了独特的方式来保留上下文,各自从不同的角度来解决这个问题。本文将从数学角度定义RAG中的切分问题,探讨这两种方法的工作原理,并比较它们的优缺点和权衡。
添加上下文中的数学难题:我们在分隔中失去了什么为了理解简单分块的局限性,让我们来分解一下当我们把文档分片段嵌入而不是作为一个整体来嵌入时会发生哪些情况。
让我们想象一份文档 𝐷 由若干标记物(𝑑₁, 𝑑₂, …, 𝑑ₙ)组成,其中完整的意义贯穿整个序列之中。我们常常将文档表示为高维向量空间中的嵌入 𝐸(𝐷) 以捕捉其语义内容。当文档被分割时,𝐷 被划分为若干子文档 {𝐷₁, 𝐷₂, …, 𝐷ₖ},每个子文档有自己的嵌入表示 𝐸(𝐷ᵢ)。
碎片化问题当我们把文档分成小块时,每个块的嵌入 𝐸(𝐷ᵢ) 只表示 𝐷 总体意义的一部分。因此,例如代词指代、缩写或跨越多个块的想法等重要的语义联系可能会丢失。想象你在读一本书时看到一章的内容,你会觉得这与从未读过这本书而只是看到这一章的感觉完全不同,就像从未读过这本书而只是看到这一章一样!
从数学上讲,如果我们定义文档嵌入 𝐸(𝐷) 为块嵌入的聚合,那么这样更准确地反映了原文的意思。
随着分块减少连贯性,近似值变得不那么准确。此外,𝐸(𝐷ᵢ) 本身并不等同于我们所说的 Eₜᵣᵤₑ(Dᵢ | D) —— 即文档 D 中分块 i 的“真实”嵌入。换句话说:
其中 E_true(D_i | D) 表示在文档 D 的上下文下,第 i 个片段应该如何嵌入。因此,嵌入的向量 E(D_i) 不仅可能丢失或扭曲 D 所携带的整体信息,也可能无法准确捕捉片段本身的语义,特别是在句子中间或跨越概念相关段落时。
这种在文档和分块层面都出现的语义完整性受损,是简单分块策略的主要挑战,晚期分块和上下文检索方法都试图解决这一问题。
策略1:延迟分块——先整体嵌入,再逐步拆分Late Chunking,由Jina AI提出的这种方法,以不同的方式解决文档连贯性问题。Late Chunking 不是将文档拆分成片段然后孤立处理,而是保持整个文档的上下文一致性,同时生成独立的片段嵌入。
该过程如下所示:
- 初始分块:给定文档D,我们首先分析文本以确定所需的分块边界(c₁, c₂, …, cₙ) ← Chunker(D, S),使用任何选定的分块策略S(例如,固定令牌长度,句子边界)。一些常见的策略包括LangChain的RecursiveCharacterTextSplitter或NLTK或Spacy之类的文本分割库。Jina的分段API使用非常复杂的正则表达式,虽然独特,但似乎效果不错。
- 全文处理:与其单独嵌入这些分块,整个文档被分词成(τ₁, …, τₘ),并相应地计算每个分词的字符长度(o₁, …, oₘ)。接着,Transformer模型将所有分词组合成一个序列进行处理,以生成分词嵌入(ϑ₁, …, ϑₘ)。每个分词嵌入ϑᵢ是一个高维向量,它表示该分词在全文中的上下文含义。
更一般地,对于任何起始位置为 cue_start 和结束位置为 cue_last 的片段,其嵌入是:
更一般地,对于任何起始位置为 cueₛₜₐᵣₜ 和结束位置为 cueₗₐₛₜ 的片段,其嵌入向量为:
图片来源: https://jina.ai/news/what-late-chunking-really-is-and-what-its-not-part-ii/
要理解这为什么重要,可以考虑一篇关于柏林的维基百科文章。在处理包含“这座城市”这一短语的段落时,传统分块方法会将该短语单独嵌入。然而,采用延迟的分块方法,这些词汇则会与先前提到的“柏林”一起嵌入,因为所有词汇是一起处理的。最终池化步骤保留了这些上下文关系,同时保持了所需的分块结构。这样得到的分块嵌入既捕捉了局部意义,也包含了更广泛的上下文信息,从而产生了更准确的语义表示。
图片来源: https://jina.ai/news/late-chunking-in-long-context-embedding-models(关于长上下文嵌入模型的迟划分)
实现与优势Late Chunking的优势在于其简洁性和快速性。只需要通过长上下文模型对整个文档进行一次处理即可,然后进行简单的分割。通过在完整文档知识的基础上创建切块,Late Chunking减少了语义损失,并支持更具有意义的检索,同时不会带来过高的计算成本。
这里是从论文中直接摘取的完整算法,但你需要知道,这是一套将整个文档的背景信息嵌入到每个片段中的方法。
延迟分块可以应用于任何支持均值池化的嵌入模型中,但最简单的方法是使用Jina的嵌入API,因为他们最新的模型支持延迟分块。要尝试延迟分块,只需将late_chunking
设置为True
即可!我们将一堆分块传递给系统,所有这些分块连接起来被视为完整文档。
一个嵌入函数可能如下所示。
# 这个函数也可用于上下文分块,参见下面的内容
JINA_API_KEY = "YOUR_JINA_API_KEY"
def get_embeddings(chunks, late_chunking=False, contexts=None):
url = 'https://api.jina.ai/v1/embeddings'
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {JINA_API_KEY}'
}
# 如果有相关上下文,则将上下文与分块合并在一起
if contexts:
input_texts = [f"{ctx} {chunk}" for ctx, chunk in zip(contexts, chunks)]
else:
input_texts = chunks
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"dimensions": "维度",
"late_chunking": late_chunking,
"embedding_type": "浮点型",
"input": input_texts
}
response = requests.post(url, headers=headers, json=data)
# 返回每个项目中的嵌入
return [item["embedding"] for item in response.json()["data"]]
这种方法特别适合较长的文档或叙述,这些文档或叙述可以适应大型模型的上下文限制,例如,Jina的限制是8192个标记。
还应注意,我们也可以将文档设置成我们想要的样子。例如,假设我们想将文档的摘要添加为全文的背景信息。论文可能超过8k标记的长度,但我们只需将摘要作为第一个部分传递,然后忽略返回的第一个嵌入向量即可!
data = {
"model": "jina-embeddings-v3",
"task": "文本匹配",
"dimensions": 1024,
"late_chunking": True, # 晚期分块设为 TRUE
"embedding_type": "float",
"input": [
"论文摘要",
"论文第27页第1块",
"论文第27页第2块",
"论文第27页第3块",
等
]
}
现在,我们只需使用 response[1:]
作为我们的向量(忽略用于摘要的部分),现在每个片段的嵌入都将包含摘要的上下文。在这里,我们可以根据需要为每个片段嵌入添加任何上下文。如果我们直接实现延迟切分,而不是通过使用 Jina 嵌入 API,我们只需忽略上下文获取片段嵌入。
这里有一个链接,指向该实现。
根据Jina的一篇关于该主题的博客文章,当文档大小接近8k标记时,延迟分组似乎效果更佳。
这对于在许多情况下我们的RAG架构几乎没有改动的情况来说,这已经相当不错了!
战略2:带上下文的检索 — 嵌入块,带有附加的上下文上下文检索策略是由Anthropic提出的一种方法,其中LLM为每个片段添加更多的背景信息。
在上下文检索(Contextual Retrieval)中,每个片段 (D_i) 都通过连接由大型语言模型生成的上下文摘要 (S(D, D_i)) 进行增强,该摘要包含了文档 (D) 和特定片段 (D_i) 的相关信息。一旦将这些信息传递给嵌入函数,我们就会得到一个增强的片段嵌入 (E(D_i + S(D, D_i))),它不仅反映了片段的局部内容,还包含了整个文档的相关上下文信息。
不好意思,数学就到这儿吧!这里有一个来自Anthropic的图,它比公式更清楚地解释。
虽然在这张图中Anthropic将向量数据库与TF-IDF索引分开,但许多向量数据库支持BM25,这是TF-IDF的一种改进变体。实际上,你会有一个单一的索引用于上下文向量,以及一个带有关键词的BM25索引。
Anthropic实现的上下文检索比延迟分块要简单得多得多:对于文档中的每个段落,将文档和段落传递给LLM以生成上下文并添加到段落中。然而,这种方法会重复很多工作,因为每个段落都是独立处理的,即使它们内容重叠。
一种提高效率的方法是使用上下文缓存。对于大型语言模型,键值缓存存储了注意力键和值,在重复使用相同的提示时,生成新标记的成本会更低。这种缓存可以使Anthropic的模型最多节省90%的成本,但完全有可能一次性生成所有分块的上下文!如果我们不使用同一个提示为所有分块生成上下文,就会有很多重复的工作,不过这可以通过并行化上下文生成过程来优化。
为了实际操作,我们可以选择:
- 分别处理每个片段以生成上下文,从而实现高质量的上下文特定嵌入效果,但这样会牺牲效率。
- 将带有标识符的多个片段输入到大型语言模型,并同时为每个ID生成上下文,这在工程上可能更经济且更简单,但可能不够准确,因为对于LLM来说,这可能是一个更复杂的任务。
Anthropic选择选项1的原因是:选项2更难实施,并且可能出现各种问题,例如长时间回复超时,或者LLM误解任务,特别是使用较小的LLM时。我只建议在使用更大规模的LLM时才选择这个选项。
这里有一个简单的实现。
chunks = [
"德国有很多非常棒的菜肴。",
"它不是欧洲最大的国家,但它是名列前茅的国家之一。",
"我一直想去参观那里的博物馆并品尝当地美食。",
"当我还是个孩子的时候,我去过德国一次。",
"那是美好的时光,我尝试了炸猪排,味道非常棒。",
"现在作为成年人,我也想去再次参观德国。",
"我还参观过欧洲的其他国家,但德国是我最喜欢的国家。"
]
document = " ".join(chunks)
首先,我们创建了一堆与德国相关的部分。所有这些都与德国有关,从文档中可以看出,但单从个别部分可能看不出与德国有关。
我们可以定义一个generate_contexts
函数,它使用大型语言模型为每个片段生成上下文,并使用上面的嵌入函数为每个片段获取原始、非上下文和上下文嵌入。
async def generate_contexts(document, chunks):
async def process_chunk(chunk):
response = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "生成简短的背景,解释这个片段如何与全文相关。"},
{"role": "user", "content": f"<document> \n{document} \n</document> \n这是我们要在整篇文章中定位的段落 \n<chunk> \n{chunk} \n</chunk> \n请给出一个简短的背景以便将该段落定位在整篇文章中,以提高对该段落的搜索检索。只需提供简洁的背景,不要回答其他内容。"}
],
temperature=0.3,
max_tokens=100
)
context = response.choices[0].message.content
return f"{context} {chunk}"
# 注:并发处理所有片段
contextual_chunks = await asyncio.gather(
*[process_chunk(chunk) for chunk in chunks]
)
return contextual_chunks
df = pd.DataFrame({'text': chunks})
contexts = await generate_contexts(document, chunks)
df['naive_embedding'] = get_embeddings(chunks, late_chunking=False)
df['late_embedding'] = get_embeddings(chunks, late_chunking=True)
df['contextual_embedding'] = get_embeddings(chunks, late_chunking=False, contexts=contexts)
df['context'] = contexts
现在我们可以将数据插入到我们的向量数据库里。我是 KDB.AI 的开发者倡导者,所以这里用它。KDB.AI 的一大优点是我们可以添加多个索引,这样我们无需重复数据,可以把所有嵌入放在一起,这意味着我们可以用单一索引,或是组合索引进行混合搜索。
当然,不过对于这个特定的例子来说,使用向量数据库这样的工具有些过于复杂。但如果你想扩展这个演示,在这种设置下运行实验会非常简单。
如果你想跟着做,你可以免费在KDB.AI获取一个实例。
让我们先初始化我们的数据库。
database_name = "contextual_rag"
table_name = "contextual_chunks"
KDBAI_ENDPOINT = "KDBAI_ENDPOINT"
KDBAI_API_KEY = "KDBAI_API_KEY"
# 创建会话
session = kdbai.Session(endpoint=KDBAI_ENDPOINT, api_key=KDBAI_API_KEY)
try:
session.database(database_name).drop() # 尝试删除数据库
except kdbai.KDBAIException:
pass # 如果删除失败,忽略错误
# 创建数据库
database = session.create_database(database_name)
# 定义表结构
schema = [
{"name": "text", "type": "str"},
{"name": "context", "type": "str"},
{"name": "naive_embedding", "type": "float32s"},
{"name": "late_embedding", "type": "float32s"},
{"name": "contextual_embedding", "type": "float32s"}
]
# 定义索引
indexes = [{
'type': 'qFlat',
'name': 'embedding_index_naive',
'column': 'naive_embedding',
'params': {'dims': 1024, 'metric': 'L2'},
},
{
'type': 'qFlat',
'name': 'embedding_index_late',
'column': 'late_embedding',
'params': {'dims': 1024, 'metric': 'L2'},
},
{
'type': 'qFlat',
'name': 'embedding_index_contextual',
'column': 'contextual_embedding',
'params': {'dims': 1024, 'metric': 'L2'},
}
]
# 创建表
table = database.create_table(table_name, schema=schema, indexes=indexes)
在这里,我们使用了qFlat索引方式来将数据存储在磁盘上,并进行精确的搜索。为了提高性能,可以考虑使用近似索引来优化性能。想了解更多关于索引的知识,请参阅我们的相关文章:https://kdb.ai/learning-hub/articles/indexing-basics/
我们现在来插入之前的数据,
table.insert(df)
最后,我们可以使用我们定义的任何索引进行搜索。让我们使用我们上下文嵌入技术进行搜索。
def search_chunks(table, query, chunking_type="naive", query_embedding=None, top_k=3):
results = table.search(
vectors={f"embedding_index_{chunking_type}": query_embedding},
n=top_k
)
return results[0]
query = "德国"
query_embedding = get_embeddings([query], late_chunking=False)
contextual_results = search_chunks(table, query, "contextual", query_embedding)
注意每个上下文片段都提到德国,这是我们查询的内容?这是一个好迹象,表明上下文分块处理可以提高我们的检索质量。上下文的质量不如人工写的那么好,表明Anthropic的提示并不完美,而且可以通过调整来提高上下文的质量。
情境检索在模型缺乏特定领域微调或特定上下文知识的场景中特别有用,尤其是在这些场景中。每个片段的嵌入向量包含了文档上下文的一部分,这使得复杂查询的检索更加准确。
在检索增强生成(RAG)中,混合搜索在这方面非常重要,因为它能显著提升检索和生成的质量。通过结合密集向量匹配和稀疏关键词搜索,混合搜索确保了高度相关的文档匹配,这为AI生成连贯的回答提供了坚实的基础。这种方法非常有效,你几乎可以确定地实施这种增强功能,而不需要进行大量的测试比较。
此外,混合搜索并不是终点——它为上下文检索和延迟分块等高级技术提供了启动平台,进一步提升检索的准确性。在这些技术上加上重排序,这将进一步优化你的流程,带来更出色的结果。虽然混合搜索是必不可少的,但叠加这些高级策略可以显著提升RAG系统的性能,达到无可比拟的准确性和连贯性。
上下文检索还有另一个优势,即可以将上下文传递给大型语言模型。当没有上下文时,语言模型会失去文档内部的关联。通过上下文检索,我们可以重新引入上下文,不仅提升了检索质量,还提高了后续生成任务的质量,从而使生成的响应更加连贯。但要做到这一点,我们需要确保在生成上下文时不会出现错误或不准确的信息。
上下文检索技术结合了稠密嵌入的优点与稀疏检索的关键词特异性,将检索错误减少至多49%,或使用重新排序模型减少至多67%。
虽然失败检索的指标不是最常用的评测检索方法的指标,但很有道理——实际上我们是将结果传递给大模型,所以只需确保正确的文档出现在搜索结果里。
它还展示了上下文检索的一个显著优势——大型语言模型知道为了正确检索某个片段,哪些信息是缺失的,从而使该片段能够被正确检索,并像人类在现实世界的检索场景中那样,将这些信息添加到片段中。
但这种方法可能并不是最理想的策略。比如,是否每个小段落都需要特定的背景信息才能理解?这可能并不需要。还有许多重复的工作,很多小段落在很多情况下需要相同的背景信息。
实践中,这种策略感觉很脆弱,难以迭代,仅在你清楚自己在做什么并且有极其可靠的评估时才应该实施。不要认为基于上下文的检索一定会提升性能,毕竟这并不是理所当然的。
图片来源:https://www.anthropic.com/news/contextual-retrieval/。
延迟分块与基于上下文的检索:分析比较 上下文延迟分块:通过首先嵌入整个文档来提供较高的上下文保全度,从而创建丰富的嵌入内容。然而,它受到嵌入模型上下文窗口大小的限制。LLM 通常具有比嵌入模型更大的上下文窗口。
上下文检索的原理: 每个文档片段都会连同周围片段或整个文档的上下文一起嵌入。这种方法特别适用于那些超过单一模型上下文窗口的长文档。通过总结或包含相关上下文,生成的嵌入更加语义准确,尤其是在理解整个文档时更为明显。当我们用检索失败作为衡量指标时,可以显著提升性能——这就像人类标注者为每个部分添加更多背景信息,使其更具上下文相关性。
可以将额外的上下文传递给语言模型(或直接传递给用户),从而产生更丰富的回答,并改进生成或回答复杂问题等下游任务。对于RAG来说,上下文检索相比延迟分块的主要优势在于,因为两者都能提升检索效果。
实际发生了什么?为什么表现越来越好?为什么添加关于文档的上下文或信息能提高检索效果呢?
为了找出答案,我使用的人工智能助手ChatGPT对其进行了PCA分析。
import numpy as np
from sklearn.decomposition import PCA
import plotly.graph_objects as go
# 获取DataFrame中的所有嵌入
naive_embeds = np.array(df['naive_embedding'].tolist())
late_embeds = np.array(df['late_embedding'].tolist())
contextual_embeds = np.array(df['contextual_embedding'].tolist())
# 获取文档嵌入
document_embedding = get_embeddings([document], late_chunking=False)[0]
# 拼接嵌入并添加查询和文档嵌入
all_embeds = np.vstack([
naive_embeds,
late_embeds,
contextual_embeds,
query_embedding, # 这里是你的查询嵌入
document_embedding # 添加文档嵌入
])
# 创建标签
labels = (
['Naive'] * len(naive_embeds) +
['Late'] * len(late_embeds) +
['Contextual'] * len(contextual_embeds) +
['Query'] +
['Document']
)
# 执行PCA
pca = PCA(n_components=2)
embeddings_2d = pca.fit_transform(all_embeds)
# 创建散点图
fig = go.Figure()
# 分离2D嵌入
naive_2d = embeddings_2d[:len(naive_embeds)]
late_2d = embeddings_2d[len(naive_embeds):len(naive_embeds) + len(late_embeds)]
contextual_2d = embeddings_2d[len(naive_embeds) + len(late_embeds):-2]
query_2d = embeddings_2d[-2:-1]
document_2d = embeddings_2d[-1:]
# 辅助函数用于调整文本位置
def adjust_text_positions(positions, offset):
return [pos + offset for pos in positions]
# 分别添加每种类型的嵌入
fig.add_trace(go.Scatter(
mode='markers+text',
x=adjust_text_positions(naive_2d[:, 0], 0.1),
y=adjust_text_positions(naive_2d[:, 1], 0.1),
name='Naive',
text=df['text'],
textposition="top center",
marker=dict(size=10, opacity=0.8)
))
fig.add_trace(go.Scatter(
mode='markers+text',
x=adjust_text_positions(late_2d[:, 0], -0.1),
y=adjust_text_positions(late_2d[:, 1], -0.1),
name='Late',
text=df['text'],
textposition="bottom center",
marker=dict(size=10, opacity=0.8)
))
fig.add_trace(go.Scatter(
mode='markers+text',
x=adjust_text_positions(contextual_2d[:, 0], 0.15),
y=adjust_text_positions(contextual_2d[:, 1], -0.15),
name='Contextual',
text=df['text'],
textposition="top right",
marker=dict(size=10, opacity=0.8)
))
fig.add_trace(go.Scatter(
mode='markers+text',
x=query_2d[:, 0],
y=query_2d[:, 1],
name='Query',
text=['查询文本'],
textposition="top center",
marker=dict(size=15, symbol='star', opacity=1.0)
))
fig.add_trace(go.Scatter(
mode='markers+text',
x=document_2d[:, 0],
y=document_2d[:, 1],
name='Document',
text=['文档文本'],
textposition="top center",
marker=dict(size=15, symbol='diamond', opacity=1.0)
))
# 更新布局使其更清晰
fig.update_layout(
title="不同嵌入策略的PCA可视化",
xaxis_title="第一主成分",
yaxis_title="第二主成分",
width=1000,
height=800,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5),
margin=dict(l=50, r=50, t=100, b=50),
)
fig.show()
哎呀,哇,这和我预期的不太一样!咱们来仔细看看吧.
首先,这些朴素的嵌入分布得很广。它们分布得很广,这很合理——它们不考虑超出该片段本身的任何上下文。因此,虽然这种独立性捕捉到了每个片段的独有特性,但这常常导致不一致,并且忽略了更广泛的联系。
现在来看一下晚段分块。嵌入形成一个靠近文档的紧密簇。这种聚类表明晚段分块有效地应用了一种变化,将每个分块拉向文档的整体表示。就像是嵌入被磁化向文档靠拢,这有助于保持文档层面的连贯性,但可能会模糊对特定查询至关重要的细节。
然后就是上下文检索技术。它的嵌入围绕着文档和查询右边的一个看似随机的点聚集。这可能是由于添加的上下文重塑了嵌入,形成了由提示影响的重复模式和结构的混合体。结果是这种看似随意的聚类,从技术上讲,虽然它离查询更近了一点,但这种接近方式无法直观理解。
但这并不意味着聚类总是好的。虽然 Late Chunking 的紧密聚类与文档内容相吻合,但这可能导致过度泛化,对于特定查询的精确度有所下降。另一方面,Contextual Retrieval 的聚类可能更符合 LLM 在这种提示方式下的某些语义习惯。这说明提示非常重要,但并没有很好地调整以适应文档的方向。
这一可视化清晰地表明:上下文检索不仅仅是像前面提到的那样,将整个文档的上下文嵌入到每个片段中。相反,它可能在修补那些因分段而丢失重要信息的薄弱片段方面表现出色。延迟片段化在保持文档级别的连贯性方面表现出色,但我猜测,上下文检索会在分段失败的地方填补空白。这两种截然不同的方法各有其独特的优势。
检索快慢与效率- 延迟分块:只需一次嵌入,其效率与朴素嵌入相当。这使得它在大型文档检索任务中非常高效且快速,特别是在模型上下文窗口足够的情况下。
- 基于上下文的检索:每个分块都带有上下文进行嵌入,但由于需要重复生成摘要/上下文,计算成本会更高。然而,一旦上下文生成完毕,我们就可以使用朴素嵌入为每个分块生成嵌入,甚至可以采用延迟分块!这些策略可以灵活结合使用。然而,在实际应用中,可能更适合只为那些有可能被大型语言模型误解的分块添加额外上下文。
- 延迟分块在我们希望在不改变架构的情况下获得性能提升时非常理想。它类似于在保持维度相同的情况下使用更高级的嵌入模型。
- 在我们需要榨取理论上的每一点性能,并希望利用由大型语言模型生成的上下文时,上下文检索效果最好。
延迟分块和上下文检索这两种方法各自提供了解决朴素分块方法中固有上下文损失的方案。延迟分块采用先行嵌入的方法,保持分块间的语义一致性,而上下文检索则通过增加文档整体的上下文信息来丰富分块,从而实现更细致的RAG过程。
在决定采用哪种方法时,请考虑您的RAG系统的需求:如果嵌入速度是最重要的因素,延迟分段处理通常是最佳选择。如果检索准确性至关重要,上下文检索通过分层方法处理分块的上下文,如果实施得当,也能取得很好的结果。这两种方法都强调了检索中上下文的重要性,这表明有效的分块不仅仅是分割文本,更重要的是保留其原意。
额外提示:其他给RAG流程添加上下文的方法为您的 RAG 管道添加上下文并不止步于延迟切分或上下文检索环节。下面是一些增强检索质量的策略:
提升检索质量的策略如下:
- 手动审核并添加背景信息
如果您的数据集较小(例如,1000个片段),可以考虑手动为片段添加背景信息。人类直觉在这方面的表现通常会优于LLM,尤其是当您深入理解您的产品或目标时。亲自审核片段可以确保背景信息更符合您的实际需求,更符合您的特定应用场景。 - 优化表现不佳的查询
识别检索效果较差的查询,并优化对应的片段以改善其排名。一种有效的方法是使用重排序模型,如Cohere Rerank,来分析结果质量较低的查询。还可以用LLM来评估搜索结果的质量,并检测其中的不准确性。调整片段的背景信息,直到结果达到您的要求。此迭代过程可使您的片段更符合用户的查询。 - 仅在必要时使用上下文检索
并非每个片段都需要额外的上下文。将片段分类为“好”(自身易于理解)或“差”(需要额外上下文)。选择性地为“差”片段应用上下文检索,在节省计算资源的同时保持检索质量。 - 在多个文档间引入上下文
当某些片段需要从多个文档中获取上下文时,探索Late Chunking之外的嵌入方法。例如,cde-small-v1
这样的小型模型通过在上下文文档嵌入论文中描述的方法实现了最先进的性能。这些策略能使您高效且有效地整合多文档的上下文。
通过结合这些方法,你可以调整你的流程,让它的表现更出色,更稳定和更易于扩展,更好地满足你的具体需求。
共同学习,写下你的评论
评论加载中...
作者其他优质文章