基于langchain 的文档问答 最佳实践(附源码)

文档问答的原理

  1. 文档读取并切割,用句向量 向量化,存入向量数据库
  2. 问题向量化,在向量数据库中进行相似性检索,并存出top K
  3. 把问题和top K 答案组成 prompt 并发给大模型,等大模型答案

基于langchain 的文档问答 最佳实践(附源码)_第1张图片

这里面涉及到的技术点有:

  • 文档加载和切分
  • 句向量
  • 向量存储
  • 向量相似度计算
  • 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上就可以跑。这样也不用国外服务器。

你可能感兴趣的:(langchain,langchain,chatgpt)