Hi,我是 Hyde。
今天我们讨论的话题是:如何通过优化 RAG 中的检索环节来最优化大模型的效果。
在最简单的 RAG 系统中,存在这样一个工作链条:
当用户进行提问时,RAG 系统首先会根据问题从预先构建好的数据库检索相关的文档,并返回给大模型进行生成。在这个过程中,我们一共做了三件事儿,分别是索引、检索和生成。为了获得更好的效果,需要对这三个环节进行针对性的优化,从而提高 RAG 系统的性能。
在上一篇文章《最优化大模型效果之 RAG(二):索引的优化策略》中,我们已经针对索引环节的优化进行了讨论,接下来的内容中,我们将重点探讨 RAG 系统里检索的优化方法和策略。
检索是 RAG 中的关键环节,目的在于从数据库中匹配与用户原始问题最相关的内容。检索到的内容质量越高、与用户原始问题越相关,则生成的效果越好、越准确,产生幻觉的可能性也更低。
为了系统的考虑对检索环节进行优化,我们可以将检索划分为 检索前(Pre-Retrieval)、检索(Retrieval)和检索后(Post-Retrieval) 三个阶段。
检索前优化
用户原始问题的质量是影响 Naive RAG 生成效果的主要因素,一个模糊、指向不明的问题会严重降低检索效率,带来糟糕的生成结果。
我们无法也不应该要求用户提出的每一个问题都是高质量问题,因此检索前优化目标是改善用户的提问质量。即使面对糟糕的用户提问,RAG 系统也有能力将其转换和拓展成一系列高质量问题。
改善原始问题质量有三种思路:
- 问题重写 Re-written
- 生成子问题 Sub-question
- 后退提示 Step-Back Prompting
问题重写 Re-written
问题重写这一策略的思想是利用大模型的理解和生成能力,将低质量的问题改写为一系列相关的高质量问题。
1. 原始问题重写 Query Rewrite
原始问题重写(Query Rewrite)是指利用大语言模型(LLM)对用户的原始提问进行重新表述,使其更加清晰、具体和易于理解。通过这种方式,RAG 系统可以更准确地捕捉用户意图,从而提高检索和生成结果的质量。
提示词示例:
python
"""
请将以下用户的原始提问改写为一个更加具体和清晰的问题,以便更好地进行检索和生成:
用户提问:{{原始提问}}
"""
2. RAG-Fusion / Multi-Query
RAG-Fusion 和 Multi-Query 的核心思想在于将单个查询拓展为多个相关的问题进行查询,从而丰富上下文内容的多样性和覆盖范围。
提示词示例:
python
# 来自 langchain 文档中关于 Multi-Query 的提示词
"""
You are an AI language model assistant.
Your task is to generate five different versions of the given user question to retrieve relevant documents from a vector database.
By generating multiple perspectives on the user question, your goal is to helpthe user overcome some of the limitations of the distance-based similarity search.
Provide these alternative questions separated by newlines.
Original question: {question}
"""
子问题 Sub-question
有时用户提出的问题可能很复杂,我们可以通过将复杂的原始问题拆解成一系列简单的问题,分别进行检索出每一个简单问题的结果,将结果合并到最终用于生成的上下文中。
举个例子,如果用户问:「Web Voyager 与 Reflection Agents 有何不同」。而我们的数据库中有两个文档分别解释了 Web Voyager 和 Reflection Agents ,但没有比较两者的文档。
在这种情况,如果分别检索 "什么是Web Voyager" 和 "什么是 Reflection Agents" ,然后合并检索结果就可以获得更好的结果,就可以获得比直接检索原始问题更高的检索结果质量。
更进一步,我们可以构建多条 RAG 流水线,来递归或并行地回答这些子问题,然后进行最终答案的生成,这就是所谓的 Least-to-Most。
Least-to-Most Prompting
Least-to-Most Prompting 引导模型通过将复杂问题分解为一系列更简单的子问题,渐进式地解决复杂问题。它由两个连续阶段组成:
1. 分解(Decompose)
分解阶段与 Sub-question 的过程相同,目的都是将复杂的问题分为一系列更简单的子问题。
2. 子问题求解(Subproblem solving)
子问题求解阶段中,我们会提示 LLM 依次解决这些子问题,而每一子问题的回答都将作为解决后续子问题时的参考。
下图是 Least-to-Most Prompting 原始论文中的插图:
后退一步 Step-Back Prompting
Step-Back 的核心思想在于第一性原理,通过 Step-Back Prompt,LLM 可以从原始问题中抽象和派生出更高级的概念和第一性原理,进而我们可以使用这些概念和第一性原理来指导 LLM 进行后续的推理步骤。
我们可以参照 Step-Back Prompting 原始论文中的案例来理解 Step-Back。
- 用户原始问题:「如果温度增加2倍,体积增加8倍,理想气体的压力P会发生什么变化?」
- Step-back 问题:「这个问题背后的物理原理是什么?」
- 基于 Step-back 找到了更本质的原理:理想气体定律:PV = nRT。
- 根据此原理,LLM 成功推理出了答案。
HyDE
HyDE 也是一种 Re-written 的策略,但与前面的三种思路针对问题的重写不同,HyDE 使用 LLM 生成问题的假设文档或答案,并使用假设的文档或答案进行检索。这种方式能够使得查询包含更多的潜在含义和语义信息,使得 HyDE 在原始问题非常简短或表述不够明确时效果非常好。
例如,当用户询问必胜客最好的产品是什么时,其潜在含义指向了食物。但由于问题中并不直接包含食物相关的关键词,会导致检索的结果不理想。而 HyDE 通过生成假设的答案,可以捕捉到问题背后的潜在含义和上下文,使得检索更加全面和精确。
Reverse HyDE
Reverse HyDE 是一种反向 HyDE,它通过从文档块生成假设的问题,并通过关注问题与问题之间的相似度进行检索。
检索优化
检索优化的目标是通过通过构造不同的检索过程和组件,来提高检索的准确率和召回率。
在 Naive RAG 中使用了向量相似度进行问题和文档的匹配,这种方式通常非常有效,能够在多语言、多模态的甚至是模糊描述的内容中进行检索。
元数据过滤
许多向量数据库支持为文本块添加元数据,例如时间戳、类别等。在进行向量检索前,我们可以使用元数据缩小匹配的范围,来获得更精准的检索结果。
混合检索 Hybrid Retrieval
但是有一些情况下,例如我们希望精准的匹配关键词(例如 Elon Musk),或者检索的内容非常稀疏时,向量搜索并不十分有效。而这类场景则是传统的稀疏搜索方式(例如 BM25 和 TF-IDF,他们通常基于词频统计等统计方法)的典型利基市场。
不同的检索方式可以捕获文档块中的不同特征,我们可以结合向量搜索(密集检索)和稀疏检索两种方式,以获得更好的检索性能。
Embedding 微调
在关于索引优化的那篇文章中提到了使用不合适的 embedding 模型会导致检索结果不理想。这通常与 embedding 模型的训练数据有关。模型的能力来自对数据的学习,当数据分布相近时效果很好,但如果测试数据与训练数据差异过大,embedding 的效果就会显著下降,导致检索阶段的召回率不高。
这时我们就可以考虑针对 embedding 模型进行微调,以提升向量检索的效果。
多路召回
除了向量查询,我们也可以结合使用关系型数据库或图数据库,来利用各类存储方式的优势。
例如:
-
向量存储:适合匹配文档间的语义关系。通过向量存储可以找到与查询语句在语义上最相关的文档。
-
关系型数据库:适合存储和查询海量的结构化数据。它们提供强大的查询功能和数据一致性,适用于需要进行复杂数据操作的场景。
-
知识图谱:在知识生产和关系提取上具有独特优势,可以基于现有的数据及其关系的抽象,生成新的知识,特别适用于需要深度理解和推理的任务。
此外,不同的数据库类型也适合存储不同的数据。
-
结构化数据:通常使用 SQL 数据库或图数据库进行存储。SQL 数据库在处理关系数据方面非常高效,而图数据库在处理复杂关系和层次结构上表现优异。
-
非结构化数据:通常存储在向量数据库中。向量数据库通过向量化技术,将文本、图像等非结构化数据转换为向量,便于进行相似性搜索和匹配。
因此,RAG 系统需要能够灵活地利用不同的存储方式。
一个有效的策略是使用 LLM 来构造查询请求(Query Construction),即根据不同的数据源和需求生成合适的查询语句,例如:
-
Text-to-metadata-filter:利用 LLM 生成查询向量,从向量数据库中检索语义相关的文档。
-
Text-to-SQL:利用 LLM 生成 SQL 查询语句,从关系型数据库中检索所需的结构化数据。
-
Text-to-SQL+ Semantic:利用 LLM 生成混合检索查询。例如 PostgreSQL 的开源拓展 pgvector,可以为 PostgreSQL 添加对向量存储的支持。
-
Text-to-Cypher:利用 LLM 生成图查询语句,从知识图谱中检索和推理复杂关系。
检索后优化
通过检索,我们获得了一系列可能与结果相关的文档块,但直接将这些文档块一股脑塞给 LLM 进行生成并非最佳选择,我们需要对这些文档进行一些处理,以获得最佳的生成效果。
在这个环节中,我们要解决的问题包含了:
- 噪声问题:档块中包含了许多对生成答案无关的信息,而这些冗余的信息对生成环节而言属于噪声,会对最终的生成结果产生负面影响,需要在此环节进行去噪处理。
- 迷失在中间:LLM 更加关注长文本的开头和结尾,中间的部分可能会被遗忘。
- 上下文窗口:过多的文档可能会超过 LLM 的上下文窗口,并拖慢 LLM 的响应速度,造成更多的 Token 使用从而产生更多的成本。
重排序 Rerank
Rerank 的过程其实就是识别哪些文档对结果生成更重要的过程。我们可以通过一些机制来评估文档块的价值,然后使用排序来突出最重要的结果,从而更好的筛选和缩小文档池。
基于规则的重排序方法 Rule-base Rerank
Rule-base Rerank 利用一组预定义的规则和指标来对文档进行重排序,常见的指标有多样性、相关性和 MRR。
- 多样性(Diversity):多样性指标用于确保返回的文档集合在内容上具有多样性。它避免了返回的文档过于相似,从而提供更广泛的信息覆盖。
- 相关性(Relevance):相关性指标用于衡量文档与查询的匹配程度。高相关性的文档应该更符合用户的查询意图。
- 最大边际相关性(MMR):MMR 可以平衡文档的相关性和多样性,能够在选择相关文档的同时,避免选择内容过于相似的文档,从而提高结果的多样性。
基于模型的重排序方法 Model-base Rerank
我们可以使用专门的 Rerank 模型来对检索检索结果进行重排序。
例如 Cohere 的 Rerank API,或开源的 Rerank 模型 bge-reranker-large
等。
文档去重
在检索结果中,经常会出现相似甚至是重复的内容,我们需要在进入生成环节前完成文档的去重。
基于哈希值的去重
我们可以比较文档的哈希值来找到完全相同的文档块。
由于使用了多个相关的拓展问题进行查询,因此检索到多个相同文档块的情况在 RAG-fusion 策略中十分常见。
基于相似度的去重
我们可以比较文档块的向量相似度来比较文档内容的相似程度,如果超过某个阈值,则认为两个文档块过于相似,需要从中选择一个保留,其余的则删除。
使用外部工具进行去重
有一些现成的库或数据清洗工具提供了去重和其他的数据清洗能力,我们可以借助这些外部工具来完成文档去重工作。
内容选择和压缩 Context Selection/Compression
内容选择和压缩的目的在于去除文档中的噪音并且尽量减少 Token 数量。
抛弃掉不那么重要的文档块是最简单的内容选择策略,通过保留更重要的文档,可以让 LLM 更聚焦于关键信息,从而提升内容的质量和相关性。
此外,学界和工业界也提出了一些更加精细化的内容选择和压缩方法。
LongLLMLingua
LongLLMLingua 是一种设计用于减少生成式任务中的上下文大小的方法。
这种方法方法通过对齐和训练一些小模型,来检测和删除文档块中不重要的标记和词元,从而使原始的文档块更加紧凑。
实际上,LongLLMLingua 会将原始文本块转换为一种人类难以理解但又能很好地被 LLM 理解的内容。
Recomp
RECOMP 是一种通过上下文压缩和选择性增强来改进检索增强语言模型(RAG)的方法。其主要目标是通过压缩检索到的文档,使其更适合于模型的处理,从而提高效率和性能。
-
上下文压缩:RECOMP 使用两种压缩方法:抽取式压缩和生成式压缩。抽取式压缩通过选择检索文档中的有用句子,生成一个简洁的摘要;生成式压缩则通过整合多个文档的信息生成简短的摘要。这些压缩摘要在与模型输入结合前进行,减少了计算成本,并降低了模型处理冗长文档的负担。
-
选择性增强:RECOMP 在上下文信息不相关或对模型无帮助时,会输出空字符串,从而避免增加无用信息。这种选择性增强确保了模型仅在需要时才利用检索到的信息,提高了模型的整体性能和效率。
OK,非常感谢你的观看,我是 Hyde,我们下次见。