终于来了!本地搭建“易速鲜花”内部知识问答系统 | 豆包MarsCode AI刷题

669 阅读9分钟

调用火山引擎大语言模型 & Embedding向量模型,本地搭建“易速鲜花”智能问答系统(完整代码在文章最后,但是你最好不要直接拉到最后)

image.png

前文勘误

错误:

在文章 https://juejin.cn/post/7433054967390535730之中,我讲了关于“在anoconda prompt中直接设置环境变量”的内容,该内容有些错误,这种方法设置的环境变量仅限于当前的虚拟环境下使用。设置环境变量的时候还是直接手动设置更稳妥(可能需要重启IDE或电脑)。

命令行设置的方法需要使用windows终端命令行(管理员),感兴趣的可以自行搜索一下,这里不再一一阐述。

一、 项目分析

  • 任务描述:使用一些本地文件(即知识库),通过文本分割、嵌入向量数据库等操作转换为大模型可以理解的数据形式,从而快速搭建一个问答系统。

  • 具体步骤:

  1. 文本处理:导入并加载本地文件,将文件内容分割成一段段的文本块;
  2. 数据转换:调用预训练好的Embedding模型,将第一步中得到的文本块转换为向量数据,并嵌入向量数据库中;
  3. 构建问答链:调用大语言模型,到向量数据库中检索与问题相似的内容,并生成答案;
  4. 问答交互:构建应用程序,设计前端页面。

二、 环境相关:

  1. 本项目使用到了预训练的Embedding模型(如果看过我之前的juejin.cn/post/743379… 应该都创建好Doubao-embedding模型了),现在需要将该模型的模型端点添加至环境变量中,变量名设置为EMBEDDING_END;同时将火山引擎的urlhttps://ark.cn-beijing.volces.com/api/v3也添加至环境变量中,变量名设置为ARK_BASE_ URL;(模型端点不建议直接写在代码里,url可以直接写在代码里,但是每一处使用url的代码都要做相应修改、不要遗漏。)
  2. 激活相应环境,安装相关库;
conda install conda-forge::pypdf
conda install conda-forge::docx2txt
conda install conda-forge::flask
  1. 由于虚拟环境的不同,可能还需要大家自行安装一些其他库,至于是哪些库遇到报错就知道了【手动狗头】。

image.png

4. 文件结构,其中`_app`是代码文件

image.png

三、代码分析

1. 导入相关库/函数,获取环境变量

  • 本例使用的是豆包模型,需要引入langchain.embeddings.base中的Embeddings类以及pydantic中的BaseModel类。
import os
import logging
# 模型
from volcenginesdkarkruntime import Ark
from langchain_openai import ChatOpenAI
from langchain.embeddings.base import Embeddings
from pydantic import BaseModel
from typing import Dict, List, Any
# 文本分割与向量数据转换、存储
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Qdrant
# 检索问答链
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.chains import RetrievalQA
# 问答交互
from flask import Flask, request, render_template
# 获取相关环境变量
api_key = os.environ.get("ARK_API_KEY")
model = os.environ.get("LLM_MODELEND")
model_embedding = os.environ.get("EMBEDDING_END")
url = os.environ.get("ARK_BASE_URL")

2. 文件加载与文本分割

此代码块中我使用了os库中定位绝对路径的函数,可以获得当前python文件所在的文件夹。(个人习惯而已,直接按照掘金小册子的写法也行)

  1. 查找知识库文档所在的文件夹,并且按照文件后缀名处理文件;
  2. 实例化一个文本分割器,其中chunk_size=200表示分割后的每个文本块大小为200个字符,chunk_overlap=30表示相邻的文本块有30个字符的重复,以确保语义连贯性和上下文检索。
# 加载文档文本
base_dir = os.path.dirname(os.path.abspath(__file__))   # 获取本py文件所在的绝对路径
folder_path = os.path.join(base_dir, 'docums')      # 文档所在文件夹的绝对路径
documents = []
# 遍历文件夹的每个文件
for file in os.listdir(folder_path):
    file_path = os.path.join(folder_path, 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())

# 文本分割
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30)
chunk_docs = text_splitter.split_documents(documents)

