LangChain实战课-3.用LangChain快速构建基于“易速鲜花”本地知识库的智能问答系统

168 阅读14分钟

我正在参加「豆包MarsCode AI练中学体验活动」

1.项目介绍

给出一份员工手册,让机器人理解其中内容,然后再对机器人提问。

具体流程分为下面5步。

  1. Loading:文档加载器把Documents 加载为以LangChain能够读取的形式。
  2. Splitting:文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
  3. Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
  4. Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
  5. Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案

2.数据的准备和载入

文件如下,注意之后修改代码中的存放路径即可

image.png 为了有一个案例我们看一份PDF,待会儿就对这段文字进行提问即可:

image.png 我们首先用LangChain中的document_loaders来加载各种格式的文本文件。 在这一步中,我们从 pdf、word 和 txt 文件中加载文本,然后将这些文本存储在一个列表中。(注意:可能需要安装PyPDF、Docx2txt等库,如果没有这个包,直接使用pip install进行安装)


# 1.Load 导入Document Loaders
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel
from volcenginesdkarkruntime import Ark


# 加载Documents
base_dir = "./OneFlower"  # 文档的存放目录
documents = []
for file in os.listdir(base_dir):
    # 构建完整的文件路径
    file_path = os.path.join(base_dir, file)
    if file.endswith(".pdf"):
        loader = PyPDFLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".docx"):
        loader = Docx2txtLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".txt"):
        loader = TextLoader(file_path)
        documents.extend(loader.load())

3.文本分割

接下来需要将加载的文本分割成更小的块,以便进行嵌入和向量存储。这个步骤中,我们使用 LangChain中的RecursiveCharacterTextSplitter 来分割文本。 其中chunk_size=200表示分割后的每个文本块大小为200个字符,chunk_overlap=30表示相邻的文本块有30个字符的重复,以确保语义连贯性和上下文检索。

# 2.Split 将Documents切分成块以便后续进行嵌入和向量存储
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)

4.向量数据库存储

紧接着,我们将这些分割后的文本转换成嵌入的形式,并将其存储在一个向量数据库中。在这个例子中,我们会使用豆包Embedding来生成嵌入,然后使用 Qdrant 这个向量数据库来存储嵌入(这里需要pip install qdrant-client)。 同样需要在火山引擎中创建推理接入点,并且需要获取ID作为model image.png

# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中

api_key = "" #YOURAPIKEY
base_url="https://ark.cn-beijing.volces.com/api/v3"
model = 'ep-20241111110355-2tp82' #YOURMODELID
from langchain_community.vectorstores import Qdrant

