跳至主要内容

通过检索增强生成(RAG)与重排序提升 LLM 准确性

借助高效的信息检索机制,挖掘 LLM 的优势。实现重排序方法,并将其融入您自己的 LLM 流水线。
· 11分钟 阅读

使用 AI 探索

想象一下,一个大型语言模型(LLM)给出的答案不仅相关,还能被精细筛选、设定优先级并打磨到恰好契合您的需求。尽管 LLM 颠覆了人工智能领域,它们也并非完美无缺。幻觉与数据过时等问题会影响输出的准确性与相关性。这正是检索增强生成(RAG)与重排序发挥作用的地方:通过与动态、最新的信息检索流程集成,为 LLM 增效。想知道这道“配方”的步骤吗?继续往下看。

为何用 RAG 增强 LLM?

LLM 颠覆了 AI 的可能性边界,并在几乎任何领域中,凭借对多种自然语言理解与生成任务的建模,成为满足多样化 NLP 需求的首选工具,如下所示。

LLM 可解决的语言任务分类

LLM 可解决的语言任务分类 | Iván Palomares

尽管功能强大,LLM 在某些场景下仍有局限。根据具体用例以及其从庞大训练数据中学到的知识,LLM 有时无法生成连贯、相关或符合语境的文本。有时,当缺乏与用户问题相关且真实的数据时,它们甚至会编造错误或荒诞的信息并将其当作事实,这种现象被称为幻觉。

例如,考虑这样一个问题:“流感的常见症状有哪些?”

一个标准的 LLM 可能会基于一般常识给出回答,列出常见症状,如发烧、咳嗽与全身酸痛。

然而,除非它在非常细分的流感病毒数据上经过训练,否则它可能无法考虑症状严重程度的差异,或区分不同流感毒株,从而不论用户的具体情况与需求为何,给出较为通用、甚至略显“自动化”的答复。

更进一步,如果该模型仅训练至 2023 年 12 月的临床流感数据,而 2024 年 1 月出现并迅速传播了一种新毒株,那么由于缺乏该领域的最新知识,单独的 LLM 将无法给出准确回答。

这种“数据过时”问题被称为知识截断。

在某些情况下,上述问题的解决方案可能是频繁地基于最新信息对 LLM 进行再训练与微调。但这一定是最佳做法吗?

众所周知,训练 LLM(大型语言模型)既困难又计算开销巨大。它们需要数百万到数十亿的文本实例,并且微调往往还需要成千上万条特定领域文本(见下图)。

大多数 LLM,即便是面向特定领域的,通常也会被微调到覆盖较宽泛的范畴,如医疗健康。因此,不断引入新、细粒度的数据以囊括领域内的所有语境细节,未必是最高效的解决方案。

LLM 的预训练与微调

LLM 的预训练与微调 | Iván Palomares

这正是RAG(检索增强生成)大显身手的地方!

RAG 是一种信息检索流程,用于优化 LLM 生成的输出。LLM 依赖其从训练数据中获得的知识来生成回答。

与此同时,RAG 指向一个外部知识库。

通过将两者结合,RAG 能够从上述知识库检索信息,以提升 LLM“原始”输出的质量、用户相关性、连贯性与真实性。

因此,无需为适应层出不穷的新情境而持续再训练 LLM 的需求,在很大程度上被消除了。

下图以简化方式描述了RAG 系统的一般工作流程:

  • 查询:LLM 接收用户查询或提示。
  • 检索:在运行时,LLM 访问对文档语料进行索引的检索组件,并找出与用户查询匹配度最高的前 K 个结果。
  • 生成:LLM 结合两部分信息——原始用户输入与检索到的文档——来生成输出,照常使用最大似然原则逐个生成输出序列中的下一个 token。

回到流感的例子,我们会发现借助 RAG,模型可以从医学数据库或最新文章中检索到最新且相关的信息,从而为患者或医生的提问生成更细致、准确的答案。

它还能纳入关于当前流感毒株、地区性症状差异或新出现的模式等见解,使其回答与用户的查询更加贴切且更为连贯。

什么是重排序?

重排序是一种信息检索流程,会对初始检索结果进行重新排序,以更贴合用户的查询、需求与语境,从而提升整体输出质量。其工作方式如下:

  1. 首先进行初次文档检索,由检索器获取 K 个相关文档。在此阶段通常采用 TF-IDF、向量空间模型等常见方法。
  2. 随后,引入一种比初始检索器更复杂或更贴合领域的机制,称为排序器(ranker)。排序器会基于额外标准(如学习到的用户偏好、语境与需求,或更复杂的算法)对检索结果进行再评估。该二次排序旨在提升面向特定用户所展示结果的相关性。

