LangChain使用之Retrieval

Retrieval直接翻译过来即"检索",本章Retrieval模块包括与检索步骤相关的所有内容,例如数据的获取、切分、向量化、向量存储、向量检索等模块。常被应用于一个"企业/私人的知识库",提示大模型整体的能力

Retrieval模块的设计意义

大模型的幻觉问题

拥有记忆后,确实拓展了AI工程的应用场景

但是在专有领域,LLM无法学习到所有的专业知识细节,因此在面向专业领域知识的提问时,无法给出可靠准确的回答,甚至会"胡言乱语",这种现象成为LLM的"幻觉"

大模型生成的内容不可控,尤其是在金融和医疗等领域,一次金额的评估错误,一次医疗诊断的失误,哪怕只出现一次都是致命的。但,对于非专业人士来说可能难以辨识。目前还没有能够百分之百解决这种情况的方案

当前大家普遍达成的共识的一个方案

首先,为大模型提供一定的上下文信息,让其输出会变得更稳定

其次,利用RAG,将检索出来的文档和提示词树丛给大模型,生成更可靠的答案

RAG的解决方案

可以说,当应用需求集中在利用大模型去回答特定私有领域的知识,且知识库足够大,那么除了微调大模型外,RAG就是非常有效的一种缓解大模型推理的"幻觉"问题的解决方案

LangChain对这一流程提供了解决方案。

如果说LangChain相当于给LLM这个"大脑"安装了"四肢和躯干",RAG则是为LLM提供了接入"人类图书馆"的能力

目前,已经出现了非常多的产品几乎完全建立在 RAG 之上,包括客服系统、基于大模型的数据分析,以及成千上万的数据驱动聊天应用,应用场景五花八门。

RAG的优缺点

RAG的优点

1)相比提示词工程,RAG有更丰富的上下文和数据样本可以不需要用户提供过多的背景描述,就能生成比较符合用户预期的答案

2)相比于模型微调,RAG可以提升问答内容的时效性可靠性

3)在一定程度上保护了业务数据的隐私性

RAG的缺点

1)由于每次问答都涉及外部系统数据检索,因此RAG的响应时延相对较高

2)引用的外部知识数据会消耗大量的模型Token资源

Retrieval流程

环节1:Source(数据源)

指的是RAG架构中所外挂的知识库。这里有三点说明:

1、原始数据源类型多样:如:视频、图片、文本、代码、文档等

2、形式的多样性:

  • 可以是上百个.csv文件,可以是上千个.json文件,也可以是上万个.pdf文件
  • 可以是某一个业务流程外放的API,可以是某个网站的实时数据等

环节2:Load(加载)

文档加载器(Document Loaders)负责将来自不同数据源的非结构化文本,加载到内存,成为文档(Document)对象

文档对象包含文档内容和相关元数据信息,例如TXT、CSV、HTML、JSON、Markdown、PDF,甚至YouTube 视频转录等。

文档加载器还支持延迟加载模式,以环节处理大文件时的内存压力

文档加载器的编程接口使用起来非常简单,以下给出加载TXT文档格式的例子

python 复制代码
from langchain.document_loaders import TextLoader

text_loader = TextLoader("./test.txt")

docs = text_loader.load()
print(docs)

环节3:Transform(转换)

文档转换器(Document TransFormers)负责对加载的文档进行转换和处理,以便更好地适应下游任务的需求

文档转换器提供了一致的接口(工具)来操作文档,主要包括以下几类:

  • 文本拆分器(Text Solitters):将长文本拆分成语义上相关的小块,以适应语言模型的上下文窗口限制
  • 冗余过滤器(Redundancy Filters):识别并过滤重复的文档
  • 元数据提取器(Metadata Extractors):从文档中提取标题、语调等结构化元数据
  • 多语言转换器(Multi-lingual Transformers):实现文档的机器翻译
  • 对话转换器Conversational Transformers):将非结构化对话转换为问答格式的文档

总的来说,文档转换器是 LangChain 处理管道中非常重要的一个组件,它丰富了框架对文档的表示和操作能力

在这些功能中,文档拆分器是必须的操作。下面单独说明

环节3.1:Text Splitting(文档拆分)

  • 拆分/分块的必要性:前一个环节加载后的文档对象可以直接传入文档拆分器进行拆分,而文档切块后才能向量化并存入数据库中
  • 文档拆分器的多样性:LangChain提供了丰富的文档拆分器,不仅能够切分普通文本,还能切分Markdown、JSON、HTML、代码等特殊格式的文本
  • 拆分/分块的挑战性:实际拆分操作中需要处理许多细节问题,不同类型的文本不同的使用场景都需要采用不同的分块策略
    • 可以按照数据类型进行切片处理,比如针对文本类数据,可以直接按照字符、段落进行切片;代码类数据则需要进一步细分以保证代码的功能性
    • 可以直接根据token进行切片处理

在构建RAG应用程序的整个流程中,拆分/分块时最具有挑战性的环节之一,它显著影响检索效果。目前还没有通用的方法可以明确指出哪一种分块策略最为有效。不同的使用场景和数据类型都会影响分块策略的选择

环节4:Embed(嵌入)

文档嵌入模型(Text Embedding Models)负责将文本转换为向量表示,即模型赋予了文本计算机可理解的数值表示,使文本可用于向量空间中的各种运算,大大拓展了文本分析的可能性,是自然语言处理领域非常重要的技术

举例:

  • 实现原理:通过特定算法(如Word2Vec)将语义信息编码为固定维度的向量,具体算法细节需后续深入
  • 关键特性:相似的词在向量空间中举例相近,例如"猫"和"犬"的向量夹角小于"猫"和"汽车

文本嵌入为 LangChain 中的问答、检索、推荐等功能提供了重要支持。具体为:

  • 语义匹配:通过计算两个文本的向量余弦相似度,判断它们在语义上的相似程度,实现语义匹配
  • 文本检索:通过计算不同文本之间的向量相似度,可以实现语义搜索,找到向量空间中最相似的文本
  • 信息推荐:根据用户的历史记录或兴趣嵌入生成用户向量,计算不同信息的向量与用户向量的相似度,推荐相似的信息
  • 知识挖掘:可以通过聚类、降维等手段分析文本向量的分布,发现文本之间的潜在关联,挖掘知识
  • 自然语言处理:将词语、句子等表示为稠密向量,为神经网络等下游任务提供输入

环节5:Store(存储)

LangChain 还支持把文本嵌入存储到向量存储或临时缓存,以避免需要重新计算它们。这里就出现了数据库,支持这些嵌入的高效存储搜索的需求

环节6:Retrieve

检索器(Retrievers)是一种用于响应非结构化查询的接口,它可以返回符合查询要求的文档

LangChain 提供了一些常用的检索器,如向量检索器文档检索器网站检索器

通过配置不同的检索器,LangChain 可以灵活地平衡检索的精度、召回率与效率。检索结果将为后续的问答生成提供信息支持,以产生更加准确和完整的回答

文档加载器Document Loaders

LangChain的设计:对于 Source 中多种不同的数据源,我们可以用一种统一的形式读取、调用

加载txt

python 复制代码
from langchain.document_loaders import TextLoader

# 2.定义TextLoader对象,file_path=".txt的位置"
text_loader = TextLoader(
    file_path = "./test.txt",
    encoding = "utf-8",
)

# 3.加载
docs = text_loader.load()

# 4.打印
print(docs)

Documment对象中有两个重要的属性:

  • page_content:真正的文档内容
  • metadata:文档内容的原数据
python 复制代码
type(docs[0]) #langchain_core.documents.base.Document
python 复制代码
docs[0].page_content
python 复制代码
docs[0].metadata # {'source': './test.txt'}

加载pdf

例1:

LangChain加载PDF文件使用的是pypdf,先安装

python 复制代码
pip install pypdf
python 复制代码
from langchain.document_loaders import PyPDFLoader
# 定义PyPDFLoader
pdfLoader = PyPDFLoader(file_path="./02-load.pdf")