class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = api_key
    model: str

    def __init__(self, **data: Any):
        super().__init__(**data)
        if self.api_key == "":
            self.api_key = os.environ["OPENAI_API_KEY"]
        self.client = Ark(
            base_url=base_url,
            api_key=self.api_key
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成输入文本的 embedding.
        Args:
            texts (str): 要生成 embedding 的文本.
        Return:
            embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
        """
        embeddings = self.client.embeddings.create(model=self.model, input=text)
        return embeddings.data[0].embedding

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        return [self.embed_query(text) for text in texts]

    class Config:
        arbitrary_types_allowed = True


vectorstore = Qdrant.from_documents(
    documents=chunked_documents,  # 以分块的文档
    embedding=DoubaoEmbeddings(
        model=model,
    ),  # 使用DoubaoEmbeddings进行嵌入
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",
)  # 指定collection_name

目前,易速鲜花的所有内部文档,都以“文档块嵌入片”的格式被存储在向量数据库里面了。那么,我们只需要查询这个向量数据库,就可以找到大体上相关的信息了。

  • 以下内容是对嵌入还有向量数据库概念的解释

词嵌入(Word Embedding)是自然语言处理和机器学习中的一个概念,它将文字或词语转换为一系列数字,通常是一个向量。简单地说,词嵌入就是一个为每个词分配的数字列表。这些数字不是随机的,而是捕获了这个词的含义和它在文本中的上下文。因此,语义上相似或相关的词在这个数字空间中会比较接近。  

举个例子,通过某种词嵌入技术,我们可能会得到: "国王" -> [1.2, 0.5, 3.1, ...] "皇帝" -> [1.3, 0.6, 2.9, ...] "苹果" -> [0.9, -1.2, 0.3, ...]  

从这些向量中,我们可以看到“国王”和“皇帝”这两个词的向量在某种程度上是相似的,而与“苹果”这个词相比,它们的向量则相差很大,因为这两个概念在语义上是不同的。  

词嵌入的优点是,它提供了一种将文本数据转化为计算机可以理解和处理的形式,同时保留了词语之间的语义关系。这在许多自然语言处理任务中都是非常有用的,比如文本分类、机器翻译和情感分析等。

向量数据库,也称为矢量数据库或者向量搜索引擎,是一种专门用于存储和搜索向量形式的数据的数据库。在众多的机器学习和人工智能应用中,尤其是自然语言处理和图像识别这类涉及大量非结构化数据的领域,将数据转化为高维度的向量是常见的处理方式。这些向量可能拥有数百甚至数千个维度,是对复杂的非结构化数据如文本、图像的一种数学表述,从而使这些数据能被机器理解和处理。然而,传统的关系型数据库在存储和查询如此高维度和复杂性的向量数据时,往往面临着效率和性能的问题。因此,向量数据库被设计出来以解决这一问题,它具备高效存储和处理高维向量数据的能力,从而更好地支持涉及非结构化数据处理的人工智能应用。

image.png 向量数据库有很多种,比如Pinecone、Chroma和Qdrant,有些是收费的,有些则是开源的。

LangChain中支持很多向量数据库,这里我们选择的是开源向量数据库Qdrant。(注意,需要安装qdrant-client)

5.相关信息的获取

当内部文档存储到向量数据库之后,我们需要根据问题和任务来提取最相关的信息。此时,信息提取的基本方式就是把问题也转换为向量,然后去和向量数据库中的各个向量进行比较,提取最接近的信息。

向量之间的比较通常基于向量的距离或者相似度。在高维空间中,常用的向量距离或相似度计算方法有欧氏距离和余弦相似度。

  • 欧氏距离:这是最直接的距离度量方式,就像在二维平面上测量两点之间的直线距离那样。在高维空间中,两个向量的欧氏距离就是各个对应维度差的平方和的平方根。
  • 余弦相似度:在很多情况下,我们更关心向量的方向而不是它的大小。例如在文本处理中,一个词的向量可能会因为文本长度的不同,而在大小上有很大的差距,但方向更能反映其语义。余弦相似度就是度量向量之间方向的相似性,它的值范围在-1到1之间,值越接近1,表示两个向量的方向越相似。

这两种方法都被广泛应用于各种机器学习和人工智能任务中,选择哪一种方法取决于具体的应用场景。

image.png

  • 什么时候使用欧氏距离,什么时候使用余弦相似度?

简单来说,关心数量等大小差异时用欧氏距离,关心文本等语义差异时用余弦相似度。

具体来说,欧氏距离度量的是绝对距离,它能很好地反映出向量的绝对差异。当我们关心数据的绝对大小,例如在物品推荐系统中,用户的购买量可能反映他们的偏好强度,此时可以考虑使用欧氏距离。同样,在数据集中各个向量的大小相似,且数据分布大致均匀时,使用欧氏距离也比较适合。

余弦相似度度量的是方向的相似性,它更关心的是两个向量的角度差异,而不是它们的大小差异。在处理文本数据或者其他高维稀疏数据的时候,余弦相似度特别有用。比如在信息检索和文本分类等任务中,文本数据往往被表示为高维的词向量,词向量的方向更能反映其语义相似性,此时可以使用余弦相似度。

在这里,我们正在处理的是文本数据,目标是建立一个问答系统,需要从语义上理解和比较问题可能的答案。因此,我建议使用余弦相似度作为度量标准。通过比较问题和答案向量在语义空间中的方向,可以找到与提出的问题最匹配的答案。

在这一步的代码部分,我们会创建一个聊天模型。然后需要创建一个 RetrievalQA 链,它是一个检索式问答模型,用于生成问题的答案。

在RetrievalQA 链中有下面两大重要组成部分。

  • LLM是大模型,负责回答问题。
  • retriever(vectorstore.as_retriever())负责根据问题检索相关的文档,找到具体的“嵌入片”。这些“嵌入片”对应的“文档块”就会作为知识信息,和问题一起传递进入大模型。本地文档中检索而得的知识很重要,因为从互联网信息中训练而来的大模型不可能拥有“易速鲜花”作为一个私营企业的内部知识
# 4. Retrieval 准备模型和Retrieval链
os.environ["LLM_MODELEND"] = "ep-20241104131149-csxf9"  # 你的Doubao-pro-32k模型
import logging  # 导入Logging工具
from langchain_openai import ChatOpenAI  # ChatOpenAI模型
from langchain.retrievers.multi_query import (
    MultiQueryRetriever,
)  # MultiQueryRetriever工具
from langchain.chains import RetrievalQA  # RetrievalQA链

# 设置Logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

# 实例化一个大模型工具 - Doubao-pro-32k
llm = ChatOpenAI(
    api_key=api_key,
    base_url=base_url,
    model=os.environ["LLM_MODELEND"], 
    temperature=0)

# 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)

# 实例化一个RetrievalQA链
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_from_llm)

现在我们已经为后续的步骤做好了准备,下一步就是接收来自系统用户的具体问题,并根据问题检索信息,生成回答。

6.生成回答并展示

# 5. 使用RetrievalQA链进行问答
query = "公司的名字全称是什么"

result = qa_chain({"query": query})
print(result)

7.v1版本总体代码

好的,代码已经写完了,如果想要构建flask进行web搭建也是很简单的事情,问题不在于web界面的好不好看,而是在于这个系统的效果并不好。于是,我自己查看langchain官网并作出v2版本,以下是v1的全部代码,除了model和apikey需要替换,多个问题只需要重复执行第五步即可:


# 1.Load 导入Document Loaders
import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel
from volcenginesdkarkruntime import Ark

base_dir = "./OneFlower"  # 文档的存放目录
api_key = "" #YOURAPIKEY
base_url="https://ark.cn-beijing.volces.com/api/v3"
model = 'ep-20241111110355-2tp82' #YOUR_EMBDDING_MODELID
os.environ["LLM_MODELEND"] = "ep-20241104131149-csxf9"  # 你的Doubao-pro-32k模型
query = "公司的名字全称是什么" #你要提问的问题


# 加载Documents

documents = []
for file in os.listdir(base_dir):
    # 构建完整的文件路径
    file_path = os.path.join(base_dir, file)
    if file.endswith(".pdf"):
        loader = PyPDFLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".docx"):
        loader = Docx2txtLoader(file_path)
        documents.extend(loader.load())
    elif file.endswith(".txt"):
        loader = TextLoader(file_path)
        documents.extend(loader.load())


# 2.Split 将Documents切分成块以便后续进行嵌入和向量存储
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30)
chunked_documents = text_splitter.split_documents(documents)

# 3.Store 将分割嵌入并存储在矢量数据库Qdrant中


from langchain_community.vectorstores import Qdrant

class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = api_key
    model: str

    def __init__(self, **data: Any):
        super().__init__(**data)
        if self.api_key == "":
            self.api_key = os.environ["OPENAI_API_KEY"]
        self.client = Ark(
            base_url=base_url,
            api_key=self.api_key
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成输入文本的 embedding.
        Args:
            texts (str): 要生成 embedding 的文本.
        Return:
            embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
        """
        embeddings = self.client.embeddings.create(model=self.model, input=text)
        return embeddings.data[0].embedding

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        return [self.embed_query(text) for text in texts]

    class Config:
        arbitrary_types_allowed = True


vectorstore = Qdrant.from_documents(
    documents=chunked_documents,  # 以分块的文档
    embedding=DoubaoEmbeddings(
        model=model,
    ),  # 使用DoubaoEmbeddings进行嵌入
    location=":memory:",  # in-memory 存储
    collection_name="my_documents",
)  # 指定collection_name


# 4. Retrieval 准备模型和Retrieval链

import logging  # 导入Logging工具
from langchain_openai import ChatOpenAI  # ChatOpenAI模型
from langchain.retrievers.multi_query import (
    MultiQueryRetriever,
)  # MultiQueryRetriever工具
from langchain.chains import RetrievalQA  # RetrievalQA链

# 设置Logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

# 实例化一个大模型工具 - Doubao-pro-32k
llm = ChatOpenAI(
    api_key=api_key,
    base_url=base_url,
    model=os.environ["LLM_MODELEND"], 
    temperature=0)

# 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)

# 实例化一个RetrievalQA链
qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_from_llm)

