08—langchain Retrieval

Retrieval(检索)

Retrieval 模块的设计意义

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

  • 大模型的幻觉问题

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

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

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

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

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

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

  • RAG 的解决方案

可以说,当应用需求集中在利用大模型去回答特定私有领域的知识 ,且知识库足够大,那么除了微调大模型 外,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()  # 返回List列表(Document对象)
print(docs)

环节 3:Transform(转换)

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

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

  • 文本拆分器(Text Splitters):将长文本拆成语义上相关的小块,以适应语言模型的上下文窗口限制。
  • 冗余过滤器(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)负责将文本 转换为向量表示,即模型赋予了文本计算机可理解的数值表示,使文本可用于向量空间中的各种运算,大大拓展了文本分析的可能性,是自然语言处理领域非常重要的技术。

举例:

  1. The :
    • 假设词嵌入为:EmbeddingThe=[0.9,2.1,4.3]\text{Embedding}_{\text{The}} = [0.9, 2.1, 4.3]EmbeddingThe=[0.9,2.1,4.3]
  2. cat :
    • 之前给出的嵌入是:Embeddingcat=[1.2,3.4,5.6]\text{Embedding}_{\text{cat}} = [1.2, 3.4, 5.6]Embeddingcat=[1.2,3.4,5.6]
  3. sat :
    • 假设词嵌入为:Embeddingsat=[2.5,3.6,4.7]\text{Embedding}_{\text{sat}} = [2.5, 3.6, 4.7]Embeddingsat=[2.5,3.6,4.7]
  • 实现原理 :通过特定算法(如 Word2Vec)将语义信息编码为固定维度的向量,具体算法细节需后续深入。
  • 关键特性:相似的词在向量空间中距离相近,例如"猫"和"犬"的向量夹角小于"猫"和"汽车"。

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

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

环节 5:Store(存储)

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

环节 6:Retrieve(检索)

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

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

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

文档加载器 Document Loaders

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

不同的文档,使用不同的文档加载器。

  • txt 文档:TextLoader
  • pdf 文档:PDFLoader
  • csv 文档:CSVLoader
  • json 文档:JSONLoader
  • html 文档:UnstructuredHTMLLoader
  • markdown 文档:UnstructuredMarkdownLoader
  • 文件目录:DirectoryLoader

创建好 XXXLoader 实例后,都需要调用 load 方法,在内存中返回一个list[Document]

TextLoader 加载 txt 文档

py 复制代码
from langchain_community.document_loaders import TextLoader

# 获取txt文档路径
file_path = "./assets/01-langchain-utf-8.txt"
# 创建TextLoader实例
text_loader = TextLoader(
    file_path=file_path,
    encoding="utf-8",
)
# 调用load,返回list[Document]
docs = text_loader.load()
print(docs)
# [Document(metadata={'source': './assets/01-langchain-utf-8.txt'}, page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,构建端到端的应用程序。它提供了一套模块化工具和接口,支持从简单的文本生成到复杂的多步骤推理任务')]
print(docs[0].metadata) # Document对象的元数据
print(docs[0].page_content) # 文档内容

PDFLoader 加载 pdf

langchain 加载 pdf 文件使用的是 pypdf,需要先进行安装

bash 复制代码
uv pip install pypdf
py 复制代码
from langchain_community.document_loaders import PyPDFLoader

# 加载本地的pdf
pdf_loader = PyPDFLoader(file_path="./assets/02-load.pdf")
docs = pdf_loader.load()
print(docs)
# 加载远程的pdf
remote_pdf_loader = PyPDFLoader(file_path="https://arxiv.org/pdf/2302.03803")
remote_docs = remote_pdf_loader.load()
print(remote_docs)

CSVLoader 加载 CSV

py 复制代码
from langchain_community.document_loaders import CSVLoader

file_loader = CSVLoader(
    file_path="./assets/03-load.csv",
    # source_column="author" # 选择value作为metadata元数据的内容
)
docs = file_loader.load()

for doc in docs:
    print(doc)
    print("======")

"""
🔥source_column未设置
page_content='id: 3
title: Web Development
content: HTML, CSS and JavaScript are core web technologies.
author: Mike Johnson' metadata={'source': './assets/03-load.csv', 'row': 2}

🔥source_column="author"
page_content='id: 3
title: Web Development
content: HTML, CSS and JavaScript are core web technologies.
author: Mike Johnson' metadata={'source': 'Mike Johnson', 'row': 2}
"""

JSONLoader 加载 JSON

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

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

bash 复制代码
uv pip install jq

示例 1:加载 json 文件中所有的数据