# 加载
docs = pdfLoader.load()
print(docs)
print(type(docs[0]))

同样的:

python 复制代码
ype(pages[0]) #langchain_core.documents.base.Document
python 复制代码
pages[0].page_content #只获取本页内容
python 复制代码
pages[0].metadata # {...,'source': './02-load.pdf',.., 'page': 0}

例2:使用load_and_split()

python 复制代码
# 导入相关的依赖 PyPDFLoader()
from langchain.document_loaders import PyPDFLoader

# 定义PyPDFLoader
py_pdfLoader = PyPDFLoader(file_path="./02-load.pdf")

# 加载
docs = py_pdfLoader.load_and_split() #底层默认使用了递归字符文本切分器
print(docs)

同样,对于PyPDFLoader,依然是使用.page_content.metadata去访问数据,也就是说,每一个文档加载器虽然代码逻辑不同,应用需求不同,但使用方式是相同的

加载CSV

例1:加载csv所有列

python 复制代码
from langchain_community.document_loaders.csv_loader import CSVLoader

loader = CSVLoader(file_path="./03-load.csv")
data = loader.load()
print(data)

print(type(data)) # <class 'list'>
print(type(data[0])) # <class 'langchain_core.documents.base.Document'>
print(len(data)) # 4
print(data[0].page_content) # id: 1 title: Introduction to Python ...

例2:使用 source_column 参数指定文件加载的列,保存在source变量中

python 复制代码
from langchain_community.document_loaders.csv_loader import CSVLoader

loader = CSVLoader(
    file_path="./03-load.csv",
    source_column='author'
)

data = loader.load()
print(data)

加载JSON

LangChain提供的JSON格式的文档加载器是JSONLoader。在实际应用场景中,JSON格式的数据占有很大比例,而且JSON的形式也是多样的。我们需要特别关注

JSONLoader 使用指定的 jq结构来解析 JSON 文件。jq是一个轻量级的命令行 JSON 处理器 ,可以对JSON 格式的数据进行各种复杂的处理,包括数据过滤、映射、减少和转换,是处理 JSON 数据的首选工具之一

python 复制代码
pip install jq

例1:使用JSONLoader文档加载器加载

python 复制代码
# 导入依赖
from langchain_community.document_loaders import JSONLoader
from pprint import pprint

# .messages[].content:遍历.messages[]中所有元素 从每一个元素中提取.content字段
json_loader=JSONLoader(
    file_path="./04-load.json",
    jq_schema=".messages[].content"
)

# 加载
docs = json_loader.load()
pprint(docs)

例2:提取04-response.json文件中嵌套在 data.items[].content 的文本

  • 如果希望处理 JSON 中的嵌套字段、数组元素提取 ,可以使用content_key配合is_content_key_jq_parsable=True,通过 jq 语法精准定位目标数据
  • 通常,对api请求结果的采集
python 复制代码
# 导入相关依赖
from langchain_community.document_loaders import JSONLoader
from pprint import pprint
# 定义json文件的路径
file_path = './04-response.json'
# 定义JSONLoader对象

loader = JSONLoader(
    file_path=file_path,
    jq_schema=".data.items[]", # 先定位到数组条目
    content_key=".content", # 再从条目中提取 content 字段
    is_content_key_jq_parsable=True # 用jq解析content_key
)
# 加载
data = loader.load()
pprint(data)
pprint(data[0].page_content)

例3:提取04-response.json文件中嵌套在 data.items[] 里的 title、content 和 其文本

python 复制代码
# 导入相关依赖
from langchain_community.document_loaders import JSONLoader
from pprint import pprint
# 定义json文件的路径
file_path = './04-response.json'
# 定义JSONLoader对象

loader = JSONLoader(
    file_path=file_path,
    # jq_schema=".data.items[] | {id, author, text: (.title + '\n' + .content)}",
    jq_schema=".data.items[]",
    content_key='.title + "\n\n" + .content',
    is_content_key_jq_parsable=True # 用jq解析content_key
)

# 加载
data = loader.load()
pprint(data)
pprint(data[0].page_content)

加载HTML

python 复制代码
pip install unstructured

例:

python 复制代码
# 导入相关的依赖
from langchain.document_loaders import UnstructuredHTMLLoader
# 定义UnstructuredHTMLLoader对象
# strategy:
# "fast" 解析加载html文件速度是比较快(但可能丢失部分结构或元数据)
# "hi_res": (高分辨率解析) 解析精准(速度慢一些)
# "ocr_only" 强制使用ocr提取文本,仅仅适用于图像(对HTML无效)
# mode :one of `{'paged', 'elements', 'single'}
# "elements" 按语义元素(标题、段落、列表、表格等)拆分成多个独立的小文档

html_loader = UnstructuredHTMLLoader(
    file_path="./05-load.html",
    mode="elements",
    strategy="fast"
)
# 加载
docs = html_loader.load()

print(len(docs)) 

# 打印
for doc in docs:
    print(doc)

加载MarkDown

python 复制代码
pip install markdown
pip install unstructured

例1:使用MarkDownLoader加载md文件

python 复制代码
# 导入相关的依赖
from langchain.document_loaders import UnstructuredMarkdownLoader
from pprint import pprint

# 定义UnstructuredMarkdownLoader对象
md_loader = UnstructuredMarkdownLoader(
    file_path="./06-load.md",
    strategy="fast"
)

# 加载
docs = md_loader.load()
print(len(docs))
# 打印
for doc in docs:
    pprint(doc)

例2:精细分割文档,保留结构信息

将Markdown文档按语义元素(标题、段落、列表、表格等)拆分成多个独立的小文档(Element对象),而不是返回单个大文档。通过指定mode="elements"轻松保持这种分离,每个分割后的元素会包含元数据

python 复制代码
# 导入相关的依赖
from langchain.document_loaders import UnstructuredMarkdownLoader
from pprint import pprint

# 定义UnstructuredMarkdownLoader对象
md_loader = UnstructuredMarkdownLoader(
    file_path="./asset/load/06-load.md",
    mode= "elements",
    strategy="fast"
)

# 加载
docs = md_loader.load()
print(len(docs))

# 打印
for doc in docs:
    # pprint(doc)
    pprint(doc.page_content)

加载File Directory

除了上述的单个文件加载,我们也可以批量加载一个文件夹内的所有文件

复制代码
pip install unstructured

例:

python 复制代码
# 导入相关的依赖
from langchain.document_loaders import DirectoryLoader
from langchain.document_loaders import PythonLoader
from pprint import pprint

# 定义DirectoryLoader对象,指定要加载的文件夹路径、要加载的文件类型和是否使用多线程
directory_loader = DirectoryLoader(
    path="./",
    glob="*.py",
    use_multithreading=True,
    show_progress=True,
    loader_cls=PythonLoader
)

# 加载
docs = directory_loader.load()

# 打印

print(len(docs))
for doc in docs:
    pprint(doc)

BaseLoader、Document源码分析

一方面:LangChain在设计时,要保证Source中多种不同的数据源,在接下来的流程中可以用一种统一的形式读取、调用

另一方面:为什么PDFLoaderTextLoader等Document Loader 都使用load()去加载,且都使用.page_content.metadata读取数据

【解答】每一个在LangChain中集成的文档加载器,都要继承自BaseLoader(文档加载器),BaseLoader提供供了一个名为"load"的公开方法,用于从配置的不同数据源加载数据,全部作为Document对象。实现逻辑如下所示:

BaseLoader类分析

BaseLoader类定义了如何从不同的数据源加载文档,每个基于不同数据源实现的loader,都需要继承BaseLoader。Baseloader要求不多,对于任何具体实现的loader,最少都要实现 load方法。

