LangChain+ChatGLM项目(https://github.com/chatchat-space/langchain-ChatGLM)实现原理如下图所示 (与基于文档的问答 大同小异,过程包括:1 加载文档 -> 2 读取文档 -> 3/4文档分割 -> 5/6 文本向量化 -> 8/9 问句向量化 -> 10 在文档向量中匹配出与问句向量最相似的top k个 -> 11/12/13 匹配出的文本作为上下文和问题一起添加到prompt中 -> 14/15提交给LLM生成回答 )
加载文件:这是读取存储在本地的知识库文件的步骤
读取文件:读取加载的文件内容,通常是将其转化为文本格式
文本分割(Text splitter):按照一定的规则(例如段落、句子、词语等)将文本分割
def _load_file(self, filename):
# 判断文件类型
if filename.lower().endswith(".pdf"): # 如果文件是 PDF 格式
loader = UnstructuredFileLoader(filename) # 使用 UnstructuredFileLoader 加载器来加载 PDF 文件
text_splitor = CharacterTextSplitter() # 使用 CharacterTextSplitter 来分割文件中的文本
docs = loader.load_and_split(text_splitor) # 加载文件并进行文本分割
else: # 如果文件不是 PDF 格式
loader = UnstructuredFileLoader(filename, mode="elements") # 使用 UnstructuredFileLoader 加载器以元素模式加载文件
text_splitor = CharacterTextSplitter() # 使用 CharacterTextSplitter 来分割文件中的文本
docs = loader.load_and_split(text_splitor) # 加载文件并进行文本分割
return docs # 返回处理后的文件数据
这通常涉及到NLP的特征抽取,可以通过诸如TF-IDF、word2vec、BERT等方法将分割好的文本转化为数值向量。
# 初始化方法,接受一个可选的模型名称参数,默认值为 None
def __init__(self, model_name=None) -> None:
if not model_name: # 如果没有提供模型名称
# 使用默认的嵌入模型
# 创建一个 HuggingFaceEmbeddings 对象,模型名称为类的 model_name 属性
self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name)
文本向量化之后存储到数据库vectorstore。
def init_vector_store(self):
persist_dir = os.path.join(VECTORE_PATH, ".vectordb") # 持久化向量数据库的地址
print("向量数据库持久化地址: ", persist_dir) # 打印持久化地址
# 如果持久化地址存在
if os.path.exists(persist_dir):
# 从本地持久化文件中加载
print("从本地向量加载数据...")
# 使用 Chroma 加载持久化的向量数据
vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings)
# 如果持久化地址不存在
else:
# 加载知识库
documents = self.load_knownlege()
# 使用 Chroma 从文档中创建向量存储
vector_store = Chroma.from_documents(documents=documents,
embedding=self.embeddings,
persist_directory=persist_dir)
vector_store.persist() # 持久化向量存储
return vector_store # 返回向量存储
def load_knownlege(self):
docments = [] # 初始化一个空列表来存储文档
# 遍历 DATASETS_DIR 目录下的所有文件
for root, _, files in os.walk(DATASETS_DIR, topdown=False):
for file in files:
filename = os.path.join(root, file) # 获取文件的完整路径
docs = self._load_file(filename) # 加载文件中的文档
# 更新 metadata 数据
new_docs = [] # 初始化一个空列表来存储新文档
for doc in docs:
# 更新文档的 metadata,将 "source" 字段的值替换为不包含 DATASETS_DIR 的相对路径
doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")}
print("文档2向量初始化中, 请稍等...", doc.metadata) # 打印正在初始化的文档的 metadata
new_docs.append(doc) # 将文档添加到新文档列表
docments += new_docs # 将新文档列表添加到总文档列表
return docments # 返回所有文档的列表
这是将用户的查询或问题转化为向量,应使用与文本向量化相同的方法,以便在相同的空间中进行比较 。
在文本向量中匹配出与问句向量最相似的top k个,这一步是信息检索的核心,通过计算余弦相似度、欧氏距离等方式,找出与问句向量最接近的文本向量。
def query(self, q):
"""在向量数据库中查找与问句向量相似的文本向量"""
vector_store = self.init_vector_store()
docs = vector_store.similarity_search_with_score(q, k=self.top_k)
for doc in docs:
dc, s = doc
yield s, dc
匹配出的文本作为上下文和问题一起添加到prompt中,这是利用匹配出的文本来形成与问题相关的上下文,用于输入给语言模型。
最后,将这个问题和上下文一起构成的prompt提交给在线(例如GPT-4/ChatGPT)或本地化部署大语言模型,让它生成回答。
class KnownLedgeBaseQA:
# 初始化
def __init__(self) -> None:
k2v = KnownLedge2Vector() # 创建一个知识到向量的转换器
self.vector_store = k2v.init_vector_store() # 初始化向量存储
self.llm = VicunaLLM() # 创建一个 VicunaLLM 对象
# 获得与查询相似的答案
def get_similar_answer(self, query):
# 创建一个提示模板
prompt = PromptTemplate(
template=conv_qa_prompt_template,
input_variables=["context", "question"] # 输入变量包括 "context"(上下文) 和 "question"(问题)
)
# 使用向量存储来检索文档
retriever = self.vector_store.as_retriever(search_kwargs={"k": VECTOR_SEARCH_TOP_K})
docs = retriever.get_relevant_documents(query=query) # 获取与查询相关的文本
context = [d.page_content for d in docs] # 从文本中提取出内容
result = prompt.format(context="\n".join(context), question=query) # 格式化模板,并用从文本中提取出的内容和问题填充
return result # 返回结果
这种通过组合langchain+LLM的方式,特别适合一些垂直领域或大型集团企业搭建通过LLM的智能对话能力搭建企业内部的私有问答系统,也适合个人专门针对一些英文paper进行问答,比如比较火的一个开源项目:ChatPDF,其从文档处理角度来看,实现流程如下(图源):