py 复制代码
from langchain_community.document_loaders import JSONLoader

json_loader = JSONLoader(
    file_path="./assets/04-load.json",
    jq_schema=".", # 表示加载数据的字段,.表示加载所有数据
    text_content=False # False表示将加载的json对象转为json字符串
)
docs = json_loader.load()
print(docs)

示例 2:加载 json 文件中指定的数据

  • 这里以 json 文件中的 messages[] 中的所有的 content 字段
py 复制代码
from langchain_community.document_loaders import JSONLoader

json_loader = JSONLoader(
    file_path="./assets/04-load.json",
    jq_schema=".messages[].content", # 加载 .messages[]的所有content字段,不要忘记前面的.
    text_content=True # 因为messages[].content内容本来就是字符串,所以可以不用将json转为字符串
)
docs = json_loader.load()

for doc in docs:
    print(doc.page_content)
    print("=====")
  • 提取 json 文件中嵌套在 data.items[].content 的内容

源文件内容:

json 复制代码
{
  "status": "success",
  "data": {
    "page": 2,
    "per_page": 3,
    "total_pages": 5,
    "total_items": 14,
    "items": [
      {
        "id": 101,
        "title": "Understanding JSONLoader",
        "content": "This article explains how to parse API responses...",
        "author": {
          "id": "user_1",
          "name": "Alice"
        },
        "created_at": "2023-10-05T08:12:33Z"
      },
      {
        "id": 102,
        "title": "Advanced jq Schema Patterns",
        "content": "Learn to handle nested structures with...",
        "author": {
          "id": "user_2",
          "name": "Bob"
        },
        "created_at": "2023-10-05T09:15:21Z"
      },
      {
        "id": 103,
        "title": "LangChain Metadata Handling",
        "content": "Best practices for preserving metadata...",
        "author": {
          "id": "user_3",
          "name": "Charlie"
        },
        "created_at": "2023-10-05T10:03:47Z"
      }
    ]
  }
}
py 复制代码
from langchain_community.document_loaders import JSONLoader

# 方式1
json_loader = JSONLoader(
    file_path="./assets/04-response.json",
    jq_schema=".data.items[].content",
)
# 方式2
# json_loader = JSONLoader(
#     file_path="./assets/04-response.json",
#     jq_schema=".data.items[]",
#     content_key=".content",
#     is_content_key_jq_parsable=True, # 表示使用jq去解析content_key
# )
docs = json_loader.load()

for doc in docs:
    print(doc.page_content)
    print("=====")

UnstructuredHTMLLoader 加载 HTML

安装unstructured

bash 复制代码
uv pip install unstructured
py 复制代码
from langchain_community.document_loaders import UnstructuredHTMLLoader

"""
strategy:
    "fast":解析加载html文件速度比较快,但是可能丢失部分结构或元数据
    "hi_res":解析精准,但是速度慢一些
    "ocr_only":强制使用ocr提取文本,仅适用于图像(对html无效)
mode:
    "paged" | "elements" | "single"
    "elements":按语义元素(标题、段落、列表等)拆分成多个独立的小文档
"""
loader = UnstructuredHTMLLoader(
    file_path="assets/05-load.html",
    mode="elements",
    strategy="fast"
)
docs = loader.load()
print(docs)

UnstructuredMarkdownLoader 加载 Markdown

安装unstructuredmarkdown

bash 复制代码
uv pip install unstructured
uv pip install markdown
py 复制代码
from langchain.document_loaders import UnstructuredMarkdownLoader

md_loader = UnstructuredMarkdownLoader(
    file_path="asset/06-load.md",
    strategy="fast"
)
docs = md_loader.load()
for doc in docs:
    print(doc)

DirectoryLoader 加载文件目录

安装unstructured

bash 复制代码
uv pip install unstructured
py 复制代码
from langchain.document_loaders import DirectoryLoader
from langchain.document_loaders import PythonLoader

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

文档拆分器 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:根据语义内容切分 :这种高级策略依据文本的语义内容来划分块,旨在保持相关信息的集中和完整,适用于需要高度语义保持的应用场景。

第 2 种方法(按照字符数切分)和第 3 种方法(按固定字符数切分结合重叠窗口)主要基于字符进行文本的切分,而不考虑文章的实际内容和语义。这种方式虽简单,但可能会导致主题或语义上的断裂

相对而言,第 4 种递归方法更加灵活和高效,它结合了固定长度切分和语义分析。通常是首选策略,因为它能够更好地确保每个段落包含一个完整的主题。