python 复制代码
class BaseLoader(ABC):
    """文档加载器接口。

    实现应当使用生成器实现延迟加载方法,以避免一次性将所有文档加载进内存。

`   load` 方法仅供用户方便使用,不应被重写。
    """

    # 子类不应直接实现此方法。而应实现延迟加载方法。
    def load(self) -> List[Document]:
        """将数据加载为 Document 对象。"""
        return list(self.lazy_load())

    def load_and_split(
        self, text_splitter: Optional[TextSplitter] = None
        ) -> List[Document]:
            """加载文档并将其分割成块。块以 Document 形式返回。
            不要重写此方法。它应被视为已弃用!
            参数:
                text_splitter: 用于分割文档的 TextSplitter 实例。默认为 RecursiveCharacterTextSplitter。
            返回:
                文档列表。
            """
                .....
                .....
            _text_splitter: TextSplitter = RecursiveCharacterTextSplitter()
        else:
            _text_splitter = text_splitter
        docs = self.load()
        return _text_splitter.split_documents(docs)

BaseLoader把数据加载成Documents object,存到Documents类中的page_content

Document类分析

Document允许用户与文档的内容进行交互,可以查看文档内容。

python 复制代码
class Document(Serializable):
    """用于存储文本及其关联元数据的类。"""

    page_content: str
    """字符串文本。"""
    metadata: dict = Field(default_factory=dict)
    """关于页面内容的任意元数据(例如,来源、与其他文档的关系等)。"""
    type: Literal["Document"] = "Document"

    def __init__(self, page_content: str, **kwargs: Any) -> None:
        """将 page_content 作为位置参数或命名参数传入。"""
        super().__init__(page_content=page_content, **kwargs)

    @classmethod
    def is_lc_serializable(cls) -> bool:
      """返回此类是否可序列化。"""
      return True

    @classmethod
    def get_lc_namespace(cls) -> List[str]:
        """获取 langchain 对象的命名空间。"""
        return ["langchain", "schema", "document"]

通过存+读的两个基类的抽象,满足不同类型加载器在数据形式上的统一。除此之外,其中的metadata会根据loader实现的不同写入不同的数据,同样是一个必要的基础属性

文档拆分器Text Splitters

为什么要拆分/分块/切分

当拿到统一的一个Document对象后,接下来需要切分成Chunks。如果不切分,而是考虑作为一个整体的Document对象,会存在两点问题:

  1. 假设提问的Query的答案出现在某一个Document对象中,那么将检索到的整个Document对象直接放入Prompt中并不是最优选择,因为其中一定会包含非常多无关的信息,而无效信息越多,对大模型后续的推理影响越大
  2. 任何一个大模型都存在最大输入的Token限制,如果一个Document非常大,比如一个几百兆的PDF,那么大模型肯定无法容纳如此多的信息

基于此,一个有效的解决方案就是将完整的Document对象进行分块处理(Chunking)。无论是在存储还是检索过程中,都将以这些块(chunks)为基本单位,这样有效地避免内容不相关性问题和超出最大输入限制的问题

Chunking拆分的策略

方法1:根据句子切分:这种方法按照自然句子边界进行切分,以保持语义完整性

方法2:按照固定字符串数来切分:这种策略根据特定的字符数量来划分文本,但可能会在不适当的位置切断句子

方法3:按固定字符串来切分,结合重叠窗口(overlapping windows) :此方法与按字符数切分相似,但通过重叠窗口技术避免切分关键内容,确保信息连贯性

方法4:递归字符切分方法:通过递归字符方式动态确定切分点,这种方法可以根据文档的复杂性和内容密度来调整块的大小

方法5:根据语义内容切分 :这种高级策略依据文本的语义内容来划分块,旨在保持相关信息的集中和完整,适用于需要高度语义保持的应用场景

这些方法各有优势和局限,选择适当的分块策略取决于具体的应用需求和预期的检索效果。接下来我们依次尝试用常规手段应该如何实现上述几种方法的文本切分

TextSplitter源码分析

python 复制代码
class TextSplitter(BaseDocumentTransformer, ABC):
    """用于将文本分割成块的接口。"""

    def __init__(
            self,
            chunk_size: int = 4000,
            chunk_overlap: int = 200, #
            length_function: Callable[[str], int] = len,
            keep_separator: bool = False,
            add_start_index: bool = False,
            strip_whitespace: bool = True
    )->None:
        """
        创建一个新的文本分割器。

        参数:
            chunk_size: 返回块的最大尺寸,单位是字符数。默认值为4000(由长度函数测量)

            chunk_overlap: 相邻两个块之间的字符重叠数,避免信息在边界处被切断而丢失。默认值为200,通常会设置为chunk_size的10% - 20%。

            length_function: 用于测量给定块字符数的函数。默认赋值为len函数。len函数在Python中按Unicode字符计数,所以一个汉字、一个英文字母、一个符号都算一个字符。

            keep_separator: 是否在块中保留分隔符,默认值为False

            add_start_index: 如果为 `True`,则在元数据中包含块的起始索引。默认值为False

            strip_whitespace: 如果为 `True`,则从每个文档的开始和结束处去除空白字符。默认值为True
        """
        if chunk_overlap > chunk_size:
            raise ValueError(
                f"重叠大小({chunk_overlap})大于块大小({chunk_size})。"
            )
        self._chunk_size = chunk_size
        self._chunk_overlap = chunk_overlap
        self._length_function = length_function
        self._keep_separator = keep_separator
        self._add_start_index = add_start_index
        self._strip_whitespace = strip_whitespace

        @abstractmethod
        def split_text(self, text: str) -> List[str]:
            """将文本分割成多个字符串。具体实现由子类决定"""

        def create_documents(
            self, texts: List[str], metadatas: Optional[List[dict]] = None
        ) -> List[Document]:
            """基于文本列表创建Documents对象。作用是将普通的字符串列表对象转化成Document列表对象,同时考虑切分"""
            _metadatas = metadatas or [{}] * len(texts)
            documents = []
            for i, text in enumerate(texts):
                index = 0
                previous_chunk_len = 0
                for chunk in self.split_text(text):
                    metadata = copy.deepcopy(_metadatas[i])
                    if self._add_start_index:
                        offset = index + previous_chunk_len - self._chunk_overlap
                        index = text.find(chunk, max(0, offset))
                        metadata["start_index"] = index
                        previous_chunk_len = len(chunk)
                    new_doc = Document(page_content=chunk, metadata=metadata)
                    documents.append(new_doc)
            return documents

    def split_documents(self, documents: Iterable[Document]) -> List[Document]:
        """分割文档。"""
        texts, metadatas = [], []
        for doc in documents:
            texts.append(doc.page_content)
            metadatas.append(doc.metadata)
        return self.create_documents(texts, metadatas=metadatas)

    @classmethod
    def from_huggingface_tokenizer(cls, tokenizer: Any, **kwargs: Any) -> TextSplitter:
        """使用 HuggingFace的分词器来计数长度的文本分割器。"""
        try:
            from transformers import PreTrainedTokenizerBase

            if not isinstance(tokenizer, PreTrainedTokenizerBase):
                raise ValueError(
                    "Tokenizer received was not an instance of PreTrainedTokenizerBase"
                )

            def _huggingface_tokenizer_length(text: str) -> int:
                return len(tokenizer.tokenize(text))

        except ImportError:
            raise ValueError(
                "Could not import transformers python package. "
                "Please install it with `pip install transformers`."
            )
        return cls(length_function=_huggingface_tokenizer_length, **kwargs)

    @classmethod
    def from_tiktoken_encoder(
        cls: Type[TS],
        encoding_name: str = "gpt2",
        model_name: Optional[str] = None,
        allowed_special: Union[Literal["all"], AbstractSet[str]] = set(),
        disallowed_special: Union[Literal["all"], Collection[str]] = "all",
        **kwargs: Any,
    ) -> TS:
        """使用 TikToken 编码器来计数长度的文本分割器。"""

    def transform_documents(
         self, documents: Sequence[Document], **kwargs: Any
    )->Sequence[Document]:
        """通过分割它们来转换文档序列。"""
        return self.split_documents(list(documents))

