热闻岛
返回全网热点

RAG 中的文档怎么存?Chunking 切割策略讲透

6月24日31 阅读
RAG 中的文档怎么存?Chunking 切割策略讲透配图
RAG 的 Chunking 不是简单把文章切碎,而是决定“检索系统能不能刚好找到那一段证据”。切错了,后面再好的向量库、再贵的大模型,也只能在错误材料上努力。 一、文档为什么不能整篇存进向量库? 很多人第一次做 RAG,会直接把 PDF、
RAG 中的文档怎么存?Chunking 切割策略讲透配图

RAG 的 Chunking 不是简单把文章切碎,而是决定“检索系统能不能刚好找到那一段证据”。切错了,后面再好的向量库、再贵的大模型,也只能在错误材料上努力。

一、文档为什么不能整篇存进向量库?

很多人第一次做 RAG,会直接把 PDF、Word 或网页整篇扔进向量库,觉得“反正向量模型能理解语义”。问题在于,向量不是原文,它是压缩后的语义坐标。整篇文档越长,里面混合的话题越多,向量越容易变成一个很模糊的平均值。

举个例子,一篇产品手册里同时写了登录、退款、配送、积分、发票。如果整篇压成一个向量,用户问“退款多久到账”,检索系统可能只知道“这篇产品手册相关”,但很难精准定位到退款那一段。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

输入限制:Embedding 模型通常有输入长度限制,长文档无法无限塞进去。

语义稀释:多个主题压成一个向量,细节会被平均掉。

定位困难:召回整篇文档没有意义,LLM 最终需要的是具体证据段落。

成本浪费:整篇返回会占用大量上下文窗口,真正相关内容反而被淹没。

二、一条 Chunk 记录到底存什么?

一个合格的 RAG 存储单元,不是只有向量。每条 Chunk 记录至少要包含三部分:向量、原文、元数据。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

向量 Embedding:用于相似度搜索,系统拿它来“找”。

原文 Text:检索命中后放进 Prompt,LLM 拿它来“读”。

元数据 Metadata:记录来源文件、页码、标题路径、权限、更新时间,用于过滤、溯源和治理。

向量和原文不能互相替代。向量是一串浮点数,LLM 不能直接读;原文没有向量,又无法做语义检索。metadata 更不能省,它决定企业级 RAG 能不能做权限隔离、引用来源和增量更新。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

三、Chunk 粒度怎么定?

Chunk 大小没有银弹。常见经验是从 500 到 1000 token 之间起步,但这只是起点,不是标准答案。真正的答案要结合文档类型、问题类型、Embedding 模型、TopK、上下文窗口和评测结果来定。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

太小:语义容易断,召回碎片多,LLM 拿到一段话可能不知道上下文。

太大:向量语义被平均,噪声增加,TopK 很快塞满上下文。

合理:一个 Chunk 应该围绕一个小话题,能被独立理解,又不会混入太多无关信息。

四、策略一:固定大小 + 重叠

固定大小切割是最容易落地的方案:按照字符数或 token 数切块,再加一段 overlap 重叠,避免把边界处的句子切碎。它适合纯文本兜底,但不要把它当成所有场景的最终答案。

RAG 中的文档怎么存?Chunking 切割策略讲透配图
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=120,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
)
chunks = splitter.create_documents(
[text],
metadatas=[{"source": "refund_manual.md", "doc_type": "manual"}],
)
for i, chunk in enumerate(chunks):
chunk.metadata["chunk_index"] = i

这里的重点不是参数本身,而是分隔符优先级。好的切割器会尽量先保留段落,再保留句子,实在不行才退到标点、空格甚至字符级。

五、策略二:语义边界 / 结构化切割

固定大小的问题是“只看长度,不看语义”。如果文档本身有结构,比如 Markdown 标题、HTML 层级、产品手册目录,就应该顺着结构来切。这样每个 Chunk 的主题更完整,metadata 里还可以保留标题路径。

RAG 中的文档怎么存?Chunking 切割策略讲透配图
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_text)
# 每个 chunk 的 metadata 会带上标题路径,便于过滤和溯源
for chunk in chunks:
print(chunk.metadata, chunk.page_content[:80])

语义切割常见做法是“先粗后细”:先按标题切,如果太大再按段落;段落还太大,再按句子;句子还太大,最后才按 token 或字符。

六、策略三:特殊内容专项处理

代码、表格、图片说明这些内容不能用同一把刀。普通文本按段落切可以,但代码最好按函数或类切,表格最好整表保留,图片要把 OCR、Caption 和周边说明一起入库。

RAG 中的文档怎么存?Chunking 切割策略讲透配图
import ast
def split_python_by_function(source_code: str):
tree = ast.parse(source_code)
lines = source_code.splitlines()
chunks = []
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
start = node.lineno - 1
end = getattr(node, "end_lineno", node.lineno)
text = "\n".join(lines[start:end])
chunks.append({
"name": node.name,
"type": type(node).__name__,
"text": text,
"metadata": {"start_line": start + 1, "end_line": end},
})
return chunks

表格尤其容易被切坏。单独一行“2 小时”没有意义,只有配上表头“响应时间:2 小时”才有语义。因此表格可以转成 Markdown 表格,或者按主题拆成小表,但不能随便按行切。

七、策略四:父子 Chunking