而第 5 种方法,基于语义的分割虽然能精确地切分出完整的主题段落,但这种方法效率较低。它需要运行复杂的分段算法(segmentation algorithm),处理速度较慢 ,并且段落长度可能极不均匀 (有的主题段落可能很长,而有的则较短)。因此,尽管它在某些需要高精度语义保持的场景下有其应用价值,但并不适合所有情况

TextSplitter 源码分析

内部定义的参数

  • chunk_size: 返回块的最大尺寸,单位是字符数。默认值为 4000(由长度函数 length_function 测量)
  • hunk_overlap: 相邻两个块之间的字符重叠数,避免信息在边界处被切断而丢失。默认值为 200, 通常会设置为 chunk_size 的 10% - 20%。
  • length_function: 用于测量给定块字符数的函数。默认赋值为 len 函数。len 函数在 Python 中按
    Unicode 字符计数,所以一个汉字、一个英文字母、一个符号都算一个字符。
  • keep_separator: 是否在块中保留分隔符,默认值为 False
  • add_start_index: 如果为 True,则在元数据中包含块的起始索引。默认值为 False
  • strip_whitespace: 如果为 True,则从每个文档的开始和结束处去除空白字符。默认值为 True

内部定义的常用方法

  • 按照字符串进行切分:
    • split_text:传入字符串,返回 List[str]
    • create_documents:传入 List[str],返回 List[Document],底层调用了split_text方法,封装成了Document对象
  • 按照 Document 对象进行切分:
    • split_documents:传入 List[Document],返回 List[Document],底层调用create_documents方法

Document 对象与 Str 的关系

文档切分器可以按照字符进行切分,也可以按照 Document 进行切分。其中 Str 可以理解为是 Document 对象的 page_content 属性。

Document:文档对象,包含 page_content 属性,以及 metadata 属性。

CharacterTextSplitter

CharacterTextSplitter按照字符进行切分

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

text = """
LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者能够更容易地构建复杂的应用程序。
"""

splitter = CharacterTextSplitter(
    chunk_size=50,  # 每块大小
    chunk_overlap=5,  # 块与块之间的重复字符数
    separator=""  # 设置为空字符串,表示禁用"分隔符优先"
)

texts = splitter.split_text(text)

for i, chunk in enumerate(texts):
    print(f"块 {i + 1}:长度:{len(chunk)}")
    print(chunk)
    print("=======")
"""
LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽[象,使开发]
chunk_overlap=5,刚好重叠5个
=======
块 2:长度:22
[象,使开发]者能够更容易地构建复杂的应用程序。
=======
"""

separator 优先原则

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

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

RecursiveCharacterTextSplitter(最常用)

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

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

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

特点

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

可指定参数

  • chunk_size :同 TextSplitter(父类)。
  • chunk_overlap :同 TextSplitter(父类)。
  • length_function :同 TextSplitter(父类)。
  • add_start_index :同 TextSplitter(父类)。
py 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    # add_start_index=True, # 在字符串中无作用,只有在document中才行
)
text = "LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
paragraphs = text_splitter.split_text(text)
for para in paragraphs:
    print(para)
    print('-------')
"""
LangChain框
-------
架特性
-------
多模型集成(GPT
-------
/Claude)
-------
记忆管理功能
-------
链式调用设计。文档
-------
分析场景示例:需要处
-------
理PDF/Word等
-------
格式。
-------
"""

add_start_index字段

py 复制代码
from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True, # 会在chunk中添加metadata信息
)
text = "LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
paragraphs = text_splitter.create_documents([text])
for chunk in paragraphs:
    print(chunk)
    print('-------')

"""
🔥add_start_index=True
page_content='LangChain框' metadata={'start_index': 0}
-------
page_content='架特性' metadata={'start_index': 10}

🔥add_start_index=False
page_content='LangChain框'
-------
page_content='架特性'
"""

TokenTextSplitter

当我们将文本拆分为块时,除了字符以外,还可以:按 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 的场景
py 复制代码
from langchain_text_splitters import CharacterTextSplitter
import tiktoken

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",  # 使用 OpenAI 的编码器,将文本转为token
    chunk_size=18,  # 最大token数为0
    chunk_overlap=0,  # 重叠token数为0
    separator="。",  # 指定中文句号为分隔符
    keep_separator=False,  # chunk中是否保留分隔符
)
text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。今天天气很好,想出去踏青。但是又比较懒不想出去,怎么办"
texts = text_splitter.split_text(text)
print(f"分割后的块数: {len(texts)}")
encoder = tiktoken.get_encoding("cl100k_base")  # 确保与CharacterTextSplitter的encoding_name一致
for i, chunk in enumerate(texts):
    tokens = encoder.encode(chunk)  # 现在encoder已定义
    print(f"块 {i + 1}: {len(tokens)} Token\n内容: {chunk}\n")