小结:几个常用的文档切分器的方法的调用

python 复制代码
#方式1:传入的参数类型:字符串; 返回值类型:List[str]
split_text(xxx)

#方式2:传入的参数类型:List[str]; 返回值类型:List[Document]
create_documents(xxx) #底层调用了split_text(xxx)

#方式3:传入的参数类型:List[Document]; 返回值类型:List[Document]
split_documents(xxx) #底层调用了create_documents()

此外,这里提供了一个可视化展示文本如何分割的工具,chunkviz.up.railway.app/

具体实现

LangChain提供了许多不同类型的文档切分器

官网地址:python.langchain.com/api_referen...

CharacterTextSplitter:Split by character

参数情况说明:

  • chunk_size:每个切块的最大token数量,默认值为4000
  • chunk_overlap:相邻两个切块之间的最大重叠token数量,默认值为200
  • separator:分割使用的分隔符,默认值为"\n\n"
  • length_function:用于计算切块长度的方法。默认赋值为父类TextSplitter的len函数

例1:字符串文本的分割

python 复制代码
from langchain.text_splitter import CharacterTextSplitte

text = """
LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者
能够更容易地构建复杂的应用程序。
"""
#定义字符分割器
splitter = CharacterTextSplitter(
    chunk_size=50, # 每块大小
    chunk_overlap=5,# 块与块之间的重复字符数
    #length_function=len,
    separator="" # 设置为空字符串时,表示禁用分隔符优先
)

#分割文本
texts = splitter.split_text(text)

#打印结果
for i, chunk in enumerate(texts):
    print(f"块 {i+1}:长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

说明:若必须禁用分隔符(如处理无空格文本),需容忍实际块长略小于 chunk_size(尤其对中文)

例2:指定分割符

python 复制代码
from langchain.text_splitter import CharacterTextSplitte

text = """
这是一个示例文本。我们将使用CharacterTextSplitter将其分割成小块。分割基于字符数。
"""
#定义字符分割器
splitter = CharacterTextSplitter(
    chunk_size=30, # 每块大小
    chunk_overlap=5,# 块与块之间的重复字符数
    separator="。" # 设置为空字符串时,表示禁用分隔符优先
)

#分割文本
texts = splitter.split_text(text)

#打印结果
for i, chunk in enumerate(texts):
    print(f"块 {i+1}:长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

注意:无重叠

separator优先原则 :当设置了separator(如"。"),分割器会首先尝试在分隔符处分割,然后再考虑 chunk_size。这是为了避免在句子中间硬性切断。这种设计是为了:

  1. 优先保持语义完整性(不切断句子)
  2. 避免产生无意义的碎片(如半个单词/不完整句子)
  3. 如果chunk_size比片段小,无法拆分片段,导致 overlap失效。
  4. chunk_overlap仅在合并后的片段之间生效(如果chunk_size足够大)。如果没有合并的片段,则 overlap失效。见例3

例3:指定分割符

注意:有重叠。此时,文本"这是第二段内容。"的token正好就是8。

python 复制代码
from langchain.text_splitter import CharacterTextSplitte

text = "这是第一段文本。这是第二段内容。最后一段结束。"
#定义字符分割器
splitter = CharacterTextSplitter(
    chunk_size=20, # 每块大小
    chunk_overlap=8,# 块与块之间的重复字符数
    separator="。", # 设置为空字符串时,表示禁用分隔符优先
    keep_separator=True #chunk中是否保留切割符
)

#分割文本
texts = splitter.split_text(text)

#打印结果
for i, chunk in enumerate(texts):
    print(f"块 {i+1}:长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

RecursiveCharacterTextSplitter:最常用

文档切分器中较常用的是RecursiveCharacterTexrSplitter(递归字符文本切分器),遇到特定字符时进行分割。默认情况下,它尝试进行切割的字符包括["\n\n","\n"," ",""]

具体为:根据第一个字符进行切块,但如果任何切块太大,则会继续移动到下一个字符继续切块,以此类推

此外,还可以考虑添加,。等分割字符。

特点

  • 保留上下文 :优先在自然语言边界(如段落、句子结尾)处分割,减少信息碎片化
  • 智能分段:通过递归尝试多种分隔符,将文本分割为大小接近chunk_size的片段。
  • 灵活适配 :适用于多种文本类型(代码、Markdown、普通文本等),是LangChain中最通用的文本拆分器

此外,还可以指定的参数包括:

  • chunk_size:同TextSplitter(父类)
  • chunk_overlap:同TextSplitter(父类)
  • length_function:同TextSplitter(父类)
  • add_start_index:同TextSplitter(父类)

例1:使用split_text()方法演示

python 复制代码
# 1.导入相关依赖
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 2.定义RecursiveCharacterTextSplitter分割器对象
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True,
)

# 3.定义拆分的内容
text="LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"

# 4.拆分器分割
paragraphs = text_splitter.split_text(text)
for para in paragraphs:
    print(para)
    print('-------')

例2:使用create_documents()方法演示,传入字符串列表,返回Document对象列表

python 复制代码
# 1.导入相关依赖
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 2.定义RecursiveCharacterTextSplitter分割器对象
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True,
)
# 3.定义分割的内容
list=["LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"]

# 4.分割器分割
paragraphs = text_splitter.create_documents(list)

for para in paragraphs:
    print(para)
    print('=====')

page_content='LangChain框' metadata={'start_index': 0}

=====

page_content='架特性' metadata={'start_index': 10}

=====

page_content='多模型集成(GPT' metadata={'start_index': 15}

=====

page_content='/Claude)' metadata={'start_index': 24}

=====

page_content='记忆管理功能' metadata={'start_index': 33}

=====

page_content='链式调用设计。文档' metadata={'start_index': 40}

=====

page_content='分析场景示例:需要处' metadata={'start_index': 49}

=====

page_content='理PDF/Word等' metadata={'start_index': 59}

=====

page_content='格式。' metadata={'start_index': 69}

=====


逐步分割过程

第一阶段:顶级分割(按 \n\n

1.首次分割:

python 复制代码
text.split("\n\n") →
[
    "LangChain框架特性",
    "多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
]
    • 第一部分长度:13字符 > 10 → 需要继续分割
    • 第二部分长度:79字符 > 10 → 需要继续分割

第二阶段:递归分割第一部分 "LangChain框架特性"

1.尝试\n:无匹配

2.尝试(空格):

    • 检查字符串" LangChain框架特性 "(无空格)

3.退回到""(字符级分割):

python 复制代码
list("LangChain框架特性") →
['L','a','n','g','C','h','a','i','n','框','架','特','性']
    • 前10字符:LangChain框
    • 剩余部分:"架特性"

第三阶段:递归分割第二部分(长段落)

1.按\n分割

python 复制代码
"多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档...".split("\n") →
[
    "多模型集成(GPT/Claude)", # 17字符
    "记忆管理功能", # 6字符
    "链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。" # 36字符
]
    • 第1块:17字符 > 10 → 继续分割
    • 第2块:6字符 ≤ 10 → 直接保留
    • 第3块:36字符 > 10 → 继续分割

2.分割多模型集成(GPT/Claude):

    • 尝试 :无空格
    • 回退到""
      • 前10字符:多模型集成(GPT
      • 剩余7字符:"/Claude)"

3.分割"链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"

    • 尝试 :无空格
    • 回退到""
      • 按10字符分段:
python 复制代码
"链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
→
[
    "链式调用设计。文档",
    "分析场景示例:需要处",
    "理PDF/Word等",
    "格式。"
]

例3:使用create_documents()方法演示,将本地文件内容加载成字符串,进行拆分

python 复制代码
# 导入相关依赖
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 打开.txt文件
with open("asset/load/08-ai.txt", encoding="utf-8") as f:
    state_of_the_union = f.read() #返回的是字符串
    
# 定义RecursiveCharacterTextSplitter(递归字符分割器)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    #chunk_overlap=0,
    length_function=len
)

# 分割文本
texts = text_splitter.create_documents([state_of_the_union])

# 打印分割文本
for text in texts:
    print(f"🔥{text.page_content}")

例4:使用split_documents()方法演示,利用PDFLoader加载文档,对文档的内容用递归切割器切割

python 复制代码
# 导入相关依赖
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 定义PyPDFLoader加载器
loader = PyPDFLoader("./02-load.pdf")

# 加载和切割文档对象
docs = loader.load() # 返回Document对象构成的list
# print(f"第0页:\n{docs[0]}")

# 定义切割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    #chunk_size=120,
    chunk_overlap=0,
    # chunk_overlap=100,
    length_function=len,
    add_start_index=True,
)

# 对pdf内容进行切割得到文档对象
paragraphs = text_splitter.split_documents(docs)
#paragraphs = text_splitter.create_documents([text])
for para in paragraphs:
    print(para.page_content)
    print('=======')

例5:自定义分隔符

有些书写系统没有单词边界,例如中文、日文和泰文。使用默认分隔符列表["\n\n", "\n", " ", ""]分割文本可能导致单词错误的分割。为了保持单词在一起,你可以自定义分割字符,覆盖分隔符列表以包含额外的标点符号

python 复制代码
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=20, # 增加重叠字符
    separators=["\n\n", "\n", "。", "!", "?", "......", ",", ""], # 添加中文标点
    length_function=len,
    keep_separator=True #保留句尾标点(如 ......),避免切割后丢失语气和逻辑
)

