大型言语模型(LLM)能够回答各种类型的问题,但有一个显着的局限,那就是:

LLM只了解训练过的内容,而不了解你的个人数据

比方,

  • 你所在公司内部的专有文档
  • LLM训练完结后新产生的数据

为了处理这个问题,在这门课程中,咱们将深入研究LangChain的一个广受欢迎的用例:

怎么运用LangChain与你的数据进行对话

咱们假设你现已把握了LangChain的基础知识,如果你还不了解LangChain是什么,欢迎先阅览以下两篇文章:

  • 精华笔记:吴恩达 x LangChain《依据LangChain的大言语模型运用开发》(上)
  • 精华笔记:吴恩达 x LangChain《依据LangChain的大言语模型运用开发》(下)

文档加载

文档加载器的介绍

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

文档加载器的作用,是将不同格局和来历的数据加载到标准的文档目标中,包括内容本身以及相关的元数据

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

LangChain提供了多种类型的文档加载器,用于处理非结构化数据,依据数据来历的不同大致可分为:

  • 公共数据源加载器,如YouTube、Twitter;

  • 专有数据源加载器,如Figma、Notion。

文档加载器也能够加载结构化数据,比方依据表格中包括的文本数据,对问题进行回答或语义搜索。

这种技术咱们称之为检索增强生成(RAG,Retrieval-Augmented Generation)。

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

在RAG中,LLM会从外部数据集中检索上下文文档,作为其履行的一部分,这对于询问特定文档的问题十分有用

下面咱们来实践运用其中的一些文档加载器。

加载PDF文档

# 导入PyPDFLoader文档加载器
from langchain.document_loaders import PyPDFLoader
# 将位于特定途径下的PDF文档放入到加载器中
loader = PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf")
# 加载PDF文档
pages = loader.load()

默许状况下,这将加载一系列的文档。以页面为单位,每个页面都是一个独立的文档。

# PDF的总页数
len(pages)
# 22

每个文档都包括「页面内容」和「与文档相关的元数据」。

页面内容:

# 仅打印前500个字符
print(page.page_content[0:500]) 

MachineLearning-Lecture01
Instructor (Andrew Ng): Okay. Good morning. Welcome to CS229, the machine learning class. So what I wanna do today is ju st spend a little time going over the logistics of the class, and then we’ll start to talk a bit about machine learning.
By way of introduction, my name’s Andrew Ng and I’ll be instru ctor for this class. And so I personally work in machine learning, and I’ ve worked on it for about 15 years now, and I actually think that machine learning i

元数据:

page.metadata
# source: 源信息,这里对应PDF的文件名
# page:页码信息,这里对应PDF的页码
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture01.pdf', 'page': 0}

加载YouTube视频

过程1:导入几个要害部分,包括

  • YouTube音频加载器:从YouTube视频加载音频文件

  • OpenAI Whisper解析器:运用OpenAI的Whisper模型(一个语音转文本的模型),将YouTube音频转换为咱们能够处理的文本格局

from langchain.document_loaders.generic import GenericLoader
from langchain.document_loaders.parsers import OpenAIWhisperParser
from langchain.document_loaders.blob_loaders.youtube_audio import YoutubeAudioLoader

过程2:指定URL及保存音频文件的目录,创立组合了过程1两个要害部分的通用加载器并履行加载

url="https://www.youtube.com/watch?v=jGwO_UgTS7I"
save_dir="docs/youtube/"
loader = GenericLoader(
    YoutubeAudioLoader([url],save_dir),
    OpenAIWhisperParser()
)
docs = loader.load()

过程3:检查加载完结的视频文稿

docs[0].page_content[0:500]

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

加载网络URL

from langchain.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://github.com/basecamp/handbook/blob/master/37signals-is-you.md")
docs = loader.load()
print(docs[0].page_content[:500])

文档切割

在上一节中,咱们将不同格局和来历的数据加载到了标准的文档目标中。但是,这些文档经过转换后仍然很大,而咱们一般只需求检索文档中与主题最相关的内容,可能仅仅几个阶段或语句,而不需求整个文档。

因而,在这一节中,咱们将运用LangChain的文本切割器,把大型的文档切割成更小的块。

文档切割的重要性

文档切割发生在数据加载之后,放入向量存储之前

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

如果简单地按字符长度来切割文档,可能会造成语句的开裂,导致语义的丢掉或紊乱。这样的切割方式,无法为咱们正确地回答问题。

合理的做法,是尽量坚持语义的连贯性和完整性,分隔出有意义的块。

文档切割的方式

在LangChain中,一切的文本切割器都遵从同一个原理,就是依据「块巨细(chunk_size)」和「两个块之间的堆叠巨细(chunk_overlap)」进行切割。

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

chunk_size指的是每个块包括的字符或Token(如单词、语句等)的数量。

chunk_overlap指的是两个块之间共享的字符或Token的数量。chunk_overlap能够协助坚持上下文的连贯性,避免因为切割而丢掉重要的信息。

LangChain提供了多种类型的切割器,主要差别在于怎么确定块的鸿沟、块由哪些字符或Token组成、以及怎么测量块的巨细(按字符仍是按Token)。

精华笔记:吴恩达 x LangChain 《使用LangChain构建与数据对话的聊天机器人》(上)

元数据(Metadata)是块切割的另一个重要部分,咱们需求在一切块中坚持元数据的一致性,一起在需求的时分增加新的元数据。

依据字符的切割

怎么切割块一般取决于咱们正在处理的文档类型。

比方,处理代码的切割器具有许多不同编程言语的分隔符,如Python、Ruby、C等。当切割代码文档时,它会考虑到不同编程言语之间的差异。

