LangChain:我们不生产模型,我们只是大模型的搬运工。
基于LangChain实现本地知识库问答:
项目地址:
流程如上图,知识库嵌入-->问题嵌入-->相似度计算-->提示文本-->回答
共涉及两个大模型,分别为文本嵌入模型和大语言模型(LLM)。
文本嵌入模型作用:
1. 对知识库中的文件中的文本片段进行嵌入,存入知识向量库;
2. 对问题进行嵌入。
LLM作用:根据提示文本回答问题。
LangChain主打大模型间的调度,附带嵌入向量相似度计算,提示模板填充等功能;
下面总结下部署时遇到的一些小问题(v0.2.5):
1. 切换文本嵌入模型时报错。
官方提供了文本嵌入模型M3E的示例,但将其更换为BGE,初始化知识向量库时,弹出以下错误:
原因:断言出错,原向量库残留了一些模型信息,self.d对应M3E的嵌入维度768,但BGE的嵌入维度为1024,不能存库。
解决方案:删掉原来存储库所在目录或重命名即可,单纯在命令行添加--recreate-vs不起作用。
2. BGE加载代码存在问题。
./Langchain-Chatchat-0.2.5/server/knowledge_base/kb_cache/base.py中137行左右:
embeddings = HuggingFaceBgeEmbeddings(model_name=embedding_model_dict[model],
model_kwargs={'device': device},
query_instruction=query_instruction)
embeddding_model_dict未声明,改成下面即可:
embeddings = HuggingFaceBgeEmbeddings(model_name=get_model_path(model),
model_kwargs={'device': device},
query_instruction=query_instruction)
3. 使用百川13B大模型时,报错AttributeError: 'BaichuanTokenizer' object has no attribute 'sp_model'
需将baichuan-inc/Baichuan2-13B-Chat/tokenization_baichuan.py中的super()放至最后执行,如下:
self.vocab_file = vocab_file
self.add_bos_token = add_bos_token
self.add_eos_token = add_eos_token
self.sp_model = spm.SentencePieceProcessor(**self.sp_model_kwargs)
self.sp_model.Load(vocab_file)
super().__init__(
bos_token=bos_token,
eos_token=eos_token,
unk_token=unk_token,
pad_token=pad_token,
add_bos_token=add_bos_token,
add_eos_token=add_eos_token,
sp_model_kwargs=self.sp_model_kwargs,
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
**kwargs,
)
4. web运行时,出现KeyError: Caught exception: 'choices'
ERROR | root | KeyError: Caught exception: 'choices'
可能是显存超了,减少匹配知识条数或减小kb_config.py中的CHUNK_SIZE等均可。
下面说一下在实际应用效果上的小问题:
1. 读取pdf时,不合宜的换行符'\n'会影响LLM对语义的理解。
例如,标题+'\n'+段落,LLM会因为'\n'认为标题和段落不存在关联,导致回答不准确。对于较为成熟的LLM(ChatGPT、文心等)并不会有太大影响,但对于ChatGLM是存在一定影响的。
解决方案:在构建知识向量库时,将全部'\n'替换为空格。替换代码所在目录:
./anaconda3/envs/LangChain/lib/python3.10/site-packages/langchain/text_splitter.py
若想在文本拆分前替换,将下面注释代码改为非注释代码即可,160行左右:
# texts.append(doc.page_content)
new_content = doc.page_content.replace('\n', ' ')
texts.append(new_content)
若想在文本拆分后替换,将下面注释代码改为非注释代码即可,150行左右:
# new_doc = Document(page_content=chunk, metadata=metadata)
new_chunk = chunk.replace('\n', ' ')
new_doc = Document(page_content=new_chunk, metadata=metadata)
效果对比:
替换前(检索出了,但未能准确回答):
替换后(能够准确回答):
2. LLM存在一定随机性,问题相同,答复却不同。
对于相对困难的问题,回答差距较大,如下:
对于简单的问题,回答方式虽然不完全相同,但内容正确,如下:
解决方案:切换表现更好的LLM,百川2-13B表现较佳,但对算力设备要求较高,在算力一般(例如RTX 3090、2080ti)的设备上速度较慢。
3. 不适宜的文本、表格拆分对语义的影响严重。
尤其是表格数据被拦腰截断后,对于未含表头的数据则不可能被正确回答。
解决方案:替换文本分割方案,采用三方库docx读取.docx,能够单独读取表格,对于PDF和doc则需事先转为docx格式。
文本分割修改代码位置:~/Langchain-Chatchat-0.2.5/server/knowledge_base/utils.py中的KnowledgeFile.docs2texts()
文本分割函数 text_split_via_me() 内可包括文档的重新读取,代码太多就不贴细节了。
def docs2texts(
self,
docs: List[Document] = None,
zh_title_enhance: bool = ZH_TITLE_ENHANCE,
refresh: bool = False,
chunk_size: int = CHUNK_SIZE,
chunk_overlap: int = OVERLAP_SIZE,
text_splitter: TextSplitter = None,
):
docs = docs or self.file2docs(refresh=refresh)
if not docs:
return []
# if self.ext not in [".csv"]:
# if text_splitter is None:
# text_splitter = make_text_splitter(splitter_name=self.text_splitter_name, chunk_size=chunk_size, chunk_overlap=chunk_overlap)
# if self.text_splitter_name == "MarkdownHeaderTextSplitter":
# docs = text_splitter.split_text(docs[0].page_content)
# for doc in docs:
# # 如果文档有元数据
# if doc.metadata:
# doc.metadata["source"] = os.path.basename(self.filepath)
# else:
# docs = text_splitter.split_documents(docs)
def text_split_via_me(docs):
...
return ...
docs = text_split_via_me(docs)
print(f"文档切分示例:{docs[0]}")
# if zh_title_enhance:
# docs = func_zh_title_enhance(docs)
self.splited_docs = docs
return self.splited_docs