使用 Python 通过 Embedding 定制开发 ChatGPT 问答客服

众所周知,ChatGPT 非常擅长于提供痛通用信息(知识),尽管依然存在一些限制。然而针对特定领域的信息,特别是针对某个企业内部产品以及业务相关的知识,ChatGPT 就完全无能力了。那么,我们如何将 ChatGPT 的能力应用到企业内部产品、业务或者内部知识管理中,使其成为专属客服机器人为用户进行问题解答呢?

在本文中,我将讲解如果通过一种简单的方法来实现这一点。针对公司内部的知识库和一个类似于 StackOverflow 的问答平台,我将结合自定义数据集使用 Python 代码通过 ChatGPT API 演示如何将其接入 ChatGPT

在深入研究之前,让我们先来了解一些术语。ChatGPT是由一系列GPT(首先是GPT-3.5,现在是GPT-4)驱动的聊天机器人。GPT(生成式预训练转换器)是 LLM(大型语言模型)的一种,这些模型包括谷歌的LaMDA和PaLM模型(用于Bard chatbot),以及开放模型如 BLOOMGPT-Neo-X。这些 AI 模型可被用于各种自然语言处理任务,如文本生成、分类、问答、摘要和翻译。

“大”指的是模型的大小,它的大小是通过它包含的参数数量来衡量的。GPT-3有1750亿个参数,而GPT-4预计将有更多数量级的参数。谷歌的LaMDA(用于Bard)有1370亿个参数,而新模型PaLM有5400亿个参数。

1. 思路

为了加强现有的大型语言模型的自定义知识,有两种主要方法:

  • 模型微调(Finetuning):使用自定义数据集进一步训练模型。
  • 上下文学习(Text Embeddings):基于嵌入文本,在查询时提供与用户查询相关的必要信息。

尽管模型微调提供了高度准确和完整的结果,但需要大量的时间、资源和算力来训练和托管自定义模型。然而,上下文学习能够提供更大的灵活性,并且成本要低得多,但会受到模型令牌(token)的限制。

截至撰写本文,假设一个自定义数据集为1M令牌(约20万个单词或400篇维基百科文章),每次查询平均使用1000个令牌,那么使用微调方式训练 Davinci 模型需要花费30000美元,每个查询约 12 美分。

相比之下,基于上下文学习的嵌入文本方式成本却不到1美元,并且每个查询最多1美分(假设每次查询及其附带的上下文总共使用4000个令牌)。据称,ChatGPT已经在约5000亿个单词上进行了训练,是上述示例样本大小的250万倍。

显然,上下文学习是一个更简单的解决方案。

1.1 Prompt工程和检索增强生成

目前,我们已经可以通过特定的方式向ChatGPT提供附加的上下文来增强其回答的质量,该方式称为 Prompt 工程。这种方式可优化使用语言模型的提示。

应用于内部知识库案例的Prompt工程意味着每次与ChatGPT交互时都从知识库中提供相关数据。您可以想象这可能会很麻烦。该过程需要智能的自动化方案。这就是检索增强生成(RAG)工作流程的作用。

这个想法很简单。流程首先使用用户问题进行搜索,以从内部数据集中检索相关文档,然后将这些文档与问题一起提供给ChatGPT。有了附加上下文,ChatGPT可以像已经训练过内部数据集一样回答。

因此,大致上,它不再是简单的 <查询>,而是:给出 <相关文本> 和 <查询> 后给出问题答案

以下是解决方案流程图:

2. 实现方法

本章节我们来谈谈 Python中RAG Q&A的实现。 Jupiter notebook(笔记本) 工程已托管在Google Colab上。

2.1数据集

出于演示目的,我使用GovTech SGTS团队提供的公共数据,已发布在:https://github.com/GovTechSG/developer.gov.sg

2.2 工作流程(aka chain)

