LLM是基于一定训练数据训练成的,如果输入的内容所涉及的知识并不在训练数据范围内,LLM通常是无法准确回答问题的,不过检索增强生成(RAG)方案可以解决这个问题。RAG简单来说就是在外部增加了一个知识库,当输入的内容所涉及的知识不在训练数据范围内时,可以通过检索知识库来获取相关知识,然后与输入的内容进行融合,再通过LLM生成最终的回答。
实现RAG的一个关键点是如何准确的找到和问题语义相关的知识,这通常需要结合向量数据库和Embedding模型来实现。Embedding模型负责将输入的内容映射成向量,向量数据库则负责存储和检索向量。向量数据库有很多种,这里我们以比较简单易用的Chroma为例进行介绍,Embedding模型选择对中文支持较好的bge-large-zh-v1.5。
执行以下命令安装加载Embedding模型和Chroma相关的依赖。
pip install sentence_transformers langchain-chroma
其次我们还需要将bge-large-zh-v1.5
模型下载到本地。
LangChain中,文档对应的封装对象是Document
。下面例子中我们手动编写了一些文档并Embedding后存入了Chroma向量数据库中,之后我们又通过向量数据库来检索和问题相关的文档。
from langchain_core.documents import Document
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_chroma import Chroma
embedding_model = HuggingFaceBgeEmbeddings(
model_name='../bge-large-zh-v1.5/',
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
documents = [
Document(
page_content='汤姆是一只有蓝色的毛发的英国短毛猫,它经常追逐杰瑞。',
metadata={'source': '宠物介绍'},
),
Document(
page_content='杰瑞是一只褐色的家鼠,它非常聪明和敏捷。',
metadata={'source': '宠物介绍'},
),
Document(
page_content='斯派克是一只又大又笨的狗,他是杰瑞的朋友。',
metadata={'source': '宠物介绍'},
)
]
vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")
result = vector_store.similarity_search('斯派克是谁?', k=1)
print(result)
上述代码中,我们首先加载了Embedding模型,随后将文档的特征使用Embedding模型处理后存入了Chroma向量数据库。documents
是我们手写的一组文档,实际开发中,文档可能来源于其它数据源,并且可能是增量更新到向量数据库的,Chroma支持对文档数据的增删改查操作,具体可以参考LangChain和Chroma集成相关的文档。最后,我们调用了similarity_search
方法并输入了一个问题,参数k=1
意味着我们指定检索结果输出1个和问题最相关的文档。
在上面例子中,加入我们的问题和“猫”或“汤姆”有关,程序通常就会检索到第一条数据,因为这条数据和我们的问题是语义相关的,其它词条也是同理。
在LangChain中,将RAG整合到Chain还需要一个Retriever
组件,下面我们使用LangChain编写一个完整的RAG应用程序。
from langchain.globals import set_debug, set_verbose
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_chroma import Chroma
from langchain_community.chat_models import ChatOllama
set_debug(True)
set_verbose(True)
# 创建Embedding模型对象
embedding_model = HuggingFaceBgeEmbeddings(
model_name='../bge-large-zh-v1.5/',
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 加载文档数据
documents = [
Document(
page_content='汤姆是一只有蓝色的毛发的英国短毛猫,它经常追逐杰瑞。',
metadata={'source': '宠物介绍'},
),
Document(
page_content='杰瑞是一只褐色的家鼠,它非常聪明和敏捷。',
metadata={'source': '宠物介绍'},
),
Document(
page_content='斯派克是一只又大又笨的狗,他是杰瑞的朋友。',
metadata={'source': '宠物介绍'},
)
]
# 将文档存入Chroma
vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")
# 创建Retriever
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 1},
)
# 组织Prompt
message = """
Answer this question using the provided context only.
{question}
Context:
{context}
"""
prompt = ChatPromptTemplate.from_messages([("human", message)])
# 创建LLM对象
llm = ChatOllama(model='qwen2:7b-instruct-q5_K_M', temperature=1)
# 拼接Chain
rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser()
q = input()
a = rag_chain.invoke(q)
print(a)
RunnablePassthrough()
:该函数用于获取Chain上的数据数据代码其实很简单,Chain的开头我们拼接了一个新的组件用于输入数据,其中context
是通过Retriever从Chroma向量数据库中查询得到的结果,question
是用户的输入,将这些参数传入Prompt模板并拼接为完整的Prompt,之后输入LLM并读取其输出,一个最简单的RAG应用我们就开发完成了。
前面我们编写的程序中,Document(文档)具有十分标准的格式,它是一个数组,数组的每个元素都是一个长度近似的词条,然而实际应用场景中我们可能并没有如此规整的数据,知识库数据可能来自一个大段的文本,数据甚至是包含在一些千奇百怪的格式中的,如CSV、Word、PDF等。加载这些数据我们需要两个组件,一个是对应格式的解析器,另外就是针对大段文本的文本分割器。
下面例子代码中,我们从微软Word(docx)文件中解析数据,并使用递归分割器将原始数据分割成固定大小的块。
from langchain.globals import set_debug, set_verbose
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_chroma import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.chat_models import ChatOllama
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
set_debug(True)
set_verbose(True)
# 创建Embedding模型对象
embedding_model = HuggingFaceBgeEmbeddings(
model_name='../bge-large-zh-v1.5/',
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# 加载文档数据
documents = Docx2txtLoader("./狗头人之世界创建指南_电子版.docx").load()
# 分割文档
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=20,
length_function=len,
is_separator_regex=False,
)
documents = text_splitter.split_documents(documents)
# 将文档存入Chroma
vector_store = Chroma.from_documents(documents, embedding=embedding_model, persist_directory="./chroma_db")
# 创建Retriever
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 3},
)
# 组织Prompt
message = """
Answer this question using the provided context only.
{question}
Context:
{context}
"""
prompt = ChatPromptTemplate.from_messages([("human", message)])
# 创建LLM对象
llm = ChatOllama(model='qwen2:7b-instruct-q5_K_M', temperature=1)
# 拼接Chain
rag_chain = {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser()
q = input('输入你的问题:')
a = rag_chain.invoke(q)
print(a)
当然,使用RecursiveCharacterTextSplitter
效果一定不如之前的结构化数据,这种方式只是针对大段未分割文本处理的一种妥协,如果有条件我们还是尽量将数据创建为结构化的形式,这样RAG才能够达到最好的效果。