Skip to content

RAG:检索增强生成

目录

1. RAG 简介

1.1 什么是 RAG

所谓检索增强生成(Retrieval Augment Generation, RAG)指,当用户提出一个问题时,AI 应用先去知识库中检索出一定数量的文档片段,然后将问题与文档片段一起提交给 AI 大语言模型,由模型生成回应。如图所示。

经过预训练与微调的大模型已经掌握了数据与知识,但是,通过将与问题相关的文档片段作为上下文在提示语中提交给模型,它能生成出与知识库更匹配的内容。当我们需要对模型生成时所参考的知识有更明确的控制时,我们也会选择使用 RAG。

RAG 是将知识提交给 AI 模型的一种方式。OpenAI 在一份文档中就做了一个类比——开卷考试 vs 闭卷考试。通过精调(fine-tune)将知识提交给模型,然后再用精调过的模型参数生成,这相当于学习后的闭卷考试;而通过 RAG 将知识在提示语中提交给模型,这相当于开卷考试。

通常来说,如果我们希望模型针对一个特定知识库来生成,RAG 是更便捷的方式。以写作为例,精调能够通过样本来让模型学会某种风格,而 RAG 则可以让模型直接引用知识库中的语句。

对于 RAG,最终用户和开发者的视角是不同:

  • 普通用户,他们使用一个带有知识库的 AI 应用,他们的每次提问都在幕后先进行知识库的检索,然后再由大模型进行回答。
  • 高阶用户,他们会上传文档(比如对话里上传PDF、GPTs中上传文档作为知识库),然后针对自己上传的文档或知识库进行问答。
  • 应用开发者的关注点则包括另一面,即要考虑如何将文档转换为可供检索的知识库,并确保检索出的内容是相关的。

在讨论 RAG 时我们经常同时提及各类向量数据库,它是开发者存放经文本嵌入处理成向量的知识的仓库。要注意的是,除了利用文本嵌入的相似性检索之外,我们也可以结合关键词搜索等多种方式来检索。

1.2 文本嵌入与检索

在有了如上背景讨论后,我们再来看 LangChain 的 RAG (其称「Retrieval 模块」)。它为开发者提供将文档转换为可检索的知识库的功能。对比而言,LlamaIndex 库则为文本检索提供了更多的方式。

来源:LangChain 检索模块,LangChain Docs

如上图所示,我们可以看到,开发者要经过如下步骤来将文档变成知识库:

  1. 加载(Load),比如加载 PDF、epub、API接口等。
  2. 转换(Transform),比如将长文档切分为较小的片段。
  3. 嵌入(Embed),采用 AI 模型将文档片段转换为向量。
  4. 存入向量库(Store),用各类向量数据库将向量存储起来,以备后续检索。

这是 RAG 的索引阶段,在用户使用时,则进入到 RAG的检索阶段。也即,RAG 包括两个阶段:

索引阶段:知识库的形成,包括如上四个环节;

检索阶段:知识库的检索,可采用多种检索策略。

LangChain 的检索模块为如上两个阶段都提供了丰富的支持。

1.3 RAG 简单示例

在之前的教程中,我们介绍了一个 RAG 示例。其中,使用Chroma 向量库做示例。嵌入我们使用的是 OpenAI 的 embedding 服务。

第一阶段(索引):文本嵌入 Embedding

我们将两段文本进行嵌入,并存入vectorstore

python
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import OpenAIEmbeddings

vectorstore = Chroma.from_texts(
    ["harrison worked at kensho", "bears like to eat honey"],
    embedding=OpenAIEmbeddings(),
)

如上这几行就是完成了「知识库的形成」,将"harrison worked at kensho"、"bears like to eat honey"转换为向量,然后存入chroma 向量库。

第二阶段(检索):用户提问-检索文本-模型回答

在第二阶段,我们先将向量库作为 retriever

python
retriever = vectorstore.as_retriever()

然后再根据用户提问检索调用。调用 LLM 提问部分代码如下,我们创建一个调用链来完成这个任务,稍后我们会详细讲解。

python
chain = setup_and_retrieval | prompt | model | output_parser

如果知识库没有变化,我们并不需要每次都进行嵌入,而可以加载保存的向量库,然后直接检索。

提问部分详细代码
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