下图展示了重排序流程:

重排序流程

重排序流程 | Iván Palomares

这里澄清一个重要概念:重排序会基于多种标准(如用户偏好)对检索到的文档重新排序。然而,重排序不同于推荐引擎(例如电商网站上推荐购买的相关商品)。

重排序用于搜索类用例,用户会实时提供输入查询。

相对地,推荐引擎会基于用户的历史互动与偏好,主动生成个性化建议。

让我们回到流感的示例。

设想一位医务人员搜索“治疗流感症状的最佳方法”。初始检索系统可能返回一组文档,包括流感通识、治疗指南与研究论文。

而重排序模型可能会利用额外的患者特定数据与上下文信息,对这些文档重新排序,将最相关且最新的治疗方案、护理建议以及直接聚焦于流感症状及其处置的同行评议研究置于更靠前的位置,从而优先呈现“直击要点”的结果。

总之,重排序会基于额外的相关性标准,重组检索文档列表,以便将对特定用户最相关的内容优先展示。

为何在搭配 RAG 的 LLM 中使用重排序?

在采用检索增强生成(RAG)的大型语言模型(LLM)中,重排序尤为有用。RAG 将 LLM 与外部文档检索相结合,以提供信息更充分、更加准确的回答。

在基于查询完成初次文档检索后,重排序过程可以对结果进一步筛优,确保 LLM 使用的是最相关、最高质量的信息。

这一过程可增强 LLM 的整体表现,提升回答的准确性与相关性,尤其适用于需要精确信息的专业领域。

重排序器的类型

实现重排序并没有单一配方。已经成熟的多种方法包括:

  • 多向量重排序器:为文档与用户查询分配多个向量表示,利用向量相似度进行重排序。
  • 学习排序(LTR):在推荐算法中广为应用,基于机器学习原理与训练数据来学习预测最优结果排序。给定一个文档列表作为输入,输出是对其“最佳”排序的预测。
  • 基于 BERT 的重排序器:BERT 是一种基于 Transformer 的模型,擅长文本分类等语言理解任务。在重排序的语境下,可利用 BERT 模型理解一组检索文档的语义细微差异,并细化其排序。
  • 强化学习重排序器:基于持续的用户交互数据,围绕长期回报函数(如用户满意度)进行优化,通过试错逐步学习,类似于强化学习算法。
  • 混合重排序器:结合多种重排序策略,例如将 LTR 与多种机器学习或深度学习模型融合。

构建带重排序的 RAG 流水线

既然我们已经了解将 RAG 融入 LLM 的益处,以及用于知识检索的重排序机制,现在就把这些要素整合起来,看看它们如何协同工作。

本示例使用 Langchain 库(社区版)来构建一个简单的带重排序的 RAG 流水线。关于使用 Langchain 开发 LLM 应用的更多内容,请参阅《开发基于 Langchain 的 LLM 应用》课程和这篇如何用 Langchain 构建 LLM 应用的教程。

代码实现在 Google Colab 笔记本中完成。

版本 1——不含重排序

pip install -U langchain-community

首先,在笔记本中安装 langchain-community。

接着,导入所需的包、类与函数。

import os
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.text_splitter import CharacterTextSplitter
from langchain.schema import Document
from langchain.chains.qa_with_sources import load_qa_with_sources_chain
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

下面定义的函数用于从本地目录中加载文档(.txt 文件),您的代码笔记本应能读取该目录。这些文档将作为检索与重排序的对象,以增强 LLM 输出:

# Function to load documents from a directory
def load_documents_from_directory(directory_path):
    documents = []
    for filename in os.listdir(directory_path):
        if filename.endswith(".txt"):
            with open(os.path.join(directory_path, filename), 'r') as file:
                documents.append(file.read())
    return documents

# Load documents from the specified directory
directory_path = "./sample_data"
documents = load_documents_from_directory(directory_path)

为优化文本文档的向量嵌入表现,我们将其大小限制为 1000,并把更长的文档拆分为更小的片段。CharacterTextSplitter 类可完成此任务。

chunk_overlap 参数设为 0,以避免不同片段间的文本重叠。

# Split documents into chunks for better embedding performance
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

我们将使用OpenAI 嵌入来构建嵌入向量库。请注意,取决于您的运行环境,可能需要在 OPENAI_API_KEY 中设置 OpenAI 的验证密钥。

# Initialize OpenAI embeddings
embeddings = OpenAIEmbeddings()

# Build FAISS vector store upong document chunk embeddings
vector_store = FAISS.from_documents(docs, embeddings)

# Load and initialize OpenAI LLM
llm = OpenAI(model="text-davinci-003", temperature=0.7)

