跳转至

2.RAG优化

简介

原文。通常RAG的优化策略主要分为:查询转换、路由、问题构建、索引、检索和生成六个方面进行 alt text

查询

查询重写(Multi-Query)与融合(Fusion)

重写指的是:利用大语言模型(LLM)对原始问题进行扩展、分解或抽象,生成多个语义相关但视角不同的子查询。如果只是重写为一条更高质量的问题,那叫做Rewrite。 alt text

prompt样例

你的任务是为给定的用户问题生成3 - 5个语义等价但表述差异化的查询变体,
目的是帮助用户克服基于距离的相似性搜索的一些局限性,以便从向量数据库中检索相关文档。
以下是原始问题:
<question>
{{question}}
</question>
请生成3 - 5个语义与原始问题等价,但表述不同的查询变体,用换行符分隔这些替代问题。
请在<查询变体>标签内写下你的答案。

在查询重写后,要对每个子问题在Vector Store中的查询结果进行合并,通常使用RRF,即倒序排名法,简单来说就是在多个子问题的检索结果中排名靠前的片段,总排名会靠前。

RRF(d)=i=1n1k+ri(d)

问题分解(Sub-question)

当原始问题很复杂时,存在两个导致LLM直接回答不好的问题:

  1. 向量存储中都是基础数据,可能不会和复杂的问题有较高的相似度,这时将原问题拆解为几个基础问题,可以增加匹配到需要文本的概率。
  2. 原问题较为复杂时,可能传递大量相关文档,与大量计算,LLM有上下文限制,很可能在一个上下文中无法完成问题解答。

使用问题分解时,有串行并行两种策略

  • 串行模式适用于逻辑依赖强的问题分解,确保步骤的连贯性。如“RAG都有哪些阶段?”,需要先找到都有哪些阶段,然后再询问各个阶段该做什么事情。
  • 并行模式适用于独立子任务的高效处理,提升响应速度。如“如何规划北京到上海的 5 天旅游行程?”,需要分解成交通、住宿、景点三个子问题,分别完成。

prompt样例

你的任务是针对输入的问题生成多个相关的子问题或子查询,
将输入问题分解成一组可以独立回答的子问题或子任务。
以下是输入的问题:
<question>
{{question}}
</question>
请生成3 - 5个与该问题相关的搜索查询,并使用换行符进行分割。
生成的子问题/子查询应具有明确的主题和可独立回答的特点。
请在<子问题>标签内写下生成的子问题/子查询。

问题回退(Step-Back Prompting)

与问题分解相反,当原始问题较为具体时,需要将问题抽象到一个更大的范围,从检索文档中寻找答案。 alt text

例如:原始问题:1954 年 8 月至 11 月期间,埃斯特拉・利奥波德就读于哪所学校?

  • 1948 年,威斯康星大学麦迪逊分校,植物学学士;
  • 1950 年,加州大学伯克利分校,植物学硕士;
  • 1955 年,耶鲁大学,植物学博士。

直接回答的答案:1954 年 8 月至 11 月期间,埃斯特拉・利奥波德就读于威斯康星大学麦迪逊分校。

回溯问题:埃斯特拉・利奥波德的教育经历是怎样的?

回溯答案:最终答案:她 1955 年就读于耶鲁大学植物学博士项目。因此,1954 年 8 月至 11 月期间,埃斯特拉・利奥波德最可能就读于耶鲁大学

prompt样例

你的任务是分析给定的问题,忽略具体细节,提炼出问题背后涉及的核心概念、原理、知识范畴或通用逻辑。
这是需要分析的问题:
<question>
{{question}}
</question>
分析时,需要提取出问题的本质,将其转化为对某一概念、原理、知识范畴或通用逻辑的探讨。
例如,如果问题是“水加热到 100℃为什么会沸腾?”,
那么分析后的新问题应该是“分析液体沸腾现象的物理原理(如相变、沸点与气压关系、能量传递机制等)”。
请在<回答>标签内写下你的分析结果。

混合检索策略

在查询检索中,常见的两种检索方式分别是稀疏检索器和密集检索器:

  • 稀疏检索器:基于关键词匹配,利用词频(TF)和逆文档频率(IDF)计算文档与查询的相关性。

    • 优点:非常高效,无需训练、对明确关键词匹配效果好;
    • 缺点:无法捕捉语义(如同义词、上下文相关性)
  • 密集检索器:使用深度学习模型生成密集向量表示,通过向量相似度(如余弦相似度)衡量相关性。

    • 优点:捕捉语义信息,解决词汇不匹配问题;
    • 缺点:需大量训练数据、计算成本高,对生僻词敏感;

将以上两种甚至多种检索方法并行,然后将检索结果通过WRRF算法(对检索器加权的RRF)。

HyDE

HyDE 使用 LLM 生成问题的假设文档或答案,再将假设的文档或答案进行编码,编码为向量,在语料向量空间进行检索,查找最相似的真实语料。这种方式能够使得查询包含更多的潜在含义和语义信息,使得 HyDE 在原始问题非常简短或表述不够明确时效果非常好。

可以有效避免问题的无效部分影响检索。如“今天回家的路上看到了美丽的风景,非常开心!想学习 python 该怎么办?”直接检索的话,会和风景相关文本有较强相关性,但是用LLM先生成假设回答,LLM可以聪明到无视前面的干扰部分。

prompt样例

