1. 概念

如果按照文档加载器给我们分割成的文档,比如说,PDF文档加载器是按照一页一页的给我们进行分割的,但是有可能文档中的描述的一个知识点或者描述的一个问题是被分成两页的(MD文档中的elements模式也会有相同的问题),后面我们将分割后的文档表示成向量的时候也是一个文档被表示成一个向量,那么此时一个问题就可能会被表示成多个向量,但是,我们肯定是希望一个单独的问题或者答案被分成一个文档,将来在被表示成向量的时候,也只生成一份向量数据。

LangChain的文本分割器组件就可以按照我们想要的规则对文档进行拆分。

文档拆分通常是将大文本分解为更小的、易于管理的块。这对于索引数据并将其传递到模型中都很有用。因为,大块更难搜索并且不适合模型的有限上下文窗口。拆分可以提高搜索结果的粒度,从而可以更精确地将查询与相关文档部分进行匹配。
在这里插入图片描述

2. 文本文档拆分(根据文档长度与文档语义拆分)

2.1 基于字符长度拆分

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 文档加载器(MD)
# single模式,只拆分成一个Document文档
loader = UnstructuredMarkdownLoader(
    "../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md",
    mode="single",    # MD 加载器默认将文档加载为一个
)

# load()方法将本地文件加载成document列表
data = loader.load()

# 定义文本分割器
text_splitter = CharacterTextSplitter(
    separator="\n\n",           # 分割符。一般来说,有一个默认的分割符优先级列表:["\n\n", "\n", " "]
    chunk_size=100,             # 块大小(这里设置的值参考标准,为了保证段落/句子完整,实际分割的时候会超出此设定的大小)
    chunk_overlap=20,           # 块重叠大小(让相邻的块有小部分重叠,保证语义的完整性)
    length_function=len,        # 测量字符长度的函数
    is_separator_regex=False,   # 是否使用正则表达式描写分隔符?
)

# 分割文档--按照定义的文本分割器分割成文档列表
documents = text_splitter.split_documents(data)
for document in documents[:10]:
    print("*" * 50)
    print(document)

输出结果:

Created a chunk of size 107, which is longer than the specified 100
Created a chunk of size 114, which is longer than the specified 100
Created a chunk of size 104, which is longer than the specified 100
Created a chunk of size 149, which is longer than the specified 100
Created a chunk of size 200, which is longer than the specified 100
Created a chunk of size 173, which is longer than the specified 100
Created a chunk of size 233, which is longer than the specified 100
Created a chunk of size 150, which is longer than the specified 100
Created a chunk of size 173, which is longer than the specified 100
Created a chunk of size 118, which is longer than the specified 100
Created a chunk of size 327, which is longer than the specified 100
Created a chunk of size 392, which is longer than the specified 100
Created a chunk of size 186, which is longer than the specified 100
Created a chunk of size 218, which is longer than the specified 100
Created a chunk of size 173, which is longer than the specified 100
Created a chunk of size 233, which is longer than the specified 100
**************************************************
page_content='1. 概念

输出解析器负责获取模型的输出,并将输出转换为更结构化的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='大型语言模型(LLM)的输出本质上是非结构化的文本。但在构建应用程序时,我们通常希望得到结构化的、机器可读的数据,这样可以将其转换为更适合下游任务的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='输出解析器的作用就是架起这座桥梁:它们将 LLM 的非结构化文本输出转换为结构化格式。

2. 与with_structured_output() 的区别' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='输出解析器和with_structured_output()都实现了相同的功能。

输出解析器是LangChain中的一个功能组件,支持使用链的方式进行组建。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='with_structured_output()是聊天模型中的一个方法,只能进行手动调用,返回一个新的、崛北了结构化输出能力的 Runnable 对象。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='3. 解析文本输出--StrOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='model = init_chat_model(model="deepseek-chat", model_provider="deepseek")' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='chain = model | StrOutputParser()

print(chain.invoke("你是谁?"))

输出结果:' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
**************************************************
page_content='输出结果:

你好!我是DeepSeek,由深度求索公司创造的AI助手。很高兴认识你!😊' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}

可以看到输出结果中有很多的标红Created a chunk of size 107, which is longer than the specified 100。这表示分割时,超出了我们设定的chunk_size=100 目标块大小。在这里要说明,这是一个在使用LangChain 的文本分割器时非常常见的问题。看到这个信息,不要担心,这不是错误,而是预期的行为。

