文档问答的原理
- 文档读取并切割,用句向量 向量化,存入向量数据库
- 问题向量化,在向量数据库中进行相似性检索,并存出top K
- 把问题和top K 答案组成 prompt 并发给大模型,等大模型答案
这里面涉及到的技术点有:
- 文档加载和切分
- 句向量
- 向量存储
- 向量相似度计算
- promot 生成
- 大模型(LLM)
langchain 把这些技术都整合到一起,让我们可以方便的搭建自己的应用。
文档问答的原型搭建
网上有很多demo ,最简单的是用llama-index,openai,gradio 进行搭建
llama-index 是基于文件的向量数据库,gradio 是web 服务器,实现了基本的ui页面,还可以提供域名服务。句向量和大模型用的openai.
这种demo 需要一台服务器,能连上openai. langchain 的安装有也些bug. 现在还是0.5的版本。 我在window2012 上安装,用miniconda, 和 visual studio,langchain 中有些c++的代码编译需要
这种应用搭demo 还可以的,但是在生产环境是不可行
- 很费钱。使用openai 的2个服务,embedding, gpt-3.5-turbo, 我训练了3篇doc 文档,就花了0.4$.
- 使用llama-index 并不是数据库,它是文件存储。检索速度慢很多。
下面我基于demo 进行了改进
- 句向量 使用 HuggingFace ‘m3e-base’,这是目前测试效果不错的句向量模型,不需要GPU 也可以跑。
- 向量数据库选用了Annoy,它是Facebook 推出的向量数据库,基于 k 树算法进行检索。
标题 | demo | 改进 |
---|---|---|
句向量 | openai embedding | HuggingFace ‘m3e-base’ |
大模型 | openai gpt-3.5-turbo | gpt-3.5-turbo |
向量数据库 | llama_index | Annoy |
import os
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Annoy
from langchain.text_splitter import CharacterTextSplitter
from langchain import OpenAI, VectorDBQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import DirectoryLoader
from langchain.chains import RetrievalQA
from langchain.indexes import VectorstoreIndexCreator
import time
from langchain import PromptTemplate
from langchain.embeddings import HuggingFaceEmbeddings,HuggingFaceInstructEmbeddings
# openAI的Key
os.environ["OPENAI_API_KEY"] = 'xxxx'
def create_index(path):
loader = DirectoryLoader('D:/download/', glob='**/*.docx')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
split_docs = text_splitter.split_documents(documents)
embeddings = HuggingFaceEmbeddings(model_name='moka-ai/m3e-base')
vector_store_path = r"./storage4"
docsearch = Annoy.from_documents(documents=split_docs,
embedding=embeddings,
persist_directory=vector_store_path)
docsearch.save_local(vector_store_path)
def search(txt):
embeddings = HuggingFaceEmbeddings(model_name='moka-ai/m3e-base')
vector_store_path = r"./storage4"
docsearch = Annoy.load_local(vector_store_path,embeddings=embeddings)
start = time.time()
prompt_template = """请注意:请谨慎评估query与提示的Context信息的相关性,只根据本段输入文字信息的内容进行回答,如果query与提供的材料无关,请回答"对不起,我不知道",另外也不要回答无关答案:
Context: {context}
Question: {question}
Answer:"""
PROMPT = PromptTemplate(template=prompt_template, input_variables=["context", "question"])
# qa = VectorDBQA.from_chain_type(llm=ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo-16k"), chain_type="stuff", vectorstore=docsearch, return_source_documents=True)
# result = qa({"query": txt})
qa = RetrievalQA.from_chain_type(llm=ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo"), chain_type="stuff", retriever=docsearch.as_retriever(search_kwargs={"k": 8}),
chain_type_kwargs={"prompt": PROMPT})
result = qa.run(txt)
print(result)
print(time.time() - start)
if __name__=="__main__":
create_index('')
search('xxx')
优化
-
文档的切分
文档切分对句向量的生成有很大影响。最理想的效果把相拟的段落切到一起,想实现这样的效果需要对文档内容比较了解,进而切分。
使用默认的 langchain CharacterTextSplitter chunk_size = 1000,这种切分的效果不是很好。它的分割符是 \n\n,先按chunk 切,再按分割符切。这样会把段落切错。
-
大模型的选型
使用openai gpt-3.5-turbo 它是有字数限制,4096个字符,top K选出的答案多了,它都答不上来。可以换用 gpt-3.5-turbo-16k, 它有16k个字符,大大的满足冲破答的需要。
换用国产大模型,如chatGLM, 它有6B 的小模型,单张GPU上就可以跑。这样也不用国外服务器。