构建基于“易速鲜花”本地知识库的智能问答系统
1 项目名称
“易速鲜花”内部员工知识库问答系统
2 项目介绍
为解决信息分散、文档冗长的问题,希望智能问答系统可以理解问题并给出答案。
3 开发框架
- 数据源(Data Sources): 数据可以有很多种,包括PDF在内的非结构化的数据(Unstructured Data)、SQL在内的结构化的数据(Structured Data),以及Python、Java之类的代码(Code)。在这个示例中,我们聚焦于对非结构化数据的处理。
- 大模型应用(Application,即LLM App): 以大模型为逻辑引擎,生成我们所需要的回答。
- 用例(Use-Cases): 大模型生成的回答可以构建出QA/聊天机器人等系统。
4 核心实现机制;数据处理管道
- Loading 数据的准备和载入
- 导入正确的OpenAI Key
- 安装工具包
- Splitting 文本的分割
- Storage 向量的数据库存储
- Retrieval 相关信息的获取
- 向量距离或相似度计算方法
- 欧式距离
- 余弦距离
- RetrievalQA链
- LLM负责回答问题
- Retriever(vectorstore.as_retriever())负责跟进问题检索相关的文档,找到具体的“嵌入片”
- 向量距离或相似度计算方法
- 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_dir和file拼接在一起,就得到了一个完整的文件路径,即file_path。- 对于每个文件名,代码检查其扩展名,以确定应该使用哪种加载器:
- 如果文件以
.pdf结尾,使用PyPDFLoader加载器来加载 PDF 文件。 - 如果文件以
.docx结尾,使用Docx2txtLoader加载器来加载 Word 文档。 - 如果文件以
.txt结尾,使用TextLoader加载器来加载文本文件。
- 如果文件以
loader.load()调用了特定类型的文档加载器(如PyPDFLoader、Docx2txtLoader或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和Embeddings。BaseModel是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_url和api_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。