一、背景
1、项目背景介绍
在数据量激增的当代社会,有效管理和检索信息成为关键,本项目旨在构建一个基于 Langchain-glm4的知识库+金融问答助手。通过高效的信息管理系统和强大的检索功能,为用户提供了一个可靠的金融信息获取平台。
本项目的核心目标是充分发挥大型语言模型在处理自然语言查询方面的优势,同时针对用户需求进行定制化开发,以实现对复杂信息的智能理解和精确回应。在项目开发过程中,团队深入分析了大型语言模型的潜力与局限,特别是其在生成幻觉信息方面的倾向。为了解决这一问题,项目集成了 RAG 技术,这是一种结合检索和生成的方法,能够在生成回答之前先从大量数据中检索相关信息,从而显著提高了回答的准确性和可靠性。
通过 RAG 技术的引入,本项目不仅提升了对信息的检索精度,还有效抑制了 Langchain 可能产生的误导性信息。这种结合检索和生成的方法确保了智能助手在提供信息时的准确性和权威性,使其成为用户在面对海量数据时的得力助手。
2、目标与意义
旨在优化用户在信息洪流中的知识获取流程,为用户提供金融信息门户,做用户身边的金融分析专家,该系统通过集成 Langchain 和自然语言处理技术,实现了对分散数据源的快速访问和整合,使用户能够通过直观的自然语言交互高效地检索和利用信息。
项目的核心价值体现在以下几个方面:
1.优化信息检索效率:利用基于 Langchain 的框架,系统能够在生成回答前先从广泛的数据集中检索到相关信息,从而加速信息的定位和提取过程。
2.强化知识组织与管理:支持用户构建个性化的知识库,通过结构化存储和分类,促进知识的积累和有效管理,进而提升用户对专业知识的掌握和运用。
3.辅助决策制定:通过精确的信息提供和分析,系统增强了用户在复杂情境下的决策能力,尤其是在需要迅速做出判断和反应的场合。
4.个性化信息服务:系统允许用户根据自己的特定需求定制知识库,实现个性化的信息检索和服务,确保用户能够获得最相关和最有价值的知识。
5.技术创新示范:项目展示了 RAG 技术在解决 Langchain 幻觉问题方面的优势,通过结合检索和生成的方式,提高了信息的准确性和可靠性,为智能信息管理领域的技术创新提供了新的思路。
6.推广智能助理应用:通过用户友好的界面设计和便捷的部署选项,项目使得智能助理技术更加易于理解和使用,推动了该技术在更广泛领域的应用和普及
3、主要功能
项目开始界面
问答演示界面
实例演示界面
二、技术实现
1、环境依赖
1.1 项目设置
克隆储存库
git clone https://github.com/logan-zou/Chat_with_Datawhale_langchain.git cd Chat_with_Datawhale_langchain
创建 Conda 环境并安装依赖项
- python>=3.9
- pytorch>=2.0.0
# 创建 Conda 环境 conda create -n llm-universe python==3.9.0 # 激活 Conda 环境 conda activate llm-universe # 安装依赖项 pip install -r requirements.txt
1.2 项目运行
- 启动服务为本地 API
- 运行项目
cd llm-universe/project/serve python run_gradio.py -model_name='chatglm_std' -embedding_model='m3e' -db_path='../../data_base/knowledge_db' -persist_path='../../data_base/vector_db'
2、开发流程简述
2.1核心
核心是针对大模型 API 实现了底层封装,基于 Langchain 搭建了可切换模型的检索问答链,并实现 API 以及 Gradio 部署的个人轻量大模型应用。
2.2 使用的技术栈
本项目为一个基于大模型的个人知识库助手,基于 LangChain 框架搭建,核心技术包括 LLM API 调用、向量数据库、检索问答链等。项目整体架构如下:
如上,本项目从底向上依次分为 LLM 层、数据层、数据库层、应用层与服务层。
① LLM 层主要基于四种流行 LLM API 进行了 LLM 调用封装,支持用户以统一的入口、方式来访问不同的模型,支持随时进行模型的切换;
② 数据层主要包括个人知识库的源数据以及 Embedding API,源数据经过 Embedding 处理可以被向量数据库使用;
③ 数据库层主要为基于个人知识库源数据搭建的向量数据库,在本项目中我们选择了 Chroma;
④ 应用层为核心功能的最顶层封装,我们基于 LangChain 提供的检索问答链基类进行了进一步封装,从而支持不同模型切换以及便捷实现基于数据库的检索问答;
⑤ 最顶层为服务层,我们分别实现了 Gradio 搭建 Demo 来支持本项目的服务访问。
三、应用详解
1、核心架构
本项目是一个RAG项目,通过langchain+LLM实现本地知识库问答,建立了全流程可使用开源模型实现的本地知识库对话应用。
整个 RAG 过程包括如下操作:
1.用户提出问题 Query
2.加载和读取知识库文档
3.对知识库文档进行分割
4.对分割后的知识库文本向量化并存入向量库建立索引
5.对问句 Query 向量化
6.在知识库文档向量中匹配出与问句 Query 向量最相似的 top k 个
7.匹配出的知识库文本文本作为上下文 Context 和问题⼀起添加到 prompt 中
8.提交给 LLM 生成回答 Answer
可以大致分为索引,检索和生成三个阶段,这三个阶段将在下面小节配合该 llm-universe 知识库助手项目进行拆解。
2、索引-indexing
创建知识库并加载文件-读取文件-文本分割(Text splitter) ,知识库文本向量化(embedding)以及存储到向量数据库的实现,
其中加载文件:这是读取存储在本地的知识库文件的步骤。读取文件:读取加载的文件内容,通常是将其转化为文本格式 。文本分割(Text splitter):按照⼀定的规则(例如段落、句子、词语等)将文本分割。**文本向量化:**这通常涉及到 NLP 的特征抽取,该项目通过本地 m3e 文本嵌入模型,openai,zhipuai 开源 api 等方法将分割好的文本转化为数值向量并存储到向量数据库
2.1 知识库搭建-加载和读取
实现了txt 格式,md 格式,以及 pdf 格式文件的加载,其中 pdf 格式文件用 PyMuPDFLoader 加载器,md格式文件用UnstructuredMarkdownLoader加载器。
from langchain.document_loaders import UnstructuredFileLoader from langchain.document_loaders import UnstructuredMarkdownLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.document_loaders import PyMuPDFLoader from langchain.vectorstores import Chroma # 首先实现基本配置 DEFAULT_DB_PATH = "../../data_base/knowledge_db" DEFAULT_PERSIST_PATH = "../../data_base/vector_db" ... ... ... def file_loader(file, loaders): if isinstance(file, tempfile._TemporaryFileWrapper): file = file.name if not os.path.isfile(file): [file_loader(os.path.join(file, f), loaders) for f in os.listdir(file)] return file_type = file.split('.')[-1] if file_type == 'pdf': loaders.append(PyMuPDFLoader(file)) elif file_type == 'md': pattern = r"不存在|风控" match = re.search(pattern, file) if not match: loaders.append(UnstructuredMarkdownLoader(file)) elif file_type == 'txt': loaders.append(UnstructuredFileLoader(file)) return ... ...
2.2 文本分割和向量化
文本分割和向量化操作,在整个 RAG 流程中是必不可少的。需要将上述载入的知识库分本或进行 token 长度进行分割,或者进行语义模型进行分割。该项目利用 Langchain 中的文本分割器根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。
- chunk_size 指每个块包含的字符或 Token(如单词、句子等)的数量
- chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
1. 设置一个最大的 Token 长度,然后根据这个最大的 Token 长度来切分文档。这样切分出来的文档片段是一个一个均匀长度的文档片段。而片段与片段之间的一些重叠的内容,能保证检索的时候能够检索到相关的文档片段。这部分文本分割代码也在 project/database/create_db.py 文件,该项目采用了 langchain 中 RecursiveCharacterTextSplitter 文本分割器进行分割。代码如下:
...... def create_db(files=DEFAULT_DB_PATH, persist_directory=DEFAULT_PERSIST_PATH, embeddings="openai"): """ 该函数用于加载 PDF 文件,切分文档,生成文档的嵌入向量,创建向量数据库。 参数: file: 存放文件的路径。 embeddings: 用于生产 Embedding 的模型 返回: vectordb: 创建的数据库。 """ if files == None: return "can't load empty file" if type(files) != list: files = [files] loaders = [] [file_loader(file, loaders) for file in files] docs = [] for loader in loaders: if loader is not None: docs.extend(loader.load()) # 切分文档 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=150) split_docs = text_splitter.split_documents(docs) .... .... ....此处省略了其他代码 .... return vectordb ...........
2. 而在切分好知识库文本之后,需要对文本进行 向量化 。本项目在 project/embedding/call_embedding.py ,文本嵌入方式可选本地 m3e 模型,以及调用 openai 和 zhipuai 的 api 的方式进行文本嵌入。代码如下:
import os import sys sys.path.append(os.path.dirname(os.path.dirname(__file__))) sys.path.append(r"../../") from embedding.zhipuai_embedding import ZhipuAIEmbeddings from langchain.embeddings.huggingface import HuggingFaceEmbeddings from langchain.embeddings.openai import OpenAIEmbeddings from llm.call_llm import parse_llm_api_key def get_embedding(embedding: str, embedding_key: str = None, env_file: str = None): if embedding == 'm3e': return HuggingFaceEmbeddings(model_name="moka-ai/m3e-base") if embedding_key == None: embedding_key = parse_llm_api_key(embedding) if embedding == "openai": return OpenAIEmbeddings(openai_api_key=embedding_key) elif embedding == "zhipuai": return ZhipuAIEmbeddings(zhipuai_api_key=embedding_key) else: raise ValueError(f"embedding {embedding} not support ")
2.3 向量数据库
在对知识库文本进行分割和向量化后,就需要定义一个向量数据库用来存放文档片段和对应的向量表示了,在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。
向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。该项目选择 chromadb 向量数据库(类似的向量数据库还有 faiss 等)。定义向量库对应的代码也在 project/database/create_db.py 文件中,persist_directory 即为本地持久化地址,vectordb.persist() 操作可以持久化向量数据库到本地,后续可以再次载入本地已有的向量库。完整的文本分割,获取向量化,并且定义向量数据库代码如下:
def create_db(files=DEFAULT_DB_PATH, persist_directory=DEFAULT_PERSIST_PATH, embeddings="openai"): """ 该函数用于加载 PDF 文件,切分文档,生成文档的嵌入向量,创建向量数据库。 参数: file: 存放文件的路径。 embeddings: 用于生产 Embedding 的模型 返回: vectordb: 创建的数据库。 """ if files == None: return "can't load empty file" if type(files) != list: files = [files] loaders = [] [file_loader(file, loaders) for file in files] docs = [] for loader in loaders: if loader is not None: docs.extend(loader.load()) # 切分文档 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=150) split_docs = text_splitter.split_documents(docs) if type(embeddings) == str: embeddings = get_embedding(embedding=embeddings) # 定义持久化路径 persist_directory = '../../data_base/vector_db/chroma' # 加载数据库 vectordb = Chroma.from_documents( documents=split_docs, embedding=embeddings, persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上 ) vectordb.persist() return vectordb
3、检索-Retriver和生成-Generator
RAG 的检索和生成,即对问句 Query 向量化后在知识库文档向量中匹配出与问句 Query 向量最相似的 top k 个片段,匹配出的知识库文本文本作为上下文 Context 和问题⼀起添加到 prompt 中,然后提交给 LLM 生成回答 Answer。下面将根据 llm_universe 个人知识库助手进行讲解。
3.1 向量数据库检索
向量数据库是一种用于有效搜索大规模高维向量空间中相似度的库,能够在大规模数据集中快速找到与给定 query 向量最相似的向量。如下面示例所示:
question="什么是机器学习" Copy to clipboardErrorCopied sim_docs = vectordb.similarity_search(question,k=3) print(f"检索到的内容数:{len(sim_docs)}")
检索到的内容数:3
for i, sim_doc in enumerate(sim_docs):
print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
3.2 大模型llm的调用
def model_to_llm(model:str=None, temperature:float=0.0, appid:str=None, api_key:str=None,Spark_api_secret:str=None,Wenxin_secret_key:str=None): """ 智谱:model,temperature,api_key """ elif model in ["chatglm_pro", "chatglm_std", "chatglm_lite"]: if api_key == None: api_key = parse_llm_api_key("zhipuai") llm = ZhipuAILLM(model=model, zhipuai_api_key=api_key, temperature = temperature) else: raise ValueError(f"model{model} not support!!!") return llm
3.3 prompt和构建问答链
最后一步,设计完基于知识库问答的 prompt,结合上述检索和大模型调用进行答案的生成。构建 prompt 的格式如下:
from langchain.prompts import PromptTemplate # template = """基于以下已知信息,简洁和专业的来回答用户的问题。 # 如果无法从中得到答案,请说 "根据已知信息无法回答该问题" 或 "没有提供足够的相关信息",不允许在答案中添加编造成分。 # 答案请使用中文。 # 总是在回答的最后说“谢谢你的提问!”。 # 已知信息:{context} # 问题: {question}""" template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答 案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。 {context} 问题: {question} 有用的回答:""" QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"], template=template) # 运行 chain
并且构建问答链:创建检索 QA 链的方法 RetrievalQA.from_chain_type() 有如下参数:
- llm:指定使用的 LLM
- 指定 chain type : RetrievalQA.from_chain_type(chain_type="map_reduce"),也可以利用load_qa_chain()方法指定chain type。
- 自定义 prompt :通过在RetrievalQA.from_chain_type()方法中,指定chain_type_kwargs参数,而该参数:chain_type_kwargs = {"prompt": PROMPT}
- 返回源文档:通过RetrievalQA.from_chain_type()方法中指定:return_source_documents=True参数;也可以使用RetrievalQAWithSourceChain()方法,返回源文档的引用(坐标或者叫主键、索引)
# 自定义 QA 链 self.qa_chain = RetrievalQA.from_chain_type(llm=self.llm, retriever=self.retriever, return_source_documents=True, chain_type_kwargs={"prompt":self.QA_CHAIN_PROMPT})
问答链效果如下:基于召回结果和 query 结合起来构建的 prompt 效果
question_1 = "什么是南瓜书?" question_2 = "王阳明是谁?"Copy to clipboardErrorCopied
result = qa_chain({"query": question_1})
print("大模型+知识库后回答 question_1 的结果:")
print(result["result"])
大模型+知识库后回答 question_1 的结果:
南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!
result = qa_chain({"query": question_2})
print("大模型+知识库后回答 question_2 的结果:")
print(result["result"])
大模型+知识库后回答 question_2 的结果:
我不知道王阳明是谁,谢谢你的提问!
上述详细不带记忆的检索问答链代码都在该项目:project/qa_chain/QA_chain_self.py 中,此外该项目还实现了带记忆的检索问答链,两种自定义检索问答链内部实现细节类似,只是调用了不同的 LangChain 链。完整带记忆的检索问答链条代码 project/qa_chain/Chat_QA_chain_self.py 如下:
from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain.vectorstores import Chroma from langchain.chains import ConversationalRetrievalChain from langchain.memory import ConversationBufferMemory from langchain.chat_models import ChatOpenAI from qa_chain.model_to_llm import model_to_llm from qa_chain.get_vectordb import get_vectordb class Chat_QA_chain_self: """" 带历史记录的问答链 - model:调用的模型名称 - temperature:温度系数,控制生成的随机性 - top_k:返回检索的前k个相似文档 - chat_history:历史记录,输入一个列表,默认是一个空列表 - history_len:控制保留的最近 history_len 次对话 - file_path:建库文件所在路径 - persist_path:向量数据库持久化路径 - appid:星火 - api_key:星火、百度文心、OpenAI、智谱都需要传递的参数 - Spark_api_secret:星火秘钥 - Wenxin_secret_key:文心秘钥 - embeddings:使用的embedding模型 - embedding_key:使用的embedding模型的秘钥(智谱或者OpenAI) """ def __init__(self,model:str, temperature:float=0.0, top_k:int=4, chat_history:list=[], file_path:str=None, persist_path:str=None, appid:str=None, api_key:str=None, Spark_api_secret:str=None,Wenxin_secret_key:str=None, embedding = "openai",embedding_key:str=None): self.model = model self.temperature = temperature self.top_k = top_k self.chat_history = chat_history #self.history_len = history_len self.file_path = file_path self.persist_path = persist_path self.appid = appid self.api_key = api_key self.Spark_api_secret = Spark_api_secret self.Wenxin_secret_key = Wenxin_secret_key self.embedding = embedding self.embedding_key = embedding_key self.vectordb = get_vectordb(self.file_path, self.persist_path, self.embedding,self.embedding_key) def clear_history(self): "清空历史记录" return self.chat_history.clear() def change_history_length(self,history_len:int=1): """ 保存指定对话轮次的历史记录 输入参数: - history_len :控制保留的最近 history_len 次对话 - chat_history:当前的历史对话记录 输出:返回最近 history_len 次对话 """ n = len(self.chat_history) return self.chat_history[n-history_len:] def answer(self, question:str=None,temperature = None, top_k = 4): """" 核心方法,调用问答链 arguments: - question:用户提问 """ if len(question) == 0: return "", self.chat_history if len(question) == 0: return "" if temperature == None: temperature = self.temperature llm = model_to_llm(self.model, temperature, self.appid, self.api_key, self.Spark_api_secret,self.Wenxin_secret_key) #self.memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) retriever = self.vectordb.as_retriever(search_type="similarity", search_kwargs={'k': top_k}) #默认similarity,k=4 qa = ConversationalRetrievalChain.from_llm( llm = llm, retriever = retriever ) #print(self.llm) result = qa({"question": question,"chat_history": self.chat_history}) #result里有question、chat_history、answer answer = result['answer'] self.chat_history.append((question,answer)) #更新历史记录 return self.chat_history #返回本次回答和更新后的历史记录
3.总结
关键点一
项目利用Langchain中的文本切割器完成知识库向量化操作前的文本分割,向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程,快速的完成个人知识库数据建立与使用。
关键点二
项目对API进行了底层封装,用户可以避免复杂的封装细节,直接调用相应的大语言模型即可。
关键点三
项目对市场上稀缺的金融大模型进行研究开发,旨在服务金融爱好者和金融行业工作者日常研投、工作和金融资讯获取。