原因是为了保持语义的完整性!当文本分割器用尽所有指定的分隔符都无法将一段文本分割到你的目标大小 chunk_size 以下时,它会选择保留整个文本块,而不是强行将其截断为无意义的片段,因此我们会看到这个提示信息。因此我们可以看到,被分割出来的段落,基本上都是语义完整的一段话。
那么分割逻辑到底是什么,可以支持保持语义完整性?

  • 尝试分割:首先,它尝试用 separator (我们设置的是"\n\n" 双换行,这通常代表段落之间)来分割文本。如果分割后的任何一个段落仍然大于chunk_size ,它会继续下一步。
  • 如果仍然有单个单词或字符串的长度超过了 100 ,分割器就陷入了两难境地:
    1. 选项A:强行把这个长字符串在任意位置(比如第100个字符处)截断。但这会破坏单词、URL或数据的完整性,导致生成无意义的片段(例如,把 “Christopher” 截断成 “Christop” 和"her"),严重影响后续的嵌入或语言模型处理效果。
    2. 选项B:保留这个完整的、超长的字符串作为一个块,并记录一条信息告知用户。
      那么很明显,这里它选择了B选项。

如果我们的大部分块都超长,可能是 chunk_size 设置得太小了。尝试适当增大它。

另外可以看到,相邻文档之间有一部分重叠的内容,这就是设置的块重叠大小的作用,为了保证语义的完整性,会适当的重叠文本。
在这里插入图片描述

2.2 基于Token长度拆分

我们知道,,LLM 大模型实际上并不是直接接收字符串,而是需要先做 token 切分编码。

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 文档加载器(MD)
# single模式,只拆分成一个Document文档
loader = UnstructuredMarkdownLoader(
    "../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md",
    mode="single",    # MD 加载器默认将文档加载为一个
)

# load()方法将本地文件加载成document列表
data = loader.load()

# tiktoken:分词器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",  # cl100k_base:是 tiktoken 分词器中的一种编码方式
    chunk_size=400,               # 块 token 大小(参考标准,为了保证段落/句子完整,会超出此设定的大小)
    chunk_overlap=50,             # 块重叠大小
)

# 分割文档--按照定义的文本分割器分割成文档列表
documents = text_splitter.split_documents(data)
for document in documents[:10]:
    print("*" * 120)
    print(document)

cl100k_basetiktoken 分词器中的一种编码方式。

import tiktoken

# 定于cl100k_base编码方式的分词器
enc = tiktoken.get_encoding("cl100k_base")

# 进行切分编码
enc_output = enc.encode("my name is LiHua!")

# 打印结果
print(f"编码后的token:{str(enc_output)}")
for token in enc_output:
    print(f"将token: {str(token)} 变成文本:{str(enc.decode_single_token_bytes(token))}")

输出结果:

编码后的token:[2465, 836, 374, 14851, 39, 4381, 0]
将token: 2465 变成文本:b'my'
将token: 836 变成文本:b' name'
将token: 374 变成文本:b' is'
将token: 14851 变成文本:b' Li'
将token: 39 变成文本:b'H'
将token: 4381 变成文本:b'ua'
将token: 0 变成文本:b'!'

可以看到采用切分编码cl100k_base ,拆解后的文本字符串为["my", "name", "is", "Li", "H", "ua", "!"] 。token 编码表示为[2465, 836, 374, 14851, 39, 4381,0]

2.3 硬性约束长度拆分

如果我们就想要求任何块都不能超过指定大小,可以使用RecursiveCharacterTextSplitter类 或 RecursiveCharacterTextSplitter.from_tiktoken_encoder 方法,它会严格遵守对块大小的硬约束。

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter

# 文档加载器(MD)
# single模式,只拆分成一个Document文档
loader = UnstructuredMarkdownLoader(
    "../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md",
    mode="single",    # MD 加载器默认将文档加载为一个
)

# load()方法将本地文件加载成document列表
data = loader.load()

# 强制按照约定的块大小分割文本
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", # cl100k_base 是tiktoken 分词器中的一种编码方式
    chunk_size=100,              # 块token大小(参考标准,为了保证段落/句子完整,会超出此设定的大小)
    chunk_overlap=0,            # 块重叠大小
)

# 分割文档--按照定义的文本分割器分割成文档列表
documents = text_splitter.split_documents(data)
for document in documents[:10]:
    print("*" * 120)
    print(document)

输出结果:

************************************************************************************************************************
page_content='1. 概念

输出解析器负责获取模型的输出,并将输出转换为更结构化的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='大型语言模型(LLM)的输出本质上是非结构化的文本。但在构建应用程序时,我们通常希望得到结构化的、机器可读的数据,这样可以将其转换为更适合下游任务的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='输出解析器的作用就是架起这座桥梁:它们将 LLM 的非结构化文本输出转换为结构化格式。

2. 与with_structured_output() 的区别

输出解析器和with_structured_output()都实现了相同的功能。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='输出解析器是LangChain中的一个功能组件,支持使用链的方式进行组建。

with_structured_output()是聊天模型中的一个方法,只能进行手动调用,返回一个新的、崛北了结构化输出能力的 Runnable 对象。

3. 解析文本输出--StrOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model(model="deepseek-chat", model_provider="deepseek")

chain = model | StrOutputParser()

print(chain.invoke("你是谁?"))