SemanticChunker

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

需要借助嵌入大模型,去将内容转为向量

语义分割 vs 传统分割

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

with open("assets/09-ai1.txt", encoding="utf-8") as f:
    state_of_the_union = f.read()  # 返回字符串

embed_model = OllamaEmbeddings(
    model="mxbai-embed-large:latest"
)
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(doc)

关于参数的说明

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

文档嵌入模型 Text Embedding Models

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

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

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

句子的向量化(embed_query)

py 复制代码
from langchain_ollama import OllamaEmbeddings

model = OllamaEmbeddings(
    model="mxbai-embed-large:latest"
)

embed_query = model.embed_query(text="Hello World!")
print(embed_query)

文档的向量化(embed_documents)

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

例 1:

py 复制代码
from langchain_ollama import OllamaEmbeddings

model = OllamaEmbeddings(
    model="mxbai-embed-large:latest"
)

embed_documents = model.embed_documents(["Hello World!"])

例 2:

py 复制代码
from langchain_community.document_loaders import CSVLoader
from langchain_ollama import OllamaEmbeddings

model = OllamaEmbeddings(
    model="mxbai-embed-large:latest"
)

loader = CSVLoader(file_path="./assets/03-load.csv", encoding="utf-8")
docs = loader.load()

embed_documents = model.embed_documents([doc.page_content for doc in docs])
print(embed_documents)

Vector Stores 向量存储

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

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

常用的向量数据库

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

向量数据库的理解

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

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

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

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

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

数据的存储

这里使用 Chromadb,需要安装 chromadblangchain-chroma

py 复制代码
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_ollama import OllamaEmbeddings
from langchain_text_splitters import CharacterTextSplitter

# 创建TextLoader实例,并加载文档
loader = TextLoader(
    file_path="./assets/09-ai1.txt",
    encoding="utf-8",
)
docs = loader.load()
# 创建文本拆分器,并拆分文档
text_splitter = CharacterTextSplitter(
    chunk_size=500, # 这个支持的大小与模型也有关系
    chunk_overlap=100
)
splitter_docs = text_splitter.split_documents(docs)
# 创建嵌入模型
embedding_model = OllamaEmbeddings(model="mxbai-embed-large:latest")
# 将文档及嵌入模型传入到Chroma相关的结构中,进行数据的存储
db = Chroma.from_documents(
    documents=splitter_docs,
    embedding=embedding_model,
)
  • 当前的数据存储在哪个地方了?

如果我们使用的from_documents()中没有显示的指明存储位置的话,则将当前的数据存储在内存中,并缓存起来。如果需要指明具体的存储位置,需要设置参数:persist_directory

py 复制代码
db = Chroma.from_documents(
    documents=splitter_docs,
    embedding=embedding_model,
    persist_directory="./assets/chroma", # 指定存储位置
)

需要明确,在向量数据库中,不仅存储了数据(或文档)的向量,而且还存储了数据(或文档)本身。

因为检索的向量要和文档做映射

数据的检索

前置代码:存储一些 mock 数据

py 复制代码
from langchain_core.documents import Document
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

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_model = OllamaEmbeddings(model="mxbai-embed-large:latest")
db = Chroma.from_documents(
    documents=raw_documents,
    embedding=embedding_model,
    persist_directory="./assets/chroma",
)
1、相似性检索(similarity_search)

接收字符串作为参数

py 复制代码
query = "哺乳动物"
docs = db.similarity_search(query, k=3) # k=3表示返回3个相关文档
for i, doc in enumerate(docs, 1):
    print(f"结果:{i}")
    print(f"内容:{doc.page_content}")
    print(f"元数据:{doc.metadata}")

"""
结果:1
内容:量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。
元数据:{'source': '物理', 'type': '科学'}
结果:2
内容:量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。
元数据:{'source': '物理', 'type': '科学'}
结果:3
内容:猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。
元数据:{'source': '动物', 'type': '哺乳动物'}
"""
2、 支持直接对问题向量查询(similarity_search_by_vector)

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

py 复制代码
query = "哺乳动物"
# 借助大模型转为向量
embedding_vector = embedding_model.embed_query(query)
docs = db.similarity_search_by_vector(embedding_vector, k=3)
3、相似性检索,支持过滤元数据(filter)
py 复制代码
query = "哺乳动物"
docs = db.similarity_search(
    query=query,
    k=3,
    filter={"source": "动物"}
)
4、通过 L2 距离分数进行搜索(similarity_search_with_score)