3. 数据转换:

  1. 首先,定义一个DoubaoEmbeddings类,该类继承Embeddings类和BaseModel类。
  • __init__初始化中,使用super().__init__初始化父类;并且判断api_key和model是否为空;最后实例化一个Ark对象,用于生成嵌入向量;
  • embed_query方法接收一个文本字符串,使用Ark客户端生成嵌入向量并返回;
  • embed_documents对于文本列表中的每一个文本字符串,循环调用embed_query方法并返回一个嵌入向量列表;
  • Config类中的arbitrary_types_allowed = True 允许 Pydantic 处理任意类型,这对于嵌入模型是必要的。
  1. 使用 model 和 api_key 参数创建一个 DoubaoEmbeddings 实例,并且将该实例以及分割好的文本chunk_docs传入 Qdrant.from_documents 方法。
# 定义自己的Doubao-embedding类
class DoubaoEmbeddings(BaseModel, Embeddings):
    client: Ark = None
    api_key: str = ""
    model: str

    def __init__(self, **data: Any,):
        super().__init__(**data)
        # 这里是为了防止未获取到环境变量或者实例化类时未输入环境变量参数,手动添加了api_key和url
        if self.api_key == "":
            self.api_key = "your_api_key"
        if self.model == "":
            self.model = "your_embedding_end"
        self.client = Ark(
            base_url=url,
            api_key=self.api_key,
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成用于接收输入文本的embeddings(List[float]),一个浮点数值列表.
        :param text: 要生成 embedding 的文本.
        :type text: str
        :return: embeddings: 输入文本的 embedding,一个浮点数值列表.
        :rtype: 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

# 将分割好的文本嵌入向量数据库Qdrant
vectorstore = Qdrant.from_documents(
    documents=chunk_docs,
    embedding=DoubaoEmbeddings(model=model_embedding, api_key=api_key,),
    location=":memory:",
    collection_name="my_docs",
)

4. 创建检索问答链:

  1. 首先创建日志,以方便查看运行过程中的状态及问题;
  2. 实例化一个Chat大模型对象,并利用该对象生成QA链。
# 创建检索式问答模型:Retrieval链
# 设置日志,记录系统每次运行的状态
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

# 实例化大模型对象
llm = ChatOpenAI(
    openai_api_key=api_key,
    openai_api_base=url,
    model=model,
    temperature=0,
)

# 实例化一个多查询检测工具
retriever_llm = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(), llm=llm)

# 实例化RetrivalQA链
retrieval_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_llm)

5. 问答交互和运行(直接copy小册子)

  1. 使用index.html中的模板实例化一个Flask应用
  2. 127.0.0.1 被称为回环地址(loopback address),它总是指向本地主机,即运行它的设备的网络接口。这个地址常用于网络软件的测试和诊断,因为它允许设备与自己通信,而不需要通过外部网络,端口5000是Flask应用的默认端口。
# 实现问答交互
template_path = os.path.join(base_dir, 'templates')
app = Flask(__name__, template_folder=template_path)

@app.route('/', methods=['GET', 'POST'])
def homepage():
    if request.method == 'POST':
        # 接收用户输入的问题
        question = request.form.get('question')
        # RetrievalQA链读取问题,并返回答案
        result = retrieval_chain({'query': question})
        # 网页渲染
        return render_template('index.html', result=result)
    return render_template('index.html')


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

6. index.html文件(依旧直接copy)

<body>
    <div class="container">
        <div class="header">
            <h1>易速鲜花内部问答系统</h1>
            <img src="{{ url_for('static', filename='flower.png') }}" alt="flower logo" width="200">
        </div>
        <form method="POST">
            <label for="question">Enter your question:</label><br>
            <input type="text" id="question" name="question"><br>
            <input type="submit" value="Submit">
        </form>
        {% if result is defined %}
            <h2>Answer</h2>
            <p>{{ result.result }}</p>
        {% endif %}
    </div>
</body>

7. 运行结果

点击蓝色下划线网址,即可进入问答页面 image.png

PS:图片是我自己在网上下载的 image.png

一些问题与总结

