现在我们已经初步获取到了辩论相关的文档数据集,但是面对庞大的数据集和过多的文档内容,我们就需要先对其进行分块。
分割文档,主要是因为模型处理长文本的能力有限——直接塞入大段内容很有可能会截断关键信息,而分割成小块后,既能适配模型的上下文窗口,又能提高检索效率。小块文本信息更集中,检索时更精准,避免返回无关内容;同时,合理分割(比如按段落或章节)能保留局部语义连贯性,让生成的答案更准确。此外,分割后的小文本计算和存储成本更低,适合大规模应用。
由于本项目获取到的文档都是json格式,所以本文就以json文档为例探讨文档的加载与分割。
上图中是本次需要进行分块的文档数据集,本文我们采用LangChain工具来实现。
首先简单介绍一下LangChain:
LangChain 是一个开源的 Python Al应用开发框架,它提供了构建基于大模型的 AI 应用所需的模块和工具。通过 LangChain,开发者可以轻松地与大语言模型 (LLM)集成,完成文本生成、问答、翻译、对话等任务。LangChain 降低了 AI 应用开发的门槛,让任何人都可以基于 LLM 来构建属于自己的创意应用。
现在我们就用 LangChain 工具来帮助我们进行文本分块。
安装 LangChain
可以直接通过 pip 进行安装:
pip install langchain
注意如果使用了虚拟环境(如 venv/conda),一定要确保已激活环境后再安装:
# 激活虚拟环境示例(以 venv 为例)
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
加载 Json 文件
with open("data.json", "r") as f: # 注意把data.json换成自己的json文件路径
data = json.load(f) # json文档加载
尝试运行若出现以下 UnicodeDecodeError 报错:
说明 JSON 文件的编码格式与 Python 默认的 gbk 解码器不兼容,Windows 系统下 Python 默认用 gbk 编码打开文件,但 JSON 文件可能实际是 utf-8 编码(尤其是包含中文等非 ASCII 字符时)。我们可以在打开文件时显式地指定编码格式为 utf-8:
with open("data.json", "r", encoding="utf-8") as f: # 添加 encoding 参数
data = json.load(f)
这样可以成功解决上述报错!
提取并分割文档内容(携带元数据)
这里我们使用 LangChain 的递归字符文本分割器,将长文本按规则切割成小片段,常用于处理大模型输入限制。以下代码是文本分割器 splitter 的定义:
splitter = RecursiveCharacterTextSplitter(
chunk_size = 300,
chunk_overlap = 50,
separators = ["\n", "。", "!", "?"]
)
解释一下上述代码中具体参数的作用:
- chunk_size 表示每个文本片段的最大长度(如 300 字符或 token,具体单位取决于配置)
- chunk_overlap 表示相邻片段之间的重叠字符数,用于保持上下文连贯性,防止关键信息被切断
- separators 定义了文本分割的标志,比如上述代码就表示优先按换行符、句号、感叹号、问号分割文本,逐级尝试直到满足 chunk_size
具体的工作逻辑就是:先用 “\n” 分割文本,若分割后的文本片段仍大于 chunk_size,则换用 “。” 进行分割,依此类推,最终确保每个片段不超过 chunk_size,且相邻片段有 50 单位重叠。
接下来正式进行文本分割:
chunks = [] # 存储分割后的文本块
metadata = [] # 存储每个文本块的元数据信息
# 遍历每个发言记录
# for speech in data["debate"]: (错误写法)
for entry in data:
for speech in entry["debate"]:
# 提取基础元数据
base_meta = {
# "competition": data["competition"], (错误写法)
"competition": entry["competition"], # 赛事名称
# "topic": data["topic"], (错误写法)
"topic": entry["topic"], # 辩论主题
"stance": speech["stance"], # 辩手立场
"debater": speech["debater"] # 辩手姓名
}
# 分割长文本,将辩手的发言内容 utterance 分割成多个小文本块
split_texts = splitter.split_text(speech["utterance"])
# 为每个分割块创建记录
for i, text in enumerate(split_texts):
chunks.append(text) # 将文本块存入列表
metadata.append({
**base_meta, # 继承基础元数据
"chunk_id": f"{speech['debater']}_{i}", # 生成唯一块标识(辩手名_序号)
"word_count": len(text) # 统计当前块的字符数
})
❗尤其注意上述代码中的错误写法,这里涉及到“列表”和“字典”的区别:
data = [
{ # 第一个元素是字典
"competition": "辩论赛A",
"topic": "主题A",
"debate": [发言记录1, 发言记录2...]
},
{ # 第二个元素是字典
"competition": "辩论赛B",
"topic": "主题B",
"debate": [发言记录1, 发言记录2...]
}
]
那么很显然在上述结构中,data是一个“列表”,“列表”中包含着多个“字典”,每个“字典”代表一个辩论场次。代码中的错误写法 data["debate"] 就试图用字符串来访问列表元素,就会报错。因此我们需要先遍历data中的每一个字典entry,然后用 entry["debate"] 就能访问到辩论场次字典中的元素了。同理在提取基础元数据时也要用 entry["competition"] 而不是 data["competition"] 。speech也一样,它用来遍历 entry["debate"] 这个列表中的每一个字典speech,然后用 speech["stance"] 访问到辩手立场元素。
分割长文本直接调用前面定义的 splitter 分割器的 split_text() 方法,最后将每个分割块的文本内容和元数据信息存储起来即可。
查看结果示例
现在chunks里就已经存好了所有分割完毕的文本段落,我们以第一块为例进行查看:
print(f"总分割块数: {len(chunks)}")
print("\n示例块内容:")
print(chunks[0][:200] + "...") # 显示第一块的前200个字符
print("\n对应元数据:")
print(metadata[0])
运行结果如下图所示:
至此我们就完成了文档的加载与分割!