LangChain实战课—“易速鲜花”本地部署 | 豆包MarsCode AI刷题

483 阅读10分钟

此项目为初步了解LangChian作为一个基于大语言模型的应用开发框架,其功能的强大。用于深入学习LangChain(包括每一个组件)之前。


项目及实现框架

项目名称:“易速鲜花”内部员工知识库问答系统。

项目介绍:“易速鲜花”作为一个大型在线鲜花销售平台,有自己的业务流程和规范,也拥有针对员工的SOP手册。新员工入职培训时,会分享相关的信息。但是,这些信息分散于内部网和HR部门目录各处,有时不便查询;有时因为文档过于冗长,员工无法第一时间找到想要的内容;有时公司政策已更新,但是员工手头的文档还是旧版内容。

基于上述需求,我们将开发一套基于各种内部知识手册的 “Doc-QA” 系统。这个系统将充分利用LangChain框架,处理从员工手册中产生的各种问题。这个问答系统能够理解员工的问题,并基于最新的员工手册,给出精准的答案。

开发框架:整个框架分为三个部分:

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

实现机制:大模型应用作为“服务器”,根据用例在数据源中找到答案并返回给用户。

img

核心实现机制: 这个项目的核心实现机制是下图所示的数据处理管道(Pipeline)。

img

在管道的每一步中,LangChain都提供了相关工具,这就是LangChain的强大之处。

具体流程是:

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

前三步为准备数据源,第四步是将用户提出的问题同样切分等操作得到一个向量,再与数据源中的向量数据库通过余弦相似度等找出类似的问题答案。【这些都是数据分析的知识】

接下来将具体讲解上面的五步操作。

项目结构:

 易速鲜花内部员工知识库问答系统
     │  DocQA.py
     │
     ├─OneFlower
     │      易速鲜花员工手册.pdf
     │      易速鲜花运营指南.docx
     │      花语大全.txt
     │
     ├─static
     │      flower.png
     │
     └─templates
             index.html

1. 加载文件

首先要准备文件,这里先给你提供样例文件:下载OneFlower中的三个文件。

我们首先用langchain_community中的document_loaders来加载各种格式的文本文件;在这一步中,我们从pdf、word和txt文件中加载文本并将这些文本存储在一个列表中。(可能还需要安装PyPDF、Docx2txt等库,根据报错安装即可)

代码如下:

 # 1.loading: 文档加载器把Documents 加载为以LangChain能够读取的形式。
 import os
 from langchain_community.document_loaders import PyPDFLoader,Docx2txtLoader,TextLoader
 ​
 # 加载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())

2. 切分文件

我们已经得到了文本信息(存储在documents数组中),接下来我们就需要将文本切分成更小的块,以便于进行嵌入和向量存储。

相关工具:LangChain中的RecursiveCharacterTextSplitter

代码如下:

 # 2.Splitting 文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
 from langchain.text_splitter import RecursiveCharacterTextSplitter
 ​
 text_splitter = RecursiveCharacterTextSplitter(chunk_size=200,chunk_overlap=10)
 chunk_documents = text_splitter.split_documents(documents)

说明:chunk_size参数为切分的文档块chunk的最大长度,这里设定200。chunk_overlap参数为相邻两个chunk之间的重叠token数量,保证文本语义的连贯性。


3. 嵌入及存储

数据源的最后一步,我们将这些分割后的文本转换成嵌入的形式,并将其存储在一个向量数据库中。

