此项目为初步了解LangChian作为一个基于大语言模型的应用开发框架,其功能的强大。用于深入学习LangChain(包括每一个组件)之前。
项目及实现框架
项目名称:“易速鲜花”内部员工知识库问答系统。
项目介绍:“易速鲜花”作为一个大型在线鲜花销售平台,有自己的业务流程和规范,也拥有针对员工的SOP手册。新员工入职培训时,会分享相关的信息。但是,这些信息分散于内部网和HR部门目录各处,有时不便查询;有时因为文档过于冗长,员工无法第一时间找到想要的内容;有时公司政策已更新,但是员工手头的文档还是旧版内容。
基于上述需求,我们将开发一套基于各种内部知识手册的 “Doc-QA” 系统。这个系统将充分利用LangChain框架,处理从员工手册中产生的各种问题。这个问答系统能够理解员工的问题,并基于最新的员工手册,给出精准的答案。
开发框架:整个框架分为三个部分:
- 数据源(Data Sources):数据可以有很多种,包括PDF在内的非结构化的数据(Unstructured Data)、SQL在内的结构化的数据(Structured Data),以及Python、Java之类的代码(Code)。在这个示例中,我们聚焦于对非结构化数据的处理。
- 大模型应用(Application,即LLM App):以大模型为逻辑引擎,生成我们所需要的回答。
- 用例(Use-Cases):大模型生成的回答可以构建出QA/聊天机器人等系统。
实现机制:大模型应用作为“服务器”,根据用例在数据源中找到答案并返回给用户。
核心实现机制: 这个项目的核心实现机制是下图所示的数据处理管道(Pipeline)。
在管道的每一步中,LangChain都提供了相关工具,这就是LangChain的强大之处。
具体流程是:
- 加载文件(Loading):文档加载器把Documents 加载为以LangChain能够读取的形式。
- 切分文件(Splitting):文本分割器把Documents 切分为指定大小的分割,我把它们称为“文档块”或者“文档片”。
- 嵌入及存储(Storage):将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
- 检索(Retrieval):应用程序从存储中检索分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
- 返回结果(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)
注意:
- 处理OpenAI的模型,还可以使用其他的嵌入模型,如Doubao-embedding-large等
- LangChain中支持很多向量数据库,比如Pinecone、Chroma和Qdrant,有些是收费的,有些则是开源的。Qdrant是开源向量数据库。
- Qdrant.from_documents返回的是一个Qdrant类的实例,这个实例代表了一个向量数据库。这个函数是创建一个向量数据库并且根据参数存储在指定位置(location=":memory:"表示存储在内存中)。可以根据这个实例访问这个向量数据库。
代码如下:
- 使用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)