Text Splitter:为什么知识库不能整篇文档直接丢给大模型?
9 / 15 章
一、前言 Document Loader 只是把资料读进来。Text Splitter 才决定这些资料怎么进入知识库。 切分太大,检索像大海捞针。切分太小,语义被切碎。一个好的 Chunk,要能被召回,也要能独立表达完整意思。 这就是 Te
一、前言
Document Loader 只是把资料读进来。Text Splitter 才决定这些资料怎么进入知识库。
切分太大,检索像大海捞针。切分太小,语义被切碎。一个好的 Chunk,要能被召回,也要能独立表达完整意思。
这就是 Text Splitter 的价值:把长文档切成模型能吃、向量库能存、Retriever 能找、Prompt 能用的小块。
二、官方怎么定义 Text Splitter?
LangChain 官方文档把 Text Splitter 分成几类:递归切分、长度切分、按文档结构切分。长度切分简单稳定;结构切分适合 Markdown、HTML、JSON、代码;递归切分会尽量保留段落、句子、词等较大的语义单元。
官方递归切分文档也说明,RecursiveCharacterTextSplitter 默认按字符列表切分,长度默认按字符数计算;想直接得到字符串,用 split_text;想得到 Document 对象,用 create_documents。
这句话很关键:Text Splitter 不是简单字符串工具。它最终要服务 RAG,所以它必须把 Chunk 重新包装成 Document。
三、为什么知识库不能整篇文档直接入库?
第一,召回颗粒度太粗。用户问一个细节,向量库可能召回整篇大文档,模型被迫在一堆无关内容里找答案。
第二,上下文成本太高。Prompt 里塞进太多文本,Token 成本和延迟都会上升。
第三,答案容易跑偏。无关段落越多,模型越容易抓错重点。
第四,溯源不清晰。用户问“依据在哪”,整篇文档很难定位到具体页、具体段、具体标题。
所以,RAG 的第一性原理不是“把资料都塞进去”,而是“只把相关资料塞进去”。Text Splitter 正是把资料变成可召回颗粒的第一步。
四、TextSplitter 是所有切分器的骨架
从源码看,TextSplitter 继承 BaseDocumentTransformer。它不是一个普通工具类,而是一个“文档转换器”。它接收 Document,输出更多 Document。
TextSplitter 的构造方法里有几个硬约束:chunk_size 必须大于 0,chunk_overlap 不能小于 0,chunk_overlap 不能大于 chunk_size。源码一开始就做了参数保护。
class TextSplitter(BaseDocumentTransformer, ABC):
def __init__(
self,
chunk_size: int = 4000,
chunk_overlap: int = 200,
length_function: Callable[[str], int] = len,
keep_separator: bool | Literal["start", "end"] = False,
add_start_index: bool = False,
strip_whitespace: bool = True,
) -> None:
if chunk_overlap > chunk_size:
raise ValueError(...)
self._chunk_size = chunk_size
self._chunk_overlap = chunk_overlap
self._length_function = length_function
这里的重点不是默认值,而是抽象。TextSplitter 只规定切分器必须实现 split_text。至于怎么切,由具体子类决定。
五、split_documents:Document 是怎么被切成多个 Document 的?
split_documents 的源码非常短,但意义很大。它先遍历输入的 Document,把 page_content 和 metadata 拿出来,然后调用 create_documents。
def split_documents(self, documents: Iterable[Document]) -> list[Document]:
texts, metadatas = [], []
for doc in documents:
texts.append(doc.page_content)
metadatas.append(doc.metadata)
return self.create_documents(texts, metadatas=metadatas)
这说明一件事:切分不是丢掉原始信息。每个 Chunk 都应该继承原始 metadata,比如来源文件、页码、标题、业务标签、版本号。
create_documents 会继续调用 split_text,把每个文本切成 chunk;然后把每个 chunk 包成新的 Document。开启 add_start_index 后,还会把 chunk 在原文中的起始位置写入 metadata。
for chunk in self.split_text(text):
metadata = copy.deepcopy(metadatas_[i])
if self._add_start_index:
metadata["start_index"] = index
new_doc = Document(page_content=chunk, metadata=metadata)
documents.append(new_doc)
这就是为什么企业知识库一定要重视 metadata。没有 metadata,答案就没法引用;没法引用,用户就不敢信。
六、_merge_splits:chunk_size 和 chunk_overlap 的真相
_merge_splits 是源码里最值得细看的地方。它不是简单截断字符串,而是把一堆小片段合并成中等大小的 Chunk。
逻辑很直接:不断往 current_doc 里塞片段;如果加上新片段会超过 chunk_size,就先输出当前 Chunk;然后从头弹出旧片段,直到保留的重叠部分不超过 chunk_overlap。
if total + len_ + separator_len > self._chunk_size:
doc = self._join_docs(current_doc, separator)
docs.append(doc)
while total > self._chunk_overlap or still_too_long:
total -= length(current_doc[0])
current_doc = current_doc[1:]
current_doc.append(d)
这段源码背后的核心思想是:不要让 Chunk 太长,也不要让相邻 Chunk 完全断开。overlap 就是两个 Chunk 之间的“缝合线”。
七、RecursiveCharacterTextSplitter:为什么它通常是默认首选?
普通 CharacterTextSplitter 更像“按某个分隔符切”。RecursiveCharacterTextSplitter 更聪明。它会拿一组分隔符,从大结构到小结构逐层尝试。
源码默认分隔符是:段落、换行、空格、空字符串。也就是先按段落切;段落太长,再按换行;还太长,再按空格;最后兜底按字符切。
self._separators = separators or ["\n\n", "\n", " ", ""]
def split_text(self, text: str) -> list[str]:
return self._split_text(text, self._separators)
源码里的 _split_text 会先找到当前文本里能匹配的 separator,然后切开。如果切出来的某个片段还超过 chunk_size,就带着剩下的 separator 递归继续切。
for s_ in separators:
if re.search(separator_, text):
separator = s_
new_separators = separators[i + 1:]
break
for s in splits:
if self._length_function(s) < self._chunk_size:
good_splits.append(s)
else:
final_chunks.extend(self._split_text(s, new_separators))
这就是它适合通用文档的原因:它尽量不破坏自然结构。先保段落,再保句子,再保词。实在没办法,才保长度。
八、参数怎么调?先看这张图
参数没有银弹。知识库越专业,越不能只靠默认值。要用真实问题集做评测,看召回命中率、答案准确率、引用完整性和成本。
一般经验:普通中文文章可以从 500 到 1000 字级别开始;技术文档优先按标题结构切;合同、公告、研报要保留页码、章节、发布日期;代码文档要用代码结构切分。
九、MarkdownHeaderTextSplitter:结构化文档不要盲目按长度切
如果文档本身有标题结构,比如 Markdown 的 #、##、###,更好的方式是先按标题切。LangChain 的 MarkdownHeaderTextSplitter 会把标题信息写入 metadata。
这对 RAG 很重要。因为同一句话放在“安装教程”下面,和放在“故障排查”下面,含义完全不同。标题就是上下文。
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
chunks = markdown_splitter.split_text(markdown_document)
源码思路并不复杂:扫描每一行,维护当前标题栈,把内容聚合到相同 metadata 的块里。最后输出 Document(page_content=内容, metadata=标题路径)。
这类结构切分器适合技术文档、产品手册、接口文档、知识库页面。它的目标不是“切得均匀”,而是“切得有语义”。
十、企业级项目里,Text Splitter 应该怎么落地?
企业项目不要把切分写成一次性脚本。建议做成可配置、可重跑、可追踪的流水线。
Java 主服务负责上传、鉴权、任务状态、数据源管理。
Python AI 服务负责文档解析、清洗、切分、Embedding、向量入库。
每个 Chunk 要有 metadata:source_id、file_name、page、section、chunk_index、version、hash。
切分策略要可配置:普通文档、Markdown、HTML、代码、PDF 表格,不能一套参数走天下。
切分结果要可回溯:用户点答案引用时,能跳回原文位置。
十一、最容易踩的坑
坑一:只调 chunk_size,不看召回效果。结果 Chunk 看起来整齐,答案却不准。
坑二:metadata 丢失。答案能生成,但没法溯源。生产环境这是硬伤。
坑三:PDF 直接切。很多 PDF 有页眉、页脚、断行、表格错位,必须先清洗。
坑四:overlap 过大。召回结果重复,Prompt 变长,成本上升。
坑五:结构化文档按长度硬切。标题和正文断开,语义就断了。
坑六:不做评测。没有命中率、准确率、成本指标,参数就是玄学。
十二、最后总结
Text Splitter 是 RAG 的地基。地基切歪了,Embedding 再强、向量库再快、模型再贵,也救不回来。
它的源码并不神秘:split_documents 负责处理 Document,create_documents 负责重建 Document,split_text 交给子类实现,_merge_splits 负责控制大小和重叠,RecursiveCharacterTextSplitter 负责按结构逐层递归。
真正的关键只有一句:不要为了切分而切分,要为了检索、引用和回答质量而切分。
相关推荐
LangChain 系列之Tools:让大模型真正连接业务系统
前面几章,我们把 RAG 的底层链路讲完了:文档进来,切分,向量化,入库,检索,重排,最后把上下文交给模型。 但这还不够。 RAG 让模型会“查资料”。Tools 让模型能“办事情”。 没有 Tools,大模型只是一个会聊天的脑子。它能分析
LangChain 系列:从 0 搭一个企业知识库问答系统
这一章不再单讲一个组件,而是把前面所有 RAG 组件合起来:从文件上传,到向量入库,再到用户提问、检索证据、模型回答、日志追踪。 01 先搞清楚:企业知识库问答不是 Demo Demo 只要能回答。企业系统要能上传、能解析、能检索、能引用、
Tensor:PyTorch 世界里的一切都是张量
1. Tensor 是 PyTorch 的基本单位 PyTorch 里,模型吃进去的是 Tensor,参数保存的是 Tensor,梯度也是 Tensor,GPU 上跑的还是 Tensor。 你可以把 Tensor 理解成“加强版数组”。但这
Rerank 与上下文压缩:为什么召回 TopK 后还要重排?
RAG 最容易踩的坑,不是“没有召回”。 而是召回了一堆看起来相关、实际会干扰模型的资料。 所以,成熟的 RAG 链路不会把 TopK 直接塞进 Prompt。 它会先召回,再重排,再压缩。 1. 为什么召回 TopK 后还要重排? 向量召
环境安装与开发姿势:CPU、CUDA、ROCm、MPS 怎么选
学 PyTorch,第一个门槛不是模型。是环境。环境装错,代码还没开始跑,报错已经堆满屏。 这一章只讲一件事:把 PyTorch 环境一次性讲清楚。不是背安装命令,而是看懂安装背后的逻辑。 因为命令会随着版本变化。逻辑不会。你只要能判断 C
LangChain 系列之Retriever:RAG 的核心不是生成,而是检索
01 前言 Retriever 是 RAG 的入口。它接收一个 query,返回一组 List[Document]。模型不是先出场,资料先出场。 前面几章我们讲了 Loader、Splitter、Embedding、VectorStore。
阅读补充
一句话看懂
一、前言 Document Loader 只是把资料读进来。Text Splitter 才决定这些资料怎么进入知识库。
事件背景
这篇内容围绕“Text”展开,热闻岛基于公开信息整理事件背景、主要进展与可继续关注的方向。
事件时间线
发布
相关信息进入公开传播
更新
热闻岛对内容进行整理与补充。
看点
- · Text的最新进展是什么
- · 相关信息对用户或行业会带来哪些影响
- · 后续是否会有新的回应或处理结果
后续关注
- · 后续官方回应或权威通报
- · 相关主体的进一步说明
- · 事件对普通用户和行业的持续影响
免责声明:本文仅代表作者观点,不构成投资建议、法律建议、医疗建议。财经类内容尤其需要注意风险;爆料类信息请以权威通报为准。
评论 (0)
登录后即可发表评论
去登录