基于 OpenAI API 使用 Python 开发文档学习机器人(附源码)

知识爆炸的时代,由于自身阅读和记忆能力的限制,笔者感觉越来越难跟上新知识涌现的步伐。现在,像ChatGPT和Llama这样的大型语言模型为解决这个问题提供了潜在的解决方案,它们提供了一种存储和处理大量信息的方法,我们可以通过提问的方式快速得到问题的答案。

然而,LLM 能回答的问题取决于它们学习的语料库,即使它们大多都有一个庞大的语料库,但这些数据仍然是滞后的。因此,ChatGPT或Bard不能直接向我提供所需的信息。

笔者经常需要阅读大量的专业文献及术语,这个过程消耗了自己太多的精力和时间。导致我即使面对一本很感兴趣的新书,也难以坚持去阅读。

今天小编就来使用 Python 实现一个文档聊天机器人(Document-based ChatBot),它能够即时学习你投喂给它的任何数据(文献、报告、论文等),然后对材料进行总结并且能够基于该资料回答您的问题,最大程度的提升您的阅读、学习及解决问题的能力及效率。类似于 chatpdf.com 的一种实现。

为了实现这个目标,有许多方法可以使用,其中一个是上下文学习(In-context Learning),另一个是模型微调(Fine-tuning)。由于涉及多样化的学习领域,因此我选择使用上下文学习技术。经过研究,我决定基于LlamaIndex以及OpenAI API的文本嵌入(Text Embedding)功能来实现我的文档机器人(Doc Chatbot)。

在本文中,我将详细对基于文档的学习型问答机器人实现方法进行讲解,确保您了解并能够开发自己的文档学习Chatbot。

1. 架构图

在本Chatbot中,我们使用llama-index作为基础,并采用Streamlit Web应用程序来提供用户输入提问及回复交互。通过利用llamaIndex工具包内部数据结构和LLM任务管理的优势,我们就可以在调用OpenAI API的时候轻松规避嵌入过程的复杂性以及提示(Prompt)大小限制问题。

2. OpenAI API密钥

LlamaIndex旨在与各种LLM兼容,默认情况下使用OpenAI的text-davinci-003模型和text-embedding-ada-002-v2进行嵌入操作。因此,当我们决定基于OpenAI GPT模型实现 Doc Chatbot 时,应向程序提供我们的OpenAI API密钥。

唯一需要完成的插入密钥的事情是通过环境变量提供它:

import os
os.environ["OPENAI_API_KEY"] = '{my-openai_key}'

3. LlamaIndex

LlamaIndex是一个Python库,提供用户私有数据和大型语言模型之间的中间层接口(中间件)。

LlamaIndex 功能及优势:

数据连接器:LlamaIndex可以连接各种数据源,包括API、PDF、文档和SQL数据库。这使您可以在不进行数据转换的情况下直接使用原始数据与LLM,这也是笔者选择它的关键原因。

  1. 提示限制:LlamaIndex可以处理提示(Prompt)限制,例如突破 Davinci 模型的4096令牌限制。这确保您的LLM即使在上下文篇幅很大的情况下仍能生成准确的结果,能够节省开发人员管理令牌计算和拆分的时间。
  2. 索引:LlamaIndex能够在原始语言数据上直接创建索引,使LLM更快、更容易地访问。
  3. 提示插入:LlamaIndex提供了一种将提示插入到数据中的方法。这意味着您可以管理LLM与数据的交互方式。
  4. 文本拆分:LlamaIndex可以将文本拆分成较小的块,这可以提高LLM的性能。
  5. 查询:LlamaIndex提供了查询索引的接口。您将能够从LLM中获取知识参数的输出。

LlamaIndex 提供了一套用于处理 LLM 的综合工具集。这包括数据连接器、索引、提示插入、提示限制、文本拆分和查询。您可以在此处找到完整的文档。

3.1 安装方法

使用以下命令安装 LlamaIndex

!pip install llama-index

3.2 实现方法

A) 如果您的应用程序只需要一次性加载某些文档文件并查询其内容,那么以下几行 Python 代码就足够了。

第一步 — 加载文档文件

from llama_index import SimpleDirectoryReader

SimpleDirectoryReader = download_loader("SimpleDirectoryReader") loader = SimpleDirectoryReader('./data', recursive=True, exclude_hidden=True) documents = loader.load_data()

