使用LangChain的生成式AI——构建像ChatGPT这样的聊天机器人

由LLM(大型语言模型)驱动的聊天机器人在客户服务等对话任务中表现出色。然而,它们在世界知识方面的缺乏限制了它们在特定领域问答中的效用。在本章中,我们将探讨如何通过检索增强生成(Retrieval-Augmented Generation,RAG)来克服这些限制。RAG通过将聊天机器人的回复基于外部证据来源,实现了更准确和信息丰富的答案。这是通过从语料库中检索相关段落来调整语言模型的生成过程来实现的。关键步骤包括将语料库编码为向量嵌入,以实现快速语义搜索,并将检索结果集成到聊天机器人的提示中。

我们还将提供将文档表示为向量的基础知识、用于高效相似性查找的索引方法以及用于管理嵌入的向量数据库。在这些核心技术的基础上,我们将使用流行的库(如Milvus和Pinecone)演示实际的RAG实现。通过逐步示例,我们将展示RAG如何显著改善聊天机器人的推理和事实正确性。最后,我们还讨论了从声誉和法律角度来看另一个重要主题:内容审核。LangChain允许您通过审核链传递任何文本,以检查其是否包含有害内容。

在整个章节中,我们将致力于使用Streamlit界面构建聊天机器人实现,您可以在GitHub存储库(github.com/benman1/gen...)中的chat_with_retrieval目录中找到。

简而言之,主要主题包括:

  1. 什么是聊天机器人?
  2. 了解检索和向量
  3. 在LangChain中加载和检索
  4. 实现聊天机器人
  5. 审核回复

我们将在本章开始时介绍聊天机器人以及其背后的最新技术。

什么是聊天机器人?

聊天机器人是通过文本或语音与用户模拟对话交互的人工智能程序。早期的聊天机器人,如ELIZA(1966年)和PARRY(1972年),使用模式匹配。近期的进展,如大型语言模型(LLM),允许更自然的对话,如ChatGPT(2022)所示。然而,在实现人类水平的对话方面仍然存在挑战。

图灵测试于1950年提出,通过计算机模拟人类对话的能力来评估智能,确立了一个里程碑。尽管存在局限性,但它为人工智能建立了哲学基础。然而,早期系统如ELIZA通过使用脚本响应而不是真正理解就通过了测试,使测试作为对人工智能评估的有效性产生了质疑。该测试还因依赖欺骗和其格式的限制导致问题的复杂性受到限制而受到批评。哲学家约翰·塞尔提出,仅凭符号操作并不能等同于人类水平的智能。然而,图灵测试影响了有关人工智能能力的讨论。

最近更先进的自然语言处理的聊天机器人可以更好地模拟对话深度。IBM Watson(2011年)回答复杂问题以击败《危险边缘》冠军。Siri(2011年)作为一种基于语音的助手,开创性地将聊天机器人集成到日常设备中。Google Duplex(2018年)等系统通过电话会话预订约会。

LLM的出现,如GPT-3,使得更具人类特色的聊天机器人系统成为可能,例如ChatGPT(2022)。然而,它们的能力仍然受到严格限制。真正的人类对话需要复杂的推理、语用学、常识和广泛的背景知识。

因此,今天的基准测试更侧重于测试特定任务的性能,以探究GPT-4等LLM的极限。虽然ChatGPT表现出令人瞩目的连贯性,但它缺乏基础,可能导致似是而非的回复。了解这些边界对于安全和有益的应用至关重要。目标不再仅仅是模仿,而是在深入理解自适应学习系统内部运作的同时发展有用的人工智能。

聊天机器人分析用户输入,理解背后的意图,并生成适当的响应。它们可以设计用于与基于文本的消息平台或基于语音的应用程序配合使用。

聊天机器人在客户服务中的一些用例包括提供全天候支持、处理常见问题、协助产品推荐、处理订单和付款以及解决简单的客户问题。

聊天机器人的一些其他用例包括:

  1. 预约安排:聊天机器人可以帮助用户安排预约、预订座位并管理日历。
  2. 信息检索:聊天机器人可以为用户提供特定信息,如天气更新、新闻文章或股票价格。
  3. 虚拟助手:聊天机器人可以充当个人助手,帮助用户设置提醒、发送消息或进行电话呼叫。
  4. 语言学习:聊天机器人可以通过提供互动对话和语言实践来协助语言学习。
  5. 心理健康支持:聊天机器人可以提供情感支持、提供资源并进行心理治疗对话,用于心理健康目的。
  6. 教育:在教育环境中,虚拟助手被探索为虚拟导师,帮助学生学习并评估他们的知识,回答问题并提供个性化的学习体验。
  7. 人力资源和招聘:聊天机器人可以通过筛选候选人、安排面试并提供有关职位空缺的信息来协助招聘流程。
  8. 娱乐:聊天机器人可以让用户参与互动游戏、测验和叙事体验。
  9. 法律:聊天机器人可以用于提供基本的法律信息、回答常见的法律问题、协助法律研究,并帮助用户导航法律流程。它们还可以协助文档准备,例如起草合同或创建法律表格。
  10. 医学:聊天机器人可以协助检查症状、提供基本医疗建议并提供心理健康支持。它们可以通过为医疗专业人员提供相关信息和建议来改善临床决策。

这只是一些例子,聊天机器人的用例在各行各业和领域不断扩展。在任何领域中,聊天技术都可以使信息更易于获取,并为寻求帮助的个人提供初始支持。但它们在推理或分析方面的无能限制了需要真正智能的角色。通过负责任的开发,聊天机器人在客户服务和其他领域中提供直观界面的前景是光明的,即使人类级别的语言掌握仍然难以实现。正在进行的研究旨在发展安全、有用的聊天机器人能力。在仅对显式用户提示做出响应的聊天机器人和那些具有更先进的主动启动对话和在没有直接提示的情况下提供信息的能力之间存在重要的区别。有意的聊天机器人旨在直接理解和满足特定用户请求和意图。然而,主动的聊天机器人旨在根据先前的互动和语境线索预测需求和偏好,采取会话主动权,主动应对潜在的用户问题。虽然响应式的有意聊天机器人可以有效地完成精确的用户指导,但主动的能力有望通过建立忠诚度和信任来实现更自然、高效的人工智能交互,通过提供预测性服务。然而,掌握上下文和推理仍然是创造既主动又可控助手的人工智能挑战。当前的研究正在推进聊天机器人在这两方面的能力,目标是在流畅、有目的的对话中平衡主动对话和对用户意图的响应。

理解检索和向量

检索增强生成(RAG)是一种通过检索和整合外部知识来增强文本生成的技术。这使输出基于事实信息,而不仅仅依赖于编码在语言模型参数中的知识。特指将检索整合到训练和推理过程中的检索增强语言模型(RALM)。

传统语言模型仅基于提示自回归生成文本。RALM通过首先使用语义搜索算法从外部语料库中检索相关上下文来扩展这一过程。语义搜索通常涉及将文档索引到向量嵌入中,从而通过近似最近邻搜索实现快速相似性查找。

然后,检索到的证据使语言模型生成更准确、与上下文相关的文本。这一循环不断重复,RALM在生成过程中动态制定查询,根据需要检索信息。主动型RALM通过交错进行检索和文本创建,在生成时通过获取澄清知识重新生成不确定的部分。

总体而言,RAG和RALM通过将响应基于外部信息来克服语言模型的内存限制。正如我们稍后将更深入地探讨的,对向量嵌入进行高效存储和索引对于在大型文档集合上实现实时语义搜索至关重要。