我使用 Langchain 库,它拥有丰富的集成能力(包括与LLM集成)。在这种情况下,它允许我实现如上所述的检索增强生成Q&A流程。Langchain的替代方案包括基于Langchain构建的 LlamaIndexHaystack

2.3 文档数据库

因为需要向量数据支持,我们采用文档数据库,而不是传统数据库,从而实现快速检索和相似性搜索,同时具备一定的CRUD操作能力、元数据过滤和横向扩展等功能。因此,用户能够描述他们想要查找的内容,而不必知道所存储对象所归属的关键字或元数据分类。向量搜索还可以返回类似或近邻匹配的结果,提供了一个更全面的结果列表。 —— What is a Vector Database & How Does it Work? Use Cases + Examples | Pinecone

为了将文档存储为向量,数据库需要进行称为嵌入的过程,将每个单词转换为数百或数千个不同维度的向量。例如,OpenAI Ada嵌入结果超过1500维。

这也意味着构建数据库会产生一些成本,具体取决于数据库的大小。但与模型微调相比,这几乎可以忽略不计。处理100万个标记,它只需要40美分(微调模型的训练成本则高达3万美元)。

在本例中,我选择使用 FAISS 作为数据库,它承担了 Facebook AI 的相似度搜索。我可以方便的将FAISS数据库保存为本地文件,并在加载后执行查询,从而大大减少了构建数据库的成本。

除了 FAISS 之外,Langchain还支持 Chroma、Pinecone、Weaviate、OpenSearch 等等。

2.4 构建数据库

安装所需软件包

包括 langchain、openai、faiss-cpu Python 工具包:

pip install langchain openai aiss-cpu

设置OPEN_API_KEY和必要变量:

import os
from getpass import getpass

os.environ["OPENAI_API_KEY"] = getpass("Paste your OpenAI API key here and hit enter:") 

REPO_URL = "https://github.com/GovTechSG/developer.gov.sg"  # Source URL
DOCS_FOLDER = "docs"  # Folder to check out to
REPO_DOCUMENTS_PATH = "collections/_products/categories/devops/ship-hats"  # Set to "" to index the whole data folder
DOCUMENT_BASE_URL = "https://www.developer.tech.gov.sg/products/categories/devops/ship-hats"  # Actual URL
DATA_STORE_DIR = "data_store"  # Folder to save/load the database

为方便测试,我定义了一个数据子集。在实际应用中,我会将collections/_products下的所有内的数据都包含进来。

2.5 克隆GitHub repo

git clone $REPO_URL $DOCS_FOLDER

加载文档并将它们拆分成块以进行嵌入转换:

repo_path = pathlib.Path(os.path.join(DOCS_FOLDER, REPO_DOCUMENTS_PATH))
document_files = list(repo_path.glob(name_filter))

def convert_path_to_doc_url(doc_path):
  # Convert from relative path to actual document url
  return re.sub(f"{DOCS_FOLDER}/{REPO_DOCUMENTS_PATH}/(.*)\.[\w\d]+", f"{DOCUMENT_BASE_URL}/\\1", str(doc_path))

documents = [
    Document(
        page_content=open(file, "r").read(),
        metadata={"source": convert_path_to_doc_url(file)}
    )
    for file in document_files
]

text_splitter = CharacterTextSplitter(separator=separator, chunk_size=chunk_size_limit, chunk_overlap=max_chunk_overlap)
split_docs = text_splitter.split_documents(documents)

embeddings = OpenAIEmbeddings()
vector_store = FAISS.from_documents(split_docs, embeddings)

这样,我们就构建了一个叫做 vector_store的数据库,数据被切分到每块包含1000个令牌的数据块中。接下来,我们将从 FAISS 数据库中提取数据,然后发送到 OpenAI 进行嵌入处理。

您可以将其保存到本地文件以供将来重用:

vector_store.save_local(DATA_STORE_DIR)

今后可以使用以下命令重新加载它:

