前言

在《RAG实战篇:构建一个最小可行性的Rag系统》一文中,已经介绍了Rag系统的实现框架,以及如何搭建一个最基本的Naive Rag。
前四篇文章分别探讨了索引(Indexing)、查询转换(Query Translation)、路由(Routing)和查询构建(Query Construction)环节的优化方案。
现在轮到检索召回(Retrieval)这个环节了。这篇文章将重点介绍如何优化RAG系统的召回结果,从而提升LLM大模型回答的准确度。
检索召回的过程大致是这样的:用户的问题被输入到嵌入模型中进行向量化,然后在向量数据库中搜索语义相似的知识文本或历史对话记录,最后返回结果。
但在Naive Rag中,系统会把所有检索到的块一股脑全扔给LLM生成回答,这样很容易出现中间内容丢失、噪声占比过高、上下文长度限制等问题。
接下来,结合源代码,详细拆解三种优化召回准确率的实用方案——Reranking(重排序)、Refinement(压缩)和Corrective Rag(纠正性Rag)。
具体的源代码地址可以在文末获取。
1. Rerank(重排序)
重排序,说白了就是给检索回来的结果重新排个队,把更相关、更靠谱的内容提到前面,从而提升检索质量。
重排序主要有两种类型:基于统计打分的重排序和基于深度学习的重排序。
基于统计的重排序,通常是汇总多个来源的候选结果列表,用多路召回的加权得分或倒数排名融合(RRF)算法来重新算分。这种方法计算简单,成本低效率高,广泛应用于对延迟敏感的传统检索系统,比如内部知识库检索、电商智能客服检索等。
在之前《RAG实战篇:优化查询转换的五种高级方法,让大模型真正理解用户意图》一文中提到的RAG Fusion中的reciprocal_rank_fusion,就是一种基于统计打分的重排序。再来回顾一下代码:
def reciprocal_rank_fusion(results: list[list], k=60):
""" Reciprocal_rank_fusion that takes multiple lists of ranked documents
and an optional parameter k used in the RRF formula """
# Initialize a dictionary to hold fused scores for each unique document
fused_scores = {}
# Iterate through each list of ranked documents
for docs in results:
# Iterate through each document in the list, with its rank (position in the list)
for rank, doc in enumerate(docs):
# Convert the document to a string format to use as a key (assumes documents can be serialized to JSON)
doc_str = dumps(doc)
# If the document is not yet in the fused_scores dictionary, add it with an initial score of 0
if doc_str not in fused_scores:
fused_scores[doc_str] = 0
# Retrieve the current score of the document, if any
previous_score = fused_scores[doc_str]
# Update the score of the document using the RRF formula: 1 / (rank + k)
fused_scores[doc_str] += 1 / (rank + k)
# Sort the documents based on their fused scores in descending order to get the final reranked results
reranked_results = [
(loads(doc), score)
for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]
# Return the reranked results as a list of tuples, each containing the document and its fused score
return reranked_results
另一种是基于深度学习模型的重排序,也就是常说的Cross-encoder Reranker。这类模型经过专门训练,能够更好地分析问题和文档之间的相关性。它给问题与文档之间的语义相似度打分,打分只取决于文本内容,不依赖文档在召回结果中的位置。优点是检索准确度更高,但成本也更高,响应更慢,适合对检索精度要求极高的场景,比如医疗问诊。
Cohere就是一个很优秀的开源工具,支持多种重排序策略,使用起来也很简单:
from langchain_community.llms import Cohere
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
from langchain.retrievers.document_compressors import CohereRerank
retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# Re-rank
compressor = CohereRerank()
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=retriever
)
compressed_docs = compression_retriever.get_relevant_documents(question)
2. Refinement(压缩)
压缩的思路更直接:检索到的内容块,别直接喂给大模型,先删掉无关的内容,突出重要的上下文,这样能减少整体提示长度,降低冗余信息对大模型的干扰。
LangChain里有一个基础的上下文压缩检索器——ContextualCompressionRetriever,可以轻松实现这个功能:
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import OpenAI
llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=retriever
)
compressed_docs = compression_retriever.invoke(
"What did the president say about Ketanji Jackson Brown"
)
还有一种更简单但更强大的压缩器——LLMChainFilter,它用LLM链来决定过滤掉哪些文档,返回哪些文档,不修改文档内容:
from langchain.retrievers.document_compressors import LLMChainFilter
_filter = LLMChainFilter.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=_filter, base_retriever=retriever
)
compressed_docs = compression_retriever.invoke(
"What did the president say about Ketanji Jackson Brown"
)
pretty_print_docs(compressed_docs)
3. Corrective Rag(纠错性Rag)
Corrective-RAG(CRAG)是一种更聪明的RAG策略:它引入了“自我反思/自我评分”机制。具体来说,采用轻量级的“检索评估器”为每个检索到的文档返回一个置信度分数,然后根据这个分数决定触发哪种检索操作。
评估器可以根据置信度分数把文档标记到三个桶里:正确、模糊、不正确。
- 如果所有文档的置信度都低于阈值,说明检索“不正确”,这时会触发新的知识来源(比如网络搜索)来弥补。
- 如果至少有一个文档的置信度高于阈值,说明检索“正确”,这时会对文档进行知识细化——把文档分割成“知识条”,根据相关性对每个条目打分,最相关的重新组合成内部知识。
所以,Corrective Rag的核心在于“检索评估器”的设计。下面是一个实现示例:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI
# Data model
class GradeDocuments(BaseModel):
"""Binary score for relevance check on retrieved documents."""
binary_score: str = Field(
description="Documents are relevant to the question, 'yes' or 'no'"
)
# LLM with function call
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
structured_llm_grader = llm.with_structured_output(GradeDocuments)
# Prompt
system = """You are a grader assessing relevance of a retrieved document to a user question. \n
If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \n
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question."""
grade_prompt = ChatPromptTemplate.from_messages(
[
("system", system),
("human", "Retrieved document: \n\n {document} \n\n User question: {question}"),
]
)
retrieval_grader = grade_prompt | structured_llm_grader
question = "agent memory"
docs = retriever.get_relevant_documents(question)
doc_txt = docs[1].page_content
print(retrieval_grader.invoke({"question": question, "document": doc_txt}))
当然,Corrective Rag也有它的局限性:严重依赖检索评估器的质量,而且容易受到网络搜索引入的偏见影响。所以,微调检索评估器往往是不可避免的。
总结
这篇文章详细介绍了优化Retrieval(检索召回)的三种实用方法:Rerank(重排序)、Refinement(压缩)、Corrective Rag(纠错性Rag)。
检索召回之后,下一步就是最终的内容生成了。即使用了上面这些优化方案,生成环节仍然可能遇到格式错误、内容不完整、整治不正确等问题。下一篇文章,将继续讨论如何优化生成环节,给整个RAG流程画上一个完美的句号。
