本次学习围绕LangChain中的Splitter(文本分割器)展开,结合文档中提供的知识点、代码案例以及RAG相关流程,系统梳理Splitter的核心概念、分类、工作原理、参数配置及实际应用场景,同时补充相关延伸知识,帮助全面理解文本分割在大语言模型应用中的重要性。笔记将从基础认知、核心知识点、代码实践、常见问题及延伸拓展五个部分展开,确保内容详实、逻辑清晰,贴合学习需求,总字数达到4000字以上。
一、前言:文本分割的核心意义
在大语言模型(LLM)的实际应用中,我们经常会遇到处理大文档的场景------无论是本地的文本文件、日志数据,还是网络上的长文档、PDF文件,其内容长度往往会超过大语言模型的上下文窗口限制(例如GPT-4的上下文窗口虽大,但面对数百MB甚至GB级别的文档,仍无法直接处理)。此时,就需要通过文本分割器(Splitter)将大文档拆解为多个符合模型处理要求的小片段(Chunk),既保证模型能够正常处理,又尽可能保留文本的语义完整性,为后续的检索增强生成(RAG)、文档问答等任务奠定基础。
文档中明确提到,Splitter的核心目标是"切割文本、保持语义、适配模型",这也是本次学习的核心主线。无论是基础的按字符分割,还是更智能的按语义分割,本质上都是在"切割效率"与"语义完整性"之间寻找平衡,而不同类型的Splitter针对不同场景有着各自的优势与适用范围。本次学习将重点围绕LangChain生态中的各类Splitter,结合具体代码案例,深入理解其工作机制与实践技巧。
二、基础认知:Splitter核心概念与前置知识
2.1 核心术语解析
在学习Splitter之前,需先明确几个核心术语,避免后续理解偏差,结合文档内容整理如下:
- Loader(加载器) :用于加载大文档的工具,文档中提到"loader加载的大Document来自@langchain/community",需要注意的是,loader加载的文档类型为文本类(如txt、md等),而mp3、mp4等非文本类型无法直接通过常规Loader加载,也不适合用Splitter进行分割(后续会详细说明)。文档中特别指出"paf doc 不是一个类型",推测是笔误,实际应为"特定格式的doc文件可能不属于Loader支持的标准类型",需注意Loader的文档类型兼容性。
- Splitter(文本分割器) :核心工具,用于将大文本Document分割为多个小的Chunk,核心要求是"符合语义、适配模型上下文窗口"。其工作流程本质上是"确定分割规则→按规则切割→调整Chunk大小→处理Chunk重叠",不同类型的Splitter核心差异在于"分割规则"的不同。
- Chunk(文本片段) :分割后的最小文本单元,每个Chunk的大小需根据模型的上下文窗口进行调整,通常用"字符长度(character length)"或"Token长度(token length)"来衡量。
- Token(令牌) :大语言模型处理文本的最小单位,不同于字符------字符是文本的基础符号(如中文的"我"、英文的"a"),而Token是模型对文本进行编码后的最小单元,通常1个英文单词约对应1个Token,1个中文字符约对应1.3个Token(不同编码方式略有差异)。文档中通过js-tiktoken库计算Token数量,这也是实际应用中常用的Token统计方式。
- Overlap(重叠) :为了避免分割过程中破坏文本的语义连贯性,相邻两个Chunk之间会保留一定的重叠内容,文档中提到"overlap牺牲一定的空间(chunk_size 10%)重复",即重叠部分的长度通常为Chunk大小的10%左右,具体可根据需求调整。
- RAG(检索增强生成) :一种结合检索与生成的大模型应用架构,核心流程包括"加载文档→分割文档→建立向量索引→检索相关Chunk→生成回答",其中Splitter是RAG流程中的关键环节,直接影响检索的准确性和生成的相关性。
2.2 前置依赖与环境说明
文档中提供的代码基于JavaScript/TypeScript环境,依赖LangChain生态及相关工具库,需提前安装配置,核心依赖如下:
- @langchain/core:LangChain的核心库,提供Document等基础类,用于定义文档结构。
- @langchain/textsplitters:LangChain的文本分割器库,包含各类Splitter的实现(如RecursiveCharacterTextSplitter、TokenTextSplitter等)。
- @langchain/community:LangChain的社区库,提供各类Loader等工具(文档中提到的大Document加载依赖此库)。
- js-tiktoken:OpenAI推出的Token编码/解码库,用于计算文本的Token数量,文档中多次使用此库统计Chunk的Token长度。
- dotenv:用于加载环境变量,方便配置模型密钥、编码方式等参数(文档中虽未直接使用环境变量,但实际项目中常用此工具管理敏感信息)。
安装命令(npm):
bash
npm install @langchain/core @langchain/textsplitters @langchain/community js-tiktoken dotenv
环境配置说明:确保Node.js版本在16.x及以上,避免因版本过低导致依赖库无法正常运行;若使用TypeScript,需配置tsconfig.json,确保支持ES模块导入(如"module": "ESNext")。
三、核心知识点:Splitter的分类与工作原理
文档中重点介绍了LangChain中的三类核心Splitter:CharacterTextSplitter(字符文本分割器)、RecursiveCharacterTextSplitter(递归字符文本分割器)、TokenTextSplitter(Token文本分割器),同时梳理了Splitter的面向对象体系,明确了各类Splitter的继承关系与适用场景。以下将逐一详细解析,结合文档内容补充细节与延伸说明。
3.1 Splitter的面向对象体系与继承关系
文档中明确提到Splitter的面向对象体系,核心父类为TextSplitter,各类具体的Splitter均为其子类,且父类与子类的职责划分清晰,具体体系如下:
- 父类:TextSplitter:所有文本分割器的基类,核心职责是"定义分割文本的抽象方法",其切割的对象仅为文本类型(如字符串、文本文件),对于mp3、mp4等非文本类型(音频、视频),TextSplitter及其子类均不适用------因为这类非文本类型需要先进行转写(如音频转文字、视频抽帧转文字),再进行分割。
- 子类1:CharacterTextSplitter:最基础的文本分割器,核心逻辑是"按指定的字符分隔符切割文本",分隔符可自定义(如中文的。、!、?、,,英文的.、!、?、,等),分割逻辑简单直接,效率高,但对语义完整性的保障较弱。
- 子类2:TokenTextSplitter:按Token数量切割文本,核心逻辑是"根据指定的Token长度(chunkSize),将文本切割为多个Token数量不超过chunkSize的Chunk",适用于对Token数量有严格要求的场景(如适配模型的上下文窗口Token限制)。
- 子类3:RecursiveTextSplitter(递归文本分割器) :文档中重点强调的分割器,语义完整性最好,其核心子类包括RecursiveCharacterTextSplitter、MarkdownTextSplitter等。其中,RecursiveCharacterTextSplitter是实际应用中最常用的分割器,而MarkdownTextSplitter之所以属于RecursiveTextSplitter子类,核心原因是其支持按Markdown的标题层级(#、##、###等)进行递归分割,确保Markdown文档的结构完整性(如一个二级标题下的内容不会被分割到不同的Chunk中)。
补充说明:RecursiveTextSplitter的"递归"核心逻辑的是"从优先级高的分隔符开始尝试分割,若分割后的Chunk仍超过指定大小,则使用优先级更低的分隔符继续分割,直至Chunk大小符合要求",这种递归分割方式能够最大程度保留文本的语义连贯性,也是其优于其他分割器的核心原因。
3.2 各类Splitter详细解析
3.2.1 CharacterTextSplitter(字符文本分割器)
文档中对CharacterTextSplitter的描述较为简洁:"直接按Character separator切割",结合LangChain官方文档及实际应用,补充其核心细节:
核心原理
CharacterTextSplitter的核心逻辑是"遍历文本,遇到指定的字符分隔符(separator)就进行切割",分割后的Chunk长度由分隔符的分布决定,若未指定分隔符,默认使用换行符(\n)作为分隔符。其优点是分割速度快、逻辑简单,适用于文本结构简单、分隔符分布均匀的场景;缺点是无法保证语义完整性------例如,若分隔符出现在一个完整句子的中间,会导致句子被切割成两个Chunk,破坏语义连贯性。
核心参数(文档未详细提及,补充官方核心参数)
- separators:字符分隔符数组,指定切割文本的字符,例如['。','!','?',','],优先级按数组顺序排列(数组第一个元素优先级最高),文档中明确提到"优先级。最优先",即先按"。"切割,若切割后的Chunk仍过大,再按后续分隔符切割(此处需注意:CharacterTextSplitter本身不支持递归切割,若需递归切割,需使用RecursiveCharacterTextSplitter)。
- chunkSize:每个Chunk的最大字符长度,若分割后的Chunk超过此长度,会继续按分隔符切割(仅当分隔符存在时),若不存在更多分隔符,则会强制切割(可能破坏语义)。
- chunkOverlap:相邻Chunk之间的重叠字符长度,用于弥补切割过程中可能出现的语义断裂,文档中提到"overlap牺牲一定的空间(chunk_size 10%)重复",即建议设置为chunkSize的10%左右(例如chunkSize=1000,chunkOverlap=100)。
适用场景与局限性
适用场景:文本结构简单、分隔符分布均匀,且对语义完整性要求不高的场景,例如日志文件(每一行有明确的分隔符)、简单的纯文本文件等。
局限性:无法处理语义连贯的长文本(如没有分隔符的长段落),强制切割会破坏语义;不适用于Markdown、HTML等有结构化格式的文本。
3.2.2 RecursiveCharacterTextSplitter(递归字符文本分割器)
文档中对RecursiveCharacterTextSplitter的描述最为详细,称其"更人性化,更努力",核心优势是"语义的完整性特别好",是实际应用中最常用的分割器。结合文档内容与官方文档,详细解析如下:
核心原理
RecursiveCharacterTextSplitter的核心逻辑是"递归切割",具体流程如下(结合文档中提到的"先character 切 再 chunkSize 最后 Overlap"):
- 确定分隔符优先级:用户指定的separators数组按顺序排列,优先级从高到低(例如文档中的['。','!','?',','],"。"优先级最高,","优先级最低)。
- 按优先级递归切割:首先使用最高优先级的分隔符切割文本,得到多个初步Chunk;若某个Chunk的长度(字符长度)超过chunkSize,则使用次高优先级的分隔符对该Chunk进行再次切割;重复此过程,直至所有Chunk的长度均不超过chunkSize。
- 处理Chunk重叠:切割完成后,为相邻的Chunk添加重叠内容(chunkOverlap),弥补递归切割过程中可能出现的语义断裂------文档中提到"尝试其他符号时,语义就弱下来了,overlap来弥补一下",即当使用低优先级分隔符(如",")切割时,语义连贯性会下降,通过重叠部分可以让模型更好地理解上下文。
补充说明:RecursiveCharacterTextSplitter的"递归"本质上是"从大到小、从语义完整到语义细碎"的切割过程,优先保留完整的句子(按"。"切割),若句子过长,再分割为短语(按","切割),最大程度保证语义完整性。
核心参数(结合文档代码与官方说明)
- separators:字符分隔符数组,优先级从高到低,文档中使用的是["\n", "。", ","],即先按换行符切割(适用于日志、多行文本),再按"。"切割,最后按","切割,可根据文本类型自定义(如英文文本可使用["\n", ".", "!", "?", ","])。
- chunkSize:每个Chunk的最大字符长度,文档中代码示例设置为200,即每个Chunk的字符长度不超过200,实际应用中需根据模型的上下文窗口调整(例如GPT-4的上下文窗口为128k Token,可对应设置较大的chunkSize,但需注意Token长度与字符长度的换算)。
- chunkOverlap:相邻Chunk之间的重叠字符长度,文档中代码示例设置为20,即重叠20个字符,约为chunkSize的10%,符合文档中提到的"overlap牺牲一定的空间(chunk_size 10%)重复"的建议。
适用场景与优势
适用场景:几乎所有文本类型,尤其是语义连贯的长文本、结构化文本(如Markdown、HTML)、日志文件等,是RAG流程中最常用的分割器。文档中的日志分割案例就使用了RecursiveCharacterTextSplitter,能够很好地保留每一条日志的完整性,同时适配chunkSize的要求。
优势:
- 语义完整性好:通过递归切割和分隔符优先级,最大程度保留完整的句子、段落语义,避免强制切割破坏语义。
- 灵活性高:可自定义分隔符和优先级,适配不同类型的文本(中文、英文、Markdown等)。
- 兼容性强:结合overlap参数,弥补低优先级分隔符切割带来的语义断裂问题,提升后续检索和生成的准确性。
3.2.3 TokenTextSplitter(Token文本分割器)
文档中对TokenTextSplitter的描述较少,但提供了完整的代码示例,结合官方文档,详细解析其核心逻辑与应用场景:
核心原理
TokenTextSplitter的核心逻辑是"按Token数量切割文本",与CharacterTextSplitter按字符长度切割不同,其直接根据文本的Token数量来划分Chunk,确保每个Chunk的Token数量不超过指定的chunkSize(模型的上下文窗口限制)。其工作流程如下:
- 指定编码方式(encodingName):通过js-tiktoken库的编码方式(如cl100k_base,OpenAI模型常用编码),将文本转换为Token序列。
- 按Token数量切割:根据chunkSize,将Token序列分割为多个子序列,每个子序列的Token数量不超过chunkSize。
- 处理Chunk重叠:通过chunkOverlap参数,让相邻Chunk的Token序列保留一定的重叠部分,确保语义连贯性。
- 将Token序列转换回文本:切割完成后,将每个子Token序列解码为文本,得到最终的Chunk。
文档中提到"不同语言 字符语义一样,但长度不一样,token 按语义(算力)来计算开销",这也是TokenTextSplitter的核心优势------无论文本是中文还是英文,Token数量能够直接反映模型处理该文本的算力开销,因此按Token切割能够更精准地适配模型的上下文窗口,避免因字符长度与Token长度换算偏差导致的Chunk超出模型限制。
核心参数(结合文档代码)
- chunkSize:每个Chunk的最大Token数量,文档中代码示例设置为50,即每个Chunk的Token数量不超过50,实际应用中需根据模型的上下文窗口调整(如GPT-3.5-turbo的上下文窗口为4k Token,可设置chunkSize=3500左右,预留部分Token用于用户问题和生成回答)。
- chunkOverlap:相邻Chunk之间的重叠Token数量,文档中代码示例设置为10,即重叠10个Token,用于弥补切割过程中的语义断裂。
- encodingName:Token编码方式,文档中使用的是'cl100k_base',这是OpenAI所有模型(如GPT-3.5-turbo、GPT-4)使用的编码方式,其他编码方式(如p50k_base)适用于旧版模型,需根据实际使用的模型选择。
适用场景与优势
适用场景:对Token数量有严格要求的场景,例如使用OpenAI、Anthropic等模型时,需要精准控制Chunk的Token数量,避免超出模型上下文窗口限制;适用于多语言文本(中文、英文、日文等),因为Token数量能够统一衡量文本的算力开销。
优势:
- 精准适配模型:直接按Token数量切割,避免字符长度与Token长度换算偏差导致的Chunk超出模型限制,降低模型调用失败的风险。
- 多语言兼容:不受语言类型影响,Token数量能够统一反映文本的处理开销,适用于多语言文档分割。
- 简单易用:无需自定义分隔符,只需指定chunkSize、chunkOverlap和encodingName,即可完成分割,适合对分隔符不熟悉的场景。
局限性:分割逻辑基于Token数量,可能会破坏句子的语义完整性(例如,一个完整的句子可能因为Token数量超标而被切割成两个Chunk),因此通常需要配合较大的chunkOverlap来弥补语义断裂。
3.2.4 三类Splitter对比总结
为了更清晰地掌握各类Splitter的差异,结合文档内容和实际应用,整理对比表格如下:
| Splitter类型 | 核心切割依据 | 语义完整性 | 优势 | 局限性 | 适用场景 |
|---|---|---|---|---|---|
| CharacterTextSplitter | 字符分隔符 | 较差 | 速度快、逻辑简单、配置灵活 | 可能破坏语义,不适用于长文本 | 简单纯文本、分隔符均匀的日志 |
| RecursiveCharacterTextSplitter | 递归字符分隔符(优先级) | 最好 | 语义完整、灵活性高、兼容性强 | 分割速度略慢于CharacterTextSplitter | 长文本、结构化文本(Markdown)、RAG核心场景 |
| TokenTextSplitter | Token数量 | 中等 | 精准适配模型、多语言兼容 | 可能破坏语义,需配合overlap | 多语言文本、对Token数量有严格要求的场景 |
补充说明:实际应用中,RecursiveCharacterTextSplitter是首选,因为其能够在"切割效率"与"语义完整性"之间达到最佳平衡;若需精准控制Token数量,可结合TokenTextSplitter使用(例如,先用RecursiveCharacterTextSplitter分割为语义完整的Chunk,再用TokenTextSplitter调整Chunk的Token数量)。
四、代码实践:Splitter实际应用案例解析
文档中提供了两个完整的代码案例:RecursiveCharacterTextSplitter分割日志文档、TokenTextSplitter分割日志文档,同时包含了Token计算的相关代码。以下将逐一对代码进行详细解析,说明代码功能、参数配置、运行结果及注意事项,帮助掌握Splitter的实际应用技巧。
4.1 案例一:RecursiveCharacterTextSplitter分割日志文档
该案例的核心目标是使用RecursiveCharacterTextSplitter分割包含多条日志的文档,同时统计每个Chunk的字符长度和Token长度,验证分割效果。代码如下(结合文档代码,补充注释说明):
javascript
// 导入依赖库
import "dotenv/config"; // 加载环境变量(实际项目中用于管理敏感信息)
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters"; // 导入递归字符分割器
import { Document } from "@langchain/core/documents"; // 导入Document类,用于定义文档
import { getEncodingNameForModel, getEncoding } from 'js-tiktoken'; // 导入Token编码相关工具
// 1. 定义日志文档
// 创建Document实例,pageContent为日志内容,可包含多条日志(含长日志)
const logDocument = new Document({
pageContent: `[2024-01-15 10:00:00] INFO: Application started
[2024-01-15 10:00:05] DEBUG: Loading configuration file
[2024-01-15 10:00:10] INFO: Database connection established
[2024-01-15 10:00:15] WARNING: Rate limit approaching
[2024-01-15 10:00:20] ERROR: Failed to process request
[2024-01-15 10:00:25] INFO: Retrying operation
[2024-01-15 10:00:30] SUCCESS: Operation completed
[2026-01-10 14:30:00] INFO: 系统开始执行大规模数据迁移任务,本次迁移涉及核心业务数据库中的用户表、订单表、商品库存表、物流信息表、支付记录表、评论数据表等共计十二个关键业务表,预计处理数据量约500万条记录,数据总大小预估为280GB,迁移过程将采用分批次增量更新策略以减少对生产环境的影响,同时启用双写机制确保数据一致性,任务预计总耗时约3小时15分钟,迁移完成后将自动触发全面的数据一致性校验流程以及性能基准测试,请相关运维人员和DBA团队密切关注系统资源使用情况、网络带宽占用率以及任务执行进度,如遇异常情况请立即启动应急预案并通知技术负责人
`
});
// 2. 初始化RecursiveCharacterTextSplitter
const logSplitter = new RecursiveCharacterTextSplitter({
separators: ["\n", "。", ","], // 分隔符优先级:换行符 > 。 > ,
chunkSize: 200, // 每个Chunk最大字符长度为200
chunkOverlap: 20, // 相邻Chunk重叠20个字符
});
// 3. 分割文档
// splitDocuments方法接收Document数组,返回分割后的Chunk数组(Promise类型,需用await)
const logChunks = await logSplitter.splitDocuments([logDocument]);
// 4. 打印分割结果
console.log("分割后的Chunk数量:", logChunks.length);
console.log("分割后的Chunk详情:", logChunks);
// 5. 统计每个Chunk的字符长度和Token长度
// 初始化Token编码(使用cl100k_base编码,适配OpenAI模型)
const enc = getEncoding("cl100k_base");
logChunks.forEach((doc, index) => {
console.log(`\nChunk ${index + 1}:`);
console.log("Chunk内容:", doc.pageContent);
console.log("字符长度(character length):", doc.pageContent.length);
console.log("Token长度(token length):", enc.encode(doc.pageContent).length);
});
代码解析
- 依赖导入:导入了RecursiveCharacterTextSplitter用于分割文本,Document用于定义日志文档,js-tiktoken相关工具用于计算Token数量,dotenv用于加载环境变量(实际项目中可用于配置编码方式、模型密钥等)。
- 文档定义:创建了一个Document实例,pageContent包含8条日志,其中最后一条日志为长文本(包含大量描述信息),用于测试RecursiveCharacterTextSplitter的递归分割能力。
- 分割器初始化:设置separators为["\n", "。", ","],优先按换行符切割(每条日志一行),若某条日志的字符长度超过200(如最后一条长日志),则按"。"切割,若仍超过,则按","切割;chunkSize=200,确保每个Chunk的字符长度不超过200;chunkOverlap=20,相邻Chunk重叠20个字符。
- 分割与统计:splitDocuments方法返回分割后的Chunk数组,通过forEach遍历每个Chunk,打印其内容、字符长度和Token长度,验证分割效果是否符合预期。
运行结果分析(预测)
结合代码配置,运行后会得到以下结果:
- 前7条日志(短日志):每条日志的字符长度均小于200,因此按换行符切割后,每条日志为一个独立的Chunk,共7个Chunk,每个Chunk的字符长度约为30-50,Token长度约为20-30。
- 第8条长日志:字符长度远超200,因此会按"。"和","进行递归切割,得到多个Chunk,每个Chunk的字符长度不超过200,相邻Chunk之间重叠20个字符,确保语义连贯。
- Token长度统计:每个Chunk的Token长度会略小于字符长度(中文1个字符约对应1.3个Token),例如一个200字符的Chunk,Token长度约为150-170,符合模型的上下文窗口要求。
注意事项
- splitDocuments方法返回的是Promise类型,因此必须在async函数中使用await调用,否则会返回Promise对象,无法获取分割后的Chunk。
- separators的优先级设置需根据文本类型调整,例如日志文本优先按换行符切割,中文长文本优先按"。"切割,英文文本优先按"."切割。
- chunkSize和chunkOverlap的设置需结合模型的上下文窗口,避免Chunk的Token长度超出模型限制(例如,若模型上下文窗口为4k Token,可将chunkSize设置为3000左右,chunkOverlap设置为300)。
4.2 案例二:TokenTextSplitter分割日志文档
该案例的核心目标是使用TokenTextSplitter按Token数量分割日志文档,统计每个Chunk的字符长度和Token长度,验证按Token分割的效果。代码如下(结合文档代码,补充注释说明):
javascript
// 导入依赖库
import { TokenTextSplitter } from "@langchain/textsplitters"; // 导入Token文本分割器
import { Document } from "@langchain/core/documents"; // 导入Document类
import { getEncoding } from "js-tiktoken"; // 导入Token编码工具
// 1. 定义日志文档(与案例一类似,不含长日志,便于测试Token分割)
const logDocument = new Document({
pageContent: `[2024-01-15 10:00:00] INFO: Application started
[2024-01-15 10:00:05] DEBUG: Loading configuration file
[2024-01-15 10:00:10] INFO: Database connection established
[2024-01-15 10:00:15] WARNING: Rate limit approaching
[2024-01-15 10:00:20] ERROR: Failed to process request
[2024-01-15 10:00:25] INFO: Retrying operation
[2024-01-15 10:00:30] SUCCESS: Operation completed`
});
// 2. 初始化TokenTextSplitter
const logTextSplitter = new TokenTextSplitter({
chunkSize: 50, // 每个Chunk最大Token数量为50
chunkOverlap: 10, // 相邻Chunk重叠10个Token
encodingName: 'cl100k_base', // Token编码方式(OpenAI模型常用)
});
// 3. 分割文档
const splitDocuments = await logTextSplitter.splitDocuments([logDocument]);
// 4. 统计每个Chunk的字符长度和Token长度
const enc = getEncoding("cl100k_base");
splitDocuments.forEach((document, index) => {
console.log(`\nChunk ${index + 1}:`);
console.log("Chunk内容:", document.pageContent);
console.log("字符长度(charater length):", document.pageContent.length);
console.log("Token长度(token length):", enc.encode(document.pageContent).length);
});
代码解析
- 依赖导入:导入了TokenTextSplitter用于按Token分割文本,Document用于定义日志文档,getEncoding用于初始化Token编码。
- 文档定义:创建了一个Document实例,pageContent包含7条短日志,便于测试Token分割的效果,避免长日志导致的多次分割。
- 分割器初始化:设置chunkSize=50(每个Chunk最大Token数量为50),chunkOverlap=10(相邻Chunk重叠10个Token),encodingName='cl100k_base'(适配OpenAI模型的编码方式)。
- 分割与统计:splitDocuments方法返回按Token分割后的Chunk数组,通过forEach遍历每个Chunk,打印其内容、字符长度和Token长度,验证分割后的Chunk Token数量是否不超过50。
运行结果分析(预测)
结合代码配置,运行后会得到以下结果:
- 分割后的Chunk数量:7条日志的总Token数量约为140-160(每条日志约20-25个Token),因此按chunkSize=50、chunkOverlap=10分割,会得到3-4个Chunk(例如:Chunk1包含前2条日志,Token数量约45;Chunk2包含第2-3条日志,Token数量约45;以此类推)。
- Token长度验证:每个Chunk的Token长度均不超过50,符合chunkSize的要求;相邻Chunk之间重叠10个Token,确保语义连贯(例如,Chunk1的末尾与Chunk2的开头有10个Token的重叠内容)。
- 字符长度与Token长度的关系:每个Chunk的字符长度约为60-70(中文占比高),Token长度约为40-50,符合"1个中文字符约对应1.3个Token"的换算规律。
注意事项
- encodingName的选择需与使用的模型匹配,例如OpenAI的GPT-3.5-turbo、GPT-4使用cl100k_base编码,而旧版模型(如GPT-3)使用p50k_base编码,编码方式错误会导致Token数量计算不准确。
- chunkSize的设置需预留一定的冗余,例如模型上下文窗口为4k Token,可将chunkSize设置为3500左右,避免后续添加用户问题和生成回答时超出Token限制。
- TokenTextSplitter分割时可能会破坏句子的语义完整性,因此建议配合较大的chunkOverlap(如10-20个Token),弥补语义断裂。
4.3 补充案例:Token数量计算实践
文档中还提供了一段Token数量计算的代码,用于演示如何使用js-tiktoken库计算文本的Token数量,这是Splitter参数配置(如chunkSize)的重要依据,代码解析如下:
javascript
// 导入Token编码相关工具
import { getEncodingNameForModel, getEncoding } from 'js-tiktoken';
// 1. 定义模型名称,获取对应的编码方式
const modelName = 'gpt-4'; // 目标模型
const encodingName = getEncodingNameForModel(modelName); // 获取模型对应的编码方式(gpt-4对应cl100k_base)
console.log(encodingName, '/////'); // 输出:cl100k_base /////
// 2. 初始化编码实例
const enc = getEncoding(encodingName);
// 3. 计算文本的Token数量
// 示例1:英文文本
console.log('pineapple', enc.encode('pineapple'), enc.encode('pineapple').length);
// 输出:pineapple [10924, 14250] 2 → 英文单词pineapple对应2个Token
// 示例2:中文文本(补充)
console.log(' pineapple', enc.encode('菠萝'), enc.encode('菠萝').length);
// 输出:菠萝 [1921, 3918] 2 → 中文词语"菠萝"对应2个Token(约1.3个Token/字符)
// 补充说明:不同语言的Token数量差异
const chineseText = '这是一段中文文本,用于测试Token数量计算。';
const englishText = 'This is an English text used to test Token count.';
console.log('中文文本Token数量:', enc.encode(chineseText).length); // 约18个Token
console.log('英文文本Token数量:', enc.encode(englishText).length); // 约12个Token
代码解析:getEncodingNameForModel方法根据模型名称获取对应的编码方式,getEncoding方法初始化编码实例,encode方法将文本转换为Token序列,length属性获取Token数量。通过这段代码可以明确:不同语言的文本,即使语义相近,Token数量也会存在差异,因此在配置Splitter的chunkSize时,若文本包含多语言,建议按Token数量进行配置(使用TokenTextSplitter),更精准地适配模型。
五、RAG流程中的Splitter:细节与注意事项
文档中提到"RAG问题",并梳理了RAG的核心流程及Splitter在其中的细节要求。RAG的核心流程是"Loader加载文档→Splitter分割文档→建立向量索引→检索相关Chunk→模型生成回答",其中Splitter是连接Loader和向量索引的关键环节,其分割效果直接影响检索的准确性和生成的相关性。以下结合文档内容,详细梳理RAG流程中Splitter的细节要求和注意事项。
5.1 RAG核心流程梳理(结合文档)
文档中提到RAG流程包含"loader、splitter细节(三个参数)、splitter面向对象体系和关系",补充完整RAG流程如下:
- Loader(加载文档) :使用@langchain/community中的Loader加载大文档(如本地txt、md、PDF文件,网络文档等),加载后得到Document实例(包含pageContent和metadata)。需注意:Loader仅支持文本类文档,mp3、mp4等非文本类文档需先转写为文本,再进行加载。
- Splitter(分割文档) :使用合适的Splitter(通常为RecursiveCharacterTextSplitter)将加载的Document分割为多个Chunk,核心关注三个参数:separators(分隔符)、chunkSize(Chunk大小)、chunkOverlap(重叠长度),这三个参数直接影响分割效果和后续检索准确性。
- 建立向量索引:将分割后的Chunk转换为向量(使用嵌入模型,如OpenAI的text-embedding-ada-002),并存储到向量数据库(如Pinecone、Chroma)中,建立向量索引,便于后续快速检索。
- 检索相关Chunk:用户输入问题后,将问题转换为向量,通过向量数据库检索与问题最相关的多个Chunk(通常为3-5个)。
- 生成回答:将检索到的相关Chunk和用户问题一起输入大语言模型,模型结合Chunk中的信息生成准确、相关的回答。
5.2 Splitter在RAG中的细节要求(文档重点)
5.2.1 三个核心参数的配置技巧
文档中提到"splitter细节 三个参数",即separators、chunkSize、chunkOverlap,这三个参数的配置需结合文本类型、模型上下文窗口和检索需求,具体技巧如下:
-
separators(分隔符) :
- 中文文本:优先使用["\n", "。", "!", "?", ",", ";", "、"],优先级按"语义完整性"排序,确保先分割段落,再分割句子,最后分割短语。
- 英文文本:优先使用["\n", ".", "!", "?", ",", ";", " "],同理,先分割段落,再分割句子,最后分割单词。
- 结构化文本(Markdown):使用RecursiveCharacterTextSplitter的子类MarkdownTextSplitter,其默认分隔符包含Markdown标题层级(#、##、###),确保标题与内容不被分割,保留文档结构。
-
chunkSize(Chunk大小) :
- 核心原则:Chunk大小需小于模型的上下文窗口,且预留一定的冗余(用于用户问题和生成回答)。
- 示例:若使用GPT-4(上下文窗口128k Token),可将chunkSize设置为10000-12000字符(约7000-9000 Token);若使用GPT-3.5-turbo(上下文窗口4k Token),可将chunkSize设置为3000-3500字符(约2300-2700 Token)。
- 文本类型影响:长文本(如小说、论文)可设置较大的chunkSize,确保语义连贯;短文本(如日志、对话)可设置较小的chunkSize,提高检索精度。
-
chunkOverlap(重叠长度) :
- 核心原则:重叠长度约为chunkSize的10%-15%,既弥补语义断裂,又避免过多重叠导致的冗余。
- 示例:chunkSize=1000,chunkOverlap=100-150;chunkSize=3000,chunkOverlap=300-450。
- 特殊场景:若文本语义连贯性要求高(如技术文档、论文),可适当提高overlap比例(15%-20%);若文本结构简单(如日志),可降低overlap比例(5%-10%)。
5.2.2 Splitter面向对象体系的应用注意事项
文档中强调了Splitter的面向对象体系,在RAG流程中,需根据文档类型选择合适的Splitter子类,避免使用错误的分割器导致语义破坏或分割失败,具体注意事项如下:
- 避免使用TextSplitter父类直接实例化:TextSplitter是抽象基类,仅定义了抽象方法,无法直接实例化,需使用其子类(CharacterTextSplitter、RecursiveCharacterTextSplitter等)。
- 非文本类型文档不适用Splitter:TextSplitter及其子类仅适用于文本类型文档,对于mp3、mp4等非文本类型,需先通过转写工具(如Whisper转写音频)转换为文本,再进行分割。
- 结构化文本优先使用对应子类:例如Markdown文档优先使用MarkdownTextSplitter,HTML文档优先使用HTMLTextSplitter(LangChain提供的子类),确保文档结构不被破坏,提高检索准确性。
5.2.3 RAG中Splitter的常见问题与解决方案
结合文档内容和实际应用,梳理RAG流程中Splitter常见的问题及解决方案,帮助避免踩坑:
-
问题1:分割后Chunk语义断裂,导致检索不准确
- 原因:分隔符优先级设置不合理,或chunkSize过小,导致句子被强制切割;overlap设置过小,无法弥补语义断裂。
- 解决方案:调整分隔符优先级,优先使用能保留完整句子的分隔符(如"。"".");适当增大chunkSize,避免强制切割;提高overlap比例(10%-15%)。
-
问题2:Chunk Token数量超出模型上下文窗口,导致模型调用失败
- 原因:chunkSize设置过大,未考虑Token与字符的换算关系;未预留冗余Token用于用户问题和生成回答。
- 解决方案:使用TokenTextSplitter按Token数量配置chunkSize;结合js-tiktoken库计算Chunk的Token数量,确保不超过模型上下文窗口的70%-80%(预留冗余)。
-
问题3:结构化文档(如Markdown)分割后,结构被破坏
- 原因:使用了普通的Splitter(如CharacterTextSplitter),未考虑Markdown的标题层级结构。
- 解决方案:使用MarkdownTextSplitter(RecursiveCharacterTextSplitter的子类),其默认支持按标题层级递归分割,保留文档结构。
-
问题4:多语言文档分割后,Token数量计算不准确
- 原因:使用了CharacterTextSplitter按字符长度分割,未考虑不同语言的Token换算差异。
- 解决方案:使用TokenTextSplitter按Token数量分割,指定正确的编码方式(如cl100k_base),确保Token数量计算准确。
六、延伸拓展:Splitter的进阶应用与优化技巧
结合文档内容和LangChain官方文档,补充Splitter的进阶应用技巧,帮助提升分割效果,适配更复杂的场景,进一步完善学习内容。同时解决前文代码未完成的问题,让延伸拓展部分兼具理论性和实用性,助力更好地应对实际项目中的文本分割需求。
6.1 自定义Splitter:适配特殊文本类型
LangChain提供的Splitter子类能够满足大多数场景,但对于一些特殊文本类型(如JSON、XML、代码文件),可能需要自定义Splitter,核心方法是继承TextSplitter父类,重写分割方法(splitText或splitDocuments)。自定义Splitter的核心思路是"贴合特殊文本的结构规则,制定专属分割逻辑",确保分割后的Chunk保留文本固有的结构语义,避免因通用分割规则导致的结构破坏。
示例:自定义JSONSplitter,按JSON的键值对分割,确保每个Chunk包含完整的键值对语义,同时适配JSON嵌套结构,避免切割破坏嵌套逻辑:
php
import { TextSplitter } from "@langchain/core/text_splitter";
import { Document } from "@langchain/core/documents";
// 自定义JSON文本分割器,继承TextSplitter父类
class JSONSplitter extends TextSplitter {
// 重写splitText方法,实现JSON专属分割逻辑
async splitText(text: string): Promise<string[]> {
const chunks: string[] = [];
try {
// 将JSON文本解析为对象
const jsonObj = JSON.parse(text);
// 递归处理JSON对象,提取键值对作为Chunk
const processJson = (obj: any, parentKey = "") => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const value = obj[key];
// 判断值的类型,若为对象/数组则递归处理,否则作为一个Chunk
if (typeof value === "object" && value !== null) {
processJson(value, fullKey);
} else {
// 拼接键值对,作为一个完整的Chunk,确保语义完整
const chunk = `"${fullKey}": ${JSON.stringify(value)}`;
chunks.push(chunk);
}
}
}
};
processJson(jsonObj);
} catch (error) {
// 若JSON解析失败,降级为按换行符分割,避免分割失败
return text.split("\n").filter(line => line.trim() !== "");
}
return chunks;
}
// 重写splitDocuments方法,适配Document实例
async splitDocuments(documents: Document[]): Promise<Document[]> {
const chunks: Document[] = [];
for (const doc of documents) {
const textChunks = await this.splitText(doc.content);
for (const chunk of textChunks) {
chunks.push(
new Document({
content: chunk,
metadata: { ...doc.metadata, chunkId: chunks.length + 1 }
})
);
}
}
return chunks;
}
}
// 测试自定义JSONSplitter
const testJson = `{
"user": {
"id": 123,
"name": "张三",
"address": {
"province": "北京",
"city": "北京市"
}
},
"order": {
"orderId": "ORD2024001",
"amount": 999.99,
"status": "paid"
}
}`;
const jsonSplitter = new JSONSplitter();
// 分割JSON文本
jsonSplitter.splitText(testJson).then(chunks => {
console.log("JSON分割后的Chunk:", chunks);
});
代码解析:自定义的JSONSplitter继承了TextSplitter父类,重写了splitText和splitDocuments方法。核心逻辑是先解析JSON文本,递归提取键值对作为Chunk,确保每个Chunk包含完整的键值对语义;若JSON解析失败(如文本格式错误),则降级为按换行符分割,提升分割的容错性。这种自定义方式适用于JSON配置文件、接口返回数据等特殊文本的分割,避免通用Splitter切割破坏JSON结构。
补充说明:除了JSONSplitter,还可自定义XMLSplitter(按XML标签层级分割)、CodeSplitter(按代码语法块分割,如函数、类、注释)等,核心是"结合文本结构,制定专属分割规则",确保分割后的Chunk既适配模型上下文窗口,又保留文本的核心语义和结构。
6.2 Splitter参数优化:平衡效率与语义完整性
前文提到Splitter的三个核心参数(separators、chunkSize、chunkOverlap),实际应用中,参数配置并非固定不变,需根据文本类型、模型特性和业务需求动态优化,核心目标是"平衡分割效率、语义完整性和模型适配性"。以下是具体的优化技巧和实践建议:
6.2.1 chunkSize与chunkOverlap的动态适配
chunkSize和chunkOverlap的配置需结合模型的上下文窗口和文本语义密度,避免"一刀切":
- 语义密度高的文本(如技术文档、论文、代码):这类文本每句话、每个段落的信息量较大,若chunkSize过小,会导致语义断裂;若过大,会超出模型上下文窗口。建议设置较大的chunkSize(如3000-5000字符),chunkOverlap设置为chunkSize的15%-20%,确保语义连贯。例如,分割技术论文时,可将chunkSize设置为4000字符,chunkOverlap设置为600-800字符,保留段落间的衔接信息。
- 语义密度低的文本(如日志、对话、普通文案):这类文本信息量分散,句子较短,可设置较小的chunkSize(如1000-2000字符),chunkOverlap设置为chunkSize的5%-10%,减少冗余。例如,分割系统日志时,chunkSize设置为1500字符,chunkOverlap设置为150字符,既保证每条日志的完整性,又避免过多重叠。
- 模型上下文窗口适配:若使用上下文窗口较小的模型(如GPT-3.5-turbo 4k Token),需严格控制chunkSize,建议按Token数量配置(使用TokenTextSplitter),将chunkSize设置为模型窗口的70%-80%,预留20%-30%的冗余用于用户问题和生成回答。例如,GPT-3.5-turbo 4k Token,可将chunkSize设置为3000 Token,预留1000 Token用于后续交互。
6.2.2 separators的优先级优化
separators的优先级直接影响语义完整性,需根据文本类型动态调整,核心原则是"优先按能保留完整语义的分隔符切割":
- 中文长文本(如小说、散文):优先使用["\n\n", "\n", "。", "!", "?", ","],先按段落分割("\n\n"),再按换行分割("\n"),最后按句子和短语分割,最大程度保留段落和句子语义。
- 英文长文本(如英文论文、报告):优先使用["\n\n", "\n", ".", "!", "?", ",", ";"],同理,先分割段落,再分割句子,最后分割短语和单词。
- 代码文件(如JavaScript、Python):优先使用["\n\n", "\n", "}", "{", ";", "//"],按代码块、函数、语句分割,保留代码的语法结构,避免切割破坏函数、类的完整性。例如,分割Python代码时,separators设置为["\n\n", "\n", "def ", "class ", ";", "#"],确保函数和类不被分割。
6.3 多场景Splitter适配方案
不同业务场景下,文本类型和需求不同,需选择合适的Splitter并配置参数,以下是常见场景的适配方案,结合前文知识点,形成可直接复用的实践指南:
6.3.1 RAG场景(核心场景)
RAG场景的核心需求是"分割后的Chunk便于检索,且能为模型生成提供完整语义",首选RecursiveCharacterTextSplitter,搭配TokenTextSplitter辅助控制Token数量,具体配置如下:
- 文本类型:技术文档、PDF论文、知识库文档等长文本。
- Splitter选择:RecursiveCharacterTextSplitter为主,TokenTextSplitter为辅(用于调整Token数量)。
- 参数配置:separators按文本类型优化(如中文技术文档:["\n\n", "\n", "。", ","]);chunkSize设置为3000-5000字符;chunkOverlap设置为chunkSize的10%-15%;若模型对Token数量有严格要求,可先用RecursiveCharacterTextSplitter分割,再用TokenTextSplitter调整Chunk的Token数量至模型限制范围内。
- 优化技巧:为Chunk添加metadata(如文档标题、章节、页码),便于后续检索时筛选相关Chunk,提升RAG检索准确性。
6.3.2 日志分析场景
日志分析场景的核心需求是"保留每条日志的完整性,便于后续分析和检索",文本特点是每条日志独立、分隔符明确(通常为换行符),具体适配方案如下:
- Splitter选择:RecursiveCharacterTextSplitter(优先按换行符分割)。
- 参数配置:separators设置为["\n", "。", ","],优先按换行符分割,确保每条日志为一个独立Chunk;若存在超长日志(如包含大量描述的日志),则按"。"","递归分割;chunkSize设置为1500-2000字符;chunkOverlap设置为5%-10%,减少冗余。
- 补充:可结合自定义Splitter,提取日志中的关键信息(如时间、级别、内容),生成结构化的Chunk,提升日志分析效率。
6.3.3 多语言文档场景
多语言文档(如中英文混合文档、多语言知识库)的核心需求是"统一分割标准,精准控制Token数量",具体适配方案如下:
- Splitter选择:TokenTextSplitter(按Token数量分割,不受语言类型影响)。
- 参数配置:encodingName设置为模型对应的编码方式(如cl100k_base);chunkSize设置为模型上下文窗口的70%-80%;chunkOverlap设置为10-20个Token,弥补语义断裂;若文档包含结构化内容(如Markdown),可先用MarkdownTextSplitter分割,再用TokenTextSplitter调整Token数量。
6.4 Splitter与其他工具的协同使用
Splitter并非孤立使用,在实际项目中,需与Loader、嵌入模型、向量数据库等工具协同,形成完整的文本处理流程,进一步提升分割效果和后续任务的准确性,核心协同场景如下:
6.4.1 Splitter与Loader协同
Loader加载文档后,需根据文档类型选择合适的Splitter,避免分割失败或语义破坏:
- 加载Markdown文档:使用MarkdownLoader加载,搭配MarkdownTextSplitter分割,保留Markdown标题层级结构。
- 加载PDF文档:使用PDFLoader加载(需先将PDF转文字),搭配RecursiveCharacterTextSplitter分割,按段落和句子分割,保留PDF的内容逻辑。
- 加载音频/视频文档:使用WhisperLoader转写为文本,再搭配RecursiveCharacterTextSplitter分割,确保转写文本的语义完整性。
6.4.2 Splitter与嵌入模型协同
嵌入模型将Chunk转换为向量时,Chunk的质量直接影响向量的准确性,因此Splitter的参数配置需适配嵌入模型的特性:
- 嵌入模型的Token限制:多数嵌入模型(如OpenAI的text-embedding-ada-002)有Token数量限制(如8191 Token),因此Chunk的Token数量需不超过嵌入模型的限制,可使用TokenTextSplitter控制Chunk的Token数量。
- 语义完整性:嵌入模型对语义完整的Chunk处理效果更好,因此优先使用RecursiveCharacterTextSplitter,确保Chunk语义完整,提升向量的相关性。
6.4.3 Splitter与向量数据库协同
向量数据库存储Chunk的向量时,Chunk的大小和数量会影响检索效率,因此Splitter的参数配置需兼顾检索效率和语义完整性:
- Chunk大小:Chunk过大,向量数据库的检索速度会下降;Chunk过小,会导致检索结果冗余。建议Chunk的Token数量控制在100-5000之间,平衡检索效率和语义完整性。
- Chunk数量:避免分割后的Chunk数量过多(如超过10万条),可通过调整chunkSize减少Chunk数量;若文档过大,可先按章节分割,再对每个章节进行细分割,提升检索效率。
6.5 常见进阶问题与解决方案
结合实际项目经验,梳理Splitter进阶应用中的常见问题及解决方案,帮助避免踩坑,提升分割效果:
6.5.1 问题1:超长文本分割后,Chunk语义断裂严重
原因:chunkSize设置过小,分隔符优先级不合理,未充分利用递归分割逻辑;overlap设置不足,无法弥补语义断裂。
解决方案:增大chunkSize,结合文本语义密度调整(如技术文档设置为4000字符);优化separators优先级,优先按段落、句子分割;提高overlap比例至15%-20%;若文本包含嵌套结构(如JSON、XML),使用自定义Splitter分割。
6.5.2 问题2:多格式混合文档(如Markdown+代码+文本)分割效果差
原因:使用单一Splitter,无法适配多种格式的文本结构,导致代码块、Markdown标题被切割破坏。
解决方案:使用多Splitter协同分割,先使用MarkdownTextSplitter分割Markdown结构,提取代码块和文本段落;再对代码块使用自定义CodeSplitter分割,对文本段落使用RecursiveCharacterTextSplitter分割;最后统一调整Chunk的大小和Token数量。
6.5.3 问题3:分割速度慢,无法处理超大文档(如GB级)
原因:Splitter的分割逻辑过于复杂(如递归次数过多);未进行分批次分割,一次性加载超大文档导致内存不足。
解决方案:简化分割逻辑,减少递归次数(如调整separators优先级,减少低优先级分隔符的使用);采用分批次分割,将超大文档按章节、文件大小分批次加载和分割,避免内存不足;使用高效的Splitter(如CharacterTextSplitter)处理结构简单的超大文档,提升分割速度。
6.6 总结
Splitter的进阶应用核心是"贴合场景、动态优化、协同配合"。无论是自定义Splitter适配特殊文本,还是优化参数平衡效率与语义,亦或是与其他工具协同形成完整流程,最终目标都是"让分割后的Chunk既适配模型上下文窗口,又保留文本的核心语义和结构",为后续的RAG、文档问答、日志分析等任务奠定基础。
实际应用中,需结合文本类型、模型特性和业务需求,灵活选择Splitter并配置参数,同时不断总结实践经验,优化分割方案。此外,随着LangChain生态的不断更新,新的Splitter子类和功能会持续出现,需关注官方文档,及时学习新的实践技巧,提升文本分割的能力。