产品和工程领导者亲身体验GenAI技术

产品和工程领域的领导者,身临其境体验GenAI技术

通过一瞥LLM-based产品内部来做出更好的产品决策

根据“为原型工作的基于机器学习的应用程序的产品所有者”提示生成的图像

介绍

如果你是一名常常开车的司机,你可能对你的车盖下装满了棉花并不在乎。但是,如果你处于负责建造更好车辆的设计和执行链的任何地方,了解不同部件以及它们如何相互配合将帮助你建造更好的车辆。

同样地,作为产品所有者、商业领导者或负责创建新的基于大型语言模型(LLM)的产品,或将LLM /生成式人工智能引入现有产品的工程师,了解构建LLM-powered产品的基本组成部分将帮助你应对涉及技术的战略和战术问题,例如:

  1. 我们的用例是否适合LLM-powered解决方案?也许传统分析、监督式机器学习或其他方法更合适?
  2. 如果LLM是一种好方法,我们的用例现在或将来可以使用现成的产品(比如ChatGPT Enterprise)吗?经典的构建与购买决策。
  3. 我们的LLM-powered产品有哪些不同的构建块?其中哪些是通用化的,哪些可能需要更多时间来构建和测试?
  4. 我们如何衡量解决方案的性能?有哪些手段可以提高我们产品的输出质量?
  5. 我们的数据质量是否符合用例要求?我们是否正确地组织了数据,并将相关数据传递给LLM?
  6. 我们能否确信LLM的响应始终是事实准确的。也就是说,在生成响应时,我们的解决方案是否会有幻觉?

虽然这些问题在文章后面有答案,但通过一些实践操作来直观地了解LLM-powered解决方案是为了帮助你自己回答这些问题,或者至少使你更有能力进一步研究。

之前的一篇文章中,我深入探讨了构建LLM-powered产品的一些基础概念。但是你不能只通过阅读博客或观看视频来学会开车-这需要你上路驾驶。幸运的是,由于我们生活的时代,我们可以免费使用工具(这些工具花费数百万美元来创建),在不到一个小时内构建自己的LLM解决方案!因此,在这篇文章中,我建议我们就是这样做。这比学开车要容易得多 😝。

构建一个允许你与网站“聊天”的聊天机器人

目标:构建一个基于提供的网站信息回答问题的聊天机器人,以便更好地理解当今流行的GenAI解决方案的基本构建块

我们将创建一个问答聊天机器人,它将根据知识库中的信息回答问题。这种解决方案模式称为检索增强生成(RAG),已成为企业中的首选解决方案模式之一。 RAG之所以受欢迎的原因之一是,它不仅依赖于LLM自己的知识,还可以以自动化的方式将外部信息引入到LLM中。在实际实施中,外部信息可以来自组织自己的知识库,其中包含了使产品能够回答有关业务、产品、业务流程等问题的专有信息。 RAG还减少了LLM的“幻觉”,因为生成的响应是基于提供给LLM的信息的。根据最近的演讲

“RAG将成为企业使用LLM的默认方式”- AnyScale首席科学家Dr. Waleed Kadous

对于我们的实践,我们将允许用户输入一个网站,我们的解决方案将“读取”该网站并将其存储在知识库中。然后,该解决方案将能够根据网站上的信息回答问题。网站只是一个占位符 – 实际上,这可以调整为从任何数据源(如PDF、Excel、其他产品或内部系统等)接收文本。这种方法也适用于其他媒体,如图片,但它们需要一些不同的LLM。现在,我们将专注于来自网站的文本。

为了举例,我们将使用为本博客创建的一个样本书单网页:Books I’d Pick Up — If There Were More Hours in the Day! 您可以使用您选择的其他网站。

以下是我们的结果将会是怎样的:

由作者创建的,基于网站信息智能回答问题的LLM动力聊天机器人。(图像来源:https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*JnkIhp5g6fOsb8NLWUAp8A.gif)

以下是我们构建解决方案的步骤:

0. 进行设置 – Google Colaboratory和OpenAI API Key1. 创建知识库2. 搜索与问题相关的上下文3. 使用LLM生成答案4. 添加“聊天”功能(可选)5. 添加一个简单的预编码界面(可选)

0.1. 进行设置 – Google Colaboratory和OpenAI API Key

要构建LLM解决方案,我们需要一个编写和运行代码的地方,以及一个用于生成问题回答的LLM。我们将使用Google Colab作为代码环境,以及ChatGPT背后的模型作为我们的LLM。

让我们从设置Google Colab开始,这是Google提供的免费服务,可以在易于阅读的格式中运行Python代码 – 无需在计算机上安装任何程序。我发现将Colab添加到Google Drive中很方便,这样我以后可以轻松找到Colab笔记本。

