AI实战课程笔记-02

90 阅读9分钟

构建基于“易速鲜花”本地知识库的智能问答系统

1 项目名称

“易速鲜花”内部员工知识库问答系统

2 项目介绍

为解决信息分散、文档冗长的问题,希望智能问答系统可以理解问题并给出答案。

3 开发框架

  • 数据源(Data Sources): 数据可以有很多种,包括PDF在内的非结构化的数据(Unstructured Data)、SQL在内的结构化的数据(Structured Data),以及Python、Java之类的代码(Code)。在这个示例中,我们聚焦于对非结构化数据的处理。
  • 大模型应用(Application,即LLM App): 以大模型为逻辑引擎,生成我们所需要的回答。
  • 用例(Use-Cases): 大模型生成的回答可以构建出QA/聊天机器人等系统。

4 核心实现机制;数据处理管道

  1. Loading 数据的准备和载入
    • 导入正确的OpenAI Key
    • 安装工具包
  2. Splitting 文本的分割
  3. Storage 向量的数据库存储
  4. Retrieval 相关信息的获取
    • 向量距离或相似度计算方法
      • 欧式距离
      • 余弦距离
    • RetrievalQA链
      • LLM负责回答问题
      • Retriever(vectorstore.as_retriever())负责跟进问题检索相关的文档,找到具体的“嵌入片”
  5. Output 生成回答并展示

4.1 Loading

base_dir = "./OneFlower"

定义文档的存放目录base_dir"./OneFlower",即根目录下的OneFlower文件夹中。

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())
  • os.listdir(base_dir)列出了base_dir目录下的所有文件和子目录。遍历其中的所有文档,完成文档加载操作。
  • os.path.join()函数是Python中用于路径拼接的函数,它可以将多个路径组件组合成一个完整的路径。通过os.path.join()函数,将base_dirfile拼接在一起,就得到了一个完整的文件路径,即file_path
  • 对于每个文件名,代码检查其扩展名,以确定应该使用哪种加载器:
    • 如果文件以 .pdf 结尾,使用 PyPDFLoader 加载器来加载 PDF 文件。
    • 如果文件以 .docx 结尾,使用 Docx2txtLoader 加载器来加载 Word 文档。
    • 如果文件以 .txt 结尾,使用 TextLoader加载器来加载文本文件。
  • loader.load() 调用了特定类型的文档加载器(如 PyPDFLoaderDocx2txtLoader 或 TextLoader)的 load 方法,该方法返回一个包含文档内容的列表。然后 documents.extend(...) 将这个列表扩展到 documents 列表中,即把新加载的文档内容添加到 documents 列表的末尾。

4.2 Splitting

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=200,chunk_overlap=10)
chunked_documents = text_splitter.split_documents(documents)
  • 利用文本分割器 RecursiveCharacterTextSplitter 将文本分割成多个部分。
  • 创建一个 RecursiveCharacterTextSplitter 实例 text_splitter ,并设置了两个参数:
    • chunk_size=200:表示每个文本块的最大字符数为 200。
    • chunk_overlap=10:表示每个文本块之间的重叠字符数为 10。这意味着每个文本块的末尾会包含前一个文本块的最后 10 个字符,以便在分割时保持上下文的连贯性。
  • 调用 text_splitter 的 split_documents 方法,将 documents 列表中的每个文档分割成多个文本块,并将这些文本块存储在 chunked_documents 列表中。

4.3 Storage

定义DoubaoEmbeddings类

class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = ""
    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=os.environ["OPENAI_BASE_URL"],
            api_key=self.api_key
        )

    def embed_query(self, text: str) -> List[float]:
        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
class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = ""
    model: str
  • 这里定义了一个新类 DoubaoEmbeddings,它继承自 BaseModel 和 EmbeddingsBaseModel 是 pydantic 库中的一个类,用于定义数据模型,而 Embeddings 是 langchain 库中的一个类,用于定义嵌入模型。
  • 下面这些属性定义了类的成员变量。client 是一个 Ark 类型的对象,用于与 API 进行交互。api_key 是 API 密钥,model 是嵌入模型的名称。
    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=os.environ["OPENAI_BASE_URL"],
            api_key=self.api_key
        )
  • 定义了一个名为__init__的方法,它的主要功能是初始化一个对象的属性,并设置与OpenAI API通信所需的参数。**data: Any表示该方法接受任意数量的关键字参数,并且这些参数可以是任何类型。

  • super().__init__(**data):调用了父类的构造函数,确保父类的初始化过程也被执行。

  • 接下来检查self.api_key是否为空。如果为空,则从环境变量中获取OPENAI_API_KEY的值,并将其赋值给self.api_key

  • 然后创建了一个Ark对象,它是一个用于与OpenAI API通信的客户端。base_urlapi_key是初始化Ark客户端所需的参数。base_url从环境变量中获取,而api_key则是之前设置的值。

    def embed_query(self, text: str) -> List[float]:
        embeddings = self.client.embeddings.create(model=self.model, 
        input=text)
        return embeddings.data[0].embedding
  • 定义了一个名为 embed_query 的嵌入查询方法,这个方法接受一个字符串参数 text,该参数表示要生成embedding的文本,并返回一个浮点数值列表,表示输入文本的嵌入向量(embedding)。
  • 调用了 self.client 的 embeddings.create 方法,传入了 model和 input 参数。self.client 是一个 Ark对象,用于与 OpenAI API 进行交互。model 参数指定了使用的嵌入模型,input 参数是要生成嵌入向量的文本。
  • embeddings.data是一个列表,包含了所有生成的嵌入向量,embeddings.data[0] 则是第一个嵌入向量。该方法最后返回了从 embeddings 对象中提取出第一个嵌入向量。
    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        return [self.embed_query(text) for text in texts]
  • 定义了一个名为 embed_documents 的方法,它接受一个字符串列表参数 texts,并返回一个嵌套的浮点数值列表,表示输入文本列表的嵌入向量列表。
  • [self.embed_query(text) for text in texts]使用列表推导式,对输入的文本列表 texts 中的每个文本调用 self.embed_query 方法,生成对应的嵌入向量,并将这些向量组成一个列表返回。
    class Config:
        arbitrary_types_allowed = True
  • 这个类定义了 DoubaoEmbeddings 类的配置。arbitrary_types_allowed = True 允许在模型中使用任意类型。