效果:算法优先在句号、省略号处切割,保持句子完整性

TokenTextSplitter/CharacterTextSplitter:Split by tokens

当我们将文本拆分为块时,除了字符以外,还可以:按Token的数量分割(而非字符或单词数),将长文本切分成多个小块

什么时Token分割

  • 对模型而言,Token是文本的最小处理单位。例如:
    • 英文:Hello->1个token,ChatGPT->2个token(Chat+GPT
    • 中文:人工智能->可能拆分为2-3个token(取决于分词器)

为什么按照Token分割?

  • 语言模型对输入长度的限制是基于Token数(如GPT-4的8k/32k Token上限),直接按字符或单词分割可能导致实际Token数超限。(确保每个文本块不超过模型的Token上限)
  • 大语言模型(LLM)通常是以token的数量作为其计量(或收费)的依据,所以采用token分割也有助于我们在使用时更方便的控制成本。

TokenTextSplitter使用说明:

  • 核心依据:Token数量 + 自然边界。(TokenTextSplitter 严格按照 token 数量进行分割,但同时会优先在自然边界(如句尾)处切断,以尽量保证语义的完整性。)
  • 优点:与LLM的Token计数逻辑一致,能尽量保持语义完整
  • 缺点:对非英语或特定领域文本,Token化效果可能不佳
  • 典型场景:需要精确控制Token数输入LLM的场景

例1:使用TokenTextSplitter

python 复制代码
# 导入相关依赖
from langchain_text_splitters import TokenTextSplitter

# 初始化 TokenTextSplitter
text_splitter = TokenTextSplitter(
    chunk_size=33, #最大 token 数为 32
    chunk_overlap=0, #重叠 token 数为 0
    encoding_name="cl100k_base", # 使用 OpenAI 的编码器,将文本转换为 token 序列
)
    
# 定义文本
text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50年代诞生以来,人工智能经历了多次起伏。"

# 开始切割
texts = text_splitter.split_text(text)

# 打印分割结果
print(f"原始文本被分割成了 {len(texts)} 个块:")
for i, chunk in enumerate(texts):
    print(f"块 {i+1}: 长度:{len(chunk)} 内容:{chunk}")
    print("=" * 10)

原始文本被分割成了 3 个块:

块 1: 长度:29 内容:人工智能是一个强大的开发框架。它支持多种语言模型和工具链。

==========

块 2: 长度:32 内容:人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50

==========

块 3: 长度:19 内容:年代诞生以来,人工智能经历了多次起伏。

==========


为什么会出现这样的分割

1.第一块(29字符):内容是一个完整的句子,以句号结尾。TokenTextSplitter识别到这是一个自然的语义边界,即使这里的 token 数量可能尚未达到 33,它也选择在此处切割,以保证第一块语义的完整性

2.第二块(32字符):内容包含了另一个完整句子"人工智能时指...一门科学。"以及下一句的开头"自20世纪50"。分割器在处理完第一个句子的 token 后,可能 token 数量已经接近chunk_size,于是在下一个自然边界(这里是句号)之后继续读取了少量 token("自20世纪50"),直到非常接近 33token 的限制

注意:"50" 之后被切断,是因为编码器很可能将"50"识别为一个独立的 token,而"年代"是另一个 token。为了保证 token 的完整性,它不会在"50"字符中间切断

3.第三块(19字符):是第二块中断内容的剩余部分,形成了一个较短的块。这是因为剩余内容本身的token 数量就较少

特别注意:字符长度不等于Token数量

例2:使用CharacterTextSplitter

python 复制代码
# 导入相关依赖
from langchain_text_splitters import CharacterTextSplitter
import tiktoken # 用于计算Token数量

# 定义通过Token切割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", # 使用 OpenAI 的编码器
    chunk_size=18,
    chunk_overlap=0,
    separator="。", # 指定中文句号为分隔符
    keep_separator=False, # chunk中是否保留分隔符
)

# 定义文本
text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。今天天气很好,想出去踏青。但是又比较懒不想出去,怎么办"

# 开始切割
texts = text_splitter.split_text(text)
print(f"分割后的块数: {len(texts)}")

# 初始化tiktoken编码器(用于Token计数)
encoder = tiktoken.get_encoding("cl100k_base") # 确保与CharacterTextSplitter的encoding_name一致

# 打印每个块的Token数和内容
for i, chunk in enumerate(texts):
    tokens = encoder.encode(chunk) # 现在encoder已定义
    print(f"块 {i + 1}: {len(tokens)} Token\n内容: {chunk}\n")

分割后的块数: 4

块 1: 17 Token

内容: 人工智能是一个强大的开发框架

块 2: 14 Token

内容: 它支持多种语言模型和工具链

块 3: 18 Token

内容: 今天天气很好,想出去踏青

块 4: 21 Token

内容: 但是又比较懒不想出去,怎么办


SemanticChunker:语义分块

SemanticChunking(语义分块)是 LangChain 中一种更高级的文本分割方法,它超越了传统的基于字符或固定大小的分块方式,而是根据文本的语义结构进行智能分块,使每个分块保持语义完整性,从而提高检索增强生成(RAG)等应用的效果

语义分割 vs 传统分割

特性 语义分割( SemanticChunking) 传统字符分割(RecursiveCharacter)
分割依据 嵌入向量相似度 固定字符/换行符
语义完整性 ✅ 保持主题连贯 ❌ 可能切断句子逻辑
计算成本 ❌ 高(需嵌入模型) ✅ 低
使用场景 需要高语义一致性的任务 简单文本预处理

例:

python 复制代码
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
import os
import dotenv
dotenv.load_dotenv()

# 加载文本
with open("asset/load/09-ai1.txt", encoding="utf-8") as f:
    state_of_the_union = f.read() #返回字符串

# 获取嵌入模型
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

embed_model = OpenAIEmbeddings(
model="text-embedding-3-large"
)
# 获取切割器
text_splitter = SemanticChunker(
    embeddings=embed_model,
    breakpoint_threshold_type="percentile",#断点阈值类型:字面值["百分位数", "标准差", "四分位距", "梯度"] 选其一
    breakpoint_threshold_amount=65.0 #断点阈值数量 (极低阈值 → 高分割敏感度)
)

# 切分文档
docs = text_splitter.create_documents(texts = [state_of_the_union])
print(len(docs))
for doc in docs:
    print(f"🔍 文档 {doc}:")

关于参数的说明

