如何在没有Langchain的情况下构建PDF聊天机器人?
如何在无Langchain情况下构建PDF聊天机器人?
介绍
自ChatGpt发布以来,AI领域的进展速度没有任何放缓的迹象,每天都有新的工具和技术在被开发。当然,这对企业和AI领域来说是一件好事,但作为一个程序员,你需要学习所有这些工具和技术来构建一些东西吗?嗯,答案是否定的。对此,一个相当实用的方法是只学习你需要的东西。有很多工具和技术承诺可以简化事情,从某种程度上来说,它们确实可以做到。但有时候,我们根本不需要它们。对于简单的用例使用庞大的框架只会让你的代码变得冗余混乱。因此,在本文中,我们将通过构建一个不需要Langchain的CLI PDF聊天机器人来探讨为什么我们并不总是需要AI框架。
学习目标
- 为什么你不需要像Langchain和Llama Index这样的AI框架。
- 何时需要框架。
- 了解向量数据库和索引。
- 使用Python从头开始构建一个CLI问答聊天机器人。
本文是数据科学博文马拉松的一部分。
你能不用Langchain吗?
在最近几个月中,像Langchain和LLama Index这样的框架因其卓越的能力在开发者中经历了显著的流行,主要是因为它们能够方便地开发LLM应用程序。但对于许多用例来说,这些框架可能会变得过于复杂。这就像是用大炮参加一场枪战。
它们提供了一些在你的项目中可能不需要的功能。Python本来就以臃肿而闻名。在此基础上,添加你几乎不需要的依赖只会让你的环境更加混乱。其中一个这样的用例是文档查询。如果你的项目不涉及AI代理或其他复杂的东西,你可以放弃Langchain,从头开始构建工作流程,从而减少不必要的冗余。此外,类似Langchain或Llama Index的框架正在快速发展中;任何代码重构都有可能破坏你的构建。
什么时候需要Langchain?
如果你有更高级的需求,比如构建一个自动化复杂软件的代理,或者需要更长时间的工程来从头开始构建的项目,使用预构建的解决方案是有道理的。除非你需要一个更好的轮子,否则不要重新发明轮子。还有很多其他类似的例子,使用现成的解决方案进行微调是完全合理的。
构建一个问答聊天机器人
LLM的最受欢迎的应用之一就是文档问答。在OpenAI公开了他们的ChatGPT端点之后,使用任何文本数据源构建一个交互式对话机器人变得更加容易。在本文中,我们将从头开始构建一个LLM问答CLI应用程序。那么,我们如何解决这个问题呢?在构建之前,让我们先了解一下我们需要做什么。
一个典型的工作流程将涉及以下步骤
- 处理提供的PDF文件以提取文本。
- 我们还需要对LLM的上下文窗口进行小心处理。因此,我们需要将这些文本划分成块。
- 要查询相关的文本块,我们需要获取这些文本块的嵌入。为此,我们需要一个嵌入模型。对于这个项目,我们将使用Huggingface的MiniLM-L6-V2模型,你可以选择任何你喜欢的模型,比如OpenAI、Cohere或Google Palm。
- 为了存储和检索嵌入,我们将使用一个向量数据库,比如Chroma。你可以选择许多不同的向量数据库,比如Qdrant、Weaviate、Milvus等。
- 当用户发送一个查询时,它将被同样的模型转换为嵌入,并获取与查询意思相似的文本块。
- 获取的文本块将与查询的末尾连接在一起,并通过API发送给LLM。
- 从模型获取的答案将返回给用户。
所有这些内容都需要一个用户界面。在本文中,我们将使用Python Argparse构建一个简单的命令行界面。
这是我们CLI聊天机器人的工作流程图:
在进入编码部分之前,让我们先了解一些关于向量数据库和索引的知识。
什么是向量数据库和索引?
顾名思义,向量数据库存储向量或嵌入。那么,为什么我们需要向量数据库?构建任何AI应用程序都需要将真实世界的数据嵌入到机器学习模型中,因为这些原始数据(如文本、图像或音频)无法直接处理。当处理大量需要重复使用的数据时,需要将其存储在某个地方。那么,为什么我们不能使用传统的数据库来存储这些数据?嗯,你可以使用传统的数据库来满足你的搜索需求,但是向量数据库提供了一个重要的优势:除了词汇搜索之外,它们还可以执行向量相似性搜索。
在我们的案例中,每当用户发送一个查询时,向量数据库将对所有嵌入进行向量相似性搜索,并获取K个最近邻。这种搜索机制非常快速,因为它使用了一种叫做HNSW的算法。
HNSW代表Hierarchical Navigable Small World。它是一种基于图的算法和索引方法,用于近似最近邻搜索(ANN)。ANN是一种查找与给定项最相似的k个项的搜索类型。
HNSW的工作原理是构建一个数据点的图。图中的节点表示数据点,图中的边表示数据点之间的相似性。然后通过遍历图来找到与给定项最相似的k个项。
HNSW算法快速、可靠且可扩展。大多数向量数据库都使用HNSW作为默认的搜索算法。
现在,我们已经准备好深入编码。
构建项目环境
与任何Python项目一样,首先创建一个虚拟环境。这样可以保持开发环境的整洁。参考本文选择适合您项目的正确Python环境。
项目文件结构很简单,我们将有两个Python文件,一个用于定义CLI,另一个用于处理、存储和查询数据。还要创建一个.env文件来存储您的OpenAI API密钥。
这是requirements.txt文件,在开始之前先安装它。
#requiremnets.txt
openai
chromadb
PyPDF2
dotenv
现在,导入必要的类和函数。
import os
import openai
import PyPDF2
import re
from chromadb import Client, Settings
from chromadb.utils import embedding_functions
from PyPDF2 import PdfReader
from typing import List, Dict
from dotenv import load_dotenv
从.env文件中加载OpenAI API密钥。
load_dotenv()
key = os.environ.get('OPENAI_API_KEY')
openai.api_key = key
Chatbot CLI的实用函数
为了存储文本嵌入和它们的元数据,我们将使用ChromaDB创建一个集合。
ef = embedding_functions.ONNXMiniLM_L6_V2()
client = Client(settings = Settings(persist_directory="./", is_persistent=True))
collection_ = client.get_or_create_collection(name="test", embedding_function=ef)
作为一个嵌入模型,我们使用的是带有ONNX运行时的MiniLM-L6-V2。它既小巧又强大,并且是开源的。
接下来,我们将定义一个函数来验证提供的文件路径是否属于有效的PDF文件。
def verify_pdf_path(file_path):
try:
# 尝试以二进制读模式打开PDF文件
with open(file_path, "rb") as pdf_file:
# 使用PyPDF2创建一个PDF阅读器对象
pdf_reader = PyPDF2.PdfReader(pdf_file)
# 检查PDF是否至少有一页
if len(pdf_reader.pages) > 0:
# 如果有页面,PDF不为空,所以什么都不做(跳过)
pass
else:
# 如果没有页面,引发异常,指示PDF为空
raise ValueError("PDF文件为空")
except PyPDF2.errors.PdfReadError:
# 处理无法读取PDF的情况(例如,文件损坏或不是有效的PDF)
raise PyPDF2.errors.PdfReadError("无效的PDF文件")
except FileNotFoundError:
# 处理指定的文件不存在的情况
raise FileNotFoundError("文件未找到,请再次检查文件地址")
except Exception as e:
# 处理其他意外异常并显示错误消息
raise Exception(f"错误:{e}")
PDF问答应用程序的一个主要部分是获取文本块。因此,我们需要定义一个函数来获取所需的文本块。
def get_text_chunks(text: str, word_limit: int) -> List[str]:
"""
将文本分割成指定字数限制的块,同时确保每个块包含完整的句子。
参数:
text (str):要分割成块的整个文本。
word_limit (int):每个块的所需字数限制。
返回:
List[str]:包含指定字数限制和完整句子的文本块的列表。
"""
sentences = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', text)
chunks = []
current_chunk = []
for sentence in sentences:
words = sentence.split()
if len(" ".join(current_chunk + words)) <= word_limit:
current_chunk.extend(words)
else:
chunks.append(" ".join(current_chunk))
current_chunk = words
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
我们已经定义了一个获取文本块的基本算法。思路是允许用户在单个文本块中创建尽可能多的单词。每个文本块都将以一个完整的句子结尾,即使它超过了限制。这是一个简单的算法。您可以根据自己的需要创建算法。
创建字典
现在,我们需要一个函数从PDF加载文本并创建一个字典来跟踪属于单个页面的文本块。
def load_pdf(file: str, word: int) -> Dict[int, List[str]]:
# 从指定的PDF文件创建PdfReader对象
reader = PdfReader(file)
# 初始化一个空字典来存储提取的文本块
documents = {}
# 遍历PDF中的每一页
for page_no in range(len(reader.pages)):
# 获取当前页面
page = reader.pages[page_no]
# 从当前页面提取文本
texts = page.extract_text()
# 使用get_text_chunks函数将提取的文本拆分为长度为'word'的块
text_chunks = get_text_chunks(texts, word)
# 使用页面号作为键将文本块存储在documents字典中
documents[page_no] = text_chunks
# 返回包含页面号为键,文本块为值的字典
return documents
ChromaDB集合
现在,我们需要将数据存储在ChromaDB集合中。
def add_text_to_collection(file: str, word: int = 200) -> None:
# 加载PDF文件并提取文本块
docs = load_pdf(file, word)
# 初始化空列表以存储数据
docs_strings = [] # 存储文本块的列表
ids = [] # 存储唯一ID的列表
metadatas = [] # 存储每个文本块的元数据的列表
id = 0 # 初始化ID
# 遍历加载的PDF中的每个页面和文本块
for page_no in docs.keys():
for doc in docs[page_no]:
# 将文本块添加到docs_strings列表
docs_strings.append(doc)
# 添加文本块的元数据,包括页面号
metadatas.append({'page_no': page_no})
# 为文本块添加唯一ID
ids.append(id)
# 增加ID
id += 1
# 将收集到的数据添加到集合中
collection_.add(
ids=[str(id) for id in ids], # 将ID转换为字符串
documents=docs_strings, # 文本块
metadatas=metadatas, # 元数据
)
# 返回成功消息
return "PDF嵌入成功添加到集合中"
在Chromadb中,元数据字段存储有关文档的其他信息。在这种情况下,文本块的页面号是其元数据。在从每个文本块中提取元数据之后,我们可以将它们存储在先前创建的集合中。这只在用户提供有效的PDF文件路径时才需要。
现在,我们将定义一个处理用户查询以从数据库中提取数据的函数。
def query_collection(texts: str, n: int) -> List[str]:
result = collection_.query(
query_texts = texts,
n_results = n,
)
documents = result["documents"][0]
metadatas = result["metadatas"][0]
resulting_strings = []
for page_no, text_chunk in zip(metadatas, documents):
resulting_strings.append(f"第{page_no['page_no']}页:{text_chunk}")
return resulting_strings
上述函数使用查询方法从数据库中检索“n”个相关数据。然后,我们创建一个格式化的字符串,该字符串以文本块的页码开头。
现在,唯一剩下的主要事项是向LLM提供信息。
def get_response(queried_texts: List[str],) -> List[Dict]:
global messages
messages = [
{"role": "system", "content": "您是一个有帮助的助手。将始终回答'ques:'中提出的问题,并在回答任何问题时引用页码,\
它始终位于提示的开头,格式为'第n页'。"},
{"role": "user", "content": ''.join(queried_texts)}
]
response = openai.ChatCompletion.create(
model = "gpt-3.5-turbo",
messages = messages,
temperature=0.2,
)
response_msg = response.choices[0].message.content
messages = messages + [{"role":'assistant', 'content': response_msg}]
return response_msg
全局变量messages存储对话的上下文。我们定义了一条系统消息,以打印LLM获取答案的页码。
最后,最终的实用函数将获取的文本块与用户查询合并,将其输入get_response()函数,并返回生成的答案字符串。
def get_answer(query: str, n: int):
queried_texts = query_collection(texts = query, n = n)
queried_string = [''.join(text) for text in queried_texts]
queried_string = queried_string[0] + f"ques: {query}"
answer = get_response(queried_texts = queried_string,)
return answer
我们完成了实用函数。现在让我们继续构建CLI。
Chatbot CLI
为了按需使用聊天机器人,我们需要一个界面。这可以是Web应用程序、移动应用程序或CLI。在本文中,我们将为聊天机器人构建一个CLI。如果您想构建一个外观漂亮的演示Web应用程序,可以使用Gradio或Streamlit等工具。查看这篇关于构建PDF聊天机器人的文章。
使用Langchain为PDF构建ChatGPT
为了构建CLI,我们将需要Argparse库。Argparse是一个强大的库,可以让您在Python中创建CLI。它具有简单易用的语法,用于创建命令、子命令和标志。因此,在深入研究之前,这里有一个关于Argparse的简短介绍。
Python Argparse
Argparse模块首次在Python 3.2中发布,为使用Python构建CLI应用程序提供了一种快速便捷的方式,无需依赖第三方安装。它允许我们解析命令行参数,在CLI中创建子命令,以及许多其他功能,使其成为构建CLI的可靠工具。
以下是Argparse实际应用的一个小例子:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--filename", help="要读取的文件名。")
parser.add_argument("-n", "--number", help="要打印的行数。", type=int)
parser.add_argument("-s", "--sort", help="对文件中的行进行排序。", action="store_true")
args = parser.parse_args()
with open(args.filename) as f:
lines = f.readlines()
if args.sort:
lines.sort()
for line in lines:
print(line)
add_argument方法让我们定义带有检查和平衡的子命令。我们可以定义参数的类型或当提供标志时它需要采取的操作,以及解释特定子命令用例的帮助参数。帮助子命令将显示所有标志及其用例。
类似地,我们将为聊天机器人CLI定义子命令。
构建CLI
导入Argparse和必要的实用函数。
import argparse
from utils import (
add_text_to_collection,
get_answer,
verify_pdf_path,
clear_coll
)
定义参数解析器并添加参数。
def main():
# 创建一个带有描述的命令行参数解析器
parser = argparse.ArgumentParser(description="PDF处理CLI工具")
# 定义命令行参数
parser.add_argument("-f", "--file", help="输入PDF文件的路径")
parser.add_argument(
"-c", "--count",
default=200,
type=int,
help="单个块中的单词数的可选整数值"
)
parser.add_argument(
"-q", "--question",
type=str,
help="提出问题"
)
parser.add_argument(
"-cl", "--clear",
type=bool,
help="清除现有的集合数据"
)
parser.add_argument(
"-n", "--number",
type=int,
default=1,
help="从集合中获取的结果数量"
)
# 解析命令行参数
args = parser.parse_args()
我们定义了一些子命令,例如 -file,-value,-question等。
- -file:PDF的字符串文件路径。
- -value:定义文本块中单词数的可选参数值。
- -question:将用户查询作为参数。
- -number:要获取的相似块的数量。
- -clear:清除当前的Chromadb集合。
现在,我们处理这些参数;
if args.file is not None:
verify_pdf_path(args.file)
confirmation = add_text_to_collection(file = args.file, word = args.value)
print(confirmation)
if args.question is not None:
if args.number:
n = args.number
answer = get_answer(args.question, n = n)
print("答案:", answer)
if args.clear:
clear_coll()
return "当前集合已成功清除"
将所有内容放在一起。
import argparse
from app import (
add_text_to_collection,
get_answer,
verify_pdf_path,
clear_coll
)
def main():
# 创建一个带有描述的命令行参数解析器
parser = argparse.ArgumentParser(description="PDF处理CLI工具")
# 定义命令行参数
parser.add_argument("-f", "--file", help="输入PDF文件的路径")
parser.add_argument(
"-c", "--count",
default=200,
type=int,
help="单个块中的单词数的可选整数值"
)
parser.add_argument(
"-q", "--question",
type=str,
help="提出问题"
)
parser.add_argument(
"-cl", "--clear",
type=bool,
help="清除现有的集合数据"
)
parser.add_argument(
"-n", "--number",
type=int,
default=1,
help="从集合中获取的结果数量"
)
# 解析命令行参数
args = parser.parse_args()
# 检查是否提供了'--file'参数
if args.file is not None:
# 验证PDF文件路径并将其文本添加到集合中
verify_pdf_path(args.file)
confirmation = add_text_to_collection(file=args.file, word=args.count)
print(confirmation)
# 检查是否提供了'--question'参数
if args.question is not None:
n = args.number if args.number else 1 # 将'n'设置为指定的数字或默认为1
answer = get_answer(args.question, n=n)
print("答案:", answer)
# 检查是否提供了'--clear'参数
if args.clear:
clear_coll()
print("当前集合已成功清除")
if __name__ == "__main__":
main()
现在打开终端并运行以下脚本。
python cli.py -f "path/to/file.pdf" -v 1000 -n 1 -q "query"
要删除收藏夹,请键入
python cli.py -cl True
如果提供的文件路径不属于PDF文件,将引发FileNotFoundError。
Github代码库:https://github.com/sunilkumardash9/pdf-cli-chatbot
实际应用场景
作为CLI工具运行的聊天机器人可以在许多实际应用中使用,例如:
学术研究:研究人员经常处理大量的PDF格式的研究论文和文章。CLI聊天机器人可以帮助他们提取相关信息、创建参考文献并高效地组织他们的参考资料。
语言翻译:语言专业人士可以使用聊天机器人从PDF中提取文本,进行翻译,然后从命令行生成翻译后的文档。
教育机构:教师和教育工作者可以从教育资源中提取内容,创建定制的学习材料或准备课程内容。学生可以从聊天机器人CLI中提取大型PDF中的有用信息。
开源项目管理:CLI聊天机器人可以帮助开源软件项目管理文档,提取代码片段,并从PDF手册生成发布说明。
结论
因此,这就是用不使用诸如Langchain和Llama Index等框架构建的PDF问答聊天机器人CLI的全部内容。以下是我们涵盖的内容的快速总结。
- Langchain和其他AI框架是开始进行AI开发的好方法。然而,重要的是要记住它们不是万能药。它们可能会使您的代码更复杂,并可能导致臃肿,因此只在需要时使用它们。
- 在项目复杂度需要更长的工程时间的情况下,使用框架是有意义的。
- 可以从第一原理开始设计一个不依赖于Langchain等框架的文档问答工作流。
这就是全部内容了。希望您喜欢这篇文章。
常见问题
本文中显示的媒体不归Analytics Vidhya所有,是根据作者的自由裁量使用的。