直观示例、原则、代码解析及关于CRAG的洞察
本文从一个常见场景开始:参加开卷考试。通常,我们有三种策略:
- 方法1:对熟悉的话题快速作答。对不熟悉的话题,查阅参考书。迅速定位相关章节,在心中整理和总结,然后在试卷上写下答案。
- 方法2:对每个话题,查阅书籍。找出相关章节,在心中总结,然后在试卷上写出回答。
- 方法3:对每个话题,查阅书籍并找出相关章节。在形成观点前,将收集的信息分为三类:
**正确**、**错误**和**模糊**。分别处理每类信息。然后,基于处理后的信息,在心中编排和总结。在试卷上写出回答。
方法1涉及自我RAG ,而方法2是经典RAG 过程。
最后,方法3,即校正检索增强生成(CRAG) ,是本文介绍的内容。
CRAG 的动机
图1 展示了大多数传统 RAG 方法并未考虑文档与问题的相关性,而是简单地合并检索到的文档。这可能会引入无关信息,阻碍模型获取准确知识,甚至可能导致模型产生幻觉问题。
此外,大多数传统 RAG 方法将检索到的整个文档作为输入。然而,这些检索到的文档中往往有很大一部分文本对于生成过程并不必要,不应同等程度地参与 RAG 过程。
CRAG 的核心理念
CRAG 设计了一个轻量级的检索评估器,用于评估针对特定查询检索到的文档的整体质量。同时,它还利用网络搜索作为辅助工具来提升检索结果。
CRAG 具有即插即用的特性,能够与基于 RAG 的各种方法无缝集成。整体架构如图 2 所示。

如图 2 所示,CRAG 通过引入检索评估器来评估检索到的文档与查询之间的关系,从而增强了传统的 RAG。
存在三种可能的判断结果:
- 如果结果为
**正确**,这意味着检索到的文档包含了查询所需的必要内容,此时将采用知识精炼算法重写检索到的文档。
- 如果检索到的文档为
**错误**,这表示查询与检索到的文档无关。因此,我们无法将该文档发送给 LLM。在 CRAG 中,使用网络搜索引擎来检索外部知识。
- 对于
**模糊** 情况,这意味着检索到的文档可能接近但不足以提供答案。在这种情况下,需要通过网络搜索获取更多信息。因此,知识精炼算法和搜索引擎都会被采用。
最后,处理后的信息会被传递给 LLM 以生成响应。图 3 提供了这一过程的正式描述。

需要注意的是,网络搜索并不直接使用用户的输入查询进行搜索。相反,它会构建一个提示,并以少量样本的方式向 GPT-3.5 Turbo 展示,以获取搜索查询。
在对该方法有了大致了解后,让我们分别讨论 CRAG 的两个关键组件:检索评估器和知识精炼算法。
检索评估器
如图4所示,检索评估器对后续流程的结果影响显著,是决定系统整体性能的关键因素。