1.breakpoint_threshold_type(断点阈值类型)

  • 作用:定义文本语义边界的检测算法,决定何时分割文本块。
  • 可选值及原理:
类型 原理说明 适用场景
percentile 计算相邻句子嵌入向量的余弦距离,取举例分布的第N百分位值作为阈值,高于此值则分割 常规文本(如文章、报告)
standard_deviation 均值+N倍标准差为阈值,识别语义突变点 语义变化剧烈的文档(如技术手册)
interquartile 四分位距(IQR) 定义异常值边界,超过则分割 长文档(如书籍)
grandient 基于嵌入向量变化的梯度检测分割点(需要自定义实现) 实验性需求

2.breakpoint_threshold_amount(断点阈值量)

  • 作用 :控制分割的颗粒敏感度,值越小分割越细(块越多),值越大分割越粗(块越少)。
  • 取值范围与示例:
    • percentile模式:0.0~100.0,用户代码设65.0表示仅当余弦距离 > 所有距离中最低的65.0%值时分割 。默认值是:95.0,兼顾语义完整性与检索效率。值过小(比如0.1),会产生大量小文本块,过度分割可能导致上下文断裂
    • standard_deviation模式:浮点数(如1.5表示均值+1.5倍标准差)
    • interquartile模式:倍数(如1.5是IQR标准值)

其他拆分器

类型1:HTMLHeaderTextSplitter:Split by HTML header

HTMLHeaderTextSplitter是一种专门用于处理HTML文档的文本分割方法,它根据HTML的标题标签(如<h1>、<h2>等将文档划分为逻辑分块,同时保留标题的层级结构信息

例:

python 复制代码
from langchain.text_splitter import HTMLHeaderTextSplitter

html_string = """
<!DOCTYPE html>
<html>
<body>
    <div>
        <h1>欢迎来到卡塞尔学院</h1>
        <p>卡塞尔学院是全世界精英的聚集地</p>
        <div>
            <h2>卡塞尔老师简介</h2>
            <p>卡塞尔学院的教授都是最顶尖的精英,虽然他们都有一些个人的小癖好</p>
            <h3>昂热校长</h3>
            <p>机密等级S</p>
        </div>
    </div>
</body>
</html>
"""

# 用于指定要根据哪些HTML标签来分割文本
headers_to_split_on = [
    ("h1", "标题1"),
    ("h2", "标题2"),
    ("h3", "标题3"),
]

# 定义HTMLHeaderTextSplitter分割器
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

# 分割器分割
html_header_splits = html_splitter.split_text(html_string)

html_header_splits

说明:

  • 标题下文本内容所属标题的层级信息保存在元数据中
  • 每个分块会自动继承父级标题的上下文,避免信息割裂
类型2:CodeTextSplitter:Split code

CodeTextSplitter是一个转为代码文件设计的文本分割器(Text Splitter),支持代码的语言包括['cpp','go', 'java', 'js', 'php', 'proto', 'python', 'rst', 'ruby', 'rust', 'scala', 'swift', 'markdown', 'latex', 'html','sol']。它能够根据编程语言的语法结构(如函数、类、代码块等)智能地拆分代码,保持代码逻辑的完整性

与递归文本分割器(如RecursiveCharacterTextSplitter)不同,CodeTextSplitter 针对代码的特性进行了优化,避免在函数或类的中间截断

例:

python 复制代码
# 导入相关依赖
from langchain.text_splitter import (
    Language,
    RecursiveCharacterTextSplitter,
)
from pprint import pprint

# 定义要分割的python代码片段
PYTHON_CODE = """
def hello_world():
    print("Hello, World!")
def hello_world1():
    print("Hello, World1!")
"""

# 定义递归字符切分器
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=50,
    chunk_overlap=0
)

# 文档切分
python_docs = python_splitter.create_documents(texts=[PYTHON_CODE])
pprint(python_docs)
类型3:MarkdownTextSplitter:md数据类型

因为Markdown格式有特定的语法,一般整体内容由h1、h2、h3等多级标题组织,所以MarkdownHeaderTextSplitter的切分策略就是根据标题来分割文本内容

例:

python 复制代码
from langchain.text_splitter import MarkdownTextSplitter

markdown_text = """
# 一级标题\n
这是一级标题下的内容\n\n
## 二级标题\n
- 二级下列表项1\n
- 二级下列表项2\n
"""

# 关键步骤:直接修改实例属性
splitter = MarkdownTextSplitter(chunk_size=30, chunk_overlap=0)
splitter._is_separator_regex = True # 强制将分隔符视为正则表达式

# 执行分割
docs = splitter.create_documents(texts = [markdown_text])

# print(len(docs))
for i, doc in enumerate(docs):
    print(f"\n🔍 分块 {i + 1}:")
    print(doc.page_content)

文档嵌入模型Text Embedding Models

嵌入模型概述

Text Embedding Models :文档嵌入模型,提供将文本编码为向量的能力,及文档向量化文档写入用户查询匹配都会先执行文档嵌入编码,即向量化

  • LangChain提供了超过25种不同的嵌入提供商和方法的集成,从开源到专有API,总有一款适合你。
  • Hugging Face等开源社区提供了一些文本向量化模型(例如BGE),效果比闭源且调用API的向量化模型效果好,并且向量化模型参数量小,在CPU上即可运行 。所以,这里推荐在开发RAG应用的过程中,使用开源的文本向量化模型。此外,开源模型还可以根据应用场景下收集的数据对模型进行微调,提高模型效果

LangChain中针对向量化模型的封装提供了两种接口,一种针对文档向量化(embed_document),一种针对于句子的向量化(embed_query)

句子的向量化(embed_query)

例:

python 复制代码
from langchain_openai import OpenAIEmbeddings
import os
import dotenv
dotenv.load_dotenv()
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
# 初始化嵌入模型
embeddings_model = OpenAIEmbeddings(model="text-embedding-ada-002")

# 待嵌入的文本句子
text = "What was the name mentioned in the conversation?"

# 生成一个嵌入向量
embedded_query = embeddings_model.embed_query(text = text)

# 使用embedded_query[:5]来查看前5个元素的值
print(embedded_query[:5])

print(len(embedded_query))

文档的向量化 (embed_documents)

文档的向量化,接收的参数是字符串数组

例:

python 复制代码
from dotenv import load_dotenv
from langchain_community.document_loaders import CSVLoader
from langchain_openai import OpenAIEmbeddings

embeddings_model = OpenAIEmbeddings(
    model="text-embedding-3-large",
)

# 情况1:
loader = CSVLoader("./03-load.csv", encoding="utf-8")
docs = loader.load_and_split()


# 存放的是每一个chrunk的embedding。

embeded_docs = embeddings_model.embed_documents([doc.page_content for doc in docs])
print(len(embeded_docs))

# 表示的是每一个chrunk的embedding的维度
print(len(embeded_docs[0]))
print(embeded_docs[0][:10])

向量存储(Vector Stores)

理解向量存储

将文本向量化之后,下一步就是进行向量的存储。这部分包含两块:

  • 向量的存储:将非结构化数据向量化后,完成存储
  • 向量的查询:查询时,嵌入非结构化查询并检索与嵌入查询"最相似"的嵌入向量。即具有相似性检索能力

常用的向量数据库

LangChain提供了超过50种不同向量存储(Vector Stores)的集成,从开源的本地向量存储云托管的私有向量存储,允许你选择最适合需求的向量存储

典型的介绍如下:

向量数据库 描述
Chroma 开源、免费的嵌入式数据库
FAISS Meta出品,开源、免费,Facebook AI相似性搜索服务。
Milvus 用于存储、索引和管理由深度神经网络和其他ML模型产生的大量嵌入向量的数据库
Pinecome 具有广泛功能的向量数据库
Redis 基于Redis的检索器

向量数据库的理解

假设你是一名摄影师,拍了大量的照片。为了方便管理和查找,你决定将这些照片存储到一个数据库中。传统的关系型数据库(如 MySQL、PostgreSQL 等)可以帮助你存储照片的元数据,比如拍摄时间、地点、相机型号等

