ARTICLE · 1133598
06-RAG 文档chunk实战(下)
这一篇(下),我们上更聪明的招:语义分块、主题分块、高级策略(小大分块 + 代理式分块)、混合分块,最后给一套"到底怎么选"的决策方法。全程配代码。
三、语义分块与主题分块
1)语义分块:按"意思变了没"来切
固定字符切分的问题在于——它不懂内容。如果一段话从"产品功能"突然跳到"售后政策",按字符切很可能把两个不相关的话题切进同一块,语义就乱了。
语义分块的思路:把句子逐个转成向量,计算相邻句子的相似度;相似度高就合并到一块,相似度明显下降(话题变了)就在这里切开。
句1 —(相似度 0.85)— 句2 —(0.82)— 句3 —(0.38 ↓ 掉了!)— 句4 合并成一块 切开控制它的关键是一个相似度阈值:阈值越低,切得越细;越高,块越大。建议从 70% 左右起步再调。
from langchain_experimental.text_splitter import SemanticChunkerfrom langchain_huggingface import HuggingFaceEmbeddingsemb = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")splitter = SemanticChunker( embeddings=emb, breakpoint_threshold_type="percentile", # 按分位数判断"相似度骤降" breakpoint_threshold_amount=70# 阈值,越低切得越细)chunks = splitter.create_documents([text])注意:语义分块要对每个句子做 Embedding,比字符切分慢(还要先加载模型),但它切出来的块话题内聚,适合话题跳跃的长文。
2)主题分块:识别每段的主导主题
主题分块会识别每段文本的主导主题,在主题切换处分块。它适合长文档,但效果不太稳定、且往往要配不少自定义清洗,生产环境用得较少,更适合探索性分析。了解即可,别一上来就上。
四、高级策略:小大分块与代理式分块
先记住一个绕不开的矛盾:
| 小块 | ||
| 大块 |
1)小大分块(父文档检索)——鱼与熊掌兼得
思路很巧:用小块去检索(保证精准),命中后返回它所属的大块给大模型(保证上下文充足)。
小块(用于检索,精准命中) ──属于──▶ 大块(父块,返回给大模型作答)LangChain 里 ParentDocumentRetriever 已经封装好了这套逻辑:
from langchain.retrievers import ParentDocumentRetrieverfrom langchain.text_splitter import RecursiveCharacterTextSplitterparent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000) # 大块:给上下文child_splitter = RecursiveCharacterTextSplitter(chunk_size=200) # 小块:用于检索retriever = ParentDocumentRetriever( vectorstore=vectorstore, # 存小块的向量 docstore=docstore, # 存大块的原文 child_splitter=child_splitter, parent_splitter=parent_splitter,)retriever.add_documents(docs)# 检索时:小块命中 → 自动返回对应的大块results = retriever.invoke("公司的核心技术是什么")既保住检索精度,又提供充足上下文,这是生产里非常实用的一招。
2)代理式分块(Agentic Chunking)——让大模型自己切
最前沿的思路:直接让大语言模型读原文,动态地按语义/知识点切分。它的语义理解比机械切分强得多,但成本高、延时大,只适合小规模、高价值的知识库(科研文献、企业核心专利等),通用场景不划算。
from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplatellm = ChatOpenAI(model="……", base_url="……", api_key="……")prompt = ChatPromptTemplate.from_template("""你是顶尖的文档分析师。请把下面的文本,按“独立的知识点/主题”拆分成若干块,每块输出:标题 + 内容。文本:{doc}""")chunks = (prompt | llm).invoke({"doc": text})# 模型会按自己的理解,把长文提取成 N 个知识块(各带标题与内容)🤔 想一想:代理式分块效果最好,为什么不所有场景都用它? —— 因为它每切一次都要调大模型,成本和延时都很高,通用大规模知识库根本扛不住这个账。
五、混合分块:现实文档往往要"组合拳"
真实文档常常是结构 + 超长内容的混合体:比如企业年报,有章节标题,但标题下又是超长段落。单一策略不够用,这时候上混合分块:
先按结构切,再对超长的块做二次递归切分。
from langchain.text_splitter import ( MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter)# 第一步:按 Markdown 标题做结构切分(保留标题元数据)md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=[("#","H1"),("##","H2")])sections = md_splitter.split_text(markdown_doc)# 第二步:对超过阈值的大块,再用递归切分做二次细分recursive = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=50)final_chunks = []for sec in sections:iflen(sec.page_content) > 400: # 超阈值才二次切for piece in recursive.split_text(sec.page_content): final_chunks.append((piece, sec.metadata)) # 继承标题元数据else: final_chunks.append((sec.page_content, sec.metadata))既保留了原始结构(标题溯源),又避免单块过大,特别适合技术白皮书、年报、多格式混合文档。
六、到底怎么选?三条核心认知
讲了这么多策略,最后给你一套决策方法:
| ① 没有银弹 | |
| ② 先简单后优化 | |
| ③ 成本 + 数据说话 |
一张决策速查表:
小结
分块两篇到这里收官。核心记住三句:
- 递归字符分块是通用首选,先用它跑基线;
- 有结构就用结构、话题跳就用语义、要精度+上下文就用小大分块、复杂文档上混合分块;
- 分块是可优化的工程模块,最终用 A/B 测试(命中率、准确率)说话,别凭感觉。
分块讲透了,RAG 系统的"下限"就稳了。这个系列后面还会继续更新——LangChain 介绍与实战、RAG + Agent 的应用案例,把 RAG 从"能用"做到"好用"。
关注不迷路。