template = """Answer the question based only on the following context:
{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

chain.invoke("where did harrison work?")
# result: 'Harrison worked at Kensho.'

2. RAG 知识库创建详解

我们接下来用一个示例逐一详解如何用 LangChain 提供的工具创建一个知识库,并进行检索增强生成(RAG)

2.1 文档处理与向量库创建

2.1.1 加载文档

LangChain 提供了多种文档加载器(Document Loader)。它们可将各种文档加载成 Documents 对象,以便后续处理。

python
from langchain_community.document_loaders import TextLoader

loader = TextLoader(file_path)
docs = loader.load()

print(docs)
# [Document(page_content='\n\nWhat I Worked On\n\nFebruary 2021\n\nBefore college the two main things ……', metadata={'source': 'paul_graham_essay.txt'})]

len(docs)
# 1

在这里,我们使用的是 TextLoaderReference),你可以看到,它将整个文档加载成一个 Document 对象。

2.1.2 拆分文档

接下来,我们可使用 LangChain 提供的文档拆分功能,将文档拆分成一系列片段,以便后续处理。在代码中,我们设定了多个拆分参数。

python
from langchain.text_splitter import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=500,
    chunk_overlap=200,
    length_function=len,
    is_separator_regex=False,
)

texts = text_splitter.split_documents(docs)

len(texts)
# 157

如上,我们使用 CharacterTextSplitter 拆分器,采用两个分行\n\n作为作为分隔符,并要求将长度拆分为 500 个字符一块。在这里,我们得到的是一个有着 157 个 Documents 元素的列表。

2.1.3 嵌入与存入向量库

接下来包括两步:首先,我们将 Documents 嵌入(Embedding)成向量,然后,将向量存入向量数据库,以备后续检索。借用 LangChain 的工具,我们可以一次性完成两个步骤:

python
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import OpenAIEmbeddings

vectorstore = Chroma.from_documents(
    documents=texts,
    embedding=OpenAIEmbeddings(),
)

这里,我们采用 OpenAI 的 Embedding 服务,并将向量存储到 Chroma 向量数据库。我们得到的是一个 vectorstore。

2.1.4 向量库查询

有了 vectorstore 之后,我们可以进行相似性查询:

python
query="what happened after the author switched to AI?"
results = vectorstore.similarity_search(query)

len(results)
#4,缺省检索出四个相关文档片段。

2.1.5 向量库:存储与载入

以下两步是可选的,我们可将 vectorstore 存储,然后下次就不必再次进行嵌入。参考文档:langchain chroma

python
from langchain_community.vectorstores import Chroma
from langchain_openai.embeddings import OpenAIEmbeddings

# save to disk
vectorstore = Chroma.from_documents(
    documents=texts,
    embedding=OpenAIEmbeddings(),
    persist_directory="./chroma_db"
)
python
# load from disk
newdb = Chroma(
        persist_directory="./chroma_db",
        embedding_function=OpenAIEmbeddings(),)

query="what happened after the author switched to AI?"
results = newdb.similarity_search(query)

len(results)
#4

2.2 从向量库中检索

LangChain 的封装让向量库检索很便捷,如下:

python
retriever = vectorstore.as_retriever()  # 将向量库设定为 retreiver

query="what happened after the author switched to AI?"

results = retriever.get_relevant_documents(query)

len(results)
#4

for doc in results:
  print(doc)

# page_content="I couldn't have put this into words when I was 18. All I knew at the time was that I kept taking philosophy courses and they kept being boring. So I decided to switch to AI." metadata={'source': 'paul_graham_essay.txt'}……

如上所示,我们可用相似性搜索从向量库中匹配文档,缺省为匹配 4 个文档片段。我们可以配置搜索参数 k 来调整数量。

python
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

实际上,在使用 LangChain 时,我们不必显式地做相似检索这一步,而可以调用链中一并设置。详见 2.3 小节。

2.3 RAG 调用链

在使用 RAG 时,我们将提问和文档片段组织成提示语,然后提交给 LLM 去进行生成。在使用 LangChain 时,调用链的结构如下:

代码如下:

python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

template = """Answer the question based only on the following context:
{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

调用与结果

python
chain.invoke("where did harrison work?")
# Harrison worked at Kensho.

调用过程解释如下:

  1. 提问被两路并行运行。其中一路去进行检索,结果作为"context"。另一路用RunnablePassthrough(),作为"question"
  2. 它们被输入提示语模板,形成给 LLM 的提示语。
  3. 模型返回后,解析为字符串。

如果你对调用链具体运转过程感兴趣的话,可以点开如下逐步调用链的逐步分析。

RAG 调用链逐步拆解
  1. 检索
python
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)

print(setup_and_retrieval)
#result:  steps={
#    'context': VectorStoreRetriever(
#           tags=['DocArrayInMemorySearch'],
#           vectorstore=<langchain_community.vectorstores.docarray.in_memory.DocArrayInMemorySearch object at 0x7b02f605fca0>),
#    'question': RunnablePassthrough()
#  }

检索结果

python
context_and_question = setup_and_retrieval.invoke("where did harrison work?")
print(context_and_question)
# {'context': [Document(page_content='harrison worked at kensho'),
#  Document(page_content='bears like to eat honey')],
# 'question': 'where did harrison work?'}
  1. 提示语模板
python
prompt_value = prompt.invoke(context_and_question)

prompt_value.to_messages()

# result: [HumanMessage(content=
#  "Answer the question based only on the following context:\n[Document(page_content='harrison worked at kensho'), Document(page_content='bears like to eat honey')]\n\nQuestion: where did harrison work?\n")]
  1. 模型调用
python
response = model.invoke(prompt_value)
print(response)
# result: content='Harrison worked at Kensho.'
  1. 输出解析器
python
output_parser.invoke(response)
# result: 'Harrison worked at Kensho.'

通过如上讨论,你已经可以了解 RAG 的基本工作原理。但是请注意,RAG 的关键并非仅仅是如上调用过程,关键是你构建的 RAG 管道的质量:

  1. 原始材料的质量及处理;
  2. 索引与检索的方式;
  3. 对检索结果质量的评估。

3. 小结

在本教程中,我们带你了解用 LangChain 实现 RAG 的基础过程。之后,你可以运用各种工具来组织的自己 RAG 调用链。

Alang.AI - Make Great AI Applications