输出结果:' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='你好!我是DeepSeek,由深度求索公司创造的AI助手。很高兴认识你!😊' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='我是一个纯文本模型,可以帮你解答问题、处理文档、进行对话交流等。我支持阅读链接和上传多种格式的文件(如图片、PDF、Word、Excel等),从中提取文字信息进行分析。虽然我不支持多模态识别,但可以通过文件上传功能处理很多任务。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='我的特点是:
- **免费使用**,无需付费
- **上下文窗口超大**(1M),可以一次性处理像《三体》三部曲那么多的内容
- **支持联网搜索**(需要手动开启)
- **知识截止日期**20255' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='有什么我可以帮你的吗?无论是学习、工作还是日常问题,随时问我!💪

若是不使用输出解析器,而是直接得到聊天模型返回的 AIMessage,文本内容则需要从消息中的content 字段获取。

4. 解析结构化对象输出--PydanticOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='要输出结构化对象,需要用到的输出解析器是PydanticOutputParser,它是class langchain_core.output_parsers.pydantic.PydanticOutputParser类,参数:pydantic_object(表示的是要解析的pydantic模型)。

from typing import Optional' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}

此时,我们使用的递归的文本分割器,就可以定义分隔符列表。

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter

# 文档加载器(MD)
# single模式,只拆分成一个Document文档
loader = UnstructuredMarkdownLoader(
    "../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md",
    mode="single",    # MD 加载器默认将文档加载为一个
)

# load()方法将本地文件加载成document列表
data = loader.load()

text_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", " "],        # 分割符。一般来说,有一个默认的分割符优先级列表:["\n\n", "\n", " "]
    chunk_size=100,          # 块大小(参考标准,为了保证段落/句子完整,会超出此设定的大小)
    chunk_overlap=0,        # 块重叠大小
    length_function=len,     # 测量字符长度的函数
    is_separator_regex=False,# 是否正则表达式描写分隔符吗?
)

# 分割文档--按照定义的文本分割器分割成文档列表
documents = text_splitter.split_documents(data)
for document in documents[:10]:
    print("*" * 120)
    print(document)

输出结果:

************************************************************************************************************************
page_content='1. 概念

输出解析器负责获取模型的输出,并将输出转换为更结构化的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='大型语言模型(LLM)的输出本质上是非结构化的文本。但在构建应用程序时,我们通常希望得到结构化的、机器可读的数据,这样可以将其转换为更适合下游任务的格式。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='输出解析器的作用就是架起这座桥梁:它们将 LLM 的非结构化文本输出转换为结构化格式。

2. 与with_structured_output() 的区别' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='输出解析器和with_structured_output()都实现了相同的功能。

输出解析器是LangChain中的一个功能组件,支持使用链的方式进行组建。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='with_structured_output()是聊天模型中的一个方法,只能进行手动调用,返回一个新的、崛北了结构化输出能力的 Runnable 对象。' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='3. 解析文本输出--StrOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='from langchain.chat_models import init_chat_model' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='from langchain_core.output_parsers import StrOutputParser' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='model = init_chat_model(model="deepseek-chat", model_provider="deepseek")' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}
************************************************************************************************************************
page_content='chain = model | StrOutputParser()

print(chain.invoke("你是谁?"))

输出结果:' metadata={'source': '../Docs/markdown/【LangChain-AI】核心组件 -- 输出解析器.md'}

但这样,其实是剥夺了一些保证语义完整性的能力,可以看到某些含义相似的内容,被强制分开。除此之外,还要再说明的是,文本内容是中文时,使用默认分隔符列表 [“\n\n”, “\n”, “”, “”] 拆分文本可能会导致一个词组被拆分成两个字,导致语义失效。若要将词组放在一起,可以覆盖分隔符列表以包含其他标点符号,例如中文的逗号, 、句号。或其他中文符号。

这样在分割时将递归用 separators 来尝试分割文本:

  • 首先,它尝试用 “\n\n” (双换行,通常代表段落)来分割,如果分割后的任何一个段落仍然大于chunk_size ,它会继续下一步。
  • 接着,它尝试用 “\n” (单换行,通常代表行之间)来分割那些仍然过大的段落。
  • 然后,它尝试用 " " (空格,单词之间)来分割。

3. 特殊文档结构拆分

对于代码等特殊文本,可以尝试使用 Language 提供的不同的分割器(如PythonCodeTextSplitter 、HTMLHeaderTextSplitter 等)效果会更好,它会理解代码的语法结构。

比如:Python

from langchain_text_splitters import PythonCodeTextSplitter

# 字符串文档
PYTHON_CODE = """
def hello_world():
    print("Hello, World!")

def hello_python():
    print("Hello, Python!")
"""

# 分割器(python代码)
splitter = PythonCodeTextSplitter(
    chunk_size=50,
    chunk_overlap=0,
)

# 创建文档:遵守分割器规则
python_docs =  splitter.create_documents([PYTHON_CODE])

for document in python_docs:
    print("*" * 30)
    print(document)

输出结果:

******************************
page_content='def hello_world():
    print("Hello, World!")'
******************************
page_content='def hello_python():
    print("Hello, Python!")'
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