SimpleDirectoryReader 是 LlamaIndex 工具集中的文件加载器之一。它支持加载用户提供的文件夹下的多个文件,示例是从子文件夹 ./data/中加载文件。这个神奇的加载器函数可以支持解析诸如 .pdf、.jpg、.png、.docx 等各种文件类型,因此您无需对文档格式进行转换。

第二步 — 构建索引

from llama_index import LLMPredictor, GPTSimpleVectorIndex, PromptHelper, ServiceContext from langchain import OpenAI

...

# 定义 LLM

llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003"))

max_input_size = 4096 num_output = 256 max_chunk_overlap = 20 prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap)

service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper)

index = GPTSimpleVectorIndex.from_documents( documents, service_context=service_context )

在调用此方法时,LlamaIndex 将与您定义的 LLM 互动以构建索引,在这个演示中,LlamaIndex 通过 OpenAI API 调用嵌入方法。

第三步 — 查询索引

有了索引的构建,查询可以通过直接输入而无需上下文数据来简化。

response = index.query("How to use Keytruda in cervical cancer cases?") print(response)

B) 如果您的应用程序具有一些附加功能,并且严重考虑了 API 使用成本,您可以采取以下可选步骤:

步骤1 — 加载各种数据资源

除了 SimpleDirectoryReader 阅读器方法读取本地文档文件外,还有数十种开源阅读器列在 llamahub.ai 上,可以通过 donwload_loader 函数加载。使用方法简单明了。

例如,如果我想从维基百科加载整个《黑暗森林》书的页面,代码将如下所示:

 from llama_index import download_loader

WikipediaReader = download_loader("WikipediaReader") loader = WikipediaReader() documents = loader.load_data(pages=['The_Dark_Forest'])

步骤2 — 保存和加载生成的索引

由于索引构建过程花费了 较多的 LLM 使用成本,因此一旦生成索引,就有必要将其存储在本地磁盘中。以后的查询仍然基于相同文档的索引,那么从磁盘加载它是最具成本效益和时间效益的使用方法。

# 保存到磁盘
index.save_to_disk('index.json')

# 从磁盘加载
index = GPTSimpleVectorIndex.load_from_disk('index.json')

步骤3 — 设置响应模式

在调用 index.query() 方法时,考虑到提示成本开销,一个有用的参数 response_mode 可以用来指定响应样式:

  • default — 适用于详细的答案
  • compact — 适用于成本可能较高的情况
  • tree-summarize — 适用于概括性答案

可通过以下代码设定参数:

response = index.query("...", response_mode="default")

4. 网页开发

鉴于便捷性考虑,我们将使用 Streamlit 库来构建我们的文档学习聊天机器人(Doc ChatBot)应用程序。

Streamlit 是一个开源的 Python 库,可以轻松创建交互式的网络应用程序。它旨在供数据科学家和机器学习工程师分享他们的工作。您只需要几行代码便可创建 Streamlit 应用程序,并且只需要一条命令就可以轻松完成部署

Streamlit 提供了一种可以用来创建交互式应用程序的小组件。这些组件包括按钮、文本框、滑块和图表。您可以从官方文档中找到所有小部件的使用方法:API Reference - Streamlit Docs

一个典型的 Streamlit 网页应用程序代码可以简单如下:

!pip install streamlit import streamlit as st

st.write(""" Hello world! """)

然后只需输入以下命令运行:

!python -m streamlit run demo.py

如果运行成功,用于访问应用的 URL 将被打印出来:

You can now view your Streamlit app in your browser.

Network URL: http://xxx.xxx.xxx.xxx:8501 External URL: http://xxx.xxx.xxx.xxx:8501

5. 文档学习机器人完整实现

为了将 LlamaIndexStreamlit 合并成一个交互式网站,全功能将包括:

  1. 通过 Streamlit st.file_uploader() 方法创建一个文件上传小部件,接受用户的文档文件上传
  2. 成功上传后,将文件保存在 ./data/ 文件夹中,并通过 LlamaIndex 生成文档对象,然后调用 st.sidebar() 方法在左侧部分打印已经经过转换的内容供用户参考
  3. 调用 llamaIndex 方法针对 ./data/ 中的文档文件生成索引对象,并将其存储在网站文件夹 ./
  4. 如果网站根目录 ./ 中已经存在索引文件(index.json) ,一对 Streamlit 小部件 st.text_input()st.button() 将被激活,允许用户输入针对当前加载的文档的查询。
  5. 在后续使用中,将加载现有的索引文件响应用户操作。