FAISS 会基于嵌入创建一个向量库。我们需要向其 from_documents() 函数传入两个参数:文档片段,以及通过 OpenAIEmbeddings 初始化的嵌入实例。

随后,主角登场:LLM!我们使用 OpenAI 加载 text-davinci-003 模型,并将温度设置为 0.7,以允许一定程度的创意生成。

再走几步:

# Define the prompt template for the LLM
prompt_template = PromptTemplate(template="Answer the question based on the context: {context}\n\nQuestion: {question}\nAnswer:")

# Define the RAG chain with reranking using a QA chain
qa_chain = load_qa_with_sources_chain(llm, prompt_template=prompt_template, retriever=vector_store.as_retriever())

# Example question
question = "What are the common symptoms of the flu and how can it be treated?"

# Generate an answer using the QA chain
response = qa_chain(question=question)

print(response)

上述代码先定义了问答提示模板,然后调用 load_qa_with_sources_chain() 来创建问答流水线(在 Langchain 术语中称为“链”)。其 retriever 参数使 LLM 流水线具备检索组件,从而实现 RAG。

最后我们提出一个问题,并运行已定义的 qa_chain。大功告成!

输出如下(取决于您读取的文档):

“流感的常见症状包括发烧、咳嗽、喉咙痛、流鼻涕或鼻塞、全身酸痛、头痛、寒战和疲劳。有些人(尤其是儿童)可能会出现呕吐和腹泻。可使用抗病毒药物治疗流感,这能减轻病情并缩短病程。抗病毒药物还可能预防诸如肺炎等严重并发症。对于病情严重或并发症高风险人群,建议及时治疗。”

版本 2——包含重排序

上一个示例不错,但尚未加入重排序阶段。将代码修改以纳入重排序相对直接。

导入列表保持不变,因为在上一版中我们已经导入了重排序阶段所需的一切——尽管其中一些尚未使用:

# Previous code here
#...

# Define the prompt template for the LLM
prompt_template = PromptTemplate(template="Answer the question based on the context: {context}\n\nQuestion: {question}\nAnswer:")

# Reranking function
def rerank_documents(question, retrieved_docs, top_n=5):
    question_embedding = embeddings.embed_text(question)
    doc_embeddings = [doc.embedding for doc in retrieved_docs]
    similarities = cosine_similarity([question_embedding], doc_embeddings)[0]
    
    ranked_indices = np.argsort(similarities)[::-1]  # Sort by descending similarity
    ranked_docs = [retrieved_docs[i] for i in ranked_indices[:top_n]]
    return ranked_docs

可以看到,我们首先定义了 rerank_documents 函数,利用嵌入上的余弦相似度来计算问题嵌入与文档嵌入之间的相似性,并返回排名靠前的 top-n 文档。

# Custom QA chain with reranking
class CustomQAWithReranking:
    def __init__(self, llm, retriever, prompt_template, top_n=5):
        self.llm = llm
        self.retriever = retriever
        self.prompt_template = prompt_template
        self.top_n = top_n

    def __call__(self, question):
        retrieved_docs = self.retriever.retrieve_documents(question)
        ranked_docs = rerank_documents(question, retrieved_docs, self.top_n)
        context = "\n".join([doc.page_content for doc in ranked_docs])
        prompt = self.prompt_template.format(context=context, question=question)
        return self.llm(prompt)

CustomQAWithReranking 类在 LLM 流水线中整合了检索与重排序步骤,将通过调用该类中的 call () 函数来触发。

# Define the custom QA chain with reranking
qa_chain = CustomQAWithReranking(llm, vector_store.as_retriever(), prompt_template)

# Example question
question = "What are the benefits of using multi-vector rerankers?"

# Generate an answer using the custom QA chain
response = qa_chain(question)

剩下的就是再次实例化 qa_chain。注意这次我们通过新建该类的对象来完成,它封装了上一版本的大部分逻辑,并加入了自定义的重排序机制。

最后,提出问题并调用该链以获取回答。

进一步了解 AI

在 LLM 与更广义的 AI 领域中,RAG 可以说已经站稳脚跟。让 LLM 在生成回答的过程中访问并检索外部来源的知识,已成为替代持续再训练与微调的被广泛接受的做法。

本文讨论了重排序,作为在 LLM 流水线中融入高效信息检索的一种有益方法。文中介绍了重排序流程的工作方式、不同类型的重排序器,以及一个利用 Langchain 与 OpenAI API 的实践示例。

想继续学习?欢迎查阅以下资源:

Iván Palomares Carrascosa

作者

Iván Palomares Carrascosa

AI 与大语言模型专家与前瞻型领导者。培训师。高校学术负责人/主任。致力于 AI 项目管理、教学与创新的推广者。

主题