# 5. 使用RetrievalQA链进行问答
result = qa_chain({"query": query})
print(result)

这是第三次看这串代码了,配合着官方文档终于弄懂了为啥没有正确给出答案的原因

如果你愿意去尝试print(len(documents))或者查看官方文档就会发现,答案是1,原因是extend这个方法并没有实现将所有文档进行合并,这也是为什么阿瓦达啃遍大瓜和我遇到的一样的问题,而且在书写的过程中,我发现LangChain的v2版本没有给出内部知识系统的例子,给出的方案仅支持langchain0.3的版本,不信的同学可以尝试,会有MIME不匹配的错误,因此v2版本我使用langchain的最新版本,下面给出v2版本的代码,注意很多环境已经更换。

8.v2版本代码,完全实现“本地搭建易速鲜花内部知识问答系统”

  • 配置环境:
pip install --upgrade 'volcengine-python-sdk[ark]'
pip install langchain==0.3.7
pip install langchain_community==0.3.7
pip install langchain_openai==0.2.8
pip install unstructured
pip install unstructured[pdf]
pip install unstructured[docx]
pip install python-magic
pip install python-magic-bin
#如果您使用linux系统,需要使用apt install libmagic1
#https://github.com/ahupp/python-magic/issues/313
pip install numpy==1.26.3 
  • 代码

