高级 RAG 06探索查询重写

高级 RAG 06探索查询重写

Barry Lv6

对齐查询与文档语义的关键技术

在检索增强生成(RAG)中,我们常遇到用户原始查询的问题,如措辞不准确或缺乏语义信息。例如,像“2020年NBA冠军是洛杉矶湖人队!告诉我什么是langchain框架?”这样的查询,如果直接搜索,可能会从大型语言模型(LLM)中得到错误或无法回答的响应。

因此,将用户查询的语义空间与文档的语义空间对齐至关重要。查询重写技术能有效解决这一问题。它在RAG中的作用如图1所示:

从位置角度看,查询重写是一种预检索方法。请注意,此图大致展示了查询重写在RAG中的位置。在下文中,我们将看到某些算法可能会改进这一过程。

查询重写是对齐查询与文档语义的关键技术。例如:

  • 假设文档嵌入(HyDE) 通过假设文档对齐查询与文档的语义空间。
  • 重写-检索-阅读(Rewrite-Retrieve-Read) 提出了一种不同于传统检索和阅读顺序的框架,专注于查询重写。
  • 后退提示(Step-Back Prompting) 允许LLM基于高级概念进行抽象推理和检索。
  • 查询转文档(Query2Doc) 使用LLM的少量提示创建伪文档,然后将其与原始查询合并以构建新查询。
  • 迭代检索生成(ITER-RETGEN) 提出了一种结合先前生成结果与前一查询的方法,随后检索相关文档并生成新结果。此过程重复多次以达到最终结果。

接下来,我们将深入探讨这些方法的细节。

假设文档嵌入 (HyDE)

论文《无需相关标签的精确零样本密集检索 》提出了一种基于假设文档嵌入 (HyDE) 的方法,主要流程如图2所示。

该流程主要分为四个步骤:

  1. 使用LLM根据查询生成**k**个假设文档。这些生成的文档可能不真实且可能包含错误,但它们应类似于相关文档。此步骤旨在通过LLM解读用户的查询。

  2. 将生成的假设文档输入编码器,映射为一个密集向量**f(dk)**。认为编码器起到过滤作用,过滤掉假设文档中的噪声。这里,**dk**表示第**k**个生成的文档,**f**表示编码器操作。

  3. 使用给定公式计算以下**k**个向量的平均值,

我们也可以将原始查询**q**视为一种可能的假设:

  1. 使用向量**v**从文档库中检索答案。如步骤3所确立,此向量包含了用户查询和期望答案模式的信息,可以提高召回率。

我对HyDE的理解如图3所示。HyDE的目标是生成假设文档,使得最终查询向量**v**尽可能接近向量空间中的实际文档。

HyDE在LlamaIndex Langchain 中都有实现。以下解释以LlamaIndex为例。

此文件 放置在**YOUR_DIR_PATH**。测试代码如下(我安装的LlamaIndex版本是0.10.12):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
from llama_index.core.query_engine import TransformQueryEngine

# 加载文档,构建VectorStoreIndex
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()
index = VectorStoreIndex.from_documents(documents)

query_str = "what did paul graham do after going to RISD"

# 未转换的查询:相同的查询字符串用于嵌入查找和摘要。
query_engine = index.as_query_engine()
response = query_engine.query(query_str)

print('-' * 100)
print("基础查询:")
print(response)

# 使用HyDE转换的查询
hyde = HyDEQueryTransform(include_original=True)
hyde_query_engine = TransformQueryEngine(query_engine, hyde)
response = hyde_query_engine.query(query_str)

print('-' * 100)
print("经过HyDEQueryTransform后:")
print(response)

首先,查看LlamaIndex中的默认HyDE提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
############################################
# HYDE
##############################################

HYDE_TMPL = (
"请写一段文字来回答问题\n"
"尽量包含尽可能多的关键细节。\n"
"\n"
"\n"
"{context_str}\n"
"\n"
"\n"
'段落:"""\n'
)

DEFAULT_HYDE_PROMPT = PromptTemplate(HYDE_TMPL, prompt_type=PromptType.SUMMARY)

HyDEQueryTransform类的代码 如下。

**def _run**函数的目的在于生成假设文档,已在**def _run**函数中添加了三个调试语句,以监控假设文档的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class HyDEQueryTransform(BaseQueryTransform):
"""假设文档嵌入 (HyDE) 查询转换。

使用LLM生成给定查询的假设答案,并使用生成的文档作为嵌入字符串。

如《无需相关标签的精确零样本密集检索》所述
(https://arxiv.org/abs/2212.10496)`
"""