CRAG采用轻量级的T5-large模型 作为检索评估器并进行微调。值得一提的是,在大型语言模型时代,T5-large也被视为轻量级模型。
对于每个查询,通常会检索出十份文档。随后,将查询与每份文档分别拼接作为输入,预测其相关性。在微调过程中,对正样本赋予标签1,对负样本赋予标签-1。在推理阶段,评估器为每份文档分配一个从-1到1的相关性得分。
这些得分将根据阈值被划分为三个等级。显然,这一划分需要两个阈值。在CRAG中,阈值设置可能因实验数据而异:
触发三种行动之一的两个置信度阈值是根据经验设定的。具体而言,在PopQA中设定为(0.59, -0.99),在PubQA和ArcChallenge中为(0.5, -0.91),而在Biography中为(0.95, -0.91)。
知识精炼算法
对于检索到的相关文档,CRAG设计了一种先分解后重组的知识提取方法,以进一步提取最关键的知识陈述,如图4所示。
首先,应用启发式规则将每个文档分解为细粒度的知识条带,旨在获得细粒度的结果。如果检索到的文档仅包含一两句话,则视为独立单元。否则,文档将被分割成更小的单元,通常由几句话组成,具体取决于总长度。每个单元预期包含一条独立的信息。
接下来,使用检索评估器计算每个知识条带的相关性得分。过滤掉相关性得分低的条带。然后,将剩余的相关知识条带重新组合,以形成内部知识。
代码解释
CRAG 是 开源项目 ,Langchain 和 LlamaIndex 都提供了各自的实现。我们将以 LlamaIndex 的实现 作为参考进行解释。
环境配置
1 2 3 4 5 6 7
| (base) Florian:~ Florian$ conda create -n crag python=3.11
(base) Florian:~ Florian$ conda activate crag
(crag) Florian:~ Florian$ pip install llama-index llama-index-tools-tavily-research
(crag) Florian:~ Florian$ mkdir "YOUR_DOWNLOAD_DIR"
|
安装完成后,LlamaIndex 和 Tavily 的对应版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| (crag) Florian:~ Florian$ pip list | grep llama llama-index 0.10.29 llama-index-agent-openai 0.2.2 llama-index-cli 0.1.11 llama-index-core 0.10.29 llama-index-embeddings-openai 0.1.7 llama-index-indices-managed-llama-cloud 0.1.5 llama-index-legacy 0.9.48 llama-index-llms-openai 0.1.15 llama-index-multi-modal-llms-openai 0.1.5 llama-index-packs-corrective-rag 0.1.1 llama-index-program-openai 0.1.5 llama-index-question-gen-openai 0.1.3 llama-index-readers-file 0.1.19 llama-index-readers-llama-parse 0.1.4 llama-index-tools-tavily-research 0.1.3 llama-parse 0.4.1 llamaindex-py-client 0.1.18
(crag) Florian:~ Florian$ pip list | grep tavily llama-index-tools-tavily-research 0.1.3
|
测试代码
测试代码如下所示。首次执行需要下载 CorrectiveRAGPack。
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
| import os os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
from llama_index.core import Document
from llama_index.core.llama_pack import download_llama_pack CorrectiveRAGPack = download_llama_pack( "CorrectiveRAGPack", "YOUR_DOWNLOAD_DIR" )
documents = [ Document( text="一群企鹅在陆地上被称为'摇摆队',它们在南极冰面上蹒跚前行,它们像燕尾服一样的羽毛在雪地上格外显眼。" ), Document( text="帝企鹅是所有企鹅种类中最高的,它们可以潜入比任何鸟类都深的地方,达到超过500米的深度。" ), Document( text="企鹅的黑白颜色是一种名为反荫蔽的伪装;从上方看,它们的黑色背部与海洋深处融为一体,而从下方看,它们的白色腹部与明亮的表面相匹配。" ), Document( text="尽管企鹅直立站立,但它们是不能飞行的鸟类;它们的翅膀已经进化成鳍状肢,使它们成为游泳高手。" ), Document( text="速度最快的种类,巴布亚企鹅,可以以每小时36公里的速度游泳,利用它们的鳍状肢和流线型身体在水中穿梭。" ), Document( text="企鹅是群居鸟类;许多种类形成大型繁殖群落,数量可达数万只。" ), Document( text="有趣的是,企鹅有出色的听力,并依靠独特的叫声在嘈杂的群落中识别它们的配偶和幼崽。" ), Document( text="最小的企鹅种类,小蓝企鹅,身高仅约40厘米,分布在澳大利亚和纽西兰的沿海地区。" ), Document( text="在繁殖季节,雄性帝企鹅会在严酷的南极冬季忍受数月,禁食并孵化它们的蛋,而雌性则在海上捕食。" ), Document( text="企鹅食用各种海鲜;它们的饮食主要由鱼、鱿鱼和磷虾组成,这些是它们在潜水探险中捕获的。" ), ]
from llama_index.packs.corrective_rag import CorrectiveRAGPack corrective_rag = CorrectiveRAGPack(documents, "YOUR_TAVILYAI_API_KEY")
query = "最小的企鹅有多高?" print('-' * 100) print("查询 " + query + " 的响应是:") response = corrective_rag.run(query, similarity_top_k=2) print(response)
|
其中 **YOUR_TAVILYAI_API_KEY** 可以通过此网站 申请。
测试代码产生了以下结果(大部分调试信息已被移除):
1 2 3 4 5
| (crag) Florian:~ Florian$ python /Users/Florian/Documents/crag.py ---------------------------------------------------------------------------------------------------- 查询 最小的企鹅有多高? 的响应是: ---------------------------------------------------------------------------------------------------- 最小的企鹅大约有40厘米(16英寸)高。
|
理解测试代码的关键在于 **corrective_rag.run()** 的实现,让我们深入探讨。
CorrectiveRAGPack 类的构造函数
首先,我们来看构造函数,其源代码 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class CorrectiveRAGPack(BaseLlamaPack): def __init__(self, documents: List[Document], tavily_ai_apikey: str) -> None: """Init params.""" llm = OpenAI(model="gpt-4") self.relevancy_pipeline = QueryPipeline( chain=[DEFAULT_RELEVANCY_PROMPT_TEMPLATE, llm] ) self.transform_query_pipeline = QueryPipeline( chain=[DEFAULT_TRANSFORM_QUERY_TEMPLATE, llm] )
self.llm = llm self.index = VectorStoreIndex.from_documents(documents) self.tavily_tool = TavilyToolSpec(api_key=tavily_ai_apikey)
|
请注意,默认设置为 **gpt-4**。如果您没有权限使用 **gpt-4**,可以手动切换至 **gpt-3.5-turbo**。
class CorrectiveRAGPack:: run()
函数 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
| class CorrectiveRAGPack(BaseLlamaPack): ... ... def run(self, query_str: str, **kwargs: Any) -> Any: """运行处理流程。""" retrieved_nodes = self.retrieve_nodes(query_str, **kwargs)
relevancy_results = self.evaluate_relevancy(retrieved_nodes, query_str) relevant_text = self.extract_relevant_texts(retrieved_nodes, relevancy_results)
search_text = ""
if "no" in relevancy_results: transformed_query_str = self.transform_query_pipeline.run( query_str=query_str ).message.content search_text = self.search_with_transformed_query(transformed_query_str)
if search_text: return self.get_result(relevant_text, search_text, query_str) else: return self.get_result(relevant_text, "", query_str)
|
上述代码与标准 CRAG 流程有三个主要区别:
- 没有对
**ambiguous**(模糊)文档的判断或处理。
- 不使用训练好的 T5-large 模型,而是利用 LLM 来评估检索到的信息。
- 跳过了知识精炼的过程。
尽管存在这些差异,LlamaIndex 提供了一种替代的思考方式(langchain 也采用了这种方式)。
使用LLM评估检索信息
代码链接 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class CorrectiveRAGPack(BaseLlamaPack): ... ... def evaluate_relevancy( self, retrieved_nodes: List[Document], query_str: str ) -> List[str]: """Evaluate relevancy of retrieved documents with the query.""" relevancy_results = [] for node in retrieved_nodes: relevancy = self.relevancy_pipeline.run( context_str=node.text, query_str=query_str ) relevancy_results.append(relevancy.message.content.lower().strip()) return relevancy_results
|
调用LLM的提示模板 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| DEFAULT_RELEVANCY_PROMPT_TEMPLATE = PromptTemplate( template="""作为评分者,你的任务是评估一个文档对于用户问题的相关性。
检索到的文档: ------------------- {context_str}
用户问题: -------------- {query_str}
评估标准: - 考虑文档是否包含与用户问题相关的关键词或主题。 - 评估不应过于严格;主要目标是识别并过滤掉明显不相关的检索结果。
决策: - 分配一个二进制分数以指示文档的相关性。 - 如果文档与问题相关,使用'yes';如果不相关,使用'no'。
请在下方提供你的二进制分数('yes'或'no'),以指示文档与用户问题的相关性。""" )
|
CRAG论文显示,ChatGPT在识别检索相关性方面的表现不如T5-Large。
此外,在实际项目中,我们当然可以使用原始的知识精炼算法。相关代码可以在这里 找到。
重写搜索查询
如前所述,网络搜索并不直接使用用户的输入查询。相反,它会构建一个提示,并采用少样本方法将该提示呈现给 GPT-3.5 Turbo 以获取搜索查询。该提示 如下:
1 2 3 4 5 6 7 8 9 10
| DEFAULT_TRANSFORM_QUERY_TEMPLATE = PromptTemplate( template="""你的任务是优化查询,确保其高效获取相关搜索结果。\n 分析给定的输入,把握核心语义意图或含义。\n 原始查询: \n ------- \n {query_str} \n ------- \n 你的目标是重述或增强此查询,以提升其搜索性能。确保修订后的查询简洁并直接符合预期的搜索目标。\n 仅回复优化后的查询:""" )
|
我对CRAG的见解与思考
CRAG与self-RAG的区别
- 从流程角度看,self-RAG能利用LLM直接提供回复,无需检索步骤,而CRAG则必须在检索后加入评估层。
- 从结构角度分析,self-RAG比CRAG更为复杂,它需要更精细的训练流程,并在生成阶段进行多次标签生成与评估,这无疑增加了推理成本。因此,CRAG相比self-RAG更为轻量级。
- 就性能而言,如图5所示,CRAG在多数情况下通常优于self-RAG。