vector_store = FAISS.load_local(DATA_STORE_DIR, OpenAIEmbeddings())

3. 提问(查询)

既然我们已经构建了数据库,那么我们就可以查询我们的自定义数据了。

设置聊天模型和特定提示(Prompt):

from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)

system_template="""Use the following pieces of context to answer the users question.
Take note of the sources and include them in the answer in the format: "SOURCES: source1 source2", use "SOURCES" in capital letters regardless of the number of sources.
If you don't know the answer, just say that "I don't know", don't try to make up an answer.
----------------
{summaries}"""
messages = [
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template("{question}")
]
prompt = ChatPromptTemplate.from_messages(messages)

from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQAWithSourcesChain

chain_type_kwargs = {"prompt": prompt}
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0, max_tokens=256)  # Modify model_name if you have access to GPT-4
chain = RetrievalQAWithSourcesChain.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vector_store.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs=chain_type_kwargs
)

def print_result(result):
  output_text = f"""### Question: 
  {query}
  ### Answer: 
  {result['answer']}
  ### Sources: 
  {result['sources']}
  ### All relevant sources:
  {' '.join(list(set([doc.metadata['source'] for doc in result['source_documents']])))}
  """

以上代码的主要部分是实现基于 OpenAI gpt-3.5-turbo 模型的 RetrievalQAWithSourcesChain 对象,然后使用 vector_store 数据库作为提取器。具体提示可以根据不同的使用场景进行自定义。请注意,我们将模型的温度设置为0,从而指定到上下文处理。

3.1 使用链来查询

query = "What is SHIP-HATS?"
result = chain(query)
print_result(result)

3.2 结果输出

这样我们就得到了答案,显然是正确的。此外,它还提供了正确答案源数据,在这里:SHIP-HATS (Secure Hybrid Integration Pipeline-Hive Agile Testing Solutions) – The CI/CD component within the Singapore Government Tech Stack (SGTS) | Singapore Government Developer Portal

这对用户来说非常有用,当他们怀疑答案正确性的时候可以验证答案的来源,或者了解更多关于搜索主题的信息。显然, ChatGPT 的能力得到了扩展和增强。

背后发生了什么?- OpenAI API 调用

为了理解发生了什么,我们可以看一下 OpenAI API 调用交互过程:

相关文本块及其来源已被添加到系统消息中,这就是 ChatGPT 实现上下文感知的秘密。

完整的 Jupiter 笔记本源码托管在这里:Google Colab

4. 进一步的增强

您可以在本思路的基础上,构建多种应用。比如接入 Slack 、Telegram 客服机器人,集成到问答平台等。

4.1 接入 Slack 或 Telegram 机器人

既然主要工作流程已经完成,我们可以轻松地将其开发接入到 Telegram 或 Slack 平台机器人,包括微信小程序等。机器人及小程序更易于用户使用,因为它们本身已被广泛使用。

下面是一个基于 ChatGPT 的 Telegram 机器人例子:

4.2 集成问答平台

我们有一个名为 Hivemind 的内部问答平台,类似于 StackOverflow。因此,我们可以通过从 Hivemind 中拉取额外的数据进行训练。

此外,系统可以允许用户选择问题和答案对并将其发布到 Hivemind 上,并在机器人无法回答的情况下发布待处理问题。随着时间的推移,机器人将拥有一个庞大的答案集,并成为我们的专属知识顾问。

下图为机器人和 Hivemind 之间的集成结构:

4.3 数据安全性

需要注意的是,如果您的数据具有极高的保密性,使用像 ChatGPT 这样的 LLM 服务并不能满足数据安全要求,因为其中的一部分数据会发送到 OpenAI。

5. 下一步规划

我们还可以做很多事情,包括使用不同的 LLM 或本地托管模型以保证数据安全,使用人工辅助答案来提高准确性,添加多模式功能以支持图像、视频和语音。

Jupiter 笔记本项目完整源码托管在这里:Google Colab