def __init__(
self,
llm: Optional[LLMPredictorType] = None,
hyde_prompt: Optional[BasePromptTemplate] = None,
include_original: bool = True,
) -> None:
"""初始化HyDEQueryTransform。

参数:
llm_predictor (Optional[LLM]): 用于生成假设文档的LLM
hyde_prompt (Optional[BasePromptTemplate]): HyDE的自定义提示
include_original (bool): 是否包含原始查询字符串作为嵌入字符串之一
"""
super().__init__()

self._llm = llm or Settings.llm
self._hyde_prompt = hyde_prompt or DEFAULT_HYDE_PROMPT
self._include_original = include_original

def _get_prompts(self) -> PromptDictType:
"""获取提示。"""
return {"hyde_prompt": self._hyde_prompt}

def _update_prompts(self, prompts: PromptDictType) -> None:
"""更新提示。"""
if "hyde_prompt" in prompts:
self._hyde_prompt = prompts["hyde_prompt"]

def _run(self, query_bundle: QueryBundle, metadata: Dict) -> QueryBundle:
"""运行查询转换。"""
# TODO: 支持生成多个假设文档
query_str = query_bundle.query_str
hypothetical_doc = self._llm.predict(self._hyde_prompt, context_str=query_str)
embedding_strs = [hypothetical_doc]
if self._include_original:
embedding_strs.extend(query_bundle.embedding_strs)

# 以下三行包含添加的调试语句。
print('-' * 100)
print("假设文档:")
print(embedding_strs)

return QueryBundle(
query_str=query_str,
custom_embedding_strs=embedding_strs,
)

测试代码运行如下:

1
2
3
4
5
6
7
8
9
10
(llamaindex_010) Florian:~ Florian$ python /Users/Florian/Documents/test_hyde.py 
----------------------------------------------------------------------------------------------------
基础查询:
Paul Graham在RISD之后回到了纽约的旧生活。他变得富有并继续他的旧习惯,但有了新的机会,比如能够轻松叫到出租车和在迷人的餐厅用餐。他还开始尝试一种新的静物画技法。
----------------------------------------------------------------------------------------------------
假设文档:
["在罗德岛设计学院 (RISD) 就读后,Paul Graham 创立了 Viaweb,一个在线商店构建器,后来以 4900 万美元被雅虎收购。Viaweb 的成功之后,Graham 成为科技行业的影响力人物,于 2005 年共同创立了创业加速器 Y Combinator。Y Combinator 已成为世界上最负盛名和最成功的创业加速器之一,帮助启动了 Dropbox、Airbnb 和 Reddit 等公司。Graham 还以其关于技术、创业和创业精神的丰富写作而闻名,他的文章在科技社区中被广泛阅读和尊重。总的来说,Paul Graham 在 RISD 之后的职业生涯以创新、成功和对创业生态系统的重大影响为标志。", 'what did paul graham do after going to RISD']
----------------------------------------------------------------------------------------------------
经过HyDEQueryTransform后:
在RISD之后,Paul Graham回到了纽约的旧生活,但现在他变得富有。他继续他的旧习惯,但有了新的机会,比如能够轻松叫到出租车和在迷人的餐厅用餐。他还开始专注于他的绘画,尝试一种新的技法。此外,他开始寻找要购买的公寓,并考虑构建一个用于制作网页应用的网页应用的想法,这最终导致他创立了一家名为Aspra的新公司。

**embedding_strs**是一个包含两个元素的列表。第一个是生成的假设文档,第二个是原始查询。它们被组合成一个列表,以便进行向量计算。

在这个例子中,HyDE通过准确想象Paul Graham在RISD之后做了什么(见假设文档),显著提高了输出质量。这提高了嵌入质量和最终输出。

当然,HyDE也有一些失败案例。感兴趣的读者可以访问此网页 进行测试。

HyDE似乎是无监督的,HyDE中没有训练模型:生成模型和对比编码器都保持不变。

总之,虽然HyDE引入了一种新的查询重写方法,但它确实有一些局限性。它不依赖于查询嵌入的相似性,而是强调一个文档与另一个文档的相似性。然而,如果语言模型对主题不熟悉,它可能不会总是产生最佳结果,可能会导致错误增加。