但是,当你想要根据照片的内容(如颜色、纹理、物体等)进行搜索时,传统数据库将无法满足你的需求,因为它们通常以数据表的形式存储数据,并使用查询语句进行精确搜索。那么此时,向量数据库就可以派上用场

我们可以构建一个多维的空间使得每张照片特征都存在于这个空间内,并用已有的维度进行表示,比如时间、地点、相机型号、颜色...此照片的信息将作为一个点,存储于其中。。以此类推,即可在该空间中构建出无数的点,而后我们将这些点与空间坐标轴的原点相连接,就成为了一条条向量,当这些点变为向量之后,即可利用向量的计算进一步获取更多的信息。当要进行照片的检索时,也会变得更容易更快捷

注意 :在向量数据库中进行检索时,检索并不是唯一的,精确的,而是查询和目标向量最为相似的一些向量,具有模糊性

思考:只要对图片、视频、商品等素材进行向量化,就可以实现以图搜图、视频相关推荐、相似宝贝推荐等功能

代码实现

使用向量数据库组件时需要同时传入包含文本块Document类对象以及文本向量化组件,向量数据库组件会自动完成将文本向量化的工作,并写入数据库中

数据的存储

例1:从TXT文档中加载数据,向量化后存储到Chroma数据库

安装模块:

python 复制代码
pip install chromadb
pip install langchain-chroma
python 复制代码
from langchain_text_splitters import CharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings

# 举例:将分割后的文本,使用 OpenAI 嵌入模型获取嵌入向量,并存储在 Chroma 中
# 获取嵌入模型
my_embedding = OpenAIEmbeddings(model="text-embedding-ada-002")
# 创建TextLoader实例,并加载指定的文档

loader = TextLoader("./09-ai1.txt", encoding='utf-8')
documents = loader.load()

# 创建文本拆分器
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)

# 拆分文档
docs = text_splitter.split_documents(documents)

# 存储:将文档和数据存储到向量数据库中
db = Chroma.from_documents(docs, my_embedding)

# 查询:使用相似度查找
query = "人工智能的核心技术都有啥?"
docs = db.similarity_search(query)
print(docs[0].page_content)

思考:此时的数据存储在哪里呢?

注意 :Chroma主要有两种存储模式:内存模式持久化模式。当使用persist_directory参数时,数据会保存到指定目录;如果没有指定,则默认使用内存存储

例2:操作csv文档,并向量化

python 复制代码
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import CSVLoader
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
import os
import dotenv
dotenv.load_dotenv()
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

# 获取嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

# 加载文档并拆分(第1次拆分)
loader = CSVLoader("./asset/load/03-load.csv", encoding='utf-8')
pages = loader.load_and_split()
#print(len(pages)) # 4

# 文本拆分(第2次拆分)
text_spliter = CharacterTextSplitter.from_tiktoken_encoder(chunk_size=500)
docs = text_spliter.split_documents(pages)

# 向量存储
db_path = './chroma_db'
db = Chroma.from_documents(docs, embeddings, persist_directory=db_path)

数据的检索

举例:一个包含构建Chroma向量数据库以及向量检索的代码

前置代码:

python 复制代码
# 导入相关依赖
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

# 定义文档
raw_documents = [
    Document(
        page_content="葡萄是一种常见的水果,属于葡萄科葡萄属植物。它的果实呈圆形或椭圆形,颜色有绿色、紫色、红色等多种。葡萄富含维生素C和抗氧化物质,可以直接食用或酿造成葡萄酒。",
        metadata={"source": "水果", "type": "植物"}
    ),

    Document(
        page_content="白菜是十字花科蔬菜,原产于中国北方。它的叶片层层包裹形成紧密的球状,口感清脆微甜。白菜富含膳食纤维和维生素K,常用于制作泡菜、炒菜或煮汤。",
        metadata={"source": "蔬菜", "type": "植物"}
    ),
    Document(
        page_content="狗是人类最早驯化的动物之一,属于犬科。它们具有高度社会性,能理解人类情绪,常被用作宠物、导盲犬或警犬。不同品种的狗在体型、毛色和性格上有很大差异。",
        metadata={"source": "动物", "type": "哺乳动物"}
    ),
    Document(
        page_content="猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。",
        metadata={"source": "动物", "type": "哺乳动物"}
    ),
    Document(
        page_content="人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。",
        metadata={"source": "生物", "type": "灵长类"}
    ),
    Document(
        page_content="太阳是太阳系的中心恒星,直径约139万公里,主要由氢和氦组成。它通过核聚变反应产生能量,为地球提供光和热。太阳活动周期约为11年,会影响地球气候。",
        metadata={"source": "天文", "type": "恒星"}
    ),
    Document(
        page_content="长城是中国古代的军事防御工程,总长度超过2万公里。它始建于春秋战国时期,秦朝连接各段,明朝大规模重修。长城是世界文化遗产和人类建筑奇迹。",
        metadata={"source": "历史", "type": "建筑"}
    ),
    Document(
        page_content="量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。",
        metadata={"source": "物理", "type": "科学"}
    ),
    Document(
        page_content="《红楼梦》是中国古典文学四大名著之一,作者曹雪芹。小说以贾、史、王、薛四大家族的兴衰为背景,描绘了贾宝玉与林黛玉的爱情悲剧,反映了封建社会的种种矛盾。",
    metadata={"source": "文学", "type": "小说"}
    ),
    Document(
        page_content="新冠病毒(SARS-CoV-2)是一种可引起呼吸道疾病的冠状病毒。它通过飞沫传播,主要症状包括发热、咳嗽、乏力。疫苗和戴口罩是有效的预防措施。",
        metadata={"source": "医学", "type": "病毒"}
    )
]

# 创建嵌入模型
embedding = OpenAIEmbeddings(model="text-embedding-ada-002")

# 创建向量数据库
db = Chroma.from_documents(
    documents=raw_documents,
    embedding=embedding,
    persist_directory="./chroma-3",
)
  1. 相似性检索(similarity_search)

接收字符串作为参数:

python 复制代码
# 5. 检索示例(返回前3个最相关结果)
query = "哺乳动物"
docs = db.similarity_search(query, k=3) # k=3表示返回3个最相关文档
print(f"查询: '{query}' 的结果:")
for i, doc in enumerate(docs, 1):
    print(f"\n结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
  1. 支持直接对问题向量查询(aimilarity_search_by_vector)

搜索与给定嵌入向量相似的文档,它接受嵌入向量作为参数,而不是字符串

python 复制代码
query = "哺乳动物"
embedding_vector = embedding.embed_query(query)
docs = db.similarity_search_by_vector(embedding_vector, k=3)
print(f"查询: '{query}' 的结果:")
for i, doc in enumerate(docs, 1):
    print(f"\n结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
  1. 相似性检索,支持过滤元数据(filter)
python 复制代码
query = "哺乳动物"
docs = db.similarity_search(
    query=query,
    k=3,
    filter={"source": "动物"})

for i, doc in enumerate(docs, 1):
    print(f"\n结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
  1. 通过L2举例分数进行检索(similarity_search_with_score)
pythonpython 复制代码
docs = db.similarity_search_with_score(
    "量子力学是什么?"
)
for doc, score in docs:
    print(f" [L2距离得分={score:.3f}] {doc.page_content} [{doc.metadata}]")
  1. 通过余弦相似度分数进行检索(_similarity_search_with_relevance_scores)

说明:分值越接近1(上限),检索到的文档越和问题相似

python 复制代码
docs = db._similarity_search_with_relevance_scores(
    "量子力学是什么?"
)
for doc, score in docs:
    print(f"* [余弦相似度得分={score:.3f}] {doc.page_content} [{doc.metadata}]")
  1. MMR(最大边际相关性, max_marginal_relevance_search)

MMR是一种平衡相关性多样性的检索策略,避免返回高度相似的冗余结果。

python 复制代码
docs = db.max_marginal_relevance_search(
    query="量子力学是什么",
    lambda_mult=0.8, # 侧重相似性
)
print("🔍 关于【量子力学是什么】的搜索结果:")
print("=" * 50)
for i, doc in enumerate(docs):
    print(f"\n📖 结果 {i+1}:")
    print(f"📌 内容: {doc.page_content}")
    print(f"🏷 标签: {', '.join(f'{k}={v}' for k, v in doc.metadata.items())}")

参数说明:lambda_mult参数值介于 0 到 1 之间,用于确定结果之间的多样性程度,其中 0 对应最大多样性,1 对应最小多样性。默认值为 0.5

检索器(召回器) Retrievers

介绍

从"向量存储组件"的代码实现数据检索中可以看到,向量数据库本身已经包含了实现召回功能的函数方法(similarity_search)。该函数通过计算原始查询向量与数据库中存储向量之间的相似度来实现召回

LangChain还提供了更复杂的召回策略,这些策略被集成在Retrievers(检索器或召回器)组件中

Retrievers(检索器)是一种用于从大量文档中检索与给定查询相关的文档或信息片段的工具。检索器不需要存储文档,只需要返回(或检索文档)即可

Retrievers 的执行步骤

步骤1:将输入查询转换为向量表示

步骤2:在向量存储中搜索与查询向量最相似的文档向量(通常使用余弦相似度或欧几里得距离等度量方法)

步骤3:返回与查询最相关的文档或文本片段,以及它们的相似度得分

代码实现

Retriever 一般和 VectorStore 配套实现,通过as_retriever()方法获取。

python 复制代码
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
import os
import dotenv
dotenv.load_dotenv()

# 定义文档加载器
loader = TextLoader(file_path='./asset/load/09-ai1.txt',encoding="utf-8")

# 加载文档
documents = loader.load()

# 定义文本切割器
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)

# 切割文档
docs = text_splitter.split_documents(documents)

# 定义嵌入模型
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large"
)

# 将文档存储到向量数据库中
db = FAISS.from_documents(docs, embeddings)

# 从向量数据库中得到检索器
retriever = db.as_retriever()

# 使用检索器检索
docs = retriever.invoke("深度学习是什么?")
print(len(docs))

# 得到结果
for doc in docs:
    print(f"⭐{doc}")

使用相关检索策略

前置代码:

python 复制代码
# 导入相关依赖
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document

# 定义文档
document_1 = Document(
    page_content="经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!",
)
document_2 = Document(
    page_content="基础设施:政府将投资1万亿美元用于修复道路、桥梁和宽带网络。",
)
document_3 = Document(
    page_content="气候变化:承诺到2030年将温室气体排放量减少50%。",
)
document_4 = Document(
    page_content=" 医疗保健:降低处方药价格,扩大医疗保险覆盖范围。",
)
document_5 = Document(
    page_content="教育:提供免费的社区大学教育。。",
)
document_6 = Document(
    page_content="科技:增加对半导体产业的投资以减少对外国供应链的依赖。。",
)
document_7 = Document(
    page_content="外交政策:继续支持乌克兰对抗俄罗斯的侵略。",
)
document_8 = Document(
    page_content="枪支管制:呼吁国会通过更严格的枪支管制法律。",
)
document_9 = Document(
    page_content="移民改革:提出全面的移民改革方案。",
)
document_10 = Document(
    page_content="社会正义:承诺解决系统性种族歧视问题。",
)
documents = [
    document_1,
    document_2,
    document_3,
    document_4,
    document_5,
    document_6,
    document_7,
    document_8,
    document_9,
    document_10,
]

# 创建向量存储
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large"
)

# 将文档向量化,添加到向量数据库索引中,得到向量数据库对象
db = FAISS.from_documents(documents, embeddings)
  1. 默认检索器使用相似性搜索
python 复制代码
# 获取检索器
retriever = db.as_retriever(search_kwargs={"k": 4}) #这里设置返回的文档数

docs = retriever.invoke("经济政策")
for i, doc in enumerate(docs):
    print(f"\n结果 {i+1}:\n{doc.page_content}\n")
  1. 分数阈值查询

只有相似度超过这个值才会召回

python 复制代码
retriever = db.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.1}
)
docs = retriever.invoke("经济政策")

for doc in docs:
    print(f"📌 内容: {doc.page_content}")

注意只会返回满足阈值分数的文档,不会获取文档的得分。如果想查询文档的得分是否满足阈值,可以使用向量数据库的similarity_search_with_relevance_scores查看

python 复制代码
docs_with_scores = db.similarity_search_with_relevance_scores("经济政策")

for doc, score in docs_with_scores:
    print(f"\n相似度分数: {score:.4f}")
    print(f"📌 内容: {doc.page_content}")
  1. MMR搜索
python 复制代码
retriever = db.as_retriever(
    search_type="mmr",
    # search_kwargs={"fetch_k":2}
)

docs = retriever.invoke("经济政策")

print(len(docs))

for doc in docs:
    print(f"📌 内容: {doc.page_content}")

结合LLM

例1:通过FAISS构建一个可搜索的向量索引数据库,并结合RAG技术让LLM去回答问题

情况1:不用RAG给LLM灌输上下文数据

python 复制代码
from langchain_openai import ChatOpenAI
import os
import dotenv
dotenv.load_dotenv()
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

# 创建大模型实例
llm = ChatOpenAI(model="gpt-4o-mini")

# 调用
response = llm.invoke("北京有什么著名的建筑?")
print(response.content)

情况2:用RAG给LLM灌输上下文数据

python 复制代码
pip install faiss-cpu
python 复制代码
# 导入所有需要的包
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI,OpenAIEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores import FAISS
import os
import dotenv
dotenv.load_dotenv()

# 创建自定义提示词模板
prompt_template = """请使用以下提供的文本内容来回答问题。仅使用提供的文本信息,如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。

文本内容:
{context}

问题:{question}

回答:
"
"""

prompt = PromptTemplate.from_template(prompt_template)

# 初始化模型
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

embedding_model = OpenAIEmbeddings(model="text-embedding-3-large")

# 加载文档
loader = TextLoader("./10-test_doc.txt", encoding='utf-8')
documents = loader.load()

# 5. 分割文档
text_splitter = CharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
)

texts = text_splitter.split_documents(documents)

# 创建向量存储
vectorstore = FAISS.from_documents(
    documents=texts,
    embedding=embedding_model
)
# 获取检索器
retriever = vectorstore.as_retriever()
docs = retriever.invoke("北京有什么著名的建筑?")

# 创建Runnable链
chain = prompt | llm

# 提问
result = chain.invoke(input={"question":"北京有什么著名的建筑?","context":docs})
print("\n回答:", result.content)
相关推荐
一泽Eze11 小时前
有效的 Context 工程(精读、万字梳理)
agent
腾讯云云开发12 小时前
云开发Copilot实战:零代码打造智能体小程序指南
agent·ai编程·小程序·云开发
阿里云云原生12 小时前
近期 AI 领域的新发布所带来的启示
agent
阿里云云原生12 小时前
分布式 Multi Agent 安全高可用探索与实践
agent
董厂长13 小时前
阅读:REACT: SYNERGIZING REASONING AND ACTING INLANGUAGE MODELS(在语言模型中协同推理与行动)
人工智能·语言模型·agent·react
大模型教程14 小时前
GraphRAG绝对是以后RAG的潮流,不服来辩
程序员·llm·agent
AI大模型15 小时前
Spring AI 番外篇03:本地RAG使用百炼知识库
程序员·llm·agent
AI大模型15 小时前
Spring AI 番外篇02:还在为 AI Agent 调试头秃?Spring AI Alibaba Admin 来救场了!
程序员·llm·agent
安思派Anspire18 小时前
为何你的RAG系统无法处理复杂问题(二)
aigc·openai·agent