完整代码如下:

import os
os.environ["OPENAI_API_KEY"] = '{my_api_key}'

import streamlit as st
from llama_index import download_loader
from llama_index.node_parser import SimpleNodeParser
from llama_index import GPTSimpleVectorIndex
from llama_index import LLMPredictor, GPTSimpleVectorIndex, PromptHelper, ServiceContext
from langchain import OpenAI

doc_path = './data/'
index_file = 'index.json'

if 'response' not in st.session_state:
    st.session_state.response = ''

def send_click():
    st.session_state.response  = index.query(st.session_state.prompt)

index = None
st.title("Yeyu's Doc Chatbot")

sidebar_placeholder = st.sidebar.container()
uploaded_file = st.file_uploader("Choose a file")

if uploaded_file is not None:

    doc_files = os.listdir(doc_path)
    for doc_file in doc_files:
        os.remove(doc_path + doc_file)

    bytes_data = uploaded_file.read()
    with open(f"{doc_path}{uploaded_file.name}", 'wb') as f: 
        f.write(bytes_data)

    SimpleDirectoryReader = download_loader("SimpleDirectoryReader")

    loader = SimpleDirectoryReader(doc_path, recursive=True, exclude_hidden=True)
    documents = loader.load_data()
    sidebar_placeholder.header('Current Processing Document:')
    sidebar_placeholder.subheader(uploaded_file.name)
    sidebar_placeholder.write(documents[0].get_text()[:10000]+'...')

    llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-003"))

    max_input_size = 4096
    num_output = 256
    max_chunk_overlap = 20
    prompt_helper = PromptHelper(max_input_size, num_output, max_chunk_overlap)

    service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, prompt_helper=prompt_helper)

    index = GPTSimpleVectorIndex.from_documents(
        documents, service_context=service_context
    )

    index.save_to_disk(index_file)

elif os.path.exists(index_file):
    index = GPTSimpleVectorIndex.load_from_disk(index_file)

    SimpleDirectoryReader = download_loader("SimpleDirectoryReader")
    loader = SimpleDirectoryReader(doc_path, recursive=True, exclude_hidden=True)
    documents = loader.load_data()
    doc_filename = os.listdir(doc_path)[0]
    sidebar_placeholder.header('Current Processing Document:')
    sidebar_placeholder.subheader(doc_filename)
    sidebar_placeholder.write(documents[0].get_text()[:10000]+'...')

if index != None:
    st.text_input("Ask something: ", key='prompt')
    st.button("Send", on_click=send_click)
    if st.session_state.response:
        st.subheader("Response: ")
        st.success(st.session_state.response, icon= "🤖")

6. 结论

笔者在完成了这个 Doc ChatBot 开发后,自己先体验了一下。我上传书籍《黑暗森林》的维基百科页面给机器人后,提出了一个较难的问题(之前话费了很多时间才获得问题的答案),ChatBot 通过组合几个故事情节完美地回答了这个问题。效果非常好!

然后,我让 ChatBot 阅读了 Keytruda 说明书(一种用于治疗癌症的人源抗体药物),并询问了其对特定癌症的用法。人工智能机器人给出的回答相当全面,涉及到了多种不同的情况。这种快速学习并获取答案的能力可以节省我大量的时间,并能在我专业知识不足难以解决问题时给我提供准确的信息。

尽管本文中的文档学习机器人实现方法相当简单,但其令人印象深刻的实用性绝对堪当一名合格的人工智能助理。它将为笔者的学习和工作生涯提供强大的支持。

本文的文档学习问答机器人同样支持中文,这取决于投喂的文档所使用的语言。

最后,想实现本文的文档学习机器人,您必须拥有 OpenAI API Key。对于国内的朋友来说,这并不容易。目前,OpenAI 已停止了赠送 API Key 18美元以及5美元的使用额度。想要继续使用 API Key ,您必须具备账户充值能力。

您可以通过这篇文章介绍的方法来对 OpenAI API Key 以及 ChatGPT Plus 进行付费:国内使用信用卡购买 ChatGPT Plus 方法详解