通过整合外部知识,RALM生成的文本更有用、更微妙且更符合事实。它们的能力通过对索引方法的优化、对检索时机的推理以及融合内部和外部上下文的手段不断提升。

通过通过RAG将LLM与特定用例的信息相结合,响应的质量和准确性得以提高。通过检索相关数据,RAG有助于减少LLM的臆测性响应。例如,在医疗应用中使用的LLM在推理过程中可以从外部来源(如医学文献或数据库)中检索相关医学信息。然后,这些检索到的数据可以被整合到上下文中,以增强生成的响应,并确保其准确性和与领域特定知识的一致性。

由于我们正在讨论向量存储,我们需要讨论向量搜索,这是一种根据它们与查询向量的相似性来搜索和检索向量(或嵌入)的技术。它通常用于诸如推荐系统、图像和文本搜索以及异常检测等应用中。我们现在将从嵌入的基础知识开始。一旦您理解了嵌入,就能够构建从搜索引擎到聊天机器人等一切。

嵌入(Embeddings)

嵌入(Embedding)是内容的一种数字表示形式,使得机器能够处理和理解。该过程的本质是将对象(如图像或文本)转换为一个向量,该向量封装了其语义内容,同时尽可能丢弃不相关的细节。嵌入将内容片段(如单词、句子或图像)映射到多维向量空间中。两个嵌入之间的距离表示相应概念(原始内容)之间的语义相似性。

嵌入是机器学习模型生成的数据对象的表示形式。它们可以将单词或句子表示为数值向量(浮点数列表)。至于OpenAI语言嵌入模型,嵌入是一个包含1,536个浮点数的向量,表示文本。这些数字来自一个复杂的语言模型,捕捉了语义内容。

举个例子,假设我们有单词"cat"和"dog" - 这些单词可以在与词汇表中的所有其他单词一起的空间中进行数值表示。如果空间是3维的,这些向量可能是[0.5, 0.2, -0.1](cat)和[0.8, -0.3, 0.6](dog)。这些向量编码了这些概念与其他单词的关系的信息。粗略地说,我们期望"cat"和"dog"的概念与"animal"的概念更接近(更相似),而不是与"computer"或"embedding"的概念。

嵌入可以使用不同的方法创建。对于文本,一种简单的方法是词袋法,其中每个单词由它在文本中出现的次数表示。这种方法在scikit-learn库中以CountVectorizer的形式实现,直到word2vec出现之前都很流行。Word2vec大致上通过在线性模型中预测句子中的单词来学习嵌入,忽略了单词的顺序。

我们可以使用这些向量进行简单的矢量运算,例如,king的向量减去man的向量加上woman的向量会得到一个接近queen的向量。嵌入的一般概念如下图所示(来源:"Analogies Explained: Towards Understanding Word Embeddings" by Carl Allen and Timothy Hospedales, 2019; arxiv.org/abs/1901.09...):

至于图像,嵌入可以来自特征提取阶段,比如边缘检测、纹理分析和颜色组合。这些特征可以在不同的窗口大小上提取,使表示既具有尺度不变性又具有平移不变性(尺度空间表示)。如今,通常使用卷积神经网络(CNN)在大型数据集上(如ImageNet)进行预训练,以学习图像属性的良好表示。由于卷积层在输入图像上应用一系列滤波器(或核)以生成特征图,从概念上讲,这类似于尺度空间。然后,当预训练的CNN运行在新图像上时,它可以输出一个嵌入向量。

今天,对于大多数领域,包括文本和图像,嵌入通常来自基于transformer的模型,这些模型考虑了句子和段落中单词的上下文和顺序。根据模型架构,尤其是参数的数量,这些模型可以捕捉非常复杂的关系。所有这些模型都是在大型数据集上训练的,以建立概念及其关系。

这些嵌入可以用于各种任务。通过将数据对象表示为数值向量,我们可以对它们执行数学运算并测量它们的相似性,或将它们用作其他机器学习模型的输入。通过计算嵌入之间的距离,我们可以执行搜索和相似性评分的任务,或对对象进行分类,例如按主题或类别。例如,我们可以通过检查产品评论的嵌入是否更接近正面或负面的概念来执行简单的情感分类器。

在LangChain中,您可以使用任何嵌入类(例如OpenAIEmbeddings类)的embed_query()方法获取嵌入。以下是一个示例代码片段:

scss 复制代码
from langchain.embeddings.openai import OpenAIEmbeddings 
embeddings = OpenAIEmbeddings() 
text = "This is a sample query." 
query_result = embeddings.embed_query(text) 
print(query_result)
print(len(query_result))

此代码将单个字符串输入传递给embed_query方法,并检索相应的文本嵌入。结果存储在query_result变量中。可以使用len()函数获取嵌入的长度(维数的数量)。我假设您已经将API密钥设置为环境变量,这是在第3章"开始使用LangChain"中推荐的。

您还可以使用embed_documents()方法获取多个文档输入的嵌入。以下是一个示例:

ini 复制代码
from langchain.embeddings.openai import OpenAIEmbeddings 
words = ["cat", "dog", "computer", "animal"]
embeddings = OpenAIEmbeddings()
doc_vectors = embeddings.embed_documents(words)

在这种情况下,使用embed_documents()方法检索多个文本输入的嵌入。结果存储在doc_vectors变量中。我们本可以检索长文档的嵌入,但我们只检索了每个单词的向量。

我们还可以在这些嵌入之间进行算术运算;例如,我们可以计算它们之间的距离:

javascript 复制代码
from scipy.spatial.distance import pdist, squareform
import numpy as np
import pandas as pd
X = np.array(doc_vectors)
dists = squareform(pdist(X))

这给出了我们的单词之间的欧氏距离作为一个方阵。让我们对它们进行可视化:

ini 复制代码
import pandas as pd
df = pd.DataFrame(
    data=dists,
    index=words,
    columns=words
)
df.style.background_gradient(cmap='coolwarm')

距离图应该类似于这样:

我们可以确认:猫和狗确实比计算机更接近动物。这里可能有很多问题,例如,狗是否比猫更像动物,或者为什么狗和猫与计算机的距离只比与动物的距离稍远一点。虽然这些问题在某些应用中可能很重要,但让我们记住这只是一个简单的例子。

在这些示例中,我们使用了OpenAI的嵌入 - 在后面的示例中,我们将使用由Hugging Face提供的模型的嵌入。LangChain中有一些集成和工具可以帮助完成这个过程,其中一些我们将在本章后面遇到。

此外,LangChain提供了一个FakeEmbeddings类,可用于在不实际调用嵌入提供程序的情况下测试您的流程。

在本章的背景下,我们将使用它们来检索相关信息(语义搜索)。然而,我们仍然需要讨论如何将这些嵌入集成到应用程序和更广泛的系统中,这就是向量存储发挥作用的地方。

矢量存储

正如前面提到的,在矢量搜索中,数据集中的每个数据点都被表示为高维空间中的一个向量。这些向量捕捉了数据点的特征或特性。其目标是找到与给定查询向量最相似的向量。

在矢量搜索中,数据集中的每个数据对象都被分配一个向量嵌入。这些嵌入是可以用作高维空间中坐标的数字数组。可以使用余弦相似度或欧氏距离等距离度量来计算向量之间的距离。为了进行矢量搜索,查询向量(表示搜索查询)与集合中的每个向量进行比较。计算查询向量与集合中每个向量之间的距离,并认为距离较小的对象更相似。

为了有效地执行矢量搜索,使用了矢量存储机制,例如矢量数据库。

矢量搜索是指根据它们与给定查询向量的相似性,在其他存储的向量中搜索相似向量的过程,例如在矢量数据库中。矢量搜索通常用于各种应用,如推荐系统、图像和文本搜索以及基于相似性的检索。矢量搜索的目标是通过使用点积或余弦相似度等相似性度量,高效而准确地检索与查询向量最相似的向量。

矢量存储是指用于存储矢量嵌入的机制,同时也涉及到如何检索这些矢量嵌入。矢量存储可以是一个独立的解决方案,专门设计用于高效存储和检索矢量嵌入。另一方面,矢量数据库是专为管理矢量嵌入而构建的,相对于使用类似 Faiss 的独立矢量索引,它们提供了一些优势。

让我们深入了解这些概念。这涉及到三个层面:

  1. 索引将矢量组织起来以优化检索,使它们结构化,以便可以快速检索矢量。这方面有不同的算法,如 k-d 树或 Annoy。
  2. 矢量库提供了进行矢量操作(如点积和矢量索引)的功能。
  3. 矢量数据库,如 Milvus 或 Pinecone,专为存储、管理和检索大量矢量而设计。它们使用索引机制以便在这些矢量上进行高效的相似性搜索。

这些组件共同用于创建、操作、存储和高效检索矢量嵌入。让我们依次查看这些内容,以便更好地理解处理嵌入的基本原理。理解这些基本原理应该使得使用 RAG 变得直观。

矢量索引

在矢量嵌入的上下文中,索引是一种组织数据以优化检索和/或存储的方法。这类似于传统数据库系统中的概念,其中索引允许更快地访问数据记录。对于矢量嵌入,索引的目标是结构化这些矢量,粗略地说,使相似的矢量相邻存储,从而实现快速的接近或相似性搜索。

在这个上下文中通常应用的算法是k维树(k-d trees),但是许多其他算法,比如球树、Annoy和Faiss,经常被实现,特别是对于传统方法在高维矢量方面可能遇到困难的情况。

还有一些常用于相似性搜索索引的其他类型的算法。其中一些包括:

  • 产品量化(PQ):PQ是一种将矢量空间划分为较小子空间并分别量化每个子空间的技术。这降低了矢量的维度,并允许有效的存储和搜索。PQ以其快速的搜索速度而闻名,但可能牺牲一些准确性。PQ的示例包括k-d树和球树。在k-d树中,构建了一种二叉树结构,根据数据点的特征值对它们进行分区。它对低维数据很有效,但随着维度的增加,其效果减弱。在球树中,构建了一种树结构,将数据点划分为嵌套的超球体。它适用于高维数据,但在低维数据上可能比k-d树慢。
  • 局部敏感哈希(LSH):这是一种基于哈希的方法,将相似的数据点映射到相同的哈希桶中。对于高维数据而言,这是高效的,但可能具有更高的误报率和漏报率。Annoy(近似最近邻Oh Yeah)算法是一种流行的LSH算法,它使用随机投影树来索引矢量。它构造了一种二叉树结构,其中每个节点表示一个随机超平面。Annoy易于使用并提供快速的近似最近邻搜索。
  • 分层可导航小世界(HNSW):HNSW是一种基于图的索引算法,它构建了一个分层图结构来组织这些矢量。它使用随机化和贪婪搜索的组合来构建可导航的网络,从而实现了高效的最近邻搜索。HNSW以其高搜索准确性和可扩展性而闻名。 除了HNSW和KNN之外,还有其他基于图的方法,如图神经网络(GNNs)和图卷积网络(GCNs),利用图结构进行相似性搜索。

这些索引算法在搜索速度、准确性和内存使用方面有不同的权衡。算法的选择取决于应用程序的具体要求和矢量数据的特征。

矢量库

矢量库,如Facebook(Meta)的Faiss或Spotify的Annoy,提供处理矢量数据的功能。在矢量搜索的上下文中,矢量库专门设计用于存储和对矢量嵌入执行相似性搜索。这些库使用近似最近邻(ANN)算法,通过矢量高效搜索并找到最相似的矢量。它们通常提供ANN算法的不同实现,如聚类或基于树的方法,并允许用户为各种应用执行矢量相似性搜索。

以下是一些用于矢量存储的开源库的简要概述,显示它们随时间的GitHub星标人气(来源:star-history.com):

你可以看到Faiss在GitHub上得到了很多星标。Annoy排名第二。其他库尚未达到相同的受欢迎程度。

让我们快速了解一下这些:

  • Faiss(Facebook AI Similarity Search)是由Meta(以前是Facebook)开发的库,提供高效的密集向量相似性搜索和聚类。它提供各种索引算法,包括PQ、LSH和HNSW。Faiss广泛用于大规模向量搜索任务,并支持CPU和GPU加速。
  • Annoy是由Spotify维护和开发的在高维空间进行近似最近邻搜索的C++库,实现了Annoy算法。它设计为高效且可扩展,适用于大规模向量数据。它使用一组随机投影树。
  • hnswlib是使用HNSW算法进行近似最近邻搜索的C++库。它为高维向量数据提供了快速和内存高效的索引和搜索功能。
  • nmslib(Non-Metric Space Library)是一个开源库,提供非度量空间中高效的相似性搜索。它支持HNSW、SW-graph和SPTAG等各种索引算法。
  • SPTAG由Microsoft实现了分布式的ANN。它带有一个k-d树和相对邻域图(SPTAG-KDT),以及一个平衡的k均值树和相对邻域图(SPTAG-BKT)。

nmslib和hnswlib都是由在亚马逊担任高级研究科学家的Leo Boytsov和Yury Malkov维护的。还有许多其他库,你可以在github.com/erikbern/an...上看到概览。

向量数据库

向量数据库旨在处理向量嵌入,使得搜索和查询数据对象变得更加便捷。它提供了额外的功能,如数据管理、元数据存储和过滤,以及可伸缩性。虽然向量存储专注于仅存储和检索向量嵌入,但向量数据库为管理和查询向量数据提供了更全面的解决方案。对于涉及大量数据并需要在多种类型的向量化数据(如文本、图像、音频、视频等)之间进行灵活高效搜索的应用,向量数据库可能特别有用。

向量数据库可用于存储和提供机器学习模型及其相应的嵌入。其主要应用是相似性搜索(也称语义搜索),其中我们可以通过基于向量表示的查询高效搜索大量文本、图像或视频,识别与查询相匹配的对象。这在文档搜索、逆图像搜索和推荐系统等应用中特别有用。

向量数据库的其他用途正在随着技术的发展不断扩展;然而,一些向量数据库的常见用途包括:

  1. 异常检测:向量数据库可通过比较数据点的向量嵌入来检测大型数据集中的异常。这在欺诈检测、网络安全或需要识别异常模式或行为的监测系统中非常有价值。
  2. 个性化:通过基于用户偏好或行为找到相似向量,向量数据库可用于创建个性化的推荐系统。
  3. 自然语言处理(NLP) :向量数据库广泛应用于NLP任务,如情感分析、文本分类和语义搜索。通过将文本表示为向量嵌入,比较和分析文本数据变得更加容易。

这些数据库之所以受欢迎,是因为它们经过优化,适用于在高维向量空间中表示和检索数据。传统数据库并非设计用于高维向量(如表示图像或文本嵌入的向量)的高效处理。

向量数据库的特征包括:

  • 高效检索相似向量:向量数据库擅长查找高维空间中的接近嵌入或相似点。这使其非常适用于逆图像搜索或基于相似性的推荐等任务。
  • 专为特定任务而设计:向量数据库旨在执行特定任务,如查找接近嵌入。它们不是通用数据库,而是为高效处理大量向量数据而定制的。
  • 支持高维空间:向量数据库可以处理具有数千维度的向量,这对于处理自然语言处理或图像识别等任务至关重要。
  • 实现先进的搜索功能:通过向量数据库,可以构建强大的搜索引擎,可搜索相似向量或嵌入。这为构建内容推荐系统或语义搜索等应用提供了可能性。

总体而言,向量数据库为处理大维度向量数据提供了一种专业且高效的解决方案,使得相似性搜索和先进搜索功能成为可能。

当前,由于多种因素,开源软件和数据库市场蓬勃发展。首先,人工智能(AI)和数据管理对企业至关重要,促使对先进数据库解决方案的高需求。

在数据库市场上,新型数据库的出现并创建新的市场类别是一种历史性现象。这些创新型数据库通常主导行业,并吸引风险投资(VC)的大量投资。例如,MongoDB、Cockroach、Neo4J和Influx等公司都是成功的公司,它们推出了创新的数据库技术,并占据了相当大的市场份额。流行的Postgres有一个用于高效向量搜索的扩展:pg_embedding。HNSW提供了与IVFFlat索引相比更快更高效的替代方案。

一些向量数据库的示例列在表5.1中。我已经突出显示了每个搜索引擎的以下几个方面:

  • 价值主张:是什么独特的特性使得这个向量搜索引擎与其他引擎不同?
  • 商业模式:引擎的一般类型,无论它是向量数据库、大数据平台还是托管/自托管的。
  • 索引:该搜索引擎在相似性/向量搜索方面采用的算法方法及其独特的功能。
  • 许可证:它是开源还是闭源。

对于开源数据库,GitHub星标的历史记录很好地反映了它们的受欢迎程度和发展动力。以下是随时间变化的趋势图(来源:star-history.com):

你可以看到Milvus非常受欢迎;然而,其他库如qdrant、weviate和chroma也在迎头赶上。

在LangChain中,可以使用vectorstores模块实现向量存储。该模块提供了各种用于存储和查询向量的类和方法。让我们看一个在LangChain中实现向量存储的示例!

Chroma

此向量存储经过优化,使用Chroma作为后端来存储和查询向量。Chroma接管了基于它们的角相似性对向量进行编码和比较。

要在LangChain中使用Chroma,您需要按照以下步骤操作:

  1. 导入必要的模块:
javascript 复制代码
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
  1. 创建Chroma的实例并提供文档(拆分)和嵌入方法:
ini 复制代码
vectorstore = Chroma.from_documents(documents=docs, embedding=OpenAIEmbeddings())

文档(或拆分,如第5章"构建类似ChatGPT的聊天机器人"中所示)将嵌入并存储在Chroma向量数据库中。我们将在本章的另一节讨论文档加载器。但为了完整起见,您可以像这样为前面的Chroma向量存储获取docs参数:

ini 复制代码
from langchain.document_loaders import ArxivLoader
from langchain.text_splitter import CharacterTextSplitter

loader = ArxivLoader(query="2310.06825")
documents = loader.load()

text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

这将加载并切分有关Mistal 7B的论文。请注意,下载将是一个PDF,您需要安装pymupdf库。

  1. 我们可以查询向量存储以检索相似的向量:
ini 复制代码
similar_vectors = vector_store.query(query_vector, k)

在这里,query_vector是您想要找到相似向量的向量,k是您想要检索的相似向量的数量。

在本节中,我们学到了嵌入和向量存储的基础知识。我们还看到了如何在向量存储和向量数据库中处理嵌入和文档。在实际应用中,如果我们想要构建一个聊天机器人,有两个要点需要了解,尤其是文档加载器和检索器,我们将在下面看到。

LangChain中的加载和检索

LangChain实现了用于构建检索系统的不同构建块的工具链。在本节中,我们将看看如何将它们组合在一起,以构建具有RAG的聊天机器人的流水线。这包括数据加载器、文档转换器、嵌入模型、向量存储和检索器。

它们之间的关系在这里的图表中有所说明(来源:LangChain文档):

在LangChain中,我们首先通过数据加载器加载文档。然后,我们可以对它们进行转换,并将这些文档传递给一个向量存储作为嵌入。然后,我们可以查询向量存储或与向量存储关联的检索器。在LangChain中,检索器可以将加载和向量存储包装为一个单一步骤。在本章中,我们将主要跳过转换;然而,您将在数据加载器、嵌入、存储机制和检索器的解释中找到带有示例的说明。

在LangChain中,我们可以通过集成的文档加载器从许多来源和多种格式加载文档。您可以使用LangChain集成中心浏览和选择适合您数据源的加载器。选择加载器后,可以使用指定的加载器加载文档。

让我们看看LangChain中的文档加载器!在实际实现RAG的流水线中,这些是第一步。

文档加载器 (Document loaders)

文档加载器用于从源加载数据作为文档对象,文档对象包括文本和相关的元数据。有几种可用的集成,例如用于加载简单 .txt 文件(TextLoader)的文档加载器,用于加载网页的文本内容(WebBaseLoader),用于加载 Arxiv 文章(ArxivLoader),或用于加载 YouTube 视频的转录(YoutubeLoader)。对于网页,Diffbot 集成提供了内容的清晰提取。还有其他用于图像的集成,例如提供图像标题(ImageCaptionLoader)。

文档加载器具有 load() 方法,该方法从配置的源加载数据并将其作为文档返回。它们还可能具有 lazy_load() 方法,用于根据需要将数据加载到内存中。

以下是从文本文件加载数据的文档加载器示例:

ini 复制代码
from langchain.document_loaders import TextLoader
loader = TextLoader(file_path="path/to/file.txt")
documents = loader.load()

documents 变量将包含加载的文档,可以进一步访问进行处理。每个文档都包括 page_content(文档的文本内容)和 metadata(相关的元数据,如源 URL 或标题)。

类似地,我们可以从维基百科加载文档:

ini 复制代码
from langchain.document_loaders import WikipediaLoader
loader = WikipediaLoader("LangChain")
documents = loader.load()

值得注意的是,具体的文档加载器实现可能会根据所使用的编程语言或框架而有所不同。

在 LangChain 中,代理或链中的向量检索是通过检索器完成的,检索器访问向量存储。接下来,让我们看看检索器的工作原理。

LangChain中的检索器

LangChain中的检索器是一种组件类型,用于从向量存储中检索和获取信息,其作为后端使用向量存储(如Chroma)进行索引和嵌入搜索。检索器在文档上回答问题方面起着关键作用,因为它们负责根据给定的查询检索相关信息。

以下是一些检索器的示例:

  • BM25检索器: 该检索器使用BM25算法根据文档与给定查询的相关性对文档进行排名。BM25是一种考虑词频和文档长度的流行信息检索算法。
  • TF-IDF检索器: 该检索器使用TF-IDF(词频-逆文档频率)算法根据文档集中的词语重要性对文档进行排名。它赋予在集合中罕见但在特定文档中频繁出现的术语更高的权重。
  • 密集检索器: 该检索器使用密集嵌入来检索文档。它将文档和查询编码为密集向量,并使用余弦相似性或其他距离度量计算它们之间的相似性。
  • kNN检索器: 该检索器利用著名的k最近邻算法根据与给定查询的相似性检索相关文档。

这些只是LangChain中可用的检索器的一些示例。每个检索器都有其优势和劣势,选择检索器取决于具体的用例和需求。例如,Arxiv检索器的目的是从Arxiv.org存档中检索科学文章。它是一个允许用户搜索和下载各种领域的学术文章的工具,如物理学、数学、计算机科学等。

Arxiv检索器的功能包括指定要下载的文档的最大数量,根据查询检索相关文档以及访问检索文档的元数据信息。

Wikipedia检索器允许用户从Wikipedia网站中检索Wikipedia页面或文档。Wikipedia检索器的目的是提供对Wikipedia上大量信息的轻松访问,并使用户能够从中提取特定信息或知识。

让我们看看一些检索器,它们适用于什么情况,以及如何自定义检索器。

kNN检索器

要使用kNN检索器,您需要创建检索器的新实例,并向其提供一个文本列表。以下是使用来自OpenAI的嵌入创建kNN检索器的示例:

ini 复制代码
from langchain.retrievers import KNNRetriever
from langchain.embeddings import OpenAIEmbeddings

words = ["cat", "dog", "computer", "animal"]
retriever = KNNRetriever.from_texts(words, OpenAIEmbeddings())

创建检索器后,您可以使用它通过调用get_relevant_documents()方法并传递查询字符串来检索相关文档。检索器将返回一个与查询最相关的文档列表。

以下是如何使用kNN检索器的示例:

ini 复制代码
result = retriever.get_relevant_documents("dog")
print(result)

这将输出与查询相关的文档列表。每个文档都包含页面内容和元数据:

css 复制代码
[Document(page_content='dog', metadata={}), Document(page_content='animal', metadata={}), Document(page_content='cat', metadata={}), Document(page_content='computer', metadata={})]

PubMed检索器

在LangChain中还有一些更专业的检索器,例如来自PubMed的检索器。PubMed检索器是LangChain中的一个组件,帮助将生物医学文献检索整合到其语言模型应用中。PubMed包含来自各种来源的数百万篇生物医学文献的引用。

在LangChain中,PubMedRetriever类用于与PubMed数据库交互,并根据给定的查询检索相关文献。该类的get_relevant_documents()方法接受一个查询作为输入,并返回来自PubMed的相关文献列表。

以下是在LangChain中使用PubMed检索器的示例:

ini 复制代码
from langchain.retrievers import PubMedRetriever 
retriever = PubMedRetriever() 
documents = retriever.get_relevant_documents("COVID")
for document in documents:
    print(document.metadata["Title"])

在此示例中,使用查询"COVID"调用get_relevant_documents()方法。该方法然后检索与查询相关的PubMed文献,并将它们作为列表返回。打印输出显示了相应的标题:

erlang 复制代码
The COVID-19 pandemic highlights the need for a psychological support in systemic sclerosis patients.
Host genetic polymorphisms involved in long-term symptoms of COVID-19.
Association Between COVID-19 Vaccination and Mortality after Major Operations.

自定义检索器

在LangChain中,我们可以通过创建一个从BaseRetriever抽象类继承的类来实现自己的自定义检索器。该类应该实现get_relevant_documents()方法,该方法接受一个查询字符串作为输入,并返回一列相关文档。

以下是实现检索器的示例:

python 复制代码
from langchain.schema import Document, BaseRetriever

class MyRetriever(BaseRetriever):
    def get_relevant_documents(self, query: str, **kwargs) -> list[Document]:
        # 在这里实现您的检索逻辑
        # 根据查询检索和处理文档
        # 返回一列相关文档
        relevant_documents = []
        # 您的检索逻辑放在这里...
        return relevant_documents

您可以定制此方法以执行任何您需要的检索操作,例如查询数据库或搜索索引文档。

一旦您实现了检索器类,就可以创建一个实例,并调用get_relevant_documents()方法来基于查询检索相关文档。

现在我们已经了解了向量存储和检索器,让我们把所有这些应用起来。让我们实现一个带有检索器的聊天机器人!

实现一个聊天机器人

现在我们将实现一个聊天机器人。我们假设您已经按照第3章《Getting Started with LangChain》中的说明,使用必要的库和API密钥设置了环境。

要在LangChain中实现一个简单的聊天机器人,您可以按照以下步骤进行:

  1. 设置文档加载器。
  2. 将文档存储在向量存储中。
  3. 设置一个带有从向量存储中检索的聊天机器人。

我们将通过多种格式对此进行泛化,并通过Streamlit在Web浏览器中提供一个界面。您将能够插入您的文档并开始提问。在生产环境中,对于企业部署和客户互动,您可以想象这些文档已经被加载进来,而您的向量存储可以是静态的。

让我们从文档加载器开始。

文档加载器

正如前面提到的,我们希望能够读取不同的格式:

python 复制代码
from typing import Any
from langchain.document_loaders import (
  PyPDFLoader, TextLoader,
  UnstructuredWordDocumentLoader,
  UnstructuredEPubLoader
)

class EpubReader(UnstructuredEPubLoader):
    def __init__(self, file_path: str | list[str], **kwargs: Any):
        super().__init__(file_path, **kwargs, mode="elements", strategy="fast")

class DocumentLoaderException(Exception):
    pass

class DocumentLoader(object):
    """Loads in a document with a supported extension."""
    supported_extentions = {
        ".pdf": PyPDFLoader,
        ".txt": TextLoader,
        ".epub": EpubReader,
        ".docx": UnstructuredWordDocumentLoader,
        ".doc": UnstructuredWordDocumentLoader
    }

这为我们提供了读取具有不同扩展名的 PDF、文本、EPUB 和 Word 文档的接口。现在我们将实现加载器逻辑:

python 复制代码
import logging
import pathlib
from langchain.schema import Document

def load_document(temp_filepath: str) -> list[Document]:
    """Load a file and return it as a list of documents."""
    ext = pathlib.Path(temp_filepath).suffix
    loader = DocumentLoader.supported_extentions.get(ext)
    if not loader:
        raise DocumentLoaderException(
            f"Invalid extension type {ext}, cannot load this type of file"
        )
    loader = loader(temp_filepath)
    docs = loader.load()
    logging.info(docs)
    return docs

目前这并不处理很多错误,但如果需要,可以进行扩展。现在我们可以从界面中使用这个加载器,并将其连接到向量存储。

向量存储

这一步包括设置嵌入机制、向量存储和一个通过的流程:我们将文档分块。接着,我们从Hugging Face设置了一个小模型用于嵌入,并设置了一个用于接收分块、创建嵌入和存储的 DocArray 接口。最后,我们的检索器正在按最大边际相关性查找文档。

我们使用 DocArray 作为我们的内存中的向量存储。DocArray 提供各种功能,如高级索引、全面的序列化协议、统一的 Pythonic 接口等。此外,它为处理自然语言处理、计算机视觉和音频处理等任务的多模态数据提供了高效而直观的处理。

我们可以使用不同的距离度量初始化 DocArray,例如余弦和欧几里得距离------余弦是默认值。

对于检索器,我们有两个主要选项:

  • 相似性搜索:我们可以根据相似性检索文档。
  • 最大边际相关性(MMR):我们可以在检索过程中对文档进行基于多样性的重新排名,以获得覆盖迄今为止检索到的文档的不同观点或观点。

在相似性搜索中,我们可以设置相似性分数阈值。我们选择了 MMR。这有助于从不同的角度检索到更广泛的相关信息,而不仅仅是重复的、冗余的命中。MMR 缓解了检索中的冗余性,并缓解了文档集合中固有的偏见。我们将 k 参数设置为 2,这意味着我们将从检索中获取 2 个文档。

检索可以通过上下文压缩来改进,这是一种检索到的文档被压缩,过滤掉不相关信息的技术。与返回原始文档相比,上下文压缩不是将完整文档原样返回,而是使用给定查询的上下文提取并仅返回相关信息。这有助于减少处理成本并提高检索系统的响应质量。

基础压缩器负责根据给定查询的上下文压缩单个文档的内容。它使用语言模型(例如 GPT-3)执行压缩。压缩器可以过滤掉不相关的信息,并仅返回文档的相关部分。

基础检索器是基于查询检索文档的文档存储系统。它可以是任何检索系统,例如搜索引擎或数据库。当查询传递给上下文压缩检索器时,它首先将查询传递给基础检索器以检索相关文档。然后,它使用基础压缩器根据查询的上下文压缩这些文档的内容。最后,只包含相关信息的压缩文档作为响应返回。

对于上下文压缩,我们有一些选项:

  • LLMChainExtractor:这会遍历返回的文档,并从每个文档中提取只有相关内容的部分。
  • LLMChainFilter:这稍微简单一些;它只过滤了只有相关文档(而不是文档中的内容)。
  • EmbeddingsFilter:这根据文档和查询的嵌入方面应用相似性过滤器。

前两个压缩器需要调用 LLM,这意味着它可能会很慢且成本高昂。因此,EmbeddingsFilter 可以是一个更有效的选择。

我们可以在此处通过简单的开关语句集成压缩(替换 return 语句):

ini 复制代码
if not use_compression:
    return retriever
embeddings_filter = EmbeddingsFilter(
  embeddings=embeddings, similarity_threshold=0.76
)
return ContextualCompressionRetriever(
  base_compressor=embeddings_filter,
  base_retriever=retriever
)

请注意,我只是编造了一个新变量 use_compression。我们可以通过 configure_qa_chain() 将 use_compression 参数传递给 configure_retriever() 方法(此处未显示)。

对于我们选择的压缩器 EmbeddingsFilter,我们需要包括两个额外的导入:

javascript 复制代码
from langchain.retrievers.document_compressors import EmbeddingsFilter
from langchain.retrievers import ContextualCompressionRetriever

现在我们有了创建检索器的机制。我们可以设置聊天链:

python 复制代码
from langchain.chains import ConversationalRetrievalChain
from langchain.chains.base import Chain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory

def configure_chain(retriever: BaseRetriever) -> Chain:
    """Configure chain with a retriever."""
    # 设置上下文会话的内存
    memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
    # 设置 LLM 和 QA 链;将温度设置得低一些,以保持幻觉受控
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo", temperature=0, streaming=True
    )
    # 传入 max_tokens_limit 会自动在提示 LLM 时截断令牌!
    return ConversationalRetrievalChain.from_llm(
        llm, retriever=retriever, memory=memory, verbose=True, max_tokens_limit=4000
    )

关于检索逻辑的最后一件事是获取文档并将其传递给检索器设置:

python 复制代码
import os
import tempfile

def configure_qa_chain(uploaded_files):
    """Read documents, configure retriever, and the chain."""
    docs = []
    temp_dir = tempfile.TemporaryDirectory()
    for file in uploaded_files:
        temp_filepath = os.path.join(temp_dir.name, file.name)
        with open(temp_filepath, "wb") as f:
            f.write(file.getvalue())
        docs.extend(load_document(temp_filepath))
    retriever = configure_retriever(docs=docs)
    return configure_chain(retriever=retriever)

现在我们有了聊天机器人的逻辑,我们需要设置界面。如前所述,我们将再次使用 Streamlit:

ini 复制代码
import streamlit as st
from langchain.callbacks import StreamlitCallbackHandler

st.set_page_config(page_title="LangChain: Chat with Documents", page_icon="")
st.title(" LangChain: Chat with Documents")

uploaded_files = st.sidebar.file_uploader(
    label="Upload files",
    type=list(DocumentLoader.supported_extentions.keys()),
    accept_multiple_files=True
)

if not uploaded_files:
    st.info("Please upload documents to continue.")
    st.stop()

qa_chain = configure_qa_chain(uploaded_files)
assistant = st.chat_message("assistant")
user_query = st.chat_input(placeholder="Ask me anything!")

if user_query:
    stream_handler = StreamlitCallbackHandler(assistant)
    response = qa_chain.run(user_query, callbacks=[stream_handler])
    st.markdown(response)

这为我们提供了一个具有检索功能的聊天机器人,可通过可视界面使用,并且还具有针对需要提问的自定义文档的插入功能。

你可以在 GitHub 上查看完整的实现。你可以尝试与聊天机器人互动,看看它的工作原理以及何时不工作。

需要注意的是,LangChain 对输入大小和成本有一些限制。你可能需要考虑处理更大知识库或优化 API 使用成本的解决方案。此外,微调模型或在内部托管 LLM 可能比使用商业解决方案更复杂,精度更低。我们将在第8章《自定义LLMs及其输出》中讨论这些用例。

在 LangChain 框架中,内存是一个组件,允许聊天机器人和语言模型记住先前的互动和信息。在聊天机器人等应用中,它是必不可少的,因为它使系统能够在对话中保持上下文和连贯性。让我们看看在 LangChain 中的内存及其机制。

记忆

记忆使聊天机器人能够保留先前互动的信息,维持连贯性和对话上下文。这类似于人类的回忆,它使对话有条理、有意义。没有记忆,聊天机器人将难以理解对先前交流的引用,导致对话断断续续,不尽如人意。

具体而言,记忆通过在整个对话中保持上下文理解来提高准确性。聊天机器人可以参考对话的这个整体视角来进行适当的回应。记忆还通过始终识别过去互动中的事实和细节来增强个性化和忠实度。

通过存储消息序列中的知识,记忆允许提取见解,以提高随时间推移的性能。像 LangChain 这样的架构实施了记忆,使聊天机器人能够借助先前的交流,回答后续问题,并维持自然而逻辑的对话。

总体而言,记忆是复杂聊天机器人的关键组成部分,使它们能够从对话中学习,并模拟人类对话者天然具备的回忆和上下文意识。对长期记忆的保留和推理的进一步发展可能导致更有意义和更有效的人工智能与人类交互。

对话缓冲区

以下是一个演示如何使用 LangChain 记忆功能的 Python 实际示例:

ini 复制代码
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain

# 创建带有记忆的对话链
memory = ConversationBufferMemory()
llm = ChatOpenAI(
  model_name="gpt-3.5-turbo", temperature=0, streaming=True
)
chain = ConversationChain(llm=llm, memory=memory)

# 用户输入一条消息
user_input = "Hi, how are you?"
# 在对话链中处理用户输入
response = chain.predict(input=user_input)
# 打印响应
print(response)

# 用户输入另一条消息
user_input = "What's the weather like today?"
# 在对话链中处理用户输入
response = chain.predict(input=user_input)
# 打印响应
print(response)

# 打印存储在记忆中的对话历史
print(memory.chat_memory.messages)

在这个例子中,我们使用 ConversationBufferMemory 创建了一个带有记忆的对话链,它是一个简单的包装器,将消息存储在一个变量中。用户的输入使用对话链的 predict() 方法进行处理。对话链保留了先前交互的记忆,使其能够提供具有上下文感知的响应。

如果不从链中分开构建记忆,我们可以简化事物:

ini 复制代码
conversation = ConversationChain(
    llm=llm,
    verbose=True,
    memory=ConversationBufferMemory()
)

我们将 verbose 设置为 True 以查看提示。

在处理用户输入后,我们打印了对话链生成的响应。此外,我们使用 memory.chat_memory.messages 打印了存储在记忆中的对话历史。使用 save_context() 方法来保存输入和输出。您可以使用 load_memory_variables() 方法查看存储的内容。为了将历史记录作为消息列表获取,将 return_messages 参数设置为 True。在本节中,我们将看到这方面的例子。

ConversationBufferWindowMemory 是 LangChain 提供的一种记忆类型,它跟踪对话随时间的推移而进行的互动。与 ConversationBufferMemory 不同,后者保留所有先前的互动,而 ConversationBufferWindowMemory 仅保留最后的 k 次互动,其中 k 是指定的窗口大小。以下是如何在 LangChain 中使用 ConversationBufferWindowMemory 的简单示例:

ini 复制代码
from langchain.memory import ConversationBufferWindowMemory

memory = ConversationBufferWindowMemory(k=1)

在这个例子中,窗口大小设置为 1,这意味着只有最后一次互动将存储在记忆中。

我们可以使用 save_context() 方法保存每次互动的上下文。它接受两个参数:user_inputmodel_output。它们代表用户的输入和相应互动的模型输出。

css 复制代码
memory.save_context({"input": "hi"}, {"output": "whats up"})
memory.save_context({"input": "not much you"}, {"output": "not much"})

我们可以通过 memory.load_memory_variables({}) 查看消息。

此外,我们还可以在 LangChain 中自定义会话记忆,包括修改用于 AI 和人类消息的前缀,并更新提示模板以反映这些更改。

要自定义会话记忆,可以按照以下步骤操作:

  1. 从 LangChain 中导入必要的类和模块:
javascript 复制代码
from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts.prompt import PromptTemplate
  1. 定义一个包含自定义前缀的新提示模板。可以通过创建一个带有所需模板字符串的 PromptTemplate 对象来实现:
ini 复制代码
template = """The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
{history}
Human: {input}
AI Assistant:"""
PROMPT = PromptTemplate(input_variables=["history", "input"], template=template)
  1. 创建对话链时使用新的提示模板。在这个例子中,将 AI 前缀设置为 "AI Assistant",而不是默认的 "AI"。
ini 复制代码
conversation = ConversationChain(
    prompt=PROMPT,
    llm=llm,
    verbose=True,
    memory=ConversationBufferMemory(ai_prefix="AI Assistant"),
)

记忆对话摘要

ConversationSummaryMemory是LangChain中的一种记忆类型,它在对话进行时生成对话摘要。与存储所有消息的逐字方式不同,它将信息压缩,提供对话的摘要版本。这对于较长的对话特别有用,因为包含所有先前的消息可能会超过令牌限制。

要使用ConversationSummaryMemory,首先创建一个实例,将语言模型(llm)作为参数传递。然后,使用save_context()方法保存交互上下文,其中包括用户输入和AI输出。要检索摘要的对话历史,请使用load_memory_variables()方法。

以下是一个示例:

python 复制代码
from langchain.memory import ConversationSummaryMemory
from langchain.llms import OpenAI

# 初始化摘要记忆和语言模型
memory = ConversationSummaryMemory(llm=OpenAI(temperature=0))

# 保存交互的上下文
memory.save_context({"input": "hi"}, {"output": "whats up"})

# 加载摘要记忆
memory.load_memory_variables({})

存储知识图谱

在LangChain中,我们还可以从对话中提取信息作为事实,并通过将知识图谱集成为内存来存储这些信息。这可以增强语言模型的能力,并使它们能够在文本生成和推理过程中利用结构化知识。

知识图谱是一种结构化的知识表示模型,以实体、属性和关系的形式组织信息。它将知识表示为图形,其中实体表示为节点,实体之间的关系表示为边。在知识图谱中,实体可以是世界上的任何概念、对象或事物,而属性描述这些实体的属性或特征。关系捕捉实体之间的连接和关联,提供语境信息并支持语义推理。

LangChain中有用于检索知识图谱的功能;但是,LangChain还提供了内存组件,可以根据我们的对话消息自动创建知识图谱。

我们将实例化ConversationKGMemory类,并将您的LLM实例作为llm参数传递:

ini 复制代码
from langchain.memory import ConversationKGMemory
from langchain.llms import OpenAI
llm = OpenAI(temperature=0)
memory = ConversationKGMemory(llm=llm)

随着对话的进行,我们可以使用ConversationKGMemory的save_context()函数将知识图谱中的相关信息保存到内存中。

结合多个记忆机制

LangChain 还允许使用 CombinedMemory 类结合多个记忆策略。当您希望保留对话历史的各个方面时,这非常有用。例如,一个记忆可以用于存储完整的对话日志:

ini 复制代码
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory, CombinedMemory, ConversationSummaryMemory

# 初始化语言模型(带有所需的温度参数)
llm = OpenAI(temperature=0)

# 定义对话缓冲内存(用于保留所有过去的消息)
conv_memory = ConversationBufferMemory(memory_key="chat_history_lines", input_key="input")

# 定义对话摘要内存(用于总结对话)
summary_memory = ConversationSummaryMemory(llm=llm, input_key="input")

# 结合两种记忆类型
memory = CombinedMemory(memories=[conv_memory, summary_memory])

# 定义提示模板
_DEFAULT_TEMPLATE = """The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Summary of conversation:
{history}
Current conversation:
{chat_history_lines}
Human: {input}
AI:"""

PROMPT = PromptTemplate(input_variables=["history", "input", "chat_history_lines"], template=_DEFAULT_TEMPLATE)

# 初始化对话链
conversation = ConversationChain(llm=llm, verbose=True, memory=memory, prompt=PROMPT)

# 开始对话
conversation.run("Hi!")

在这个例子中,我们首先实例化语言模型和我们正在使用的几种记忆类型 --- ConversationBufferMemory 用于保留完整的对话历史,ConversationSummaryMemory 用于创建对话摘要。然后,我们使用 CombinedMemory 结合这些记忆。我们还定义了一个适应我们内存使用的提示模板,最后,我们通过提供语言模型、记忆和提示来创建和运行 ConversationChain。

长期持久性

在专用后端中存储对话的方法有很多种。Zep就是其中的一个例子,它提供了一个持久的后端,使用向量嵌入和自动记数令牌来存储、汇总和搜索聊天历史。这种具有快速向量搜索和可配置汇总的长期内存使得具备上下文感知能力的对话型AI更为强大。

使用Zep的一个实际例子是将其集成为聊天机器人或AI应用的长期内存。通过使用ZepMemory类,开发人员可以使用Zep服务器的URL、API密钥和用户的唯一会话标识符来初始化ZepMemory实例。这允许聊天机器人或AI应用存储和检索聊天历史或其他相关信息。

例如,在Python中,您可以如下初始化ZepMemory实例:

ini 复制代码
from langchain.memory import ZepMemory
ZEP_API_URL = "http://localhost:8000"
ZEP_API_KEY = "<your JWT token>"
session_id = str(uuid4())
memory = ZepMemory(
    session_id=session_id,
    url=ZEP_API_URL,
    api_key=ZEP_API_KEY,
    memory_key="chat_history",
)

这样设置了一个ZepMemory实例,您可以在您的链中使用它。请注意,URL和API密钥需要根据您的设置进行设置。正如前面提到的,一旦内存设置完成,您就可以在聊天机器人的链中或与AI代理一起使用它来存储和检索聊天历史或其他相关信息。总体而言,Zep简化了保存、搜索和丰富聊天机器人或AI应用历史的过程,使开发人员能够专注于开发他们的AI应用,而不是构建内存基础设施。

在接下来的部分中,我们将看看如何使用审查来确保响应是适当的。审查对于为用户创建一个安全、尊重和包容的环境、保护品牌声誉以及遵守法律义务非常重要。

审查响应

审查的作用在于确保机器人的响应和对话是合适、道德和尊重的。它涉及实施机制来过滤出具有冒犯性或不适当内容的内容,并阻止用户的滥用行为。这是部署到客户端的任何应用程序的重要组成部分。

在审查的上下文中,宪法指的是管理聊天机器人行为和响应的一组准则或规则。它概述了聊天机器人应该遵循的标准和原则,比如避免冒犯性语言、促进尊重的互动,以及保持道德标准。宪法作为一个框架,确保聊天机器人在期望的边界内运作,并提供积极的用户体验。

审查和拥有宪法在聊天机器人中是重要的,原因如下:

  1. 确保道德行为:聊天机器人可以与各种用户互动,包括脆弱的个体。审查有助于确保机器人的响应是道德的、尊重的,并且不促进有害或冒犯性的内容。
  2. 保护用户免受不适当内容的侵害:审查有助于防止传播不适当或冒犯性语言、仇恨言论或可能对用户有害或冒犯的内容。它为用户提供了与聊天机器人互动的安全和包容的环境。
  3. 维护品牌声誉:聊天机器人通常代表一个品牌或组织。通过实施审查,开发者可以确保机器人的响应与品牌的价值观一致,并保持积极的声誉。
  4. 防止滥用行为:审查可以阻止用户参与滥用或不当行为。通过实施规则和后果,比如在示例中提到的"两次警告"规则,开发者可以阻止用户使用挑衅性语言或参与滥用行为。
  5. 法律合规:根据法域的不同,可能存在审查内容并确保其符合法律法规的法律要求。拥有宪法或一组准则有助于开发者遵守这些法律要求。

您可以向 LLMChain 实例或 Runnable 实例添加一个审查链,以确保语言模型生成的输出不会对用户造成伤害。

如果传递到审查链中的内容被认为有害,有几种处理它的方法。您可以选择在链中抛出一个错误并在应用程序中处理它,或者您可以向用户返回一条消息,说明文本是有害的。具体的处理方法取决于应用程序的要求。

在 LangChain 中,首先要创建 OpenAIModerationChain 类的实例,这是 LangChain 提供的一个预构建的审查链。该链专门设计用于检测和过滤有害内容:

ini 复制代码
from langchain.chains import OpenAIModerationChain 
moderation_chain = OpenAIModerationChain()

接下来,创建 LLMChain 类的实例或 Runnable 实例的实例,表示您的语言模型链。这是您定义提示并与语言模型交互的地方。我们可以使用我们在第 4 章"构建功能强大的助手"中介绍的 LCEL 语法来实现这一点:

javascript 复制代码
from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.schema import StrOutputParser
cot_prompt = PromptTemplate.from_template(
    "{question} \nLet's think step by step!"
)
llm_chain = cot_prompt | ChatOpenAI() | StrOutputParser()

这是一个带有"思维链"(CoT)提示的链,其中包含逐步思考的指令。

要将审查链附加到语言模型链,可以使用 SequentialChain 类或 LCEL(推荐使用)。这允许您以顺序方式将多个链链接在一起:

ini 复制代码
chain = llm_chain | moderation_chain

现在,当您想要使用语言模型生成文本时,您会首先将输入文本通过审查链,然后通过语言模型链。

ini 复制代码
response = chain.invoke({"question": "What is the future of programming?"})

第一个链将得出初步答案。然后,审查链将评估此答案并过滤掉任何有害内容。如果输入文本被认为有害,审查链可以选择抛出错误或返回一条消息,指示文本不允许。我在 GitHub 上的聊天机器人应用程序中添加了一个审查的示例。

此外,可以使用护栏来定义语言模型在特定主题上的行为,阻止其参与不需要的主题的讨论,引导对话沿着预定的路径进行,强制执行特定的语言风格,提取结构化数据等。

在LLM的上下文中,护栏(rails)指的是控制模型输出的具体方式。它们提供了一种添加可编程约束和准则的方法,以确保语言模型的输出符合所需的标准。

以下是护栏的一些用法:

  1. 控制主题:护栏允许您定义语言模型或聊天机器人在特定主题上的行为。您可以阻止它参与不需要或敏感主题的讨论,比如政治。
  2. 预定义对话路径:护栏使您能够为对话定义一个预定义的路径。这确保语言模型或聊天机器人遵循特定的流程并提供一致的响应。
  3. 语言风格:护栏允许您指定语言模型或聊天机器人应该使用的语言风格。这确保输出符合您期望的语气、形式或特定的语言要求。
  4. 结构化数据提取:护栏可用于从对话中提取结构化数据。这对于捕获特定信息或根据用户输入执行操作很有用。

总的来说,护栏提供了一种在LLM和聊天机器人中添加可编程规则和约束的方法,使它们在与用户互动时更加可信、安全。通过将审查链附加到语言模型链中,您可以确保生成的文本经过审查,在应用程序中使用是安全的。

总结

在上一章中,我们讨论了工具增强的LLM,这涉及利用外部工具或知识资源,如文档语料库。在本章中,我们专注于通过矢量搜索从源中检索相关数据并将其注入上下文。这些检索到的数据充当了对LLM提示的补充信息。我还介绍了检索和矢量机制,我们讨论了实现聊天机器人、记忆机制的重要性以及适当响应的重要性。

本章始于对聊天机器人的概述,它们的演变以及聊天机器人当前状态的讨论,强调了当前技术能力的实际影响和增强。我们讨论了主动沟通的重要性。我们探讨了检索机制,包括矢量存储,旨在提高聊天机器人响应的准确性。我们详细介绍了加载文档和信息的方法,包括矢量存储和嵌入。

此外,我们讨论了用于维护知识和进行中对话状态的记忆机制。本章以对审查的讨论结束,强调确保响应是尊重的并符合组织价值观的重要性。

本章讨论的特性为探讨记忆、上下文和言论审查等问题提供了一个起点,同时也对幻觉等问题具有趣味性。

相关推荐
真忒修斯之船2 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
SpikeKing2 小时前
LLM - 使用 LLaMA-Factory 微调大模型 环境配置与训练推理 教程 (1)
人工智能·llm·大语言模型·llama·环境配置·llamafactory·训练框架
光芒再现dev4 小时前
已解决,部署GPTSoVITS报错‘AsyncRequest‘ object has no attribute ‘_json_response_data‘
运维·python·gpt·语言模型·自然语言处理
数据智能老司机1 天前
LLM工程师手册——监督微调
深度学习·架构·llm
知来者逆1 天前
使用 GPT-4V 全面评估泛化情绪识别 (GER)
人工智能·gpt·语言模型·自然语言处理·gpt-4v
github_czy1 天前
使用GPT-SoVITS训练语音模型
人工智能·gpt
AI_小站1 天前
LLM——10个大型语言模型(LLM)常见面试题以及答案解析
人工智能·程序人生·语言模型·自然语言处理·大模型·llm·大模型面试
Yeats_Liao1 天前
昇思大模型平台打卡体验活动:基于MindSpore实现GPT1影评分类
gpt·分类·数据挖掘
龙的爹23331 天前
论文 | Evaluating the Robustness of Discrete Prompts
人工智能·gpt·自然语言处理·nlp·prompt·agi
waiting不是违停1 天前
LangChain Ollama实战文献检索助手(二)少样本提示FewShotPromptTemplate示例选择器
langchain·llm·ollama