为此,请导航至Google Drive(使用浏览器)>新建 >更多 >连接更多应用[在Google Marketplace中搜索“Colaboratory” >安装。

要开始使用Colabobatory(“Colab”),您可以选择新建更多Google Colaboratory。这将在您的Google Drive中创建一个新笔记本,因此您可以随时返回它。

在Google Drive中访问Google Colaboratory。(图像来源:https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*c5nxufr32Fe-i4cxbuA4LA.png)

接下来,让我们获取LLM访问权限。有几个开源和专有的选项可供选择。虽然开源LLM是免费的,但功能强大的LLM通常需要强大的GPU来处理输入并生成响应,而GPU的运行费用也相应较低。在我们的示例中,我们将使用OpenAI的服务来使用ChatGPT使用的LLM。为此,您需要一个API密钥,它类似于将用户名/密码合并在一起,让OpenAI知道是谁在尝试访问LLM。截至撰写本文时,OpenAI为新用户提供了5美元的信用额度,这应足以满足这个实践教程的要求。以下是获取API密钥的步骤:

前往OpenAI的平台网站开始 > 注册 使用电子邮件和密码或使用Google或Microsoft帐户进行注册。您可能还需要一个电话号码进行验证。

登录后,点击右上角的个人资料图标 > 查看 API 密钥 > 创建新的秘密密钥。密钥会类似于以下内容(仅用于信息目的的虚假密钥)。保存它以备后用。

sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64

现在我们已经准备好构建解决方案了。

0.2. 构建解决方案前的准备工作

我们需要在 Colab 环境中安装一些软件包以便使用我们的解决方案。只需在 Colab 中的文本框(称为“单元格”)中输入以下代码,然后按“Shift + 回车(enter)”。或者,只需点击单元格左侧的“播放”按钮或使用笔记本顶部的“运行”菜单。您可能需要使用菜单来插入新的代码单元格以运行后续代码:

# 安装 OpenAI 和 tiktoken 包以使用嵌入模型和聊天完成模型!pip install openai tiktoken# 安装 langchain 包以便实现大部分解决方案的功能,包括处理文档和使用 LLM 进行“聊天”!pip install langchain# 安装 ChromaDB - 内存中的向量数据库包 - 以保存我们的解决方案所依赖的“知识”来回答问题!pip install chromadb# 安装 HTML to text 包以将网页内容转换为更易读的格式!pip install html2text# 安装 gradio 以为我们的解决方案创建一个基本的用户界面!pip install gradio

接下来,我们应该引入我们安装的软件包中的代码,以便在编写的代码中使用这些软件包。您可以使用新的代码单元格,再次按下“Shift + 回车”,然后继续这种方式运行后续的每个代码块。

# 导入启用解决方案不同功能所需的软件包from langchain.document_loaders import AsyncHtmlLoader # 将网站内容加载到文档中from langchain.text_splitter import MarkdownHeaderTextSplitter # 根据文档标题将文档分成较小的块from langchain.document_transformers import Html2TextTransformer # 将 HTML 转换为 Markdown 文本from langchain.chat_models import ChatOpenAI # 使用 OpenAI 的 LLMfrom langchain.prompts import PromptTemplate # 用于制定指令/提示from langchain.chains import RetrievalQA, ConversationalRetrievalChain # 用于 RAGfrom langchain.memory import ConversationTokenBufferMemory # 用于维护聊天历史记录from langchain.embeddings.openai import OpenAIEmbeddings # 将文本转换为数字表示from langchain.vectorstores import Chroma # 与向量数据库交互import pandas as pd, gradio as gr # 用于显示数据表和构建用户界面import chromadb, json, textwrap # 向量数据库、将 json 转换为文本和美化打印from chromadb.utils import embedding_functions # 设置嵌入函数,遵循 Chroma 所需的协议

最后,将 OpenAI API 密钥添加到一个变量中。请注意,这个密钥就像您的密码一样,请勿分享它。此外,在未删除 API 密钥之前,请不要分享您的 Colab 笔记本。

# 将您的 OpenAI API 密钥添加到一个变量中# 将密钥保存在变量中是不好的做法。它应该加载到环境变量中并从那里加载,但是这对于快速演示来说是可以的OPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # 虚假密钥 - 在此处使用您自己的真实密钥

现在我们已经准备好开始构建解决方案了。以下是下一步的高级概述:

构建 RAG 解决方案的核心步骤(由作者提供的图片)

在编码时,我们将使用 LangChain,它已成为构建此类解决方案的流行框架。它为每个步骤提供了方便的软件包,从连接到数据源到与 LLM 之间的信息发送和接收。LlamaIndex 是简化构建基于 LLM 的应用程序的另一个选择。虽然不严格要求使用 LangChain(或 LlamaIndex),并且在某些情况下高级抽象可能会使团队对底层发生的情况一无所知,但我们将使用 LangChain 并经常查看底层发生的情况。

请注意,由于创新的速度非常快,所以该代码中使用的包很可能会更新,某些更新可能会导致代码停止工作,除非相应地进行更新。我不打算保持该代码的最新状态。尽管如此,本文旨在作为演示,代码可以作为参考,或者作为您根据自己的需求进行适应的起点。

1. 创建知识库

1.1. 鉴别和读取文档让我们访问书籍列表并将内容读入我们的Colab环境中。内容最初加载为HTML格式,这对于网页浏览器非常有用。然而,我们将使用HTML转文本转换器将其转换为更易读的格式。

url = "https://ninadsohoni.github.io/booklist/" # 随意使用其他网站,但请注意,某些代码可能需要进行编辑以正确显示内容# 从URL加载HTML并转换为更易读的文本格式docs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())# 让我们再次快速浏览一下看看现在的情况print("\n\n包含的元数据:\n", textwrap.fill(json.dumps(docs[0].metadata), width=100), "\n\n")print("已加载页面内容:")print('...', textwrap.fill(docs[0].page_content[2500:3000], width=100, replace_whitespace=False), '...')

以下是在Google Colab上运行代码后生成的结果:

上述代码执行结果。网站内容加载到Colab环境中。(图片作者提供)

1.2. 将文档拆分为更小的摘录在将博客信息加载到知识库(实质上是我们选择的数据库)之前,还有一个步骤。文本不应作为其自身加载到数据库中。它应该首先被拆分成更小的块。原因有几个:

  1. 如果我们的文本太长,由于超过文本长度阈值限制(称为“上下文大小”),它无法发送到LLM。
  2. 较长的文本可能包含广泛的、松散相关的信息。我们将依赖LLM选择相关部分,但这并不总是按预期进行。通过使用检索机制,我们可以使用较小的块来识别只有相关信息的部分,后面我们将看到。
  3. LLM更容易在文本的开头和结尾处产生较强的注意力,因此较长的块可能导致LLM在后面的内容中注意力较少(称为“中间丢失”)。

每个用例的合适块大小将根据用例的具体情况而异,包括内容的类型、使用的LLM和其他因素。在最终确定解决方案之前,建议尝试不同的块大小并评估响应质量。对于此演示,让我们使用上下文感知的拆分,即列表中的每个书籍推荐都有自己的块。

# 现在,我们将整个网站内容拆分为较小的块# 每个书评都将有自己的块,因为我们是通过标题来拆分的# 这里使用的LangChain分割器也将创建一个包含标题的元数据集合,并将其与每个块中的文本关联起来headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"),    ("###", "Header 3"), ("####", "Header 4"), ("#####", "Header 5") ]splitter = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on)chunks = splitter.split_text(docs[0].page_content)print(f"{len(chunks)}个较小的块从原始文档生成")# 让我们看一个块print("\n查看样本块:")print("包含的元数据:\n", textwrap.fill(json.dumps(chunks[5].metadata), width=100), "\n\n")print("已加载页面内容:")print(textwrap.fill(chunks[5].page_content[:500], width=100, drop_whitespace=False), '...')
拆分原始内容后的众多文档块之一。(图片作者提供)

请注意,如果到目前为止创建的块仍然比所需的更长,可以使用其他文本分割算法进一步拆分,这些算法可以轻松地通过LangChain或LlamaIndex获得。例如,每本书的评论可以根据需要分成段落。

1.3. 将摘录加载到知识库中现在可以将文本块加载到知识库中。首先,将其通过嵌入模型转换为捕捉文本含义的一系列数字。然后,将实际文本以及数值表示(即嵌入)加载到矢量数据库中,即我们的知识库。请注意,嵌入也是由LLM(语言模型)生成的,只不过与聊天LLM不同。如果您想阅读更多关于嵌入的信息,之前的文章通过示例演示了该概念。

我们将使用矢量数据库存储所有信息。这将成为我们的知识库。矢量数据库是专门用于按嵌入相似性进行搜索的数据库。如果我们想从数据库中搜索某些内容,则首先将搜索条件通过嵌入模型转换为数值表示,然后将问题嵌入与数据库中的所有嵌入进行比较。与问题嵌入最接近的记录(在我们的案例中,关于列表上每本书的文本块)将作为搜索结果返回,只要它们达到一个阈值。

# 我们将使用来自OpenAI的嵌入模型为每个块(以及后续的问题)获取嵌入openai_embedding_func = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)# 初始化矢量数据库并创建一个集合persistent_chroma_client = chromadb.PersistentClient()collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func)cur_max_id = collection.count() # 为了不覆盖现有数据# 将数据添加到矢量数据库的集合中collection.add(    ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],    documents=[t.page_content for t in chunks],    metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]    )print(f"矢量数据库中有{collection.count()}个文档")#  矢量数据库中有25个文档# 可选:我们将编写一个简单的辅助函数以更好地打印数据 -#   它限制了在屏幕上显示的嵌入长度(因为这些嵌入是超过1,000位数的)。
#   还显示了文档的一部分文本和元数据字段def render_vectorDB_content(chromadb_collection):    vectordb_data = pd.DataFrame(chromadb_collection.get(include=["embeddings", "metadatas", "documents"]))    return pd.DataFrame({'IDs': [str(t) if len(str(t)) <= 10 else str(t)[:10] + '...'for t in vectordb_data.ids],                         'Embeddings': [str(t)[:27] + '...' for t in vectordb_data.embeddings],                         'Documents': [str(t) if len(str(t)) <= 300 else str(t)[:300] + '...' for t in vectordb_data.documents],                         'Metadatas': ['' if not t else json.dumps(t) if len(json.dumps(t))  <= 90 else '...' + json.dumps(t)[-90:] for t in vectordb_data.metadatas]                        })# 使用我们的辅助函数查看矢量数据库中的内容。我们将查看前4个块render_vectorDB_content(collection)[:4]
通过作者提供的图像,查看加载到矢量数据库的前几个文本块以及数值表示(即嵌入)。

2. 搜索与问题相关的上下文

我们最终希望我们的解决方案从我们的矢量数据库知识语料库中挑选出相关信息,并将其与我们希望LLM回答的问题一起传递给LLM。让我们通过询问问题“你能推荐几本侦探小说吗?”来尝试矢量数据库搜索。

# 在这里,我们使用之前创建的ChromaDB数据库的实例链接到一个LangChain ChromaDB客户端vectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection",                   embedding_function=OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)                  ) # 可选:我们将定义另一个简单的辅助函数以更好地打印数据def render_source_documents(source_documents):    return pd.DataFrame({'#': range(1, len(source_documents) + 1),                          'Documents': [t.page_content if len(t.page_content) <= 300 else t.page_content[:300] + '...' for t in source_documents],                         'Metadatas': ['' if not t else '...' + json.dumps(t.metadata, indent=2)[-88:] for t in source_documents]                         })# 这里我们正在根据问题运行对矢量数据库的搜索question = "你能推荐几本侦探小说吗?"# 根据我们的问题对矢量数据库运行搜索relevant_chunks = vectordb.similarity_search(question)# 打印结果print(f"前{len(relevant_chunks)}个搜索结果")render_source_documents(relevant_chunks)
“你能推荐几本侦探小说吗?”问题的热门搜索结果(图片由作者提供)

我们默认获取前4个搜索结果,除非我们明确将值设置为不同的数字。在这个例子中,排名第一的搜索结果(一本福尔摩斯的侦探小说)直接提到了“侦探”这个词。第二个结果(《杰克尔之日》)没有提到“侦探”这个词,但提到了“警察机构”和“揭开阴谋”,与“侦探小说”有语义关联。第三个结果(《卧底经济学家》)提到了“卧底”这个词,尽管它是关于经济的。我认为最后一个结果是因为它与小说/书籍的联系而被获取,而不仅仅是与“侦探小说”有关,因为需要获取四个结果。

此外,并不一定非要使用向量数据库。您可以加载嵌入并在其他形式的存储中进行搜索。可以使用“普通”关系型数据库甚至Excel。但是您必须处理“相似性”计算,当使用OpenAI嵌入时,可以是点积在应用程序逻辑中。另一方面,向量数据库会为您完成这一任务。

请注意,如果我们想要根据元数据预先过滤一些搜索结果,我们可以这样做。对于我们的演示,让我们按照流派进行过滤,而流派在我们从书目中加载的元数据的“Header 2”下。

# 让我们尝试根据特定元数据过滤和访问匹配的记录。根据需要,这可以转换为预过滤pd.DataFrame(vectordb.get(where = {'Header 2':'金融'}))
基于应用元数据预过滤的搜索结果,仅显示关键列。(图片由作者提供)

LLMs提供的一个有趣的机会是使用LLM本身检查用户问题,查看可用元数据,评估是否需要和可能需要基于元数据的预过滤,并制定预过滤查询代码,然后可以在向量数据库上使用该代码来预过滤数据。有关此信息的更多详细信息,请参见LangChain的自查询检索器

3. 使用LLM生成答案

接下来,我们将向LLM添加指令,基本上是告诉它:“我将为您提供一些信息片段和一个问题。请使用提供的信息片段回答问题。”然后,我们将这些指令、向量数据库的搜索结果和我们的问题捆绑成一个数据包,发送给LLM进行回答。这些步骤都是由以下代码完成的。

请注意,LangChain提供了抽象一部分代码的机会,因此您的代码不必像下面的代码那样冗长。但是,下面的代码的目标是展示发送给语言模型的指令。您还可以根据需要对其进行自定义 – 比如在本例中,将默认指令更改为要求LLM保持回答尽可能简洁。如果默认设置适用于您的用例,您的代码可以完全跳过问题模板部分,LangChain在向LLM发送请求时会使用其自己的默认提示。

# 让我们选择免费版ChatGPT背后的语言模型:GPT-3.5-turbollm = ChatOpenAI(model_name ='gpt-3.5-turbo',temperature = 0,openai_api_key = OPENAI_API_KEY)# 让我们构建一个提示。这实际上是发送给ChatGPT LLM的内容,其中注入了来自我们的向量数据库和问题的上下文template = """使用以下上下文片段来回答最后的问题。如果您不知道答案,只需说可用信息不足以回答问题。不要试图编造答案。确保答案简明扼要,限制在五个句子内。{context}问题:{question}有帮助的答案:"""QA_CHAIN_PROMPT = PromptTemplate.from_template(template)# 定义一个检索QA链,它将接受问题,从向量数据库中获取相关上下文,并将两者传递给语言模型进行回复qa_chain = RetrievalQA.from_chain_type(llm,                                        retriever=vectordb.as_retriever(),                                       return_source_documents=True,                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}                                       )

现在,让我们再次寻求侦探小说推荐,看看我们得到什么样的回答。

# 让我们提出一个问题并运行问题回答链question = "你能推荐几本侦探小说吗?"result = qa_chain({"query": question})# 让我们看看结果result["result"]
来自解决方案的推荐侦探小说的回应(图片由作者提供)

让我们确认一下模型是否回顾了之前的四个搜索结果,还是只记住了回答中提到的两个结果?

# 让我们看一下由LLM用作上下文的源文件# 我们将使用之前的辅助函数来限制显示的信息的大小render_source_documents(result["source_documents"])
作为上下文与问题一起传递给LLM的内容,以便促进回答。(图片由作者提供)

我们可以看到,LLM仍然能够访问所有四个搜索结果,并推断出只有前两本书是侦探小说。

需要注意的是,LLM的回应可能会每次提问时都有所变化,尽管发送的指令和向量数据库的信息是相同的。例如,当询问关于奇幻书推荐时,LLM有时会给出三本书的推荐,有时会给出更多 – 虽然都来自书单。在所有情况下,最受推荐的书都是相同的。请注意,尽管将一致性 – 创造性范围(“温度”参数)配置为0以减少变异,这些变化仍然存在。

4. 添加“聊天”功能(可选)

目前,解决方案已经具备了必要的核心功能 – 能够从网站中读取信息并根据该信息回答问题。但它目前还没有提供“对话”式的用户体验。感谢ChatGPT,现在“聊天界面”已成为主要设计:我们现在期望以这种“自然”的方式与生成式AI及特别是LLM进行交互 😅。达到聊天界面的第一步是为解决方案添加“记忆”功能。

这里的“记忆”是一种幻觉,LLM实际上并没有记住迄今为止的对话 – 在每个回合中,它需要展示完整的对话历史。因此,如果用户向LLM提出后续问题,解决方案将打包原始问题、LLM的原始回答和后续问题,并发送给LLM。LLM会阅读整个对话并生成一个有意义的回应,以继续对话。

在像我们正在构建的问答聊天机器人中,这种方法需要进一步扩展,因为有中间步骤需要联系向量数据库并提取相关信息以制定对用户后续问题的回应。问答聊天机器人中模拟“记忆”的方式如下:

  1. 保留所有问题和回答(在一个变量中)作为“聊天历史”
  2. 当用户提问时,将聊天历史和新问题发送给LLM,并要求其生成一个独立的问题
  3. 此时,聊天历史不再需要。使用独立问题在向量数据库上运行新的搜索
  4. 传递独立问题和搜索结果,以及向LLM发送指令以获得最终答案。这一步类似于我们在前一阶段“使用LLM生成答案”中实现的

虽然我们可以在较简单的变量中跟踪聊天历史,但我们将使用LangChain的一种记忆类型。我们将使用的特定内存对象具有一个很好的功能,即当达到您指定的大小限制时自动截断较旧的聊天历史,通常是所选LLM可以接受的文本的大小。在我们的情况下,LLM应该能够接受略大于4,000个“令牌”(即单词部分),大约相当于一个Word文档中的3,000个词或~5页。OpenAI还提供了一个16k的ChatGPT LLM变种,可以接受四倍的输入。因此,有必要配置内存大小。

这是实现这些步骤的代码。再次说明,LangChain提供了一个更高层次的抽象,代码不需要那么明确。这个版本只是暴露了发送给LLM的底层指令,首先将聊天历史压缩成一个独立的问题,然后根据向量DB搜索结果生成对生成的独立问题的回答。

# 让我们创建一个内存对象来跟踪聊天历史。这将开始积累人类消息和AI回复。# 这里使用的是基于“token”的内存,用于限制可以传递到所选LLM中的聊天历史的长度。 # 通常,配置的最大令牌长度将取决于LLM的选择。假设我们使用的是LLM的4K版本,# 我们将将令牌的最大长度设置为3K,为问题提示留出一些空间。# LLM参数用于让LangChain了解所选LLM的分词方案。memory = ConversationTokenBufferMemory(memory_key="chat_history", return_messages=True, input_key="question", output_key="answer", max_token_limit=3000, llm=llm)# 虽然LangChain包含一个默认提示来根据用户的最新问题和# 对话中的任何上下文生成一个独立问题,我们将扩展默认提示以包括附加的指令.standalone_question_generator_template = """给定以下对话和一个后续问题,请重新表述后续问题为一个独立问题,使用原始语言。在明确表述独立问题时,尽可能具体,并包括解释独立问题所需的任何上下文。对话内容:{chat_history}后续问题:{question}独立问题:"""updated_condense_question_prompt = PromptTemplate.from_template(standalone_question_generator_template)# 让我们重建最终提示(再次说明,这是可选的,因为LangChain使用默认提示,尽管可能会有一些不同)final_response_synthesizer_template = """使用以下上下文片段回答最后的问题。如果你不知道答案,只需说可用信息不足以回答问题。不要试图编造答案。将答案保持简洁,限制在五个句子以内。上下文:{context}问题:{question}有帮助的回答:"""custom_final_prompt = PromptTemplate.from_template(final_response_synthesizer_template)qa = ConversationalRetrievalChain.from_llm(    llm=llm,     retriever=vectordb.as_retriever(),     memory=memory,    return_source_documents=True,    return_generated_question=True,    condense_question_prompt= updated_condense_question_prompt,    combine_docs_chain_kwargs={"prompt": custom_final_prompt})# 让我们再次提问之前问过的检索QA链的问题query = "你能推荐几本侦探小说吗?"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
解决方案中的侦探小说推荐。与之前仅使用“问答”能力而无“内存”时收到的响应相同(作者提供的图片)

让我们提一个后续问题并查看响应,以验证解决方案现在是否具有“内存”并能够对后续问题进行对话式回答:

query = "告诉我更多关于第二本书的信息"result = qa({"question": query})print(textwrap.fill(result['answer'], width=100))
对后续问题的响应,询问关于“第二本书”的更多信息。解决方案返回关于之前同一本书的更多信息(作者提供的图片)

让我们来看看在验证解决方案确实经历了本节开始时概述的四个步骤。让我们从聊天历史开始验证解决方案是否真的记录了到目前为止的对话:

# 让我们看一下到目前为止的聊天记录result ['chat_history']  
询问第二个问题后的聊天记录。请注意,此时的回复也包含在对话中。(作者提供的图片)

让我们看看除了聊天记录之外,解决方案还跟踪了哪些内容:

# 让我们打印结果的其他部分print("这是由LLM根据聊天记录生成的独立问题:")print(textwrap.fill(result['generated_question' , width=100 ))print("\n这是模型引用的源文件:")display(render_source_documents(result['source_documents']))print(textwrap.fill(f"\n生成的答案:{result ['answer']}", width=100, replace_whitespace=False) )  
第二个问题后的聊天记录之外的输出。(作者提供的图片)

解决方案在内部首先使用LLM将问题“告诉我更多关于第二本书”转换为“What additional information can you provide about ‘The Day of the Jackal’ by Frederick Forsyth?”. 凭借这个问题,解决方案能够搜索向量数据库以获取任何相关信息,并首先检索到《袭击者之日》这一部分。请注意,还包括其他一些与其他书籍相关的不相关的搜索结果。

快速可选侧边栏讨论潜在问题

潜在问题#1-独立问题生成质量差: 在我的测试中,聊天解决方案在生成一个好的独立问题方面并不总是成功,除非对问题生成器提示进行了调整。例如,对于后续问题“告诉我第二本书的情况”,生成的后续问题往往是“你能告诉我关于第二本书的情况吗?”它本身并没有特别有意义,导致了随机的搜索结果,因此,似乎是随机生成的LLM回答。

潜在问题#2-原始问题和后续问题之间的搜索结果变化: 值得注意的是,尽管第二个生成的问题明确命名了感兴趣的书籍,但向量数据库搜索的返回结果包括其他书籍结果,并且这些搜索结果与原始问题的搜索结果不同!在这个例子中,搜索结果的这种变化是可取的,因为问题从“推荐侦探小说”变为了特定的小说。然而,当用户询问后续问题,意图深入研究一个主题时,问题的表述变体或LLM生成的独立问题的变化可能导致不同的搜索结果或搜索结果的不同排名,这可能是不可取的。

这个问题可能在一定程度上通过从向量数据库进行更广泛的初始搜索来自动缓解,返回许多结果,而不仅仅是我们的示例中的4-5个,并对它们进行重新排序,以确保最相关的结果浮出水面,并始终发送给LLM生成最终答案的(参见Cohere的’reranking’)。此外,对于应用程序来说,识别搜索结果是否发生了变化,应该比较简单。可能可以应用一些启发式算法,判断搜索结果的变化程度(通过排名和重叠指标来衡量),以及问题的变化程度(通过余弦相似度等距离指标来衡量)是否相当。至少在聊天轮次中搜索结果出现意外变化的情况下,可以提醒最终用户并引导他们更近一步的检查,具体取决于用例的关键性和最终用户的培训或复杂性。

控制这种行为的另一个方法是利用LLM来决定后续问题是否需要再次访问向量数据库,还是可以通过之前获取的结果有意义地回答问题。一些用例可能希望生成两组搜索结果和响应,并让LLM在答案之间进行裁决,另一些用例可能通过赋予用户控制上下文的责任来控制上下文,例如,通过赋予用户冻结上下文的能力(根据用例、用户培训或复杂性以及其他考虑因素),还有一些可能对后续问题的搜索结果变化持宽容态度。

正如你可能已经注意到的,要得到一个基本的解决方案并不难,但要做到完美,这才是困难的部分。这里提到的问题只是冰山一角。好了,回到主题…

5. 添加预定义的用户界面

最后,聊天机器人的功能准备就绪。现在,我们可以添加一个漂亮的用户界面,以提高用户体验。这在Python库(如Gradio和Streamlit)的帮助下相对容易实现,这些库可以根据用Python编写的指令构建前端小部件。在这里,我们选择使用Gradio快速创建用户界面。

下面的两个代码块是独立的,可以在全新的Colab笔记本中运行,以生成完整的聊天机器人。这旨在方便那些还没有执行到这一步骤的人,同时还展示了实现同一目标的一些变化。

# 初始设置 - 在代码环境中安装必要的软件!pip install openai tiktoken langchain chromadb html2text gradio   # 若您从这里开始或尚未安装任何东西,请取消注释(删除开头的'#')# 导入需要的包以启用解决方案的不同功能from langchain.document_loaders import AsyncHtmlLoader # 将网站内容加载到文档中from langchain.text_splitter import MarkdownHeaderTextSplitter # 按文档标题将文档分割为更小的块from langchain.document_transformers import Html2TextTransformer # 将HTML转换为Markdown文本from langchain.chat_models import ChatOpenAI # 使用OpenAI的LLMfrom langchain.prompts import PromptTemplate # 用于制定指令/提示from langchain.chains import RetrievalQA, ConversationalRetrievalChain # 用于RAGfrom langchain.memory import ConversationTokenBufferMemory # 用于保持聊天记录from langchain.embeddings.openai import OpenAIEmbeddings # 将文本转换为数值表示from langchain.vectorstores import Chroma # 与向量数据库交互import pandas as pd, gradio as gr # 用于将数据显示为表格,用于构建用户界面,分别使用Chroma数据库、将JSON转换为文本和美观打印等方面的工具import chromadb, json, textwrap # 向量数据库、将JSON转换为文本和美观打印from chromadb.utils import embedding_functions # 根据Chroma所需的协议设置嵌入函数# 将OpenAI API密钥添加到变量中# 将密钥以这种方式保存在变量中是不好的做法。应该将其加载到环境变量中然后从那里加载,但这对于快速演示来说是可以的OPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # 假密钥 - 在此处使用您自己的真实密钥

在运行下一组代码以渲染聊天机器人用户界面之前,请注意,当通过Colab渲染时,该应用程序在3天内对任何具有链接的人都是公开访问的(链接在Colab笔记本单元格输出中提供)。理论上,通过更改代码中的最后一行为demo.launch(share=False),可以使该应用程序保持私密,但我无法让该应用程序正常工作。相反,我更喜欢在Colab中以“调试”模式运行它,这样Colab单元格会一直“运行”,直到停止,然后终止聊天机器人。或者,您可以在一个不同的Colab单元格中运行下面显示的代码,以终止聊天机器人并删除加载到Chroma向量数据库中的内容。

# 在最后运行以终止演示聊天机器人demo.close() # 结束聊天会话并终止共享演示# 检索并删除为聊天机器人创建的向量数据库集合vectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)vectordb.delete_collection()

下面是将聊天机器人作为应用程序运行的代码。大部分代码都是重用了此文章到目前为止的代码,所以应该看起来很熟悉。请注意,与之前的代码相比,下面的代码存在一些差异,包括但不限于不再使用LangChain的’token’记忆对象进行内存管理。这意味着随着对话的进行,历史记录会变得过长,无法传递给语言模型的上下文,因此应用程序将需要重新启动。

# 初始化OpenAI嵌入函数。有两种情况,因为当直接将函数传递给Chroma DB或通过LangChain与Chroma DB一起使用时,函数协议不同openai_embedding_func_for_langchain = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)openai_embedding_func_for_chroma = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)# 使用GPT 3.5 Turbo模型初始化LangChain聊天模型对象llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0, openai_api_key=OPENAI_API_KEY)# 初始化向量数据库并创建集合persistent_chroma_client = chromadb.PersistentClient()collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)# 将网站内容加载到向量数据库的函数def load_content_from_url(url):  # 从URL加载HTML并将其转换为更可读的文本格式  docs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())  # 将文档按节拆分  headers_to_split_on = [ ("#", "标题 1"), ("##", "标题 2"), ("###", "标题 3"), ("####", "标题 4"), ("#####", "标题 5") ]  chunks = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on).split_text(docs[0].page_content)  # 此处我们连接到先前创建的ChromaDB数据库的实例  vectordb_collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)  # 让我们将数据添加到向量数据库;特别是添加到向量数据库中的集合  cur_max_id = vectordb_collection.count()  vectordb_collection.add(ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],                           documents=[t.page_content for t in chunks],                           metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]                           )  # 提示用户内容已加载完毕,可以查询  gr.Info(f"网站内容已加载。向量数据库现在有{vectordb_collection.count()}个块")  return# 定义UI和聊天函数with gr.Blocks() as demo:    # 使用文档作为上下文与语言模型进行聊天的函数  def predict(message, history):    # 通过LangChain ChromaDB客户端与先前创建的ChromaDB数据库连接    langchain_chroma = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)    # 将其转换为LangChain聊天历史格式 - 列表中的元

您可以通过给应用程序提供不同的URL来玩弄这个应用程序,以加载内容。不言而喻:这不是一个生产级应用程序,只是为了展示基于RAG的GenAI解决方案构建模块而创建的。充其量,这只是一个早期的原型,如果将其转换为常规产品,大部分软件工程工作将在前面。

重温介绍中的常见问题解答

在创建的聊天机器人的上下文和知识的基础上,让我们重新审视一些在介绍中提出的问题,并稍微深入一些。

  1. 我们的用例是否适合LLM驱动的解决方案?也许传统的分析、监督式机器学习或其他方法更合适?LLM在“理解”语言相关任务以及遵循指令方面表现出色。因此,LLM的早期用例包括问答、摘要、生成(在本例中为文本)、改进基于意义的搜索、情感分析、编码等。LLM还具备问题解决和推理能力。例如,如果您提供了答案键,LLM可以作为学生作业的自动成绩评分器,甚至有时甚至不需要答案键。另一方面,基于大量数据点的预测或分类、用于营销优化的多臂老虎机实验、推荐系统、强化学习系统(Roomba、Nest温控器、优化功耗或库存水平等)是其他类型的分析或机器学习的强项...至少暂时是这样。传统的机器学习模型向LLM提供信息,并且相反,应该被视为解决核心业务问题的一种综合解决方案考虑。
  2. 如果LLM是正确的选择,我们的用例是否可以通过现成的产品(比如ChatGPT企业版)在现在或不久的将来得到解决?传统的建立与购买决策。OpenAI、AWS和其他公司提供的服务和产品将越来越广泛、更好,并可能更便宜。例如,ChatGPT允许用户上传文件进行分析,Bing Chat和Google的Bard可以指向外部网站进行问题回答,AWS Kendra将语义搜索引入企业的信息,Microsoft Copilot可以将LLM引入Word、Powerpoint、Excel等。由于公司不构建自己的操作系统或数据库的原因,公司应该考虑是否需要构建可能会被当前和未来的现成产品所取代的AI解决方案。另一方面,如果公司的用例是特定的,或在某种程度上受限-例如由于敏感性而不能将敏感数据发送给任何供应商,或由于监管指导-那么,可能需要在公司内部开发生成性AI产品以解决用例。使用LLM的推理能力,但进行任务或生成与供应解决方案有着显著区别的输出的产品可能需要内部开发。例如,系统监控工厂车间、生产过程或库存水平等可能需要定制开发,尤其是如果没有良好的领域特定产品提供。此外,如果应用程序需要专门的领域知识,那么依据特定领域数据进行微调的LLM可能会比OpenAI的通用LLM更出色,因此可以考虑内部开发。
  3. 我们的LLM驱动产品的不同构建模块是什么?其中哪些模块已经成熟,哪些需要更多的时间来构建和测试?像我们构建的RAG解决方案一样,高级构建模块包括数据管道、向量数据库、检索、生成以及当然还有LLM。对于LLM和向量数据库,有许多很好的选择。为了优化用例,数据管道、检索和生成的提示工程将需要进行一些传统的数据科学实验。一旦初始解决方案就位,产品化将需要大量的工作,这对于任何数据科学/机器学习管道来说都是真实的。这个演讲提供了在产品化方面的积累的智慧:生产中的LLM:经验教训,罗意德·卡杜斯博士,首席科学家,AnyScale
  4. 如何衡量我们解决方案的性能?有哪些杠杆可以提高产品输出的质量?与任何技术(或非技术)解决方案一样,业务影响应该使用领先的关键绩效指标来衡量。一些难以测量的直接指标会被替代为间接指标,例如每日活跃用户的平均数量(DAU)和其他产品指标。应该将业务指标与技术指标相结合,评估RAG解决方案的性能。响应的整体质量-系统的响应与专家人工生成的最佳响应或最先进的前沿模型(如GPT-4)相比有多好,可以使用一系列测试信息性、真实性、相关性、毒性等的指标来评估。深入研究解决方案的各个组成部分的性能,以进行迭代并改进每个组件:解决方案将用作上下文的信息质量、检索和生成。ii. 数据的质量如何?如果组织机构可用的向量数据库中的数据没有所需的信息,没有人类或LLM可以根据它制作响应。ii. 检索的效果如何?假设信息可用,系统是否成功找到并获取相关的内容?iii. 合成(即生成i. )的质量如何?假设信息可用,正确检索并传递给LLM以生成最终响应,LLM是否按预期使用信息?每个区域都可以单独评估,并在不断改进中改进整体输出。改善数据质量:企业需要努力改善数据管道,以将良好的信息输入系统。如果向量数据库中存在信息质量低劣,出色的LLM也无法大幅改善输出结果。除了采用传统的数据质量和治理框架之外,公司还应考虑改进拆分质量(

    结论

    如果你在11个月前就知道这一切,那么与公司的首席执行官进行演示将会得到合理的解释。甚至可能会有更广泛的观众进行TED演讲。如今,这已经成为AI素养基础的一部分,特别是如果你参与生成式AI产品的交付。希望通过这个练习,你可以跟上最新进展!👍

    以下是几个结束的思考:

    • 这项技术有着巨大的潜力 - 还有多少其他技术可以达到这种程度的“思考”,并能作为“推理引擎”来使用(引用Dr. Andrew Ng的话)。
    • 虽然前沿模型(目前是GPT-4)将继续进步,但开源模型及其领域特定和任务特定的微调变体在许多任务上将具有竞争力,并且会有很多应用。
    • 不管好坏,这项耗费了数百万(数亿?)美元开发的前沿技术现在可以免费使用 - 你只需填写一个表格即可下载Meta强大的Llama2模型,并获得非常宽松的许可证。几乎有30万个基线LLM或其微调变体可以在HuggingFace的模型库中找到。硬件也变得平凡无奇。
    • OpenAI模型现在能够意识到并使用“工具”(函数、API等),让解决方案不仅可以与人类和数据库进行交互,还可以与其他程序进行交互。LangChain和其他软件包已经演示了将LLMs用作具有自主代理能力的“大脑”,它们可以接受输入,决定采取什么行动,并执行,重复这些步骤直到代理达到目标。我们简单的聊天机器人在确定的顺序中使用了两次LLM调用 - 生成独立问题和将搜索结果综合成连贯的自然语言回复。想象一下,如果使用具有自主能力的快速演变的LLM进行数百次调用,能取得什么成就!
    • 这些快速发展是由于维基智能的巨大势头所带来的,它将通过我们的设备在企业和日常生活中普及。一开始是以简化的方式,但随后会在越来越复杂的应用中利用这项技术的推理和决策能力,将其与传统的AI相结合。
    • 最后,现在是参与的好时机,至少对于应用这项技术来说,这个竞技场是相对公平的 - 自从2022年12月的ChatGPT热潮以来,人们几乎在同一时间学习这个技术。当然,在研发领域情况是不同的,大型科技公司已经花费了多年时间和数十亿美元来开发这项技术。不过,为了以后构建更复杂的解决方案,现在是开始的绝佳时机!

    其他资源