你的任务是实现HyDE零样本检索策略,根据用户输入的查询生成假设文档。
生成的内容要反映相关性模式,同时允许存在虚构细节。
以下是用户输入的查询:
<查询>
{{QUERY}}
</查询>
在生成假设文档时,请遵循以下要点:
1. 仔细理解查询的核心内容和意图。
2. 围绕查询构建文档,让文档与查询具有明显的相关性。
3. 可以适当添加一些虚构的细节,但不能偏离查询的主题。
4. 输出的文档应具有一定的逻辑性和连贯性。
请在<生成文档>标签内写下你生成的假设文档。
<生成文档>
[在此生成假设文档]
</生成文档>

路由

数据源路由

根据不同问题,使用LLM确定在哪个向量存储里检索,没什么可说的。

prompt样例

你是一位擅长将用户问题路由到适当数据源的专家。
你的任务是根据问题涉及的编程语言,将问题路由到相关的数据源。
首先,请仔细阅读以下数据源信息:
<data_sources>
{{DATA_SOURCES}}
</data_sources>
现在,请仔细阅读以下用户问题:
<question>
{{QUESTION}}
</question>
为了将问题路由到合适的数据源,请按照以下步骤操作:
1. 仔细分析问题,识别其中涉及的编程语言。
2. 查看数据源信息,找出与该编程语言相关的数据源。
3. 如果问题涉及多种编程语言,找出与所有涉及语言都相关或与主要语言相关的数据源。
4. 如果没有合适的数据源,指出“没有合适的数据源”。

请在<回答>标签内写下路由结果。
<回答>
[在此输出路由结果]
</回答>

prompt路由

根据不同场景,切换不同路由,效果会更好。我们可以把prompt段落和原始问题嵌入到统一向量空间,再用语义相似度来寻找最合适的prompt模板。

alt text

问题构建

自查询

将原始问题中的隐式携带的筛选条件提取出来。如“2023年的新闻”在向量存储的高维空间里,其实和“2022年的……”很相近,但实际上要加上year=2023的条件。

对于不同的数据库,都可以进行查询构建。

  • 关系型数据库:使用LLM将原始问题中的筛选信息转换为SQL语句
  • 图数据库:使用 LLM 将自然语言转换成图查询语句
  • 向量存储:还记得入门中提到,Document对象有metadata属性,LLM将原始问题转化为针对metadata的筛选,或者向量检索器。

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\\"))"
}}
``

索引

多维度索引

建立向量存储时,如果能从多个维度进行嵌入,记录文档不同方面的信息,会增加检索的准确率。一般来说,维度有以下几个:

  • 把文档分成更小的块:将一个文档块继续拆分成更小的块,通过检索小的块,定位父文档。
  • 存储摘要信息:将一个文档通过LLM生成摘要信息,将其和原文档一起存到向量数据库中,查询时只返回原文档。
  • 假设性问题:使用 LLM 为每个文档块生成适合回答的假设性问题,将其和原文档一起嵌入或者代替,查询时返回原文档。

alt text

分层索引

使用RAPTOR进行,当文档很多很长时,可以建立一个文档树,其中叶节点是同类文档,父节点是这些文档的总结。我们把每个文档先嵌入到向量空间,再进行降维(到比如3维),这样就可以对文档进行聚类,对每个聚类的所有文本进行LLM总结,总结成一个文档,作为一个聚类的父结点,然后再对这些总结性文档进行嵌入/降维/聚类

alt text

检索时,可以进行树遍历查找,即从根开始,检查问题和所有文档的cosine相似性,然后在对应的节点的子节点中递归寻找。还可以进行折叠树查找,就是把所有文档平铺为一层,检索最相似的文本。

切块优化

字符分割器就是根据指定分割符,将文档切割成多个文档块。它通常会支持控制文档块的大小,避免超出大模型上下文限制。除此之外,还可以控制块与块之间重叠的内容大小,尽可能保留上下文信息。

  • 递归字符分割器:对大文档块会使用更多的分隔符使其变小,对小文档块进行合并使其保留更多的信息。
  • 语义文档分割器:文档分割器都是使用特定字符对文本进行拆分,没有考虑句子之间的语义相似性。语义文档分割器计算相邻句子之间的向量距离,按阈值对文本进行合并或者分割。

检索

ReRank 重排序

重排序是使用频率最高,性价比最高,通常与混合检索一起搭配使用,也是目前主流的优化策略。

重排序的核心思想见字知其意,即对检索到的文档 调整顺序,除此之外,重排序 一般还会增加剔除无关/多余数据的步骤,其中RRF就是重排序中最基础的一种

CRAG 纠正性RAG

在 CRAG 中引入了一个轻量级的检索评估器来评估检索到的文档的质量,并根据评估结果触发不同的知识检索动作,以确保生成结果的准确性。整体流程如下图所示: alt text

prompt样例

  • 评估节点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

Self-RAG 全称为自我反思 RAG,即对原始查询、检索的内容、生成的内容进行自我反思,根据反思的结果执行不同的操作,例如:直接输出答案、重新检索、剔除不相关的内容、检测生成内容是否存在幻觉、检测生成内容是否有帮助等。

可以把 Self-RAG看成是一个拥有自我反思能力的智能体,这个智能体主要用来依据相关知识库回复用户问题,自我迭代,直到输出满意的结果。SELF-RAG训练了一个任意的LLM, 使其能够在给定任务输入时反思自己的生成过程,同时生成任务输出和临时的特殊标记(称为反思标记)。这些反思标记分为检索和评论标记,分别表示了是否需要检索以及生成的质量。流程如下图所示:

alt text

检索标记用于Retrieval as Needed(按需检索),如果只是LLM本身就能回答,不需要检索。反思标记产生于生成阶段,有多种不同角度的反思,哪种没过就重新哪个步骤。最后将生成阶段多次改动的答案陈列,由评估模型挑选最好的一版。