父子 Chunking 是工程里非常实用的增强策略。它把“检索粒度”和“阅读粒度”拆开:小 Chunk 用于精准召回,大 Chunk 用于给 LLM 阅读完整上下文。

RAG 中的文档怎么存?Chunking 切割策略讲透配图
# 简化示例:小 chunk 建索引,大 chunk 作为 parent 存储
parent_chunks = split_by_section(document_text, max_tokens=1200)
for parent_id, parent_text in enumerate(parent_chunks):
children = split_by_tokens(parent_text, chunk_size=220, overlap=40)
doc_store[parent_id] = parent_text
for child_id, child_text in enumerate(children):
vector_store.add(
text=child_text,
embedding=embed(child_text),
metadata={
"parent_id": parent_id,
"child_id": child_id,
"source": "refund_manual.md",
},
)
# 查询时:先命中 child,再取 parent 作为上下文
hits = vector_store.search(embed(query), top_k=5)
parent_ids = {hit.metadata["parent_id"] for hit in hits}
contexts = [doc_store[parent_id] for parent_id in parent_ids]

父子 Chunking 的代价是存储和索引更复杂,但在企业知识库问答、制度检索、合同问答等需要完整上下文的场景里,它通常比单一粒度更稳。

八、进阶策略:Late Chunking

传统 Chunking 是先切块,再分别送入 Embedding 模型。这样每个块在编码时看不到周围上下文。Late Chunking 反过来:先让长上下文 Embedding 模型处理整篇文档,得到 token 级表示,再按 Chunk 边界做 pooling。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

优点:每个 Chunk 的向量能保留更长范围的上下文信息。

适合:长文档、代词引用多、上下文依赖强的材料。

限制:需要支持长上下文且能拿到 token 级向量的 Embedding 模型,工程实现成本更高。

如果你的系统用的是封闭 Embedding API,只能拿到整段文本的向量,通常无法直接实现完整 Late Chunking。可以先用父子 Chunk、标题 metadata、上下文补齐等方案获得稳定收益。

九、不同 Chunking 策略怎么选?

不要把 Chunking 当成一个固定参数,而要当成一套策略路由。不同文档类型走不同的切割器,最后统一落到相同的 Chunk Schema。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

一个比较稳的默认组合是:普通文本用递归切割做兜底;Markdown/HTML 按标题结构切;代码按 AST 或语法节点切;表格转 Markdown 后整体保留;高质量问答用父子 Chunk;长文档场景再尝试 Late Chunking。

十、Chunking 要怎么评测?

Chunking 不能靠感觉。你需要准备一批真实问题和标准答案,然后用多组切分参数跑对比实验。只看“回答看起来不错”不够,至少要拆成检索指标和生成指标两层看。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

检索侧:Recall@K、MRR、nDCG、Context Precision,看证据有没有被找回来、有没有排在前面。

生成侧:Faithfulness、Answer Relevancy,看答案是否基于证据、是否回答了问题。

线上侧:无答案率、引用命中率、用户追问率、人工纠错率。

评测时要注意控制变量:同一批问题、同一个 Embedding 模型、同一个 TopK、同一个生成模型,只替换切分策略和参数。否则你无法判断提升到底来自哪里。

十一、生产级落地架构

生产环境里,Chunking 不能写死在一个脚本里。它应该是数据流水线的一部分,具备版本管理、权限过滤、质量评测和回滚能力。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

Chunk Schema 要稳定:chunk_id、parent_id、doc_id、title_path、page、offset、version、ACL。

切分策略要可配置:不同 doc_type 走不同 splitter,便于灰度和回滚。

索引要可重建:切分策略升级后,需要支持重新生成 Chunk 与 Embedding。

权限要前置:metadata 过滤必须发生在检索阶段,不能等生成阶段再补救。

引用要可追溯:答案里的每个关键事实都能回到源文件、页码、标题。

十二、常见坑

只按字符切,不看句子和标题,导致语义被截断。

Overlap 过大,索引膨胀,重复召回严重。

Chunk 里没有标题路径,用户问“申请条件”时无法区分属于哪个业务。

metadata 缺少权限字段,后期做企业级隔离非常痛苦。

表格拆成单行,LLM 拿到数字但不知道数字是什么意思。

没有评测集,参数全靠拍脑袋。

十三、面试怎么回答?

如果面试官问“RAG 中的文档怎么存?粒度多大?Chunking 怎么做?”,可以按下面这个顺序回答。

RAG 中的文档怎么存?Chunking 切割策略讲透配图

推荐回答:文档不能整篇存进向量库,通常要先切成 Chunk。每条 Chunk 记录包含向量、原文和 metadata。粒度没有固定答案,我会从 500 到 1000 token 试起,但会根据文档类型调整策略。普通文本用固定大小加重叠做兜底,有标题结构的文档按语义边界切,代码按函数或类切,表格整表保留。对高质量问答,我会用父子 Chunking,小块负责检索,大块负责给 LLM 阅读。最后用 Recall@K、MRR、Context Precision、Faithfulness 等指标做评测,而不是靠感觉调参数。

要点速读

RAG 的 Chunking 不是简单把文章切碎,而是决定“检索系统能不能刚好找到那一段证据”。切错了,后面再好的向量库

  • RAG 的 Chunking 不是简单把文章切碎,而是决定“检索系统能不能刚好找到那一段证据”
  • 切错了,后面再好的向量库
  • 更多细节仍在持续更新中