在 LangChain 中,除了常用的文档分割器,还设计了一些适用于特定场景的分割器,涵盖了以下类型:基于 HTML 标题/段的分割器、Markdown 标题分割器、递归 JSON 分割器、基于 Token 计数的分割器等。这些分割器的使用方式与字符文本分割器非常接近,主要针对特定格式的文档进行优化处理。
2.1 HTML/Markdown 标题与段分割器
LangChain 提供了针对 HTML 类型文档的分割器——HTMLHeaderTextSplitter
和 HTMLSectionSplitter
,它们的作用如下:
HTMLHeaderTextSplitter
- 按照 HTML 文档中的元素级别进行分割。
- 在分割时会查找每一块文本的内容以及所有关联的标题。
- 为每个相关的标题块提供元数据,顺序逐层向上查找,直到找到所有嵌套层级的标题。
- 这种分割器适合提取文本及其完整的标题层级结构。
HTMLSectionSplitter
- 按照 HTML 文档中的元素级别进行分割。
- 在分割时会查找每一块文本的内容及其最近的副标题。
- 查找规则是顺序逐层向上查找,一旦找到最近的副标题就停止。
- 这种分割器适合提取文本及其局部的副标题信息。
层级关系说明
理解这些分割器的工作原理非常简单,层级关系并不是 HTML 元素的嵌套结构,而是类似目录导航的层级关系。例如:
- 在文档的侧边栏或导航栏中,标题可能分为一级标题、二级标题和三级标题。
- 某段内容所在的目录层级决定了它被认为属于哪个标题下,而并不依赖 HTML 的实际嵌套结构。
例如:
这意味着这段内容在分割后,会被归类到与该标题相关的层级下。
更多详细文档分割器的使用说明,可以参考以下链接:LangChain 文档分割器翻译文档。
资料推荐
示例代码:
from langchain_text_splitters import HTMLHeaderTextSplitter
html_string = """
<!DOCTYPE html>
<html>
<body>
<div>
<h1>标题1</h1>
<p>关于标题1的一些介绍文本。</p>
<div>
<h2>子标题1</h2>
<p>关于子标题1的一些介绍文本。</p>
<h3>子子标题1</h3>
<p>关于子子标题1的一些文本。</p>
<h3>子子标题2</h3>
<p>关于子子标题2的一些文本。</p>
</div>
<div>
<h3>子标题2</h2>
<p>关于子标题2的一些文本。</p>
</div>
<br>
<p>关于标题1的一些结束文本。</p>
</div>
</body>
</html>
"""
headers_to_split_on = [
("h1", "一级标题"),
("h2", "二级标题"),
("h3", "三级标题"),
]
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text(html_string)
print(html_header_splits)
输出内容:
[
Document(page_content='标题1'),
Document(metadata={'一级标题': '标题1'}, page_content='关于标题1的一些介绍文本。 \n子标题1 子子标题1 子子标题2'),
Document(metadata={'一级标题': '标题1', '二级标题': '子标题1'}, page_content='关于子标题1的一些介绍文本。'),
Document(metadata={'一级标题': '标题1', '二级标题': '子标题1', '三级标题': '子子标题1'}, page_content='关于子子标题1的一些文本。'),
Document(metadata={'一级标题': '标题1', '二级标题': '子标题1', '三级标题': '子子标题2'}, page_content='关于子子标题2的一些文本。'),
Document(metadata={'一级标题': '标题1'}, page_content='子标题2'),
Document(metadata={'一级标题': '标题1', '三级标题': '子标题2'}, page_content='关于子标题2的一些文本。'),
Document(metadata={'一级标题': '标题1'}, page_content='关于标题1的一些结束文本。')
]
另外在 LangChain
中除了 HTML
类型的文档可以使用这套分割规则,Markdown 类的文件也有类似的分割规则,可以使用 Markdown 标题分割器—— MarkdownHeaderTextSplitter
完成同样的文档分割。
详细文档:https://imooc-langchain.shortvar.com/docs/how_to/markdown_header_metadata_splitter/
2.2 递归 JSON 分割器
对于 JSON
类的数据,在 LangChain
中也封装了一个递归 JSON
分割器——RecursiveJsonSplitter
,这个分割器会按照深度优先的方式遍历 JSON
数据,并构建较小的 JSON
块,而且尽可能保持嵌套 JSON
对象完整,但如果需要保持文档块大小在最小块大小和最大块大小之间,则会将它们拆分。
在 JSON
数据中,如果值不是嵌套的 JSON
,而是一个非常大的字符,则不会对该字符串进行拆分,可以配合 递归字符文本分割器 强制性拆分字符串,确保块大小在限制的范围内。
RecursiveJsonSplitter
的参数非常简单,只需传递 max_chunk_size
和 min_chunk_size
(可选) 即可。
资料推荐
例如这里有一个很大的 json
文本(https://api.smith.langchain.com/openapi.json)
,使用递归 JSON
分割器对其进行拆分,
示例代码:
import requests
from langchain_text_splitters import RecursiveJsonSplitter
# 1.获取并加载json
url = "https://api.smith.langchain.com/openapi.json"
json_data = requests.get(url).json()
# 2.递归JSON分割器
text_splitter = RecursiveJsonSplitter(max_chunk_size=300)
# 3.分割json数据并创建文档
json_chunks = text_splitter.split_json(json_data=json_data)
chunks = text_splitter.create_documents(json_chunks)
for chunk in chunks[:3]:
print(chunk)
输出内容:
page_content='{"openapi": "3.1.0", "info": {"title": "LangSmith", "version": "0.1.0"}, "paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session."}}}}'
page_content='{"paths": {"/api/v1/sessions/{session_id}": {"get": {"operationId": "read_tracer_session_api_v1_sessions__session_id__get", "security": [{"API Key": []}, {"Tenant ID": []}, {"Bearer Auth": []}]}}}}'
page_content='{"paths": {"/api/v1/sessions/{session_id}": {"get": {"parameters": [{"name": "session_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Session Id"}}, {"name": "include_stats", "in": "query", "required": false, "schema": {"type": "boolean", "default": false, "title": "Include Stats"}}, {"name": "accept", "in": "header", "required": false, "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Accept"}}]}}}}'
RecursiveJsonSplitter
分割器的运行流程其实也非常简单,这个分割器会按照 深度优先 的方式遍历整个 JSON
,即一层一层往下读取数据,然后将对应的数据提取生成一个新的 JSON
,直到数据大小接近块大小(极端情况下还是会超过预设的块大小,例如 JSON
数据中的 Key
很长,亦或者 Value 很长,甚至出现单条数据就超过了预设大小)。
所以如果要使用该分割器,一般会结合 RecursiveCharacterTextSplitter
降低单条数据超过预设大小的风险,思路就是将递归 JSON
分割器生成的文档列表进行二次分割。
2.3 基于标记的分割器
对于大语言模型来说,上下文的长度计算应该通过 token 进行计算,而不是通过字符长度 len()
函数,在 OpenAI
的 GPT
模型中,一个汉字大约等于 1.5
个 Token
,一个单词为 1
个 Token
,所以使用 len()
函数可能会导致很大的误差。
资料推荐
在开发中,不同的模型对于 Token
的计算并不相同,但是可以使用 tiktoken
这个包来大致计算文本的 token
数,误差也相对较小,首先安装 tiktoken
包,
pip install -U tiktoken
接下来定义一个基于 tiktoken
的长度计算函数,如下:
def calculate_token_count(query: str) -> int:
"""计算传入文本的token数"""
encoding = tiktoken.encoding_for_model("text-embedding-3-large")
return len(encoding.encode(query))
然后将该函数传递给分割器的 length_function
,代码如下:
import tiktoken
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1.定义加载器和文本分割器
loader = UnstructuredFileLoader("./科幻短篇.txt")
text_splitter = RecursiveCharacterTextSplitter(
separators=[
"\n\n",
"\n",
"。|!|?",
"\.\s|\!\s|\?\s", # 英文标点符号后面通常需要加空格
";|;\s",
",|,\s",
" ",
""
],
is_separator_regex=True,
chunk_size=500,
chunk_overlap=50,
length_function=calculate_token_count,
)
# 2.加载文档并执行分割
documents = loader.load()
chunks = text_splitter.split_documents(documents)
# 3.循环打印分块内容
for chunk in chunks:
print(f"块大小: {len(chunk.page_content)}, 元数据: {chunk.metadata}")
输出内容:
块大小: 334, 元数据: {'source': './科幻短篇.txt'}
块大小: 409, 元数据: {'source': './科幻短篇.txt'}
块大小: 372, 元数据: {'source': './科幻短篇.txt'}
块大小: 95, 元数据: {'source': './科幻短篇.txt'}
在 LangChain
中,除了传递 length_function
方法,还可以直接调用分割器的类方法 from_tiktoken_encoder()
来快速创建基于 tiktoken
分词器的文本分割器,例如:
RecursiveCharacterTextSplitter.from_tiktoken_encoder(
model_name="gpt-4",
chunk_size=500,
chunk_overlap=50,
separators=[
"\n\n",
"\n",
"。|!|?",
"\.\s|\!\s|\?\s", # 英文标点符号后面通常需要加空格
";|;\s",
",|,\s",
" ",
""
],
is_separator_regex=True,
)
资料推荐