什么是 L2 距离?

拿直角三角形为例,两个直角边的长度 a+b 为 L1 距离,而斜边的长度为 L2 距离。

py 复制代码
query = "量子力学是什么"
docs = db.similarity_search_with_score(query)

for doc, score in docs:
    print(f"[L2距离得分={score:.3f}] {doc.page_content} [{doc.metadata}]")

说明:分数值越小(score:.3f),检索到的文档越和问题相似。分值取值范围:[0,正无穷]

5、 通过余弦相似度分数进行搜索(_similarity_search_with_relevance_scores)
py 复制代码
query = "量子力学是什么"
docs = db.similarity_search_with_relevance_scores(query)

for doc, score in docs:
    print(f"[L2距离得分={score:.3f}] {doc.page_content} [{doc.metadata}]")

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

6、MMR(最大边际相关性,max_marginal_relevance_search)

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

相关和多样是相反的,相关度为 100%,多样度就为 0%。

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

多样性,1 对应最小多样性。默认值为 0.5。

py 复制代码
query = "量子力学是什么"
docs = db.max_marginal_relevance_search(
    query,
    lambda_mult=0.8 # 侧重相似性
)

Retrievers 检索器(召回器)

向量数据库本身已经包含了实现召回功能的函数方法(similarity_search)。该函数通过计算原始查询向量与数据库中存储向量之间的相似度来实现召回。

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

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

召回器组件类名 功能
MultiQueryRetriever 用 LLM 将用户查询改写成多个查询
ContextualCompressionRetriever 用 LLM 对召回文本进行压缩或过滤
EnsembleRetriever 集成多个召回器
LongContextReorder 将召回内容进行重新排序
MultiVectorRetriever 为每个候选文档保存多个向量表示
ParentDocumentRetriever 小块文本向量当作索引,返回大块文本
SelfQueryRetriever 将自然语言查询转化为结构化查询
TimeWeightedVectorStoreRetriever 召回文本时考虑时效性因素

Retrievers 的执行步骤

  1. 步骤 1:将输入查询转换为向量表示。
  2. 步骤 2:在向量存储中搜索与查询向量最相似的文档向量(通常使用余弦相似度或欧几里得距离等度量方法)。
  3. 步骤 3:返回与查询最相关的文档或文本片段,以及它们的相似度得分。

基础使用

py 复制代码
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_ollama import OllamaEmbeddings
from langchain_text_splitters import CharacterTextSplitter

loader = TextLoader(
    file_path="./assets/09-ai1.txt",
    encoding="utf-8",
)
documents = loader.load()
text_splitter = CharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=0
)
splitter_docs = text_splitter.split_documents(documents)
embedding_model = OllamaEmbeddings(model="mxbai-embed-large:latest")
db = Chroma.from_documents(
    documents=splitter_docs,
    embedding=embedding_model,
)

# 基于向量数据库来获取检索器
retriever = db.as_retriever()
docs = retriever.invoke("深度学习是什么?")

使用相关检索策略

1、默认检索器使用相似性搜索
py 复制代码
# 获取检索器
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")
2、分数阈值查询

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

py 复制代码
retriever = db.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.1}
)

3、MMR 搜索

py 复制代码
retriever = db.as_retriever(
    search_type="mmr",
)
相关推荐
Dontla6 小时前
黑马大模型RAG与Agent智能体实战教程LangChain提示词——6、提示词工程(提示词优化、few-shot、金融文本信息抽取案例、金融文本匹配案例)
redis·金融·langchain
JaydenAI7 小时前
[LangChain之链]LangChain的Chain——由Runnable构建的管道
python·langchain
草帽lufei7 小时前
LangChain 框架核心向量存储
langchain
猫头虎7 小时前
如何使用Docker部署OpenClaw汉化中文版?
运维·人工智能·docker·容器·langchain·开源·aigc
qq_5470261798 小时前
LangChain 1.0 核心概念
运维·服务器·langchain
uXrvbWJGleS8 小时前
三相两电平整流器Simulink仿真探究
langchain
猫头虎8 小时前
手动部署开源OpenClaw汉化中文版过程中常见问题排查手册
人工智能·langchain·开源·github·aigc·agi·openclaw
程序员ken9 小时前
深入理解大语言模型(8) 使用 LangChain 开发应用程序之上下文记忆
人工智能·python·语言模型·langchain
一切尽在,你来19 小时前
第二章 预告内容
人工智能·langchain·ai编程