重写-检索-阅读

这一概念源自论文“Query Rewriting for Retrieval-Augmented Large Language Models ”。该论文认为,原始查询在现实场景中可能并不总是最优的,尤其是对于大型语言模型(LLM)的检索而言。

因此,论文建议我们应首先使用LLM重写查询。随后进行检索和答案生成,而不是直接从原始查询中检索内容并生成答案,如图4(b)所示。

为了说明查询重写如何影响上下文检索和预测性能,考虑以下示例:查询“2020年NBA冠军是洛杉矶湖人队!告诉我什么是langchain框架?”通过重写得到了准确处理。

这一实现使用了Langchain ,安装所需的核心库如下:

1
2
3
4
5
pip install langchain
pip install openai
pip install langchainhub
pip install duckduckgo-search
pip install langchain_openai

环境配置与库导入:

1
2
3
4
5
6
7
8
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"

from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

构建链并执行简单查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def june_print(msg, res):
print('-' * 100)
print(msg)
print(res)

base_template = """仅基于以下上下文回答用户的问题:

<context>
{context}
</context>

问题:{question}
"""

base_prompt = ChatPromptTemplate.from_template(base_template)

model = ChatOpenAI(temperature=0)

search = DuckDuckGoSearchAPIWrapper()

def retriever(query):
return search.run(query)

chain = (
{"context": retriever, "question": RunnablePassthrough()}
| base_prompt
| model
| StrOutputParser()
)

query = "2020年NBA冠军是洛杉矶湖人队!告诉我什么是langchain框架?"

june_print(
'查询结果:',
chain.invoke(query)
)

june_print(
'检索到的上下文结果:',
retriever(query)
)

操作结果如下:

1
2
3
4
5
6
7
(langchain) Florian:~ Florian$ python /Users/Florian/Documents/test_rewrite_retrieve_read.py 
----------------------------------------------------------------------------------------------------
查询结果:
很抱歉,但提供的上下文没有提及任何关于langchain框架的信息。
----------------------------------------------------------------------------------------------------
检索到的上下文结果:
洛杉矶湖人队是2020年NBA冠军!观看他们的冠军庆祝视频!订阅NBA:https://on.nba.com/2JX5gSN 完整比赛高亮... 202384日。2020年的洛杉矶湖人队无疑是过去十年中最完整的球队之一。勒布朗·詹姆斯的第四个冠军是他职业生涯中最大的时刻之一。2020年湖人队中只有两名球员仍在队中。在NBA的悠久历史中,很少有球队能像湖人队那样吸引球迷的想象力并留下持久的影响... 詹姆斯得到28分、14个篮板和10次助攻,湖人队周日晚上以106-93击败迈阿密热火队,在六场比赛中赢得NBA总决赛。詹姆斯也被评为NBA最有价值球员... 波特兰开拓者队的明星达米安·利拉德最近谈到了2020年NBA“泡沫”季后赛,并对最终的冠军洛杉矶湖人队所面临的批评提出了有趣的看法。但也许最令人惊讶的是阿德巴约对2020年NBA总决赛的看法。热火队在六场比赛中被勒布朗·詹姆斯和洛杉矶湖人队击败。米勒问:“告诉我关于...”

结果表明,根据检索到的上下文,关于“langchain”的信息非常有限。

现在开始构建重写器以重写搜索查询。

1
2
3
4
5
6
7
8
9
10
11
12
rewrite_template = """为回答给定问题,提供一个更适合网络搜索引擎的搜索查询,查询以’**’结尾。问题:\
{x} 答案:"""
rewrite_prompt = ChatPromptTemplate.from_template(rewrite_template)

def _parse(text):
return text.strip("**")

rewriter = rewrite_prompt | ChatOpenAI(temperature=0) | StrOutputParser() | _parse
june_print(
'重写后的查询:',
rewriter.invoke({"x": query})
)

结果如下:

1
2
3
----------------------------------------------------------------------------------------------------
重写后的查询:
langchain框架是什么以及它是如何工作的?

构建**rewrite_retrieve_read_chain**并利用重写后的查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rewrite_retrieve_read_chain = (
{
"context": {"x": RunnablePassthrough()} | rewriter | retriever,
"question": RunnablePassthrough(),
}
| base_prompt
| model
| StrOutputParser()
)