不太稳定,有些存在知识库的问题只会回答“不知道”,还有花语的问题只会回答txt文件中最后一行的花及其花语或者回答“不知道”。可能是本地文件的问题,或者文本处理的问题,暂时我还没有去解决,估计现在我还不具备这个能力,希望下面的课对解决这个问题有帮助吧。

  1. langchain提供了调用多种大模型的接口确实很方便,但是某些方法的使用需要下功夫查看相应的使用声明:选中一个库或者方法——>单机右键后鼠标移至Go To——> 选择Declaration and Usages。建议百度、gpt和使用声明一起使用,这样比较简单快速; image.png

  2. 学会看报错很重要,这一点我在上篇文章的总结里已经提到过,看报错无非就是Traceback第一行(有时候涉及到多个报错例如多个import方法被弃用时可能分成多行进行提示)和最后一行,并且也会给出代码出错位置,看懂之后能解决大部分的问题。建议:如果英文有时候看不懂复制到翻译软件上看,然后自己回代码中查错误,锻炼自己debug的能力(设置断点一行一行进行调试的方法,通常用于实现某些数学算法的程序中、查找哪一个步骤的输出有问题);

  3. 建议首次学习时不要私自改变量名,因为你也不知道什么名字会不会凑成什么巧合,查起问题来会很痛苦,有的问题可能还没有明显的报错;

image.png

  1. 学习要脚踏实地、稳扎稳打,不能急于求成;

完整代码

image.png

import os
import logging

from volcenginesdkarkruntime import Ark
from langchain_openai import ChatOpenAI
# from langchain_openai.embeddings import OpenAIEmbeddings
from langchain.embeddings.base import Embeddings
from pydantic import BaseModel
from typing import Dict, List, Any

from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import Docx2txtLoader
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Qdrant
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.chains import RetrievalQA

from flask import Flask, request, render_template

api_key = os.environ.get("ARK_API_KEY")
model = os.environ.get("LLM_MODELEND")
model_embedding = "ep-20241108110047-jln8b"  # os.environ.get("EMBEDDING_END")
url = os.environ.get("ARK_BASE_URL")

# 加载文档文本
base_dir = os.path.dirname(os.path.abspath(__file__))   # 获取本py文件所在的绝对路径
folder_path = os.path.join(base_dir, 'docums')      # 文档所在文件夹的绝对路径
documents = []
# 遍历文件夹的每个文件
for file in os.listdir(folder_path):
    file_path = os.path.join(folder_path, 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())

# 文本分割
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=30)
chunk_docs = text_splitter.split_documents(documents)

# print(documents)


# 定义自己的Doubao-embedding类
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.get("ARK_API_KEY")
        # if self.model == "":
        #     self.model = os.environ.get("EMBEDDING_END")
        self.client = Ark(
            base_url=url,
            api_key=self.api_key,
        )

    def embed_query(self, text: str) -> List[float]:
        """
        生成用于输入文本的embeddings(List[float]),一个浮点数值列表.
        :param text: 要生成 embedding 的文本.
        :type text: str
        :return: embeddings: 输入文本的 embedding,一个浮点数值列表.
        :rtype: 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


# 将分割好的文本嵌入向量数据库Qdrant
vectorstore = Qdrant.from_documents(
    documents=chunk_docs,
    embedding=DoubaoEmbeddings(model=model_embedding, api_key=api_key,),
    location=":memory:",
    collection_name="my_docs",
)

# 创建检索式问答模型:Retrieval链
# 设置日志
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)

# 实例化大模型对象
llm = ChatOpenAI(
    openai_api_key=api_key,
    openai_api_base=url,
    model=model,
    temperature=0,
)

# 实例化一个多查询检测工具
retriever_llm = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(), llm=llm)

# 实例化RetrivalQA链
retrieval_chain = RetrievalQA.from_chain_type(llm, retriever=retriever_llm)

# 实现问答交互
template_path = os.path.join(base_dir, 'templates')
app = Flask(__name__, template_folder=template_path)


@app.route('/', methods=['GET', 'POST'])
def homepage():
    if request.method == 'POST':
        # 接收用户输入的问题
        question = request.form.get('question')
        # RetrievalQA链读取问题,并返回答案
        result = retrieval_chain({'query': question})
        # 网页渲染
        return render_template('index.html', result=result)
    return render_template('index.html')


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