创建 Qdrant 向量存储实例

vectorstore = Qdrant.from_documents(
    documents=chunked_documents, 
    embedding=DoubaoEmbeddings(
        model=os.environ["EMBEDDING_MODELEND"],
    ), # 用OpenAI的Embedding Model做嵌入
    location=":memory:", # in-memory 存储
    collection_name="my_documents",
    ) # 指定collection_name
  • 利用 Qdrant.from_documents 方法用于从文档和嵌入模型创建一个 Qdrant 向量存储实例 vectorstore
  • 准备文档 chunked_documents 是已经分块处理后的文档列表。
  • 设置嵌入模型 DoubaoEmbeddings 是一个我们自定义的嵌入模型类,它使用环境变量中的 EMBEDDING_MODELEND 作为模型名称。这个模型用于将文档转换为向量表示。
  • 指定存储位置location=":memory:" 表示将向量存储在内存中,这是一个临时的存储方式,通常用于测试或演示目的。
  • 指定集合名称collection_name="my_documents" 定义了在 Qdrant 数据库中存储这些向量的集合名称。

4.4 retrieval

设置Logging

logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)
  • 设置基本日志配置:调用logging模块的basicConfig方法,该方法用于进行日志系统的基本配置。默认情况下,它会配置一个根日志记录器(root logger),并设置日志级别为WARNING,日志格式为默认格式。
  • 设置特定模块的日志级别:调用logging模块的getLogger方法获取名为langchain.retrievers.multi_query的日志记录器,并将其日志级别设置为INFO。这意味着该模块的日志信息将被记录,并且只有INFO级别及以上的日志会被输出。

实例化一个大模型工具

llm = ChatOpenAI(model=os.environ["LLM_MODELEND"], temperature=0)
  • 实例化了一个ChatOpenAI对象,指定从环境变量中获取模型名称。temperature=0 表示模型将生成最确定的文本。 实例化一个MultiQueryRetriever
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)
  • MultiQueryRetriever.from_llm 是 MultiQueryRetriever 类的一个静态方法,用于从语言模型(LLM)和检索器(retriever)创建一个 MultiQueryRetriever 对象。MultiQueryRetriever 对象是 langchain 库中的一个工具,它的主要作用是从文本数据中生成多个查询,然后使用这些查询从向量数据库中检索相关的文档。
  • retriever(vectorstore.as_retriever())负责根据问题检索相关的文档,找到具体的“嵌入片”。这些“嵌入片”对应的“文档块”就会作为知识信息,和问题一起传递进入大模型。
  • LLM是大模型,负责回答问题。我们将之前创建的 llm 对象(一个 ChatOpenAI 实例)作为参数传递给 from_llm 方法。

实例化一个RetrievalQA链

qa_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_from_llm)
  • 创建了一个 RetrievalQA 对象,该对象是 langchain 库中的一个类,用于构建基于检索的问答系统。
  • 该代码调用了 RetrievalQA 类的 from_chain_type 方法,该方法用于从给定的语言模型(llm)和检索器(retriever)创建一个 RetrievalQA 对象

4.5 Output

这部分将创建一个简单的 Web 应用,用户可以通过表单提交问题,然后应用会使用 qa_chain生成答案,并将答案渲染在网页上。

导入必要模块并创建Flask应用实例

from flask import Flask, request, render_template

app = Flask(__name__) # Flask APP
  • 导入了 Flask 框架的核心模块 Flask,以及用于处理 HTTP 请求的 request 模块和用于渲染模板的 render_template 模块。
  • 创建了一个 Flask 应用实例,并将当前模块的名称作为参数传递给它。

定义路由和视图函数

@app.route("/", methods=["GET", "POST"])
  • 定义了一个路由,该路由处理根路径(即 /)的请求,并且支持 GET 和 POST 方法。
def home():
    if request.method == "POST":
        # 接收用户输入作为问题
        question = request.form.get("question")
        # RetrievalQA链 - 读入问题,生成答案
        result = qa_chain({"query": question})
        # 把大模型的回答结果返回网页进行渲染
        return render_template("index.html", result=result)
    return render_template("index.html")
  • 在 home 函数内部,首先检查请求方法是否为 POST。
  • 如果是 POST 请求,则从表单中获取用户输入的问题,并使用 qa_chain 生成答案。然后,将答案作为参数传递给 render_template 函数,渲染名为 index.html 的模板,并将结果返回给用户。
  • 如果请求方法是 GET,则直接渲染 index.html 模板,不进行任何其他操作。

运行应用

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True, port=5000)

这段代码确保只有在直接运行当前脚本时才会启动 Flask 应用。app.run 函数启动了一个开发服务器,监听所有网络接口(host="0.0.0.0"),开启调试模式(debug=True),并使用端口 5000。