乐于分享
好东西不私藏

为什么你的 RAG 总答不准?先检查文档是怎么切的

为什么你的 RAG 总答不准?先检查文档是怎么切的

RAG 数据准备不是“文本切块加 Embedding”,而是一条包含解析、结构恢复、清洗、切分、元数据、版本、权限和质量门禁的数据 Pipeline。输入结构已经损坏时,调整 Chunk Size 只是在更细粒度地索引噪声。

一、先把文档恢复成结构
先恢复结构,再讨论切分

版面解析需要保留标题层级、段落、列表、表格、公式、页码与阅读顺序。普通数字文本型文档可以采用通用解析器;扫描件、复杂表格和多栏排版需要OCR或版面模型;低置信度页面应进入降级与人工复核。

Code

原始文档  → 页面与版面识别  → 结构化 Markdown  → 去除重复页眉页脚  → 恢复标题树和表格  → Chunk Pipeline

解析质量门禁可以检查:正文是否为空、乱码比例、标题数量、页码连续性、表格保留率和解析器置信度。失败文档不应直接进入索引。

统一 Chunk 数据模型

Code

from typing import Literalfrom pydantic import BaseModelclass Chunk(BaseModel):    document_id: str    chunk_id: str    parent_id: str | None    heading_path: list[str]    page_start: int | None    page_end: int | None    content: str    content_hash: str    token_count: int    chunk_type: Literal["section", "paragraph", "table"]    source_uri: str    document_version: str    access_level: str    embedding_version: str

元数据决定系统能否增量更新、权限过滤和引用来源。没有稳定documentidchunkid,旧版本无法清理;没有页码和标题路径,答案无法溯源;没有访问级别,检索可能越权。

二、选择适合检索的切分策略
三类切分策略
策略
优点
主要风险
固定 Token
简单、吞吐稳定
切断标题、定义和表格
结构感知
语义边界清晰
依赖上游解析质量
语义感知
可按主题变化动态切分
计算成本和实现复杂度较高

通用工程起点是“结构优先、固定长度兜底”:先按 H2/H3、段落和表格建立 Section,只有 Section 超长时才做 Sliding Window。

Code

MAX_TOKENS = 600OVERLAP_TOKENS = 80def split_section(section, tokenizer):    if token_len(section.content, tokenizer) <= MAX_TOKENS:        return [section]    units = split_by_paragraph_then_sentence(section.content)    return sliding_pack(        units=units,        max_tokens=MAX_TOKENS,        overlap_tokens=OVERLAP_TOKENS,        tokenizer=tokenizer,    )

600/80 只是实验初值。参数要结合文档结构、Embedding 上下文长度、检索指标和生成 Token 成本评估。Overlap 过小会丢边界,过大会制造重复证据并降低 ContextPrecision。

图:三类 Chunk 切分策略的边界与取舍

固定长度适合快速建立基线;结构感知能够保留标题、段落、列表和表格边界,通常应作为默认工程起点;语义切分则适合在评测集成熟后,用更高计算成本换取动态粒度。

不能轻易切开的结构
  • 标题与标题后的第一段。
  • 表头、数据行、单位和脚注。
  • 列表引导句与列表项。
  • 定义、适用范围和例外条件。
  • 公式与变量解释。
  • 代码块与对应说明。

超长表格可以重复表头后按行组切分,并保持同一个table_id。代码和公式优先整体保留,必要时把解释作为 Parent、局部代码作为 Child。

Parent-Child Retrieval

Child Chunk 粒度小,适合精确召回;Parent Section 保留完整语义,适合作为模型上下文。检索时先命中 Child,再按parent_id扩展并去重。

Code

child_hits = vector_search(query, top_k=20)parent_ids = []seen = set()for hit in child_hits:    if hit.parent_id not in seen:        parent_ids.append(hit.parent_id)        seen.add(hit.parent_id)contexts = load_parent_chunks(parent_ids[:8])

Parent 扩展后仍需 Context Builder:按相关性、来源权威性、时间和权限排序,去除内容重复,再依据 Token Budget 裁剪。

图:Parent-Child Retrieval 的检索与上下文组装时序

这条链路把“召回粒度”和“生成上下文粒度”解耦:Child 负责命中,Parent 负责补全语义;父段过长时不整段回填,而是围绕命中 Child 截取邻近窗口,再经过重排和 Token Budget 裁剪后交给模型。

三、让索引可以增量更新
内容 Hash 驱动增量索引

Code

import hashlibdef normalized_hash(text: str) -> str:    normalized = " ".join(text.split())    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
变化
动作
Section Hash 不变
复用 Chunk 与向量
Section 内容变化
删除旧 Child,重新切分与向量化
Section 删除
删除全文索引和向量索引记录
仅 Embedding 版本变化
复用解析和 Chunk,只重建向量
权限变化
更新过滤元数据并重新验证索引
四、用评测选择 Chunk 参数
怎样评测 Chunk

评测集应包含问题、必要证据、正确来源和答案。比较固定长度、结构切分、Sliding Window、Parent-Child 四种策略,至少观察 Recall@K、ContextPrecision、证据完整率、平均 Chunk 数、平均 Token 和索引成本。

还要建立结构 Hard Case:跨页表格、脚注、长列表、嵌套标题、公式和代码。平均指标无法代表这些高价值文档是否可靠。

方法小结
  • 1. 解析质量优先于 Chunk 参数。
  • 2. 结构感知切分优先,固定 Token 兜底。
  • 3. Child 负责召回,Parent 负责完整上下文。
  • 4. Chunk 必须带来源、版本、权限和内容 Hash。
  • 5. 解析、切分、Embedding 和索引应支持独立增量更新

RAG 数据准备仍然是数据工程:有模型、有主键、有版本、有质量、有权限、有增量和对账。

下一篇进入 Data Agent,说明结构化查询如何通过MDL、Schema Linking、SQLAST和数据库权限形成确定性约束链。

Data+AI项目获取/加入社群:

公众号主页私信【项目】