june_print(
'rewrite_retrieve_read_chain的结果:',
rewrite_retrieve_read_chain.invoke(query)
)

操作结果如下:

1
2
3
----------------------------------------------------------------------------------------------------
rewrite_retrieve_read_chain的结果:
LangChain是一个Python框架,旨在帮助构建由语言模型(尤其是大型语言模型LLM)驱动的AI应用。它提供了一个通用的接口来访问不同的基础模型,一个管理提示的框架,以及一个与长期记忆、外部数据、其他LLM等交互的中心接口。它简化了与LLM交互的过程,并可用于构建各种应用,包括自然地与用户交互的聊天机器人。

至此,通过重写查询,我们成功获得了正确的答案。

回溯提示法

回溯提示法 是一种简单的提示技术,使大型语言模型(LLMs)能够抽象化,从包含特定细节的实例中提炼出高级概念和基本原理。其核心思想是将“回溯问题”定义为从原始问题衍生出的更抽象的问题。

例如,如果一个查询包含大量细节,LLM很难检索到相关事实来解决任务。如图5中的第一个例子所示,对于物理问题“如果理想气体的温度增加一倍,体积增加八倍,压力P会发生什么变化?”LLM在直接推理问题时可能会偏离理想气体定律的第一原理。

同样,问题“Estella Leopold在1954年8月至11月期间就读于哪所学校?”由于特定时间范围的限制,直接回答也颇具挑战性。

在这两种情况下,提出一个更广泛的问题可以帮助模型有效回答具体的查询。我们不必直接询问“Estela Leopold在特定时间就读于哪所学校”,而是可以询问“Estela Leopold的教育背景”。

这个更广泛的话题涵盖了原始问题,并能提供推断“Estela Leopold在特定时间就读于哪所学校”所需的所有信息。值得注意的是,这些更广泛的问题通常比原始的具体问题更容易回答。

从这种抽象中得出的推理有助于防止在图5(左)所示的“思维链”中间步骤中出现错误。

总结来说,回溯提示法涉及两个基本步骤:

  • 抽象化:首先,我们提示LLM提出一个关于高级概念或原理的广泛问题,而不是直接回应查询。然后,我们检索与该概念或原理相关的信息。
  • 推理:LLM可以根据这些关于高级概念或原理的事实推断出原始问题的答案。我们称之为抽象推理。

为了展示回溯提示法如何影响上下文检索和预测性能,以下是使用Langchain 实现的演示代码。

环境配置与库导入:

1
2
3
4
5
6
7
8
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_openai import ChatOpenAI
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper

构建链并执行原始查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def june_print(msg, res):
print('-' * 100)
print(msg)
print(res)


question = "was chatgpt around while trump was president?"

base_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.

{normal_context}

Original Question: {question}
Answer:"""

base_prompt = ChatPromptTemplate.from_template(base_prompt_template)

search = DuckDuckGoSearchAPIWrapper(max_results=4)
def retriever(query):
return search.run(query)

base_chain = (
{
# Retrieve context using the normal question (only the first 3 results)
"normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
# Pass on the question
"question": lambda x: x["question"],
}
| base_prompt
| ChatOpenAI(temperature=0)
| StrOutputParser()
)


june_print('The searched contexts of the original question:', retriever(question))
june_print('The result of base_chain:', base_chain.invoke({"question": question}) )

结果如下:

1
2
3
4
5
6
7
(langchain) Florian:~ Florian$ python /Users/Florian/Documents/test_step_back.py 
----------------------------------------------------------------------------------------------------
The searched contexts of the original question:
While impressive in many respects, ChatGPT also has some major flaws. ... [President's Name]," refused to write a poem about ex-President Trump, but wrote one about President Biden ... The company said GPT-4 recently passed a simulated law school bar exam with a score around the top 10% of test takers. By contrast, the prior version, GPT-3.5, scored around the bottom 10%. The ... These two moments show how Twitter's choices helped former President Trump. ... With ChatGPT, which launched to the public in late November, users can generate essays, stories and song lyrics ... Donald Trump is asked a question—say, whether he regrets his actions on Jan. 6and he answers with something like this: " Let me tell you, there's nobody who loves this country more than me ...
----------------------------------------------------------------------------------------------------
The result of base_chain:
Yes, ChatGPT was around while Trump was president. ChatGPT is an AI language model developed by OpenAI and was launched to the public in late November. It has the capability to generate essays, stories, and song lyrics. While it may have been used to write a poem about President Biden, it also has the potential to be used in various other contexts, including generating responses from hypothetical scenarios involving former President Trump.

显然,这个结果是错误的。

开始构建**step_back_question_chain****step_back_chain**以获得正确结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# Few Shot Examples
examples = [
{
"input": "Could the members of The Police perform lawful arrests?",
"output": "what can the members of The Police do?",
},
{
"input": "Jan Sindel’s was born in what country?",
"output": "what is Jan Sindel’s personal history?",
},
]
# We now transform these to example messages
example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{input}"),
("ai", "{output}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)

step_back_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:""",
),
# Few shot examples
few_shot_prompt,
# New question
("user", "{question}"),
]
)
step_back_question_chain = step_back_prompt | ChatOpenAI(temperature=0) | StrOutputParser()
june_print('The step-back question:', step_back_question_chain.invoke({"question": question}))
june_print('The searched contexts of the step-back question:', retriever(step_back_question_chain.invoke({"question": question})) )



