检索增强生成(Retrieval-Augmented Generation, RAG)结合了搜寻检索生成能力和自然语言处理架构,透过这个架构,模型可以从外部知识库搜寻相关信息,然后使用这些信息来生成response。要完成检索增强生成主要包含四个步骤:
1.TextLoad:
2.TextSplit:
3.Generate and save Embedding:
4.Search relavant context and get final anwser.
DocumentLoader
Langchain的document_loader目录下提供多种Load不同类型文档的方法,文档大类型总体可以分为结构化文档和非结构化文档。不同Loader的API信息如下图所示,更多Loader相关的API信息可查看这里。
下面是Load csv格式文件数据的代码和结果,import CSVLoader,print load的结果。可以看到,结果是一个List,List中每一个item是一个Document对象,包含page_content 和metadata数据,metadata包含source和row两个信息。
除了Load csv格式文件,还可以load pdf文件,import PyPDFLoader,load后的数据也是一个List,list中的每个value是Document对象,具体结果如下所示:
在Load PDF格式文档的时候,可以选择多种Loader,例如UnstructuredPDFLoader,MathpixPDFLoader,OnlinePDFLoader,PyPDFium2Loader,PDFMinerLoader,PyMuPDFLoader,PyPDFDirectoryLoader,PDFPlumberLoader。另外,在load PDF文档时还可以通过参数控制是否需要解析图片上的文字信息等。总之,PDF的loader是非常成熟和丰富的。
除了load PDF格式文档,当然还支持txt格式,markdown格式,html格式等文档,在load的时候可以通过参数选择是否显示load进度,是否使用多线程进行load,多线程load可提升load效率。代码如下所示:
python
loader = DirectoryLoader('testdata', glob="**/*.txt",
show_progress=True, use_multithreading=True)
docs = loader.load()
print(docs)
TextSplitter
langchain提供了多种TextSplitter,具体如下图所示,这些spliter中比较常用的有TextSplitter,CharacterTextSplitter,RecursiveCharacterTextSplitter等。
下面是调用CharacterTextSplitter对文本进行切割,需要指定separator,chunk_size,chunk_overlap等参数。chunk_size
:定义文本应该被分割成的最大块的大小。在这个例子中,设置为 1000,所以每个块最多包含 1000 个字符。这只是一个最大字符数量控制,真正在分割的时候还要参考separator。chunk_overlap
:块之间的最大重叠量。这里设置为 200,所以连续块之间最多可以有 200 个字符的重叠。length_function
:用于计算块长度的函数。在这个例子中,使用内置的 len
函数,所以块的长度就是它的字符数.
python
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 1000,
chunk_overlap = 200,
length_function = len,
is_separator_regex = False,
)
上面是最简单的文本切割方式,在切割分本时,我们需要尽量保证文本的语句含义不要丢失,即相关性很强的内容最好能放到一个chunk内,上面的切割方式虽然简单,但是容易把文本含义丢失。所以除了上面最简单的切割方式外,langchain还提供了RecursiveCharacterTextSplitter 切割方式。该切割方式的默认separator是["\n\n", "\n", " ", ""]。文本分割器首先尝试在每个双换行符 ("\n\n") 处拆分文本,这通常用于分隔文本中的段落。如果生成的块过大,它接着尝试在每个换行符 ("\n") 处拆分,这通常用于分隔句子。如果块仍然过大,它最后尝试在每个空格 (" ") 处拆分,这用于分隔单词。如果块仍然过大,它会在每个字符 ("") 处拆分,尽管在大多数情况下,这种细粒度的拆分是不必要的。这种方法的优点是它尽量保留了语义上下文,通过保持段落、句子和单词的完整性。这些文本单元往往具有强烈的语义关系,其中的单词在意义上通常密切相关,这对于许多自然语言处理任务是有益的。
根据经验,采用RecursiveCharacterTextSplitter进行文本切割时,chunk_size设置在500-1000分为内效果较好,chunk_overlap设置在chunk_size大小的10%-20%左右。
如果文档内容包含格式信息,例如html,markdown等,langchain还提供了HTMLHeaderTextSplitter和MarkdownHeaderTextSplitter切割方式。即在切割的时候按标题头切割,如果是在一个标题下的内容就放在一起。这样能最大程度保证相关性强的内容被切割在一个chunk里面。除了对文本进行切割,Langchain还可以对Code进行切割。下面的代码中就是加载代码目录地址,对该目录下的代码进行切割。
python
repo_path = "/Users/taoli/study/Python/langchain/langchain"
loader = GenericLoader.from_filesystem(
repo_path,
glob="**/*",
suffixes=[".py"],
parser=LanguageParser(language=Language.PYTHON, parser_threshold=500),
)
documents = loader.load()
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, chunk_size=1000, chunk_overlap=100
)
texts = python_splitter.split_documents(documents)
Embedding model
介绍完了Splitter,接着来看看Embedding。langchain支持多种Embedding model的载入。部分Embedding model如下图所示,更多Embedding信息可查看这里。比较常用的有OpenAIEmbeddings和HuggingFaceEmbeddings。
如下面代码所示,在调用向量数据库的classmethod from_document,就需要指定调用的Embedding方法,可以使用HuggingFace中某个特定的Embedding模型,也可以直接使用OpenAIEmbedding。设置Embedding后,调用from_document方法,就会把texts中的内容转换成向量并存放在向量数据库中,用于后面的检索获取。
python
db = Chroma.from_documents(
texts, HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2"))
db = Chroma.from_documents(texts,OpenAIEmbeddings)
在调用from_document方法时,除了指定embedding function,texts内容外,还可以指定persist_directory,即持久化向量信息的目录等多个参数信息。
Search relavant context and get final anwser
介绍完Embedding后,再来看看相关性检索和得到最终结果的部分。如下面代码所示,在获取到向量库对象后,需要转换成retriever。Retriever类可以根据输入的查询问题,从文档中检索相关性高的内容并返回,检索器不需要存储文档,只需要返回(或检索)文档。向量存储可以用作检索器的支撑,但还有其他类型的检索器。
python
retriever = db.as_retriever(search_type="mmr",
search_kwargs={"k": 8})
llm = ChatOpenAI(model_name="gpt-3.5-turbo")
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever, chain_type_kwargs={"prompt": rag_prompt})
result = qa_chain({"query": "What classes are derived from the Chain class?"})
print(result)
memory = ConversationSummaryMemory(
llm=llm, memory_key="chat_history", return_messages=True
)
qa = ConversationalRetrievalChain.from_llm(
llm, retriever=retriever, memory=memory)
questions = [
"What is the class hierarchy?",
"What classes are derived from the Chain class?",
"What one improvement do you propose in code in relation to the class hierarchy for the Chain class?",
]
for question in questions:
result = qa(question)
print(f"-> **Question**: {question} \n")
print(f"**Answer**: {result['answer']} \n")
在调用as_retriever()方法中可以指定search_type,search_type主要包括两种:query- Similarity Search and Max Marginal Relevance Search (MMR Search),其中MMR Search旨在提高搜索结果的多样性,常用于推荐系统。如果想检索更准确则推荐选择query- Similarity Search,默认的search_type也是query- Similarity Search。
search_kwargs中可以传入多个参数,常见的参数包括:
k 定义返回多少个文档;默认为 4。
如果search_type是 "similarity_score_threshold" ,可以设置score_threshold,即减少相关性的分数阀值。
fetch_k 确定传递给MMR算法的文档数量;默认为 20。
lambda_mult 控制MMR算法返回结果的多样性,1表示最小多样性,0表示最大多样性。默认为 0.5。
filter 允许您根据文档的元数据定义文档检索的筛选条件。如果Vectorstore不存储任何元数据,则此选项无效。
获取到retriver后,在检索文档生成答案时,有多种方式,既可以通过RetrievalQA这个chain, 还可以通过ConversationalRetrievalChain来获取最终的答案。在调用chain的时候还可以指定chain_type,langchain提供了四种类型的chain_type.Stuff,Refine,Map Reduce和Map re-rank.默认是使用Stuff类型。
Stuff:stuff文件链("stuff "是指 "to stuff "或 "to fill")是最直接的文件链。它接收一个文件列表,将它们全部插入一个提示,并将该提示传递给一个LLM。这条链非常适合于文件数量较少且大多数调用只传递几个文件的应用。
Refine:完善文档链通过循环输入文档和迭代更新其答案来构建一个响应。对于每个文档,它将所有非文档输入、当前文档和最新的中间答案传递给LLM链,以获得一个新的答案。由于Refine链每次只向LLM传递一个文档,所以它很适合需要分析更多文档的任务,这条链会比Stuff文档链等调用更多的LLM。当然,也有一些任务是很难迭代完成的。例如,当文档之间经常相互参照,或者当一个任务需要许多文档的详细信息时,Refine链的表现就很差。下图是Refine的过程源代码中Refine背后的Prompt信息。
Map_Reduce: map reduce文档链首先对每个文档单独应用LLM链(map步骤),将链的输出视为一个新的文档。然后,它将所有的新文档传递给一个单独的组合文档链,以获得一个单一的输出(Reduce步骤)。
Map re_rank:map re-rank文件链对每个文件进行初始提示,不仅试图完成一项任务,而且对其答案的确定程度进行评分。得分最高的回答会被返回。
在使用chain生成最终答案时,如果调用ConversationalRetrievalChain,可以在from_llm中传入如下的参数,包括chain_type,自定义的prompt等。例如要传入自定义的prompt,那么可以通过condense_question_prompt传入,或者通过combine_docs_chain_kwargs参数传入:
combine_docs_chain_kwargs={"prompt": TEST_PROMPT},
在使用chain生成最终答案时,如果调用RetrievalQA的from_chain_type方法,可以通过chain_type_kwargs传入自定义prompt等参数。
以上就是RAG的过程,总结而言包含Document Loader,Document splitter,Embedding,Search and get final answer四个步骤。步骤示意图如下所示: