AI Agents实战——理解代理的记忆和知识

145 阅读35分钟

本章内容包括:

  • AI功能中的知识/记忆检索
  • 使用LangChain构建检索增强生成工作流
  • Nexus中用于代理知识系统的检索增强生成
  • 代理中的记忆检索模式
  • 使用记忆和知识压缩改进增强检索系统

现在我们已经探讨了通过外部工具(如以本地或语义功能形式的插件)执行的代理行为,接下来可以研究在代理和聊天界面中,记忆和知识使用检索的角色。我们将描述记忆和知识及其与提示工程策略的关系,然后为了理解记忆知识,我们将研究文档索引,使用LangChain构建检索系统,利用LangChain使用记忆,并使用Nexus构建语义记忆。

8.1 理解AI应用中的检索

在代理和聊天应用中的检索是一种机制,用于获取通常是外部且长期存在的知识并存储起来。无结构知识包括对话或任务历史、事实、偏好或其他用于上下文提示的信息。结构化知识通常存储在数据库或文件中,通过本地函数或插件进行访问。

如图8.1所示,记忆和知识是用于为提示添加更多上下文和相关信息的元素。提示可以通过各种信息进行增强,从文档内容到以前的任务或对话,以及其他参考信息。

image.png

图8.1所示的提示工程策略可以应用于记忆和知识。知识并不被视为记忆,而是现有文档对提示的增强。知识和记忆都使用检索作为查询无结构信息的基础。

检索机制,被称为检索增强生成(RAG),已成为为提示提供相关上下文的标准。支持RAG的机制也支持记忆/知识,因此理解其工作原理至关重要。在下一部分,我们将深入探讨RAG是什么。

8.2 检索增强生成(RAG)的基础

RAG已经成为支持文档聊天或问答聊天的流行机制。系统通常通过用户提供相关文档(如PDF)并使用RAG和大型语言模型(LLM)来查询文档的方式进行工作。

图8.2展示了RAG如何通过LLM允许查询文档。在任何文档可以被查询之前,必须首先加载文档,将其转换为上下文块,将其嵌入到向量中,并存储在向量数据库中。

image.png

用户可以通过提交查询来查询之前索引的文档。该查询随后被嵌入到一个向量表示中,以在向量数据库中搜索相似的块。与查询相似的内容被用作上下文,并作为增强信息填充到提示中。然后,提示被推送到大型语言模型(LLM),该模型可以利用上下文信息帮助回答查询。

无结构的记忆/知识概念依赖于某种文本相似性搜索格式,遵循图8.2所示的检索模式。图8.3展示了记忆如何使用相同的嵌入和向量数据库组件。与预加载文档不同,整个对话或对话的一部分会被嵌入并保存到向量数据库中。

image.png

检索模式和文档索引是微妙的,需要仔细考虑才能成功应用。这要求理解数据是如何存储和检索的,我们将在下一部分开始展开讨论。

8.3 深入了解语义搜索和文档索引

文档索引将文档的信息转换为更容易恢复的形式。如何查询或搜索索引也是一个重要因素,无论是搜索一组特定的词语,还是想要逐句匹配短语。

语义搜索是一种通过词语和含义匹配搜索内容的方式。按含义(语义)进行搜索的能力非常强大,值得深入探讨。在下一部分,我们将探讨如何通过向量相似性搜索为语义搜索奠定基础。

8.3.1 应用向量相似性搜索

现在让我们看看如何将文档转换为语义向量,或将文本表示为可以用于执行距离或相似性匹配的向量。将文本转换为语义向量有多种方法,我们将展示一种简单的方法。

在新的Visual Studio Code(VS Code)工作区中打开chapter_08文件夹。创建一个新环境并通过pip安装requirements.txt文件中的所有章节依赖项。如果需要帮助设置新的Python环境,请参阅附录B。

现在在VS Code中打开document_vector_similarity.py文件,并查看清单8.1中的顶部部分。这个示例使用了词频-逆文档频率(TF–IDF)。这个数值统计反映了一个词对于文档集或文档集合中一个文档的重要性,方法是根据词在文档中出现的次数按比例增加,并通过词在文档集中的频率进行偏移。TF–IDF是理解一个文档在文档集合中的重要性的经典衡量标准。

清单8.1 document_vector_similarity(转换为向量)

import plotly.graph_objects as go
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

documents = [      #1
    "The sky is blue and beautiful.",
    "Love this blue and beautiful sky!",
    "The quick brown fox jumps over the lazy dog.",
    "A king's breakfast has sausages, ham, bacon, eggs, toast, and beans",
    "I love green eggs, ham, sausages and bacon!",
    "The brown fox is quick and the blue dog is lazy!",
    "The sky is very blue and the sky is very beautiful today",
    "The dog is lazy but the brown fox is quick!"
]

vectorizer = TfidfVectorizer()     #2
X = vectorizer.fit_transform(documents)      #3
#1 文档样本
#2 使用TF–IDF进行向量化
#3 向量化文档。

让我们通过示例句子“天空是蓝色的和美丽的”并关注词“blue”来分解TF–IDF的两个组成部分。

词频(TF)

词频衡量一个术语在文档中出现的频率。因为我们只考虑一个文档(我们的示例句子),所以可以计算“blue”的最简单的TF,即“blue”在文档中出现的次数除以文档中的总词数。我们来计算一下:

  • blue在文档中出现的次数:1
  • 文档中的总词数:6
  • TF = 1 ÷ 6 = 0.16

逆文档频率(IDF)

逆文档频率衡量一个术语在整个语料库中的重要性。计算方法是将文档总数除以包含该词的文档数,然后取该商的对数:

  • IDF = log(文档总数 ÷ 包含该词的文档数) 在这个例子中,语料库包含八个文档,而“blue”出现在其中四个文档中。
  • IDF = log(8 ÷ 4)

TF–IDF计算

最后,计算“blue”在示例句子中的TF–IDF分数,将TF和IDF分数相乘:

  • TF–IDF = TF × IDF

让我们使用提供的示例来计算“blue”的实际TF–IDF值;首先,计算词频(该词在文档中出现的频率)如下:

  • TF = 1 ÷ 6

假设对数的底数为10(常用的),计算逆文档频率如下:

  • IDF = log10 (8 ÷ 4)

现在让我们计算“blue”在句子“天空是蓝色的和美丽的”中的准确TF–IDF值:

  • 词频(TF)约为0.1670。
  • 逆文档频率(IDF)约为0.301。

因此,“blue”的TF–IDF(TF × IDF)分数约为0.050。

这个TF–IDF分数表示“blue”在给定文档(示例句子)中的相对重要性,且在指定语料库(八个文档,其中“blue”出现在四个文档中)的上下文中。较高的TF–IDF分数意味着该词的重要性更大。

我们在这里使用TF–IDF,因为它简单易用且易于理解。现在我们已经将元素表示为向量,我们可以使用余弦相似度来衡量文档相似性。余弦相似度是一个衡量两个非零向量在多维空间中夹角余弦的度量,表示它们的相似性,而不考虑它们的大小。

图8.4展示了如何通过余弦距离比较两篇文本或文档的向量表示。余弦相似度返回一个从-1(不相似)到1(完全相同)的值。余弦距离是一个归一化值,范围从0到2,通过计算1减去余弦相似度得出。余弦距离为0表示完全相同,2表示完全相反。

image.png

清单8.2展示了如何使用scikit-learn中的cosine_similarity函数计算余弦相似度。相似度是通过对每个文档与集合中所有其他文档进行比较计算的。计算出的文档相似度矩阵存储在cosine_similarities变量中。然后,在输入循环中,用户可以选择要查看其与其他文档相似度的文档。

清单8.2 document_vector_similarity(余弦相似度)

cosine_similarities = cosine_similarity(X)      #1

while True:      #2
    selected_document_index = input(f"Enter a document number
↪ (0-{len(documents)-1}) or 'exit' to quit: ").strip()

    if selected_document_index.lower() == 'exit':
        break

    if not selected_document_index.isdigit() ornot 0 <= int(selected_document_index) < len(documents):
        print("Invalid input. Please enter a valid document number.")
        continue

    selected_document_index = int(selected_document_index)    #3

    selected_document_similarities = cosine_similarities[selected_document_index]     #4

图8.5展示了在VS Code中运行示例的输出(调试模式按F5)。在选择文档后,您将看到集合中各个文档之间的相似度。一个文档与其自身的余弦相似度为1。请注意,由于使用了TF–IDF向量化,您不会看到负相似度。稍后我们将探讨其他更复杂的语义相似度度量方法。

image.png

向量化方法将决定文档之间语义相似度的度量。在我们继续探讨更好的文档向量化方法之前,我们将先研究如何存储向量以执行向量相似度搜索。

8.3.2 向量数据库和相似度搜索

在向量化文档后,文档可以存储在向量数据库中,以便后续进行相似度搜索。为了演示这一过程,我们可以通过Python代码高效地复制一个简单的向量数据库。

在VS Code中打开document_vector_database.py文件,如清单8.3所示。此代码演示了如何在内存中创建一个向量数据库,然后允许用户输入文本来搜索数据库并返回结果。返回的结果显示了文档文本和相似度分数。

清单8.3 document_vector_database.py

# 上面的代码省略
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(documents)
vector_database = X.toarray()     #1

def cosine_similarity_search(query,
                             database, 
                             vectorizer, 
                             top_n=5):     #2
    query_vec = vectorizer.transform([query]).toarray()
    similarities = cosine_similarity(query_vec, database)[0]
    top_indices = np.argsort(-similarities)[:top_n]  # Top n indices
    return [(idx, similarities[idx]) for idx in top_indices]

while True:      #3
    query = input("Enter a search query (or 'exit' to stop): ")
    if query.lower() == 'exit':
        break
    top_n = int(input("How many top matches do you want to see? "))
    search_results = cosine_similarity_search(query,
                                              vector_database, 
                                              vectorizer, 
                                              top_n)

    print("Top Matched Documents:")
    for idx, score in search_results:
        print(f"- {documents[idx]} (Score: {score:.4f})")   #4

    print("\n")

输出

Enter a search query (or 'exit' to stop): blue
How many top matches do you want to see? 3
Top Matched Documents:
- The sky is blue and beautiful. (Score: 0.4080)
- Love this blue and beautiful sky! (Score: 0.3439)
- The brown fox is quick and the blue dog is lazy! (Score: 0.2560)

#1 将文档向量存储到数组中
#2 执行相似度匹配的函数,返回匹配项和相似度分数
#3 主输入循环
#4 遍历结果并输出文本和相似度分数

运行此练习以查看输出(在VS Code中按F5)。输入任何你喜欢的文本,并查看返回的文档结果。此搜索形式非常适合匹配单词和短语及其相似的单词和短语。但这种搜索方式忽略了文档中的词汇上下文和含义。在下一部分,我们将探讨一种将文档转换为向量的方法,这种方法更好地保留了文档的语义含义。

8.3.3 解密文档嵌入

TF–IDF是一种简单的形式,试图捕捉文档中的语义含义。然而,它不可靠,因为它仅仅统计词频,并没有理解词与词之间的关系。一个更好、更现代的方法是使用文档嵌入,这是一种文档向量化形式,能更好地保留文档的语义含义。

嵌入网络是通过在大数据集上训练神经网络来构建的,目的是将单词、句子或文档映射到高维向量,基于上下文和数据中的关系捕捉语义和句法关系。通常,你会使用一个在大数据集上训练的预训练模型来嵌入文档并执行嵌入操作。这些模型可以从许多来源获取,包括Hugging Face,当然,还有OpenAI。

在我们的下一个场景中,我们将使用OpenAI的嵌入模型。这些模型通常非常适合捕捉嵌入文档的语义上下文。清单8.4展示了相关代码,使用OpenAI将文档嵌入为向量,随后将向量降至三维,并渲染为图表。

清单8.4 document_visualizing_embeddings.py(相关部分)

load_dotenv()      #1
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
    raise ValueError("No API key found. Please check your .env file.")
client = OpenAI(api_key=api_key)     #1            

def get_embedding(text, model="text-embedding-ada-002"):     #2
    text = text.replace("\n", " ")
    return client.embeddings.create(input=[text],
              model=model).data[0].embedding                #2

# Sample documents (省略)

embeddings = [get_embedding(doc) for doc in documents]    #3
print(embeddings_array.shape)

embeddings_array = np.array(embeddings)    #4

pca = PCA(n_components=3)   #5
reduced_embeddings = pca.fit_transform(embeddings_array)
#1 将所有项目连接成字符串 ', '。
#2 使用OpenAI客户端创建嵌入
#3 为每个文档生成1536维的嵌入
#4 将嵌入转换为NumPy数组,供PCA使用
#5 应用PCA将维度降至3,以便绘图

当使用OpenAI模型对文档进行嵌入时,它会将文本转换为一个1536维的向量。我们无法直接可视化这个维度数,因此我们通过主成分分析(PCA)等降维技术,将1536维的向量转换为3维。

图8.6展示了在VS Code中运行文件后生成的输出。通过将嵌入降至三维,我们可以绘制输出,显示语义上相似的文档是如何被分组的。

image.png

选择使用哪种嵌入模型或服务取决于你自己。OpenAI的嵌入模型被认为是通用语义相似度的最佳选择。这使得这些模型成为大多数记忆和检索应用的标准。通过了解文本如何通过嵌入进行向量化并存储在向量数据库中,我们可以在下一节中继续看一个更现实的例子。

8.3.4 从Chroma查询文档嵌入

我们可以将所有部分结合起来,使用名为Chroma DB的本地向量数据库来查看完整的示例。虽然有许多向量数据库选项,但Chroma DB是一个非常适合开发或小型项目的本地向量存储。你也可以稍后考虑其他更强大的选项。

清单8.5展示了来自document_query_chromadb.py文件的新相关代码部分。请注意,结果是按距离而不是相似度打分的。余弦距离通过以下公式确定:

Cosine Distance(A,B) = 1 – Cosine Similarity(A,B)

这意味着余弦距离的范围是:最相似的为0,语义上完全相反的为2。

清单8.5 document_query_chromadb.py(相关代码部分)

embeddings = [get_embedding(doc) for doc in documents]     #1
ids = [f"id{i}" for i in range(len(documents))]           #1

chroma_client = chromadb.Client()               #2
collection = chroma_client.create_collection(
                       name="documents")       #2
collection.add(     #3
    embeddings=embeddings,
    documents=documents,
    ids=ids
)

def query_chromadb(query, top_n=2):      #4
    query_embedding = get_embedding(query)
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_n
    )
    return [(id, score, text) for id, score, text in
            zip(results['ids'][0],
                results['distances'][0], 
                results['documents'][0])]

while True:     #5
    query = input("Enter a search query (or 'exit' to stop): ")
    if query.lower() == 'exit':
        break
    top_n = int(input("How many top matches do you want to see? "))
    search_results = query_chromadb(query, top_n)

    print("Top Matched Documents:")
    for id, score, text in search_results:
        print(f"""
ID:{id} TEXT: {text} SCORE: {round(score, 2)}
""")    #5

    print("\n")

输出

Enter a search query (or 'exit' to stop): dogs are lazy
How many top matches do you want to see? 3
Top Matched Documents:
ID:id7 TEXT: The dog is lazy but the brown fox is quick! SCORE: 0.24
ID:id5 TEXT: The brown fox is quick and the blue dog is lazy! SCORE: 0.28
ID:id2 TEXT: The quick brown fox jumps over the lazy dog. SCORE: 0.29

#1 为每个文档生成嵌入并分配ID
#2 创建Chroma DB客户端和集合
#3 将文档嵌入添加到集合中
#4 查询数据存储并返回前n个相关文档
#5 用户输入循环,并输出相关文档/分数

如前面的场景所示,现在你可以使用语义意义来查询文档,而不仅仅是按关键词或短语进行查询。这些场景应该能为你提供背景,帮助你理解检索模式如何在低级别上工作。在下一节中,我们将看到如何使用LangChain应用检索模式。

8.4 使用LangChain构建RAG

LangChain最初是一个开源项目,专注于抽象化跨多个数据源和向量存储的检索模式。此后,它发展成了更多的功能,但从基础上来看,它仍然为实现检索提供了出色的选项。

图8.7展示了LangChain中的一个流程图,标识了存储文档以供检索的过程。这些步骤可以完全或部分复制,以实现记忆检索。文档和记忆检索之间的关键区别在于来源以及内容如何转化。

image.png

我们将探讨如何使用LangChain实现这些步骤,并理解伴随该实现的细微差别和细节。在下一节中,我们将从使用LangChain拆分和加载文档开始。

8.4.1 使用LangChain拆分和加载文档

检索机制通过提供与请求相关的特定信息来增强给定提示的上下文。例如,您可能会请求关于本地文档的详细信息。对于早期的语言模型,由于令牌限制,将整个文档作为提示的一部分提交是不可能的。

如今,对于许多商业LLM(如GPT-4 Turbo),我们可以将整个文档作为提示请求的一部分提交。然而,结果可能不会更好,并且由于令牌数量增加,成本可能更高。因此,更好的选择是拆分文档并使用相关部分来请求上下文——这正是RAG和记忆所做的。

拆分文档对于将内容分解成语义和特定相关的部分至关重要。图8.8展示了如何拆分包含《鹅妈妈童谣》的HTML文档。通常,将文档拆分成上下文语义块需要仔细考虑。

image.png

理想情况下,当我们将文档拆分成块时,它们应该根据相关性和语义意义进行拆分。虽然LLM或代理可以帮助我们完成这一过程,但我们将查看LangChain中当前的工具包选项来拆分文档。在本章后面,我们将查看一个语义函数,帮助我们将内容语义地划分,以便进行嵌入。

在下一个练习中,打开VS Code中的langchain_load_splitting.py,如清单8.6所示。此代码展示了我们在前一节中从清单8.5中继续的部分。这次我们加载的是《鹅妈妈童谣》文档,而不是之前的示例文档。

清单8.6 langchain_load_splitting.py(代码段和输出)

from langchain_community.document_loaders 
                     ↪ import UnstructuredHTMLLoader    #1
from langchain.text_splitter import RecursiveCharacterTextSplitter
#previous code

loader = UnstructuredHTMLLoader(
                   "sample_documents/mother_goose.xhtml")   #2
data = loader.load    #3

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=25,     #4
    length_function=len,
    add_start_index=True,
)
documents = text_splitter.split_documents(data)

documents = [doc.page_content 
                ↪ for doc in documents] [100:350]   #5

embeddings = [get_embedding(doc) for doc in documents]     #6
ids = [f"id{i}" for i in range(len(documents))]

输出

Enter a search query (or 'exit' to stop): who kissed the girls and made 
them cry?
How many top matches do you want to see? 3
Top Matched Documents:
ID:id233 TEXT: And chid her daughter,
        And kissed my sister instead of me. SCORE: 0.4

#1 新的LangChain导入
#2 作为HTML加载文档
#3 加载文档
#4 将文档拆分为每块100个字符,重叠部分为25个字符
#5 只嵌入250个块,这样更便宜且更快
#6 为每个文档返回嵌入

在清单8.6中可以看到,HTML文档被拆分成100个字符的块,且每块有25个字符的重叠。重叠部分可以确保文档的片段不会截断具体的思路。我们选择这个拆分器是因为它易于使用、设置和理解。

请继续运行VS Code中的langchain_load_splitting.py文件(F5)。输入一个查询,看看得到什么结果。清单8.6中的输出显示了一个具体例子下的良好结果。记住,我们仅嵌入了250个文档块,以减少成本并保持练习简短。当然,你可以尝试嵌入整个文档,或使用一个较小的输入文档示例。

构建正确检索的最关键元素可能就是文档拆分过程。你可以使用多种方法来拆分文档,包括多种并行方法。多次传递并拆分文档,为同一文档提供多个嵌入视图。在下一节中,我们将探讨一种更通用的文档拆分技术,使用令牌和标记化。

8.4.2 使用LangChain按令牌拆分文档

标记化是将文本拆分成单词标记的过程。一个单词标记代表文本中的一个简洁元素,标记可以是像“hold”这样的单词,甚至可以是像左大括号({)这样的符号,这取决于其相关性。

使用标记化拆分文档为文本如何被语言模型解释和语义相似度提供了更好的基础。标记化还可以去除不相关的字符,如空格,使得文档的相似度匹配更加相关,通常也能提供更好的结果。

在下一个代码练习中,打开VS Code中的langchain_token_splitting.py文件,如清单8.7所示。现在我们使用标记化拆分文档,这样文档就会被拆分成不等大小的部分。这种不等大小是由于原始文档中的大段空白部分。

清单8.7 langchain_token_splitting.py(相关的新代码)

loader = UnstructuredHTMLLoader("sample_documents/mother_goose.xhtml")
data = loader.load()
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=50, chunk_overlap=10      #1
)

documents = text_splitter.split_documents(data)
documents = [doc for doc in documents][8:94]      #2

db = Chroma.from_documents(documents, OpenAIEmbeddings())

def query_documents(query, top_n=2):
    docs = db.similarity_search(query, top_n)      #3
    return docs

输出

Created a chunk of size 68, 
which is longer than the specified 50
Created a chunk of size 67, 
which is longer than the specified 50     #4
Enter a search query (or 'exit' to stop): 
                     who kissed the girls and made them cry?
How many top matches do you want to see? 3
Top Matched Documents:
Document 1: GEORGY PORGY

        Georgy Porgy, pudding and pie,
        Kissed the girls and made them cry.

#1 将块大小更新为50个令牌,重叠为10个令牌
#2 选择仅包含童谣的文档
#3 使用数据库的相似度搜索
#4 由于空白部分,拆分成不规则大小的块

运行VS Code中的langchain_token_splitting.py代码(F5)。你可以使用我们上次使用的查询或你自己的查询。注意结果比之前的练习要好得多。然而,结果仍然存在疑问,因为查询使用了几个相似的单词并保持相同的顺序。

一个更好的测试方法是尝试使用不同单词的语义相似短语,并检查结果。保持代码运行,输入一个新的查询短语:“Why are the girls crying?” 清单8.8展示了执行该查询后的结果。如果你自己运行这个示例并向下滚动输出,你会看到Georgy Porgy出现在返回的第二个或第三个文档中。

清单8.8 查询:Who made the girls cry?

Enter a search query (or 'exit' to stop): Who made the girls cry?
How many top matches do you want to see? 3
Top Matched Documents:
Document 1: WILLY, WILLY

        Willy, Willy Wilkin…

此练习展示了如何使用各种检索方法语义地返回文档。建立了这个基础后,我们可以看到如何将RAG应用于知识和记忆系统。下一节将讨论RAG在代理知识和代理系统中的应用。

8.5 将RAG应用于构建代理知识

在代理中,知识包括使用RAG跨无结构文档进行语义检索。这些文档可以是任何内容,从PDF到Microsoft Word文档,再到所有文本,包括代码。代理知识还包括使用无结构文档进行问答、参考查询、信息增强和其他未来的模式。

Nexus是与本书共同开发的代理平台,在上一章中有所介绍,使用完整的知识和记忆系统来支持代理。在这一节中,我们将揭示知识系统是如何工作的。

要为本章安装Nexus,请参见清单8.9。打开chapter_08文件夹中的终端,并执行清单中的命令来下载、安装并运行Nexus,使用正常模式或开发模式。如果你想参考代码,应该以开发模式安装项目并配置调试器,以便从VS Code运行Streamlit应用。如果需要复习任何步骤,请参阅第7章。

清单8.9 安装Nexus

# 安装并运行
pip install git+https://github.com/cxbxmxcx/Nexus.git

nexus run
# 以开发模式安装
git clone https://github.com/cxbxmxcx/Nexus.git

# 以可编辑模式安装克隆的仓库
pip install -e Nexus

无论你决定以何种方式运行应用,登录后,导航到知识存储管理器页面,如图8.9所示。创建一个新的知识存储,然后上传sample_documents/back_to_the_future.txt电影脚本。

image.png

该脚本是一个大文档,加载、拆分和将各部分嵌入到Chroma DB向量数据库中可能需要一段时间。等待索引完成后,您可以检查嵌入并运行查询,如图8.10所示。

image.png

现在,我们可以将知识存储连接到支持的代理并提问。使用Nexus界面左上角的选择器选择聊天页面。然后,选择一个代理和time_travel知识存储,如图8.11所示。您还需要选择一个支持知识的代理引擎。每个代理引擎都需要适当的配置才能访问。

image.png

目前,在本章中,Nexus一次仅支持访问一个知识存储。在未来的版本中,代理可能能够一次选择多个知识存储。这可能包括更高级的选项,从语义知识到采用其他形式的RAG。

您还可以在知识存储管理器页面的配置标签中配置RAG设置,如图8.12所示。目前,您可以选择拆分器的类型(Chunking Option字段)来拆分文档,以及Chunk Size字段和Overlap字段。

image.png

加载、拆分、块化和嵌入选项是目前LangChain支持的唯一基本选项。在未来版本的Nexus中,将提供更多选项和模式。支持其他选项的代码可以直接添加到Nexus中。

我们不会详细介绍执行RAG的代码,因为它与我们已经讨论过的非常相似。您可以随时查看Nexus代码,特别是knowledge_manager.py文件中的KnowledgeManager类。

虽然知识和记忆的检索模式在增强方面非常相似,但这两种模式在填充存储时有所不同。在下一节中,我们将探讨代理中的记忆是什么使其独特。

8.6 在代理系统中实现记忆

在代理和AI应用中,记忆通常与认知记忆功能相同来描述。认知记忆描述了我们用来记住30秒前做了什么或30年前我们有多高的那种记忆。计算机记忆也是代理记忆的一个重要组成部分,但在本节中我们不会考虑这一部分。

图8.13展示了记忆如何分解为感觉记忆、短期记忆和长期记忆。这种记忆可以应用于AI代理,并且这个列表描述了每种记忆形式如何映射到代理功能:

  • AI中的感觉记忆:像RAG这样的功能,但用于图像/音频/触觉数据形式。简要地保留输入数据(例如,文本和图像)以进行即时处理,但不进行长期存储。
  • AI中的短期/工作记忆:作为会话历史的活动记忆缓冲区。我们保持有限的近期输入和上下文以进行即时分析和生成响应。在Nexus中,短期和长期会话记忆也以线程的上下文形式保存。
  • AI中的长期记忆:与代理或用户生活相关的长期记忆存储。语义记忆提供了强大的能力来存储和检索相关的全球或本地事实和概念。

image.png

虽然记忆使用与知识完全相同的检索和增强机制,但在更新或附加记忆时,通常会有显著不同。图8.14突出了捕捉和使用记忆来增强提示的过程。由于记忆通常不同于完整文档的大小,我们可以避免使用任何拆分或块化机制。

image.png

Nexus提供了一个类似于知识存储的机制,允许用户创建可以为各种用途和应用配置的记忆存储。它还支持图8.13中突出的更高级的记忆形式。以下部分将探讨基本记忆存储在Nexus中的工作原理。

8.6.1 在Nexus中使用记忆存储

记忆存储的操作和构建方式与Nexus中的知识存储类似。它们都高度依赖于检索模式。不同之处在于,记忆系统在构建新记忆时需要额外的步骤。

请继续启动Nexus,如果需要安装,请参见清单8.9。登录后,选择“Memory”页面,并创建一个新的记忆存储,如图8.15所示。选择一个代理引擎,然后添加一些关于您的个人事实和偏好。

image.png

我们需要代理(LLM)的原因在图8.14中已有展示。当信息被输入到记忆存储中时,它通常通过使用记忆功能的LLM进行处理,其目的是将陈述/对话处理成与记忆类型相关的语义信息。

清单8.10展示了用于从对话中提取信息到记忆的对话记忆功能。是的,这只是发送到LLM的提示的头部部分,指示它如何从对话中提取信息。

清单8.10 对话记忆功能

Summarize the conversation and create a set of statements that summarize 
the conversation. Return a JSON object with the following keys: 'summary'. 
Each key should have a list of statements that are relevant to that 
category. Return only the JSON object and nothing else.

在生成了关于你的一些相关记忆后,返回到Nexus中的聊天区域,启用my_memory记忆存储,看看代理对你的了解程度如何。图8.16展示了使用不同代理引擎的示例对话。

image.png

这是一个基本记忆模式的示例,它从对话中提取事实/偏好,并将它们作为记忆存储在向量数据库中。许多其他的记忆实现遵循图8.13中展示的模式。我们将在下一节中实现这些功能。

8.6.2 语义记忆及其在语义、情景和程序记忆中的应用

心理学家根据记忆的信息类型,将记忆分类为多种形式。语义记忆、情景记忆和程序记忆代表不同类型的信息。情景记忆是关于事件的,程序记忆是关于过程或步骤的,而语义记忆代表的是意义,可能包括感受或情绪。其他形式的记忆(例如地理空间记忆)在这里未被描述,但也可以存在。

因为这些记忆依赖于额外的分类层次,它们也依赖于另一个层次的语义分类。一些平台,如Semantic Kernel(SK),将其称为语义记忆。这可能会让人困惑,因为语义分类也应用于提取情景记忆和程序记忆。

图8.17展示了语义记忆分类过程,有时也称为语义记忆。语义记忆和普通记忆的区别在于额外的步骤,即通过语义处理输入并提取相关问题,这些问题可以用来查询与记忆相关的向量数据库。

image.png

使用语义增强的好处在于能够更好地提取相关记忆。我们可以通过回到Nexus并创建一个新的语义记忆存储来观察这一操作。

图8.18展示了如何使用语义记忆配置一个新的记忆存储。目前,您无法配置特定的函数提示用于记忆、增强和总结。然而,阅读每个函数提示以了解它们的工作原理是很有帮助的。

image.png

现在,如果你回去添加事实和偏好,它们将转换为相关记忆类型的语义。图8.19展示了将相同一组陈述填充到两种不同类型记忆中的示例。通常,输入到记忆中的陈述将更具体地与记忆的形式相关。

image.png

记忆和知识可以显著帮助代理处理各种应用类型。实际上,单一的记忆/知识存储可以为一个或多个代理提供支持,从而实现对这两种存储类型的进一步专业化解读。我们将在本章最后讨论记忆/知识压缩。

8.7 理解记忆和知识压缩

就像我们自己的记忆一样,记忆存储随着时间的推移可能会被冗余信息和大量不相关的细节所堆积。我们的内心通过压缩或总结记忆来处理这种记忆杂乱。我们的心智记住更重要的细节而非较不重要的部分,并且更频繁访问的记忆更容易被记住。

我们可以将类似的记忆压缩原理应用于代理记忆和其他检索系统,以提取重要细节。压缩原理与语义增强相似,但为预先分组的相关记忆群体添加了另一层,便于将这些记忆整体总结。

图8.20展示了记忆/知识压缩的过程。记忆或知识首先使用如k-means之类的算法进行聚类。然后,记忆群体通过一个压缩函数,该函数对这些项目进行总结,并将它们收集成更简洁的表示。

image.png

Nexus通过使用k-means优化聚类提供了知识和记忆存储的压缩功能。图8.21展示了记忆压缩的界面。在压缩界面中,您将看到以3D形式展示的项目并进行聚类。聚类的大小(项目数量)显示在左侧的表格中。

image.png

压缩记忆甚至知识通常是推荐的,尤其是当一个聚类中的项目数量较大或不平衡时。压缩的使用案例可能因记忆的使用和应用而有所不同。然而,通常来说,如果在存储中的项目检查包含重复或冗余信息,那么这是进行压缩的好时机。以下是一些应用场景的总结,它们会从压缩中受益。

知识压缩的案例

知识检索和增强也被证明能从压缩中显著受益。结果会根据使用案例有所不同,但通常来说,知识源越冗长,越能从压缩中获益。包含文学散文的文档,例如故事和小说,比起代码库,通常会受益更多。然而,如果代码本身也非常冗余,那么压缩同样可能表现出其好处。

压缩应用频率的案例

记忆通常会受益于定期应用压缩,而知识存储则通常只在首次加载时有所帮助。压缩应用的频率将在很大程度上取决于记忆的使用、频率和数量。

多次应用压缩的案例

已证明在同一时间进行多次压缩有助于提高检索性能。其他模式也建议在不同的压缩层次上使用记忆或知识。例如,某个知识存储被压缩两次,从而形成三种不同的知识层次。

混合知识和记忆压缩的案例

如果系统专门针对某个特定的知识源,并且该系统也使用记忆,那么可能会有进一步的优化来整合存储。另一种方法是直接使用文档的起始知识填充记忆。

多个记忆或知识存储的案例

在更高级的系统中,我们将看到代理使用多个与其工作流相关的记忆和知识存储。例如,代理可以使用单独的记忆存储作为与个别用户对话的一部分,可能还包括能够与不同群体共享不同记忆组的能力。记忆和知识的检索是代理系统的基石,我们现在可以总结我们所讨论的内容,并在下一节中回顾一些学习练习。

8.8 练习

使用以下练习来提高您对材料的理解:

练习1 — 加载并拆分不同的文档(中级)
目标 — 通过使用LangChain理解文档拆分对检索效率的影响。
任务

  • 选择一个不同的文档(例如,新闻文章、科学论文或短篇小说)。
  • 使用LangChain加载并拆分文档成块。
  • 分析文档如何拆分成块,并分析其对检索过程的影响。

练习2 — 实验语义搜索(中级)
目标 — 通过执行语义搜索来比较各种向量化技术的有效性。
任务

  • 选择一组文档进行语义搜索。
  • 使用如Word2Vec或BERT嵌入的向量化方法,而不是TF–IDF。
  • 执行语义搜索,并将结果与使用TF–IDF获得的结果进行比较,以了解差异和效果。

练习3 — 实现自定义RAG工作流(高级)
目标 — 在实践中应用RAG的理论知识,使用LangChain。
任务

  • 选择一个特定的应用场景(例如,客户服务查询或学术研究查询)。
  • 使用LangChain设计并实现自定义的RAG工作流。
  • 调整工作流以适应选定的应用,并测试其效果。

练习4 — 构建知识存储并实验拆分模式(中级)
目标 — 理解不同拆分模式和压缩如何影响知识检索。
任务

  • 构建一个知识存储,并用几个文档填充它。
  • 实验不同形式的拆分/块化模式,并分析其对检索的影响。
  • 压缩知识存储,并观察对查询性能的影响。

练习5 — 构建并测试各种记忆存储(高级)
目标 — 理解不同类型记忆存储的独特性和使用场景。
任务

  • 构建各种形式的记忆存储(会话记忆、语义记忆、情景记忆和程序记忆)。
  • 使用每种类型的记忆存储与代理进行交互,并观察其差异。
  • 压缩记忆存储,并分析对记忆检索的影响。

总结

  • AI应用中的记忆区分了无结构和结构化记忆,突出它们在为提示提供上下文、实现更相关交互中的作用。

  • 检索增强生成(RAG)是一种通过外部文档的上下文增强提示的机制,使用向量嵌入和相似度搜索来检索相关内容。

  • 通过文档索引的语义搜索使用TF–IDF和余弦相似度将文档转换为语义向量,增强了对索引文档执行语义搜索的能力。

  • 向量数据库和相似度搜索将文档向量存储在向量数据库中,促进高效的相似度搜索并提高检索准确性。

  • 文档嵌入捕捉语义意义,使用如OpenAI模型等生成嵌入,保持文档的上下文并促进语义相似度搜索。

  • LangChain提供了多种工具来执行RAG,并抽象化了检索过程,允许在各种数据源和向量存储中轻松实现RAG和记忆系统。

  • LangChain中的短期和长期记忆实现了会话记忆,区分了短期缓冲模式和长期存储解决方案。

  • 将文档向量存储在数据库中以进行高效的相似度搜索,对于实现可扩展的检索系统在AI应用中的重要性不言而喻。

  • 代理知识直接与执行文档或其他文本信息问答的RAG模式相关。

  • 代理记忆是与RAG相关的模式,它捕捉代理与用户、自己以及其他系统的交互。

  • Nexus是一个实现代理知识和记忆系统的平台,包括设置知识存储以进行文档检索和设置记忆存储以进行各种形式的记忆。

  • 语义记忆增强(语义记忆)区分了不同类型的记忆(语义记忆、情景记忆、程序记忆),通过语义增强实现它们,增强代理根据记忆的性质回忆和使用相关信息的能力。

  • 记忆和知识压缩是通过聚类和总结技术来压缩存储在记忆和知识系统中的信息,提高检索效率和相关性。