response_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.

{normal_context}
{step_back_context}

Original Question: {question}
Answer:"""
response_prompt = ChatPromptTemplate.from_template(response_prompt_template)


step_back_chain = (
{
# Retrieve context using the normal question
"normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
# Retrieve context using the step-back question
"step_back_context": step_back_question_chain | retriever,
# Pass on the question
"question": lambda x: x["question"],
}
| response_prompt
| ChatOpenAI(temperature=0)
| StrOutputParser()
)

june_print('The result of step_back_chain:', step_back_chain.invoke({"question": question}) )

我们可以看到,通过将原始查询“退后”到一个更抽象的问题,并且使用抽象化和原始查询进行检索,大型语言模型(LLM)提高了其沿着正确推理路径寻找解决方案的能力。

正如Edsger W. Dijkstra所说:“抽象的目的不是为了模糊不清,而是为了创造一个新的语义层面,在其中可以绝对精确。”

Query2doc

Query2doc: 利用大型语言模型进行查询扩展 介绍了query2doc。它通过从大型语言模型中获取少量提示生成伪文档,然后将这些伪文档与原始查询结合,创建一个新的查询,如图6所示:

在密集检索中,新查询(记为**q+**)是原始查询(**q**)和伪文档(**d’**)的简单拼接,中间用**[SEP]**分隔:**q+ = concat(q, [SEP], d’).**

Query2doc认为,HyDE隐含地假设真实文档和伪文档用不同的词汇表达了相同的语义,但这对于某些查询可能不成立。

Query2doc与HyDE的另一个区别在于,Query2doc在论文中概述了训练一个有监督的密集检索器。

目前,在Langchain或LlamaIndex中,尚未发现query2doc的复现。

ITER-RETGEN

ITER-RETGEN方法 利用生成内容指导检索过程。它在一个“检索-阅读-再检索-再阅读”的流程中,迭代实施“增强生成的检索”和“增强检索的生成”。

如图7所示,对于给定的问题**q**和检索语料库**D = {d}**,其中**d**代表一个段落,ITER-RETGEN持续进行**T**次检索生成迭代。

在每次迭代**t**中,我们首先利用前一次迭代中的生成结果**yt-1**,结合**q**,检索出前k个段落。接着,我们提示LLM **M**生成输出**yt**,该输出将检索到的段落(表示为**Dyt-1||q**)和**q**纳入提示中。因此,每次迭代可以表述如下:

最终的输出**yt**将作为最终响应。

与Query2doc类似,目前在Langchain或LlamaIndex中,尚未发现相关复现。

结论

本文介绍了多种查询重写技术,其中部分技术附有代码演示。

在实际应用中,这些查询重写方法均可尝试,具体采用哪种方法或方法组合,需根据实际效果而定。

然而,无论采用何种重写方法,调用LLM都会涉及一定的性能权衡,这在实际使用中需加以考虑。

此外,还有一些方法,如查询路由、将查询分解为多个子问题等,它们不属于查询重写范畴,但属于预检索方法,这些方法未来有机会再作介绍。

  • 标题: 高级 RAG 06探索查询重写
  • 作者: Barry
  • 创建于 : 2024-03-04 15:52:50
  • 更新于 : 2024-08-31 06:59:45
  • 链接: https://wx.role.fun/2024/03/04/7a03a7ac265f49ffa77a61e306f7f269/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。