微信扫码
添加专属顾问
我要投稿
RAG优化策略深度解析,助力提升大模型准确性和响应速度。 核心内容: 1. LLM当前面临的主要挑战和问题 2. RAG工作流程及其优化策略概览 3. 查询转换在RAG优化中的应用和实例
目前LLM虽然已经具备了强大的能力,但是在某些情况下,它们仍可能无法提供准确的答案。目前 LLM 面临的主要问题有:
对于 Transformer架构类型的大模型来说,想要提高LLM生成内容的准确性,一般只需要 3 个步骤:
RAG 是一个完整的系统,其工作流程可以简单地分为数据预处理、检索、增强和生成四个阶段:
一个完整的RAG应用开发流程,涉及到文档加载器、向量数据库、检索器、Prompt、记忆、输出解析器、大语言模型、多个功能模块,如下所示:
通常RAG的优化策略主要分为:查询转换、路由、问题构建、索引、检索和生成六个方面进行,如下与所示:
如果直接使用原始问题进行检索,可以因为用户的表述偏差导致检索不到相关的文档。多查询重写策略的核心思想是利用大语言模型(LLM)对原始问题进行扩展、分解或抽象,生成多个语义相关但视角不同的子查询,从而提高检索系统对用户意图的覆盖能力。这种方法能有效解决单一查询可能存在的表述偏差或信息不全问题。整体流程如下所示:
一个简易的prompt如下所示:
你的任务是为给定的用户问题生成3 - 5个语义等价但表述差异化的查询变体,目的是帮助用户克服基于距离的相似性搜索的一些局限性,以便从向量数据库中检索相关文档。
以下是原始问题:
<question>
{{question}}
</question>
请生成3 - 5个语义与原始问题等价,但表述不同的查询变体,用换行符分隔这些替代问题。
请在<查询变体>标签内写下你的答案。
由于需要转换问题一般较小,以及生成子问题时对 LLM 的能力要求并不高,在实际的 LLM 应用开发中,通常使用参数较小的本地模型+针对性优化的 prompt 即可完成任务,并将 temperature
设置为 0
,确保生成的文本更加有确定性。
调用样例如下:
在多查询重写策略中,每个子问题都会检索出相应的文档片段。针对如何合并这些文档的问题,便延伸出多查询结果融合策略。主要思想对其检索结果进行重新排序(即 reranking)后输出 Top K 个结果,最后再将这 Top K 个结果喂给 LLM 并生成最终答案。通常使用的算法是RRF(Reciprocal Rank Fusion),即倒排序排名算法。公式如下:
该算法会对全集 D 进行二重遍历,外层遍历文档全集 D,内层遍历文档子集,在做内层遍历的时候,我们会累计当前文档在其所在子集中的位置并取倒数作为其权重。也就是说如果该子文档在每个子问题检索位置越靠前,则权重越高。RFF的代码实现如下所示:
def rrf(results: list[list], k: int = 60) -> list[tuple]:
"""倒数排名融合RRF算法,用于将多个结果生成单一、统一的排名"""
# 1.初始化一个字典,用于存储每一个唯一文档的得分
fused_scores = {}
# 2.遍历每个查询对应的文档列表
for docs in results:
# 3.内层遍历文档列表得到每一个文档
for rank, doc in enumerate(docs):
# 4.将文档使用langchain提供的dump工具转换成字符串
doc_str = dumps(doc)
# 5.检测该字符串是否存在得分,如果不存在则赋值为0
if doc_str notin fused_scores:
fused_scores[doc_str] = 0
# 6.计算多结果得分,排名越小越靠前,k为控制权重的参数
fused_scores[doc_str] += 1 / (rank + k)
# 7.提取得分并进行排序
reranked_results = [
(loads(doc), score)
for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]
return reranked_results
当提问的原始问题非常复杂时,无论是使用原始问题进行检索,亦或者生成多个相关联的问题进行检索,往往都很难在向量数据库中找到关联性高的文档,导致 RAG 效果偏差。造成这个问题的原因有几种:
问题分解策略就是将一个复杂问题分解成多个子问题或者子步骤。问题分解后的子问题跟原始问题是“父子”关系,而查询重写跟原始问题则是“兄弟”关系。
问题分解策略一共有两种分别是串行模式和并行模式。
串行模式适用于逻辑依赖强的问题分解,确保步骤的连贯性。如“RAG都有哪些阶段?”,需要先找到都有哪些阶段,然后再询问各个阶段该做什么事情。
并行模式适用于独立子任务的高效处理,提升响应速度。如“如何规划北京到上海的 5 天旅游行程?”,需要分解成交通、住宿、景点三个子问题,分别完成。
两种模式的流程如下所示:
一个简单问题分解的prompt如下所示:
你的任务是针对输入的问题生成多个相关的子问题或子查询,将输入问题分解成一组可以独立回答的子问题或子任务。
以下是输入的问题:
<question>
{{question}}
</question>
请生成3 - 5个与该问题相关的搜索查询,并使用换行符进行分割。生成的子问题/子查询应具有明确的主题和可独立回答的特点。
请在<子问题>标签内写下生成的子问题/子查询。
调用样例如下:
问题回退策略和问题分解策略相反,当用户问题非常具体时,可能无法检索的对应文档,就需要将问题进行抽象。比如“李开复在2000年是在哪个公司工作?”,重新抽象成“李开复的工作经历是什么?”。处理流程如下:
下面是一个执行样例:
应用场景
prompt举例:
你的任务是分析给定的问题,忽略具体细节,提炼出问题背后涉及的核心概念、原理、知识范畴或通用逻辑。
这是需要分析的问题:
<question>
{{question}}
</question>
分析时,需要提取出问题的本质,将其转化为对某一概念、原理、知识范畴或通用逻辑的探讨。例如,如果问题是“水加热到 100℃为什么会沸腾?”,那么分析后的新问题应该是“分析液体沸腾现象的物理原理(如相变、沸点与气压关系、能量传递机制等)”。
请在<回答>标签内写下你的分析结果。
调用样例如下:
在数据库中存储的数据一般都是文档层面上的,文档包含的数据会远远比用户的查询数据要大很多,所以 query
和 doc
之间是不对称检索,能找到的相似性文档相对来说也比较少。
例如:今天回家的路上看到了美丽的风景,非常开心!想学习 python 该怎么办?这个请求中,前面的风景、开心等词语均为无关信息。会对真实的请求学习 python 产生干扰。如果直接搜索用户的请求,可能会产生不正确或无法回答的 LLM 响应。因此,有必要使得用户查询的语义空间与文档的语义空间保持一致。
整体流程如下:
HyDE 将检索过程分解为两个阶段:
关键优势
prompt参考:
你的任务是实现HyDE零样本检索策略,根据用户输入的查询生成假设文档。生成的内容要反映相关性模式,同时允许存在虚构细节。
以下是用户输入的查询:
<查询>
{{QUERY}}
</查询>
在生成假设文档时,请遵循以下要点:
1. 仔细理解查询的核心内容和意图。
2. 围绕查询构建文档,让文档与查询具有明显的相关性。
3. 可以适当添加一些虚构的细节,但不能偏离查询的主题。
4. 输出的文档应具有一定的逻辑性和连贯性。
请在<生成文档>标签内写下你生成的假设文档。
<生成文档>
[在此生成假设文档]
</生成文档>
调用样例如下:
局限性:对于 doc-doc 类型的检索,虽然在语义空间上保持了一致,但是在 query->doc
的过程中,受限于各种因素,仍然可能产生错误信息。
例如提问 Bel是什么?
,在没有执行 HyDE 混合策略而是直接查询得到答案如下:
Bel 是由 Paul Graham 在四年的时间里(2015年3月26日至2019年10月12日),用 Arc 语言编写的一种编程语言。它基于 John McCarthy 最初的 Lisp,但添加了额外的功能。它是一个以代码形式表达的规范,旨在成为计算的形式化模型,是图灵机的一种替代方案。
但是执行 HyDE 混合策略生成假设性 doc
如下:
Bel 是 Paul Graham 的化名,他是这段信息背后的作者,当时需要种子资金以维持生活,并且参与了一项交易,后来成为 Y Combinator 模式的典范。
在这个例子中,HyDE
在没有文档上下文的情况下错误地解释了 Bel,这会导致完全检索不到相关的文档信息。
在查询检索中,常见的两种检索方式分别是稀疏检索器和密集检索器:
混合检索策略就是将多种检索方式混合起来,可以利用不同算法的优势,从而获得比任何单一算法更好的性能,这也是常用的检索策略。这个流程如下所示:
在Langchain中,代码实现样例如下:
doc_list = [
"我喜欢苹果",
"我喜欢橙子",
"苹果和橙子是水果",
]
# 初始化 BM25 检索器和 FAISS 检索器
bm25_retriever = BM25Retriever.from_texts(doc_list)
bm25_retriever.k = 2
embedding = OpenAIEmbeddings()
faiss_vectorstore = FAISS.from_texts(doc_list, embedding)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 2})
# 初始化集成检索器
ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5])
docs = ensemble_retriever.get_relevant_documents("苹果")
[Document(page_content='我喜欢苹果', metadata={}), Document(page_content='苹果和橙子是水果', metadata={})]
在 RAG 应用开发中,想根据不同的问题检索不同的向量数据库,其实只需要设定要对应的 Prompt,然后让 LLM 根据传递的问题返回需要选择的向量数据库的名称,然后根据得到的名称选择不同的检索器即可。整体流程如下:
路由数据源的prompt样例如下:
你是一位擅长将用户问题路由到适当数据源的专家。你的任务是根据问题涉及的编程语言,将问题路由到相关的数据源。
首先,请仔细阅读以下数据源信息:
<data_sources>
{{DATA_SOURCES}}
</data_sources>
现在,请仔细阅读以下用户问题:
<question>
{{QUESTION}}
</question>
为了将问题路由到合适的数据源,请按照以下步骤操作:
1. 仔细分析问题,识别其中涉及的编程语言。
2. 查看数据源信息,找出与该编程语言相关的数据源。
3. 如果问题涉及多种编程语言,找出与所有涉及语言都相关或与主要语言相关的数据源。
4. 如果没有合适的数据源,指出“没有合适的数据源”。
请在<回答>标签内写下路由结果。
<回答>
[在此输出路由结果]
</回答>
调用样例如下:
在RAG应用开发中,针对不同场景的问题使用特定化的prompt模板 效果一般都会比通用模板会好一些,例如在教培场景,制作一个可以教学物理的授课机器人,如果使用通用的 prompt模板,会导 prompt编写变得非常复杂;反过来如果prompt写的简单,有可能没法起到很好的回复效果。
如果能针对用户的提问,例如用户提问的内容是数学相关的则使用数学的模板,提问的内容是物理相关的则使用物理的模板,针对性选择不同的模板,LLM 生成的内容会比使用通用模板会更好,例如下方有两个 prompt模板:
物理老师:
你将扮演一位非常聪明的物理教授,以简洁易懂的方式回答物理问题。当你不知道问题的答案时,要坦率承认自己不知道。
以下是需要你回答的物理问题:
<query>
{{query}}
</query>
在回答问题时,请遵循以下指南:
1. 确保回答简洁易懂。
2. 如果不知道问题的答案,直接表明“我不知道这个问题的答案”。
请在<回答>标签内写下你的答案。
数学老师:
你将扮演一位非常优秀的数学家,专门负责回答数学问题。你需要将复杂的问题分解成多个小步骤,回答这些小步骤,然后将它们整合起来回答更广泛的问题。
这是需要你解答的数学问题:
<问题>
{{query}}
</问题>
在解答问题时,请按照以下步骤进行:
1. 仔细阅读问题,理解问题的核心。
2. 将问题分解成多个小步骤。
3. 依次解答每个小步骤。
4. 最后将小步骤的解答整合起来,给出完整的答案。
请在<回答>标签内写下你的答案,确保答案清晰、全面且包含每一个关键步骤。
基于文本向量模型,可以根据查询问题的语义,查找到相似度更高的prompt模板,语义的prompt路由流程如下所示:
检索外部数据时,最后在执行检索的时候使用的都是固定的筛选条件(没有附加过滤的相似性搜索)。但是在某些情况下,用户发起的原始提问其实隐式携带了 筛选条件,例如提问:请帮我整理下关于2023年全年关于AI的新闻汇总。
在这段 原始提问中,如果执行相应的向量数据库相似性搜索,其实是附加了 筛选条件的,即 year=2023
,但是在普通的相似性搜索中,是不会考虑 2023 年这个条件的(因为没有添加元数据过滤器,2022年和2023年数据在高维空间其实很接近),存在很大概率会将其他年份的数据也检索出来。
那么有没有一种策略,能根据用户传递的原始问题构建相应的元数据过滤器呢?这样在搜索的时候带上对应的元数据过滤器,不仅可以压缩检索范围,还能提升搜索的准确性。这个思想其实就是 查询构建或者称为 自查询。
并且除了 向量数据库,类比映射到 关系型数据库、图数据库也是同样的操作技巧,即:
在Langchain中,封装了一个自查询检索器(SelfQueryRetriever),执行流程如下:
将对应的prompt翻译后如下所示:
你的任务是根据提供的信息,生成一个符合特定结构的JSON对象。该JSON对象将用于查询和过滤文档。
以下是允许使用的比较器和逻辑运算符:
<allowed_comparators>
{{ALLOWED_COMPARATORS}}
</allowed_comparators>
<allowed_operators>
{{ALLOWED_OPERATORS}}
</allowed_operators>
现在,请根据以下信息构建JSON对象:
<< Data Source >>
```json
{{{{
"content": "{content}",
"attributes": {attributes}
}}}}
```
在构建JSON对象时,请遵循以下规则:
1. 查询字符串应仅包含预期与文档内容匹配的文本。过滤条件中的任何条件不应在查询中提及。
2. 逻辑条件语句由一个或多个比较和逻辑操作语句组成。
- 比较语句的形式为:`comp(attr, val)`,其中`comp`为允许的比较器,`attr`为要应用比较的属性名称,`val`为比较值。
- 逻辑操作语句的形式为:`op(statement1, statement2, ...)`,其中`op`为允许的逻辑运算符,`statement1`, `statement2`, ... 为比较语句或逻辑操作语句。
3. 仅使用上述列出的比较器和逻辑运算符,不使用其他运算符。
4. 过滤条件仅引用数据源中存在的属性。
5. 过滤条件仅使用应用了函数的属性名称及其函数名。
6. 处理日期数据类型的值时,过滤条件仅使用`YYYY - MM - DD`格式。
7. 过滤条件仅在需要时使用。如果没有要应用的过滤条件,`filter`的值应返回 "NO_FILTER"。
8. `limit`必须始终为整数类型的值。如果该参数没有意义,请留空。
请在<回答>标签内输出符合以下格式的JSON对象:
```json
{
"query": "文本字符串,用于与文档内容进行比较",
"filter": "用于过滤文档的逻辑条件语句",
"limit": 要检索的文档数量
}
```
<<样例>>
Data Source:
```json
{{
"content": "Lyrics of a song",
"attributes": {{
"artist": {{
"type": "string",
"description": "Name of the song artist"
}},
"length": {{
"type": "integer",
"description": "Length of the song in seconds"
}},
"genre": {{
"type": "string",
"description": "The song genre, one of \"pop\", \"rock\" or \"rap\""
}}
}}
}}
```
User Query:
What are songs by Taylor Swift or Katy Perry about teenage romance under 3 minutes long in the dance pop genre
Structured Request:
```json
{{
"query": "teenager love",
"filter": "and(or(eq(\\"artist\\", \\"Taylor Swift\\"), eq(\\"artist\\", \\"Katy Perry\\")), lt(\\"length\\", 180), eq(\\"genre\\", \\"pop\\"))"
}}
```
通常我们会为一个文档生成一个向量信息,并存储到向量数据库中。如果能从多个维度记录该文档块的信息,会大大增加该文档块被检索到的概率,多个维度记录信息等同于为文档块生成多个向量。通常建立的维度有以下几种:
整体流程如下:
在传统的 RAG 中,我们通常依靠检索短的连续文本块来进行检索。但是,当我们处理的是长上下文时,我们就不能仅仅将文档分块嵌入到其中,或者仅仅使用上下文填充所有文档。相反,我们希望为 LLM 的长下文找到一种好的最小化分块方法,这就是 RAPTOR 的用武之地,在 RAPTOR 中,均衡了多文档、超长上下文、高准确性、超低成本等特性。
RAPTOR 其实是一种用树状组织检索的递归抽象处理技术,它采用了一种自下而上的方法,通过对文本片段(块)进行聚类和归纳来形成一种分层结构。
构建过程如下图所示:
检索策略分为两种:树遍历检索和折叠树检索。其中树遍历检索流程如下所示:
折叠树检索过程如下图所示:
字符分割器就是根据指定分割符,将文档切割成多个文档块。它通常会支持控制文档块的大小,避免超出大模型上下文限制。除此之外,还可以控制块与块之间重叠的内容大小,尽可能保留上下文信息。
但是在划分的过程中,可能会出现文档块过小或者过大的情况,这会让 RAG 变得不可控,例如:
递归字符分割器对大文档块会使用更多的分隔符使其变小,对小文档块进行合并使其保留更多的信息,整体流程如下所示:
文档分割器都是使用特定字符对文本进行拆分,这种拆分模式虽然考虑了文档中的上下文切断的问题,但是并没有考虑句子之间的语义相似性,如果有一篇长文本,需要将其分割成语义相关的块,以便更好地理解和处理,这个时候就需要使用语义文档分割器。整体流程如下所示:
除了对文档进行切割的方式,对于结构化数据可以直接使用对应结构化的文档转换器,如HTML文档转换器、代码文档转换器。除此之外,还有两种特殊的转换器:
{'question': '产品发布活动的日期是什么时候?', 'answer': '7月15日'}
在完成对问题的改写、不同数据库查询的构建以及路由逻辑、向量数据库索引方面的优化后,我们可以考虑进一步优化 筛选阶段
,一般涵盖了 重排序
、纠正性RAG 两种策略。其中重排序是使用频率最高,性价比最高,通常与 混合检索一起搭配使用,也是目前主流的优化策略。
重排序的核心思想见字知其意,即对检索到的文档 调整顺序,除此之外,重排序 一般还会增加剔除无关/多余数据的步骤,其中RRF就是重排序中最基础的一种。
纠正性检索增强生成(Corrective Retrieval-Augmented Generation,CRAG)是一种先进的自然语言处理技术,旨在提高检索的生成方法的鲁棒性和准确性。在 CRAG 中引入了一个轻量级的检索评估器来评估检索到的文档的质量,并根据评估结果触发不同的知识检索动作,以确保生成结果的准确性。整体流程如下图所示:
评估节点prompt:
你是一名评分员,负责评估检索到的文档与用户问题的相关性。你的任务是根据给定的标准,判断文档是否与问题相关,并给出“yes”或“no”的二元评分。
以下是用户的问题:
<question>
{{QUESTION}}
</question>
以下是检索到的文档:
<document>
{{DOCUMENT}}
</document>
判断文档是否相关的标准为:如果文档包含与问题相关的关键词或语义含义,则判定为相关。
首先,在<思考>标签中详细分析文档是否包含与问题相关的关键词或语义含义,说明你的分析过程。然后在<回答>标签中给出最终的二元评分(“yes”或“no”)。
<思考>
[在此详细说明你对文档与问题相关性的分析过程]
</思考>
<回答>
[在此给出“yes”或“no”的评分]
</回答>
问题重写prompt:
你是一个问题改写器,任务是将输入的问题转换为一个更适合网络搜索的优化版本。你需要仔细分析输入问题,挖掘其潜在的语义意图和含义。
以下是需要改写的问题:
<question>
{{QUESTION}}
</question>
在改写问题时,请遵循以下方法:
1. 去除不必要的修饰词和语气词,使问题简洁明了。
2. 提取问题的核心内容,突出关键信息。
3. 调整语序,使问题更符合网络搜索的习惯。
请在<改写后的问题>标签内写下改写后的问题。
知识精炼prompt:
你是一位信息精炼专家,负责从给定文档中提取与特定主题直接相关的关键事实、数据、观点和结论,过滤掉不相关的背景信息、示例和解释。
请仔细阅读以下文档:
<document>
{{DOCUMENT}}
</document>
需要围绕的主题是:
<topic_name>
{{TOPIC_NAME}}
</topic_name>
在精炼信息时,请遵循以下要求:
1. 仅提取与主题直接相关的关键事实、数据、观点和结论。
2. 过滤掉所有不相关的背景信息、示例和解释。
3. 输出尽量保持简洁明了。
请在<回答>标签内写下精炼后的信息。
RAG-prompt:
你是一个负责回答问题的助手。你的任务是利用提供的检索到的上下文来回答问题。如果不知道答案,就直接表明不知道。回答最多使用三句话,保持简洁。
以下是检索到的上下文:
<retrieved_context>
{{RETRIEVED_CONTEXT}}
</retrieved_context>
这是问题:
<question>
{{QUESTION}}
</question>
请在<回答>标签内写下你的答案。
<回答>
[在此给出答案]
</回答>
Self-RAG 全称为自我反思 RAG,见名知其意,即对原始查询、检索的内容、生成的内容进行自我反思,根据反思的结果执行不同的操作,例如:直接输出答案、重新检索、剔除不相关的内容、检测生成内容是否存在幻觉、检测生成内容是否有帮助等,可以把 Self-RAG看成是一个拥有自我反思能力的智能体,这个智能体主要用来依据相关知识库回复用户问题,自我迭代,直到输出满意的结果。SELF-RAG训练了一个任意的LLM,使其能够在给定任务输入时反思自己的生成过程,同时生成任务输出和临时的特殊标记(称为反思标记)。这些反思标记分为检索和评论标记,分别表示了是否需要检索以及生成的质量。流程如下图所示:
一个 Self-RAG应用主要有三大步骤组成:
判断是否有幻觉prompt:
你是一名评分员,负责评估大语言模型(LLM)的生成内容是否有一组检索到的事实作为依据。你的任务是根据给定的事实集,判断生成内容是否能得到事实的支持,并给出“是”或“否”的二元评分。“是”表示答案有事实依据,“否”则表示没有。
首先,请仔细阅读以下检索到的事实集:
<检索到的事实集>
{{RETRIEVED_FACTS}}
</检索到的事实集>
现在,请仔细阅读以下大语言模型的生成内容:
<大语言模型生成内容>
{{LLM_GENERATION}}
</大语言模型生成内容>
评估这份生成内容时,请考虑生成内容中的所有陈述是否都能在检索到的事实集中找到支持。
在<思考>标签中详细分析你的判断依据,然后在<判断>标签中给出“是”或“否”的判断。例如:
<思考>
[在此详细说明你做出判断的依据]
</思考>
<判断>
[在此给出“是”或“否”的判断]
</判断>
请现在开始你的评估。
答案评估prompt:
你的任务是评估一个答案是否解决了相应的问题,并给出“yes”或“no”的二元评分。“yes”表示答案解决了问题,“no”表示答案未解决问题。
以下是问题:
<question>
{{QUESTION}}
</question>
以下是答案:
<answer>
{{ANSWER}}
</answer>
在评估时,请仔细对比答案内容与问题,判断答案是否直接回应并解决了问题。
请在<判断>标签内给出你的最终判断,使用“yes”或“no”。
<判断>
[在此给出“yes”或“no”的判断]
</判断>
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-04-16
强强联合!LangChain与CrewAI构建基于RAG的智能查询解答系统
2025-04-16
向量嵌入四种实现方式
2025-04-15
Cloudflare AutoRAG:把RAG应用变得和安装微信一样简单
2025-04-15
其实RAG也是智商税,聊聊他与AI知识库的关系
2025-04-15
泄漏!知名程序员AI受害,3000字带你避坑
2025-04-15
Dify+RAGFLow:基于占位符的图片问答升级方案(最佳实践)
2025-04-14
Open WebUI中调用RAGFlow的聊天机器人——适合构建个人和企业级知识问答助手
2025-04-14
RAG系统中的“幕后英雄”:重排器如何提升信息检索的精准度?
2024-10-27
2024-09-04
2024-07-18
2024-05-05
2024-06-20
2024-06-13
2024-07-09
2024-07-09
2024-05-19
2024-07-07