相关工具:

  • 嵌入工具:OpenAIEmbeddings(OpenAI的Embedding Model)
  • 向量数据库:Qdrant(需安装库qdrant-client

注意:

  1. 处理OpenAI的模型,还可以使用其他的嵌入模型,如Doubao-embedding-large等
  2. LangChain中支持很多向量数据库,比如Pinecone、Chroma和Qdrant,有些是收费的,有些则是开源的。Qdrant是开源向量数据库。
  3. Qdrant.from_documents返回的是一个Qdrant类的实例,这个实例代表了一个向量数据库。这个函数是创建一个向量数据库并且根据参数存储在指定位置(location=":memory:"表示存储在内存中)。可以根据这个实例访问这个向量数据库。

代码如下:

  1. 使用OpenAI模型:
 # 3.Store 将分割嵌入并存储在矢量数据库Qdrant中
 from langchain_community.vectorstores import Qdrant
 from langchain.embeddings import OpenAIEmbeddings
 ​
 vecto_rstore = Qdrant.from_documents(
     documents=chunked_documents, # 以分块的文档
     embedding=OpenAIEmbeddings(), # 用OpenAI的Embedding Model做嵌入
     location=":memory:",  # 存储在内存中
     collection_name="my_documents", # 指定collection_name
 ) 

2. 使用豆包模型:

使用豆包模型就有些复杂,首先你需要在[火山方舟管理控制台 (volcengine.com)](https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint?config=%7B%7D)新创建一个Doubao-embedding-large或Doubao-embedding的接入点,然后在API调用中获取其base\_url和model,接着创建对应的环境变量(当然可以直接定义);

安装包`volcengine-python-sdk`:pip install volcengine-python-sdk;

定义一个DoubaoEmbeddings类(目前还没弄明白实现原理),然后做嵌入使用这个类
 # 3.Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
 from typing import Dict, List, Any
 from langchain.embeddings.base import Embeddings
 from langchain.pydantic_v1 import BaseModel
 from volcenginesdkarkruntime import Ark
 ​
 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["ARK_API_KEY"]
         self.client = Ark(
             base_url=os.environ.get("EMBEDDING_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
 ​
 from langchain_community.vectorstores import Qdrant
 ​
 vector_store = Qdrant.from_documents(
     documents=chunk_documents,  # 以分块的文档
     embedding=DoubaoEmbeddings(
         model=os.environ.get("EMBEDDING_MODELEND"),
     ),   # 用豆包的Embedding Model做嵌入
     location=":memory:",    # 存储在内存中
     collection_name="my_document",  # 指定collection_name
 )

4. 检索

检索器

相关工具:MultiQueryRetriever工具、RetrievalQA链

此工具用于从向量存储(vector_store)中检索与查询相关的多个文档。

检索器即retriever(vector_store.as_retriever()),将向量存储转化为检索器,然后使用MultiQueryRetriever利用大型语言模型的能力来创建一个多查询检索器,这个检索器可以处理和优化多个查询,从而提高检索的效果和效率。

最后使用LangChain下的RetrievalQA建立“一条链QA”,它的作用将用户与查询并返回形成“一条链”。之后通过RetrievalQA.from_chain_type函数返回的对象传入字典{“query”:问题}就可以使用了。

 # 4.Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片).
 import logging
 from langchain_openai import ChatOpenAI
 from langchain.retrievers.multi_query import MultiQueryRetriever
 from langchain.chains import RetrievalQA
 ​
 # 设置Logging
 logging.basicConfig()
 logging.getLogger('langchain.retrievers.multi_query').setLevel(logging.INFO)
 ​
 # 实例化一个大模型工具 豆包
 chat = ChatOpenAI(
     api_key = os.environ.get("ARK_API_KEY"),
     base_url = os.environ.get("BASE_URL"),
     model = os.environ.get("LLM_MODELEND"),
     temperature = 0.8,
     max_tokens = 600,
 )
 # 实例化一个MultiQueryRetriever
 retriever_from_llm = MultiQueryRetriever.from_llm(retriever=vector_store.as_retriever(), llm=chat)
 # 实例化一个RetrievalQA链
 qa_chain=  RetrievalQA.from_chain_type(chat,retriever=retriever_from_llm)

注意:logging的作用是对查询建立日志;可以去掉【但还是不要去掉】


5. 返回结果并可视化

可视化用一个flask建立一个网页,然后通过输入问题向服务端请求,之后返回结果到页面即可。

 # 5.Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案。
 from flask import Flask,request,render_template
 ​
 app = Flask(__name__) # Flask APP
 ​
 @app.route('/',methods=['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')
 ​
 if __name__ == '__main__':
     app.run(host='0.0.0.0', debug=True, port=5000)

html代码可根据你的需要进行更改,以下是样例(注意html放入template文件夹中):

 <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>

最终代码

 # 1.loading 文档加载器把Documents 加载为以LangChain能够读取的形式。
 import os
 from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader
 ​
 # 加载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())
 ​
 # 2.Splitting 文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
 from langchain.text_splitter import RecursiveCharacterTextSplitter
 ​
 text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
 chunk_documents = text_splitter.split_documents(documents)
 ​
 # 3.Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
 from typing import Dict, List, Any
 from langchain.embeddings.base import Embeddings
 from pydantic.v1 import BaseModel
 from volcenginesdkarkruntime import Ark
 ​
 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["ARK_API_KEY"]
         self.client = Ark(
             base_url=os.environ.get("EMBEDDING_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
 ​
 from langchain_community.vectorstores import Qdrant
 ​
 vector_store = Qdrant.from_documents(
     documents=chunk_documents,  # 以分块的文档
     embedding=DoubaoEmbeddings(
         model=os.environ.get("EMBEDDING_MODELEND"),
     ),  # 用豆包的Embedding Model做嵌入
     location=":memory:",  # 存储在指定路径
     collection_name="my_document",  # 指定collection_name
 )
 ​
 ​
 ​
 # 4.Retrieval:应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片).
 import logging
 from langchain_openai import ChatOpenAI
 from langchain.retrievers.multi_query import MultiQueryRetriever
 from langchain.chains import RetrievalQA
 ​
 # 设置Logging
 logging.basicConfig()
 logging.getLogger('langchain.retrievers.multi_query').setLevel(logging.INFO)
 ​
 # 实例化一个大模型工具 豆包
 chat = ChatOpenAI(
     api_key = os.environ.get("ARK_API_KEY"),
     base_url = os.environ.get("BASE_URL"),
     model = os.environ.get("LLM_MODELEND"),
     temperature = 0.8,
     max_tokens = 600,
 )
 # 实例化一个MultiQueryRetriever
 retriever_from_llm = MultiQueryRetriever.from_llm(retriever=vector_store.as_retriever(), llm=chat)
 # 实例化一个RetrievalQA链
 qa_chain=  RetrievalQA.from_chain_type(chat,retriever=retriever_from_llm)
 ​
 # 5.Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案。
 from flask import Flask,request,render_template
 ​
 app = Flask(__name__) # Flask APP
 ​
 @app.route('/',methods=['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')
 ​
 if __name__ == '__main__':
     app.run(host='0.0.0.0', debug=True, port=5000)