Langchain-Chatchat文档分块算法对比:固定 vs 语义切分
在构建本地知识库问答系统时,一个常被低估却至关重要的环节浮出水面:如何把一篇几千字的PDF或Word文档“掰开”喂给大模型?
直接丢进去显然不行——主流LLM上下文长度有限,GPT-4 Turbo撑死也就128k,更别说很多私有部署模型还卡在32k甚至更低。而硬生生截断又容易把一句话劈成两半,让模型看得一头雾水:“这前言不搭后语的是什么意思?”
于是,“文档分块”成了绕不开的技术门槛。尤其在像Langchain-Chatchat这类强调私有知识库、支持多格式文档解析的开源项目中,分块策略的选择直接影响后续检索效果和回答质量。
目前主流做法大致分两条路:一条是简单粗暴但高效的“固定长度切”,另一条是聪明细腻但耗资源的“按语义分”。到底该选哪一种?我们不妨从实际工程角度深入拆解一番。
固定长度分块:稳扎稳打的通用打法
先说最常见的——固定长度分块。听起来很机械,但它其实是大多数系统的默认选项,原因也很现实:够快、够稳、够省事。
它的核心逻辑非常直观:你设定一个“块大小”(比如500字符),再加点“重叠”(比如50字符),然后就像拉窗帘一样,从左往右一格一格滑过去,每滑一次就切下一小段文本。
别看方法朴素,LangChain里的RecursiveCharacterTextSplitter其实已经做了不少优化。它不是傻乎乎地按字符硬切,而是优先尝试用自然断点来分割:
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""] )注意这个separators列表,它是有顺序讲究的。程序会先找双换行符(\n\n)——通常代表段落结束;找不到就退一步看单换行;还不行再看中文句号、感叹号……直到最后才不得已按空格甚至逐字符切。
这种“递归回退”机制,让它在保持规则性的同时,尽可能尊重了原始文本结构。哪怕遇到没有标点的长串内容,也不会彻底失控。
而且加上chunk_overlap后,还能缓解关键句子被拦腰斩断的问题。例如某句话恰好落在两个块的交界处,那么它会在前后两个chunk中都出现一次,确保信息不丢失。
这类方法的优势非常明显:
-计算开销极低:不需要任何模型推理,纯字符串操作,CPU就能扛;
-输出可控:每个chunk大小基本一致,便于批量处理和索引管理;
-调试友好:参数明确,结果可复现,适合快速搭建原型。
所以在资源受限环境(比如边缘设备)、对响应速度要求高的场景(如客服机器人),或者面对结构清晰的制度文件、说明书这类文档时,固定分块依然是首选方案。
但问题也存在——它本质上是个“盲切”过程。不管当前是不是话题转折点,只要到了预定位置就得切。这就可能导致一个问题的答案被拆到多个毫不相干的chunk里,迫使LLM去拼凑碎片信息,增加了幻觉风险。
语义感知分块:让机器学会“读懂再切”
为了解决这个问题,语义感知分块应运而生。它的目标不再是均匀切割,而是理解文本内在逻辑,在真正该停的地方停下来。
举个例子:一段讲“公司报销流程”的文字突然跳到“员工绩效考核标准”,虽然中间可能没空行也没句号,但从语义上看已经是两个独立主题了。这时候如果还在按字符数硬切,显然不合理。
语义分块的做法是:先把文本切成句子,然后用嵌入模型(如 BGE、Sentence-BERT)给每个句子生成向量表示,接着计算相邻句子之间的语义相似度。当发现前后句差异过大时,就判断发生了“语义跃迁”,在此处分块。
下面是简化版实现:
from sentence_transformers import SentenceTransformer import numpy as np from sklearn.metrics.pairwise import cosine_similarity import re class SemanticTextSplitter: def __init__(self, embed_model_name='bge-small-zh-v1.5', similarity_threshold=0.75, min_chunk_size=100): self.embedder = SentenceTransformer(embed_model_name) self.similarity_threshold = similarity_threshold self.min_chunk_size = min_chunk_size def split_text(self, text): sentences = re.split(r'[。!?\.\!\?]', text) sentences = [s.strip() for s in sentences if len(s.strip()) > 10] if len(sentences) < 2: return sentences embeddings = self.embedder.encode(sentences) chunks = [] current_chunk = sentences[0] current_embedding = embeddings[0].reshape(1, -1) for i in range(1, len(sentences)): prev_emb = current_embedding curr_emb = embeddings[i].reshape(1, -1) sim = cosine_similarity(prev_emb, curr_emb)[0][0] if sim < self.similarity_threshold and len(current_chunk) >= self.min_chunk_size: chunks.append(current_chunk) current_chunk = sentences[i] current_embedding = curr_emb else: current_chunk += "。" + sentences[i] current_embedding = np.mean([prev_emb[0], curr_emb[0]], axis=0).reshape(1, -1) if current_chunk: chunks.append(current_chunk) return chunks这段代码虽然简单,但体现了语义分块的核心思想:用向量空间的距离反映语义距离。当两句之间余弦相似度低于阈值(比如0.75),说明它们很可能不属于同一主题,此时触发分块。
这样做出来的chunk,大小不一,但每一个都更接近“完整表达一个意思”的单元。对于科研论文、技术白皮书这类逻辑跳跃频繁的文档尤其有用。
更重要的是,在向量数据库中做检索时,query更容易命中语义完整的chunk,减少“查到了但看不懂”的尴尬情况。LLM拿到的结果上下文自洽,自然也能给出更准确的回答。
当然,代价也很明显:
- 需要加载嵌入模型,内存占用高;
- 推理延迟显著增加,不适合高频实时处理;
- 相似度阈值、最小块长等参数需要调优,维护成本上升。
所以是否采用语义分块,本质上是一次精度与效率的权衡。
实际应用中的选择逻辑
在 Langchain-Chatchat 的整体架构中,文档分块处于这样一个关键节点:
原始文档 → 文档加载器(Loader) ↓ 文本提取 ↓ 文档分块(Splitter) ↓ 向量化(Embedding Model) ↓ 向量数据库(如 FAISS、Chroma) ↓ 检索-生成问答链(RetrievalQA)可以看到,分块模块直接影响后续所有环节的质量。选得好,事半功倍;选得不好,后面再强的模型也难救回来。
那到底怎么选?
看硬件条件
如果你部署在树莓派或低配服务器上,没有GPU加速,跑BGE这类模型都会卡顿,那就老老实实用RecursiveCharacterTextSplitter。固定分块几乎不依赖额外资源,稳定性极高。
反之,若已有NPU/GPU支持,并且长期运行,可以考虑启用语义分块提升整体体验。
看文档类型
结构规整的行政公文、产品手册、FAQ列表,本身就有很多天然分隔符,固定分块完全够用。
但如果是会议纪要、学术综述、法律合同这些内容密集、逻辑嵌套深的文档,固定切法很容易把关键论点割裂。这时候引入语义判断,能有效保留上下文完整性。
看业务需求
客服场景追求响应速度,用户不能等三秒才看到答案,这时低延迟的固定分块更合适。
但如果是在医疗咨询、金融风控、司法辅助这类高准确性要求的领域,宁可慢一点,也要确保每次召回的内容都是“完整片段”,这时候语义分块的价值就凸显出来了。
看团队能力
固定分块配置简单,改个参数就行,新人也能快速上手。而语义分块涉及模型版本管理、相似度调参、异常处理等问题,对运维能力有一定要求。
建议的做法是:先用固定分块快速上线验证需求,等业务稳定后再针对重点知识库逐步替换为语义分块进行精细化优化。这是一种典型的“渐进式增强”思路。
写在最后
文档分块看似只是预处理的一个小步骤,实则是整个知识库问答系统的“第一公里”。走歪了,后面步步错。
固定长度分块像一把锋利的菜刀,简单直接,人人可用;语义感知分块则像智能料理机,功能强大,但得插电才能工作。
两者没有绝对优劣,只有适不适合。真正的高手,不是只会用高级工具的人,而是知道什么时候该用手动模式,什么时候该打开自动挡。
未来,随着动态分块、层次化分割、基于摘要的自适应切分等新方法的发展,文档处理将越来越智能化。但在当下,理解这两种基础策略的本质差异与适用边界,依然是每一位开发者必须掌握的基本功。
毕竟,让AI真正“读懂”你的知识库,第一步,就是别把它喂得太碎。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考