检索评估器的改进
检索评估器可视为一种评分分类模型。该模型用于判断查询与文档的相关性,类似于RAG中的重排序模型 。
通过整合更多与实际场景相符的特征,此类相关性判断模型可以得到改进。例如,科学论文问答RAG包含许多专业术语,而旅游领域的RAG则倾向于有更多口语化的用户查询。
通过在检索评估器的训练数据中加入场景特征,它能更好地评估检索到的文档的相关性。其他特征,如用户意图和编辑距离,也可以被整合进来,如图6所示:

此外,考虑到T5-Large获得的结果,轻量级模型似乎也能取得良好效果。这为CRAG在小规模团队或公司中的应用带来了希望。
检索评估器的评分与阈值
如前所述,不同类型的数据阈值有所不同。此外,我们发现**模糊**和**不正确**的阈值基本在-0.9左右,表明大部分检索到的知识与查询相关。完全摒弃这些检索到的知识而仅依赖网络搜索可能并不明智。
在实际应用中,我们需要根据实际问题和需求进行调整。
结论
本文从一个直观的例子出发,概述了CRAG的基本流程,并辅以代码解释,同时包含了我的见解和思考。
总的来说,CRAG作为一个即插即用的插件,能显著提升RAG的性能。它提供了一种轻量级的解决方案来改进RAG。
如果你对RAG技术感兴趣,欢迎查阅我的其他文章。
最后,如有任何错误或遗漏,或你有任何疑问,请随时在评论区讨论。