# 1.Load 导入Document Loaders
import os
from langchain_community.document_loaders import DirectoryLoader
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel
from volcenginesdkarkruntime import Ark

base_dir = "./OneFlower"  # 文档的存放目录
api_key = "" #YOURAPIKEY
base_url="https://ark.cn-beijing.volces.com/api/v3"
model = 'ep-20241111110355-2tp82' #YOURMODELID
os.environ["LLM_MODELEND"] = "ep-20241104131149-csxf9"  # 你的Doubao-pro-32k模型

# 加载Documents

documents = []
loader = DirectoryLoader(
    base_dir,
    show_progress=True
    ) #默认读取全部非隐藏文件
documents = loader.load()

from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)

class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = api_key
    model: str

    def __init__(self, **data: Any):
        super().__init__(**data)
        if self.api_key == "":
            self.api_key = os.environ["OPENAI_API_KEY"]
        self.client = Ark(
            base_url=base_url,
            api_key=self.api_key
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成输入文本的 embedding.
        Args:
            texts (str): 要生成 embedding 的文本.
        Return:
            embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
        """
        embeddings = self.client.embeddings.create(model=self.model, input=text)
        return embeddings.data[0].embedding

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        return [self.embed_query(text) for text in texts]

    class Config:
        arbitrary_types_allowed = True



splits = text_splitter.split_documents(documents)
vectorstore = InMemoryVectorStore.from_documents(
    documents=splits, embedding=DoubaoEmbeddings(
        model=model
    )
)

retriever = vectorstore.as_retriever()

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

system_prompt = (
    "You are an assistant for question-answering tasks. "
    "Use the following pieces of retrieved context to answer "
    "the question. If you don't know the answer, say that you "
    "don't know. Use three sentences maximum and keep the "
    "answer concise."
    "\n\n"
    "{context}"
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{input}"),
    ]
)

from langchain_openai import ChatOpenAI  # ChatOpenAI模型
# 实例化一个大模型工具 - Doubao-pro-32k
llm = ChatOpenAI(
    api_key=api_key,
    base_url=base_url,
    model=os.environ["LLM_MODELEND"], 
    temperature=0)

question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)

results = rag_chain.invoke({"input": "易速鲜花员工手册中董事长致辞中的企业精神有哪些"})

# results
# print(results)
# print(results["context"][0].page_content)
# print(results["context"][0].metadata)
print(results["answer"])

image.png

Nice 终于完结这章内容

9.延申阅读

1.LangChain官网

2.What is Document Question Answering? - Hugging Face

3,论文开放式表格与文本问题回答:[2010.10439] Open Question Answering over Tables and Text (arxiv.org)

4.Tasks - Hugging Face