过程1:导入文本切割器

# RecursiveCharacterTextSplitter-递归字符文本切割器
# CharacterTextSplitter-字符文本切割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitter

过程2:设定块巨细和块堆叠巨细

chunk_size =26
chunk_overlap = 4

过程3:初始化文本切割器

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap
)
c_splitter = CharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap
)

过程4:运用不同的切割器对字符串进行切割

递归字符文本切割器

text2 = 'abcdefghijklmnopqrstuvwxyzabcdefg'
r_splitter.split_text(text2)
# ['abcdefghijklmnopqrstuvwxyz', 'wxyzabcdefg']

能够看到,第二个块是从「wxyz」开端的,刚好是咱们设定的块堆叠巨细。

text3 = "a b c d e f g h i j k l m n o p q r s t u v w x y z"
r_splitter.split_text(text3)
# ['a b c d e f g h i j k l m', 'l m n o p q r s t u v w x', 'w x y z']

字符文本切割器

c_splitter.split_text(text3)
# ['a b c d e f g h i j k l m n o p q r s t u v w x y z']

能够看到,字符文本切割器实践并没有切割这个字符串,这是因为字符文本切割器默许是以换行符为分隔符的,为此,咱们需求将分隔符设置为空格。

c_splitter = CharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap,
    separator = ' '
)
c_splitter.split_text(text3)
# ['a b c d e f g h i j k l m', 'l m n o p q r s t u v w x', 'w x y z']

过程5:递归切割长阶段

some_text = """When writing documents, writers will use document structure to group content. \
This can convey to the reader, which idea's are related. For example, closely related ideas \
are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n  \
Paragraphs are often delimited with a carriage return or two carriage returns. \
Carriage returns are the "backslash n" you see embedded in this string. \
Sentences have a period at the end, but also, have a space.\
and words are separated by space."""
r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150,
    chunk_overlap=0,
    separators=["\n\n", "\n", " ", ""]
)
r_splitter.split_text(some_text)

这里,咱们传入一个分隔符列表,顺次为双换行符、单换行符、空格和一个空字符。

这就意味着,当你切割一段文本时,它会首要采用双换行符来尝试初步切割,并视状况顺次运用其他的分隔符来进一步切割。

终究切割结果如下:

[“When writing documents, writers will use document structure to group content. This can convey to the reader, which idea’s are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.”,

‘Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the “backslash n” you see embedded in this string. Sentences have a period at the end, but also, have a space.and words are separated by space.’]

如果需求依照语句进行分隔,则还要用正则表达式增加一个句号分隔符:

r_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150,
    chunk_overlap=0,
    separators=["\n\n", "\n", "(?<=\. )", " ", ""]
)
r_splitter.split_text(some_text)

[“When writing documents, writers will use document structure to group content. This can convey to the reader, which idea’s are related.”,

‘For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.’,

‘Paragraphs are often delimited with a carriage return or two carriage returns.’,

‘Carriage returns are the “backslash n” you see embedded in this string.’,

‘Sentences have a period at the end, but also, have a space.and words are separated by space.’]

这就是递归字符文本切割器名字中“递归”的意义,总的来说,咱们更建议在通用文本中运用递归字符文本切割器。

依据Token的切割

许多LLM的上下文窗口长度约束是依照Token来计数的。因而,以LLM的视角,依照Token对文本进行分隔,一般能够得到更好的结果。

为了理解依据字符切割和依据Token切割的区别,咱们能够用一个简单的例子来阐明。

from langchain.text_splitter import TokenTextSplitter
text_splitter = TokenTextSplitter(chunk_size=1, chunk_overlap=0)
text1 = "foo bar bazzyfoo"
text_splitter.split_text(text1)

这里,咱们创立了一个Token文本切割器,将块巨细设为1,块堆叠巨细设为0,相当于将任意字符串切割成了单个Token组成的列表,每个Token的内容如下:

['foo', ' bar', ' b', 'az', 'zy', 'foo']

因而,Token的长度和字符长度是不一样的,Token一般为4个字符。

切割Markdown文档

分块的目的旨在将具有一起上下文的文本放在一起

一般,咱们能够通过运用指定分隔符来进行分隔,但有些类型的文档(例如 Markdown)本身就具有可用于切割的结构(如标题)。

Markdown标题文本切割器会依据标题或子标题来切割一个Markdown文档,并将标题作为元数据增加到每个块中。

过程1:界说一个Markdown文档

from langchain.document_loaders import NotionDirectoryLoader
from langchain.text_splitter import MarkdownHeaderTextSplitter
markdown_document = """# Title\n\n \
## Chapter 1\n\n \
Hi this is Jim\n\n Hi this is Joe\n\n \
### Section \n\n \
Hi this is Lance \n\n 
## Chapter 2\n\n \
Hi this is Molly"""

过程2:界说想要切割的标题列表和称号

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

过程3:初始化Markdown标题文本切分器,切割Markdown文档

markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on
)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits[0]
# Document(page_content='Hi this is Jim  \nHi this is Joe', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1'})
md_header_splits[1]
# Document(page_content='Hi this is Lance', metadata={'Header 1': 'Title', 'Header 2': 'Chapter 1', 'Header 3': 'Section'})

能够看到,每个块都包括了页面内容和元数据,元数据中记录了该块所属的标题和子标题。

咱们现已了解了怎么将长文档切割为语义相关的块,并且包括正确的元数据。下一步则是将这些分块后的数据移动到向量存储中,以便进行检索或生成,敬请期待。