RAG 建库前需要把原始文件转换成结构化文本,再切成适合检索的 chunk。

flowchart LR
    A["原始文件"] --> B["文档解析工具"]
    B --> C["Markdown / JSON"]
    C --> D["按结构切片"]
    D --> E["补充元数据"]
    E --> F["Embedding"]
    F --> G[("向量数据库")]

文档清洗交给成熟工具

PDF、Office、网页和扫描件的解析涉及 OCR、版面分析、表格识别、公式识别和阅读顺序恢复,没有必要在 RAG 项目里重新实现。

比如 PDF 就有以下成熟工具:

  • RapidDoc:可将复杂 PDF 解析为 Markdown、JSON 等结构化格式;
  • MinerU:可将 PDF、图片和 Office 文档转换为适合后续处理的 Markdown 或 JSON。

常见切片方式

下面使用专用的文档切片库 Chonkie 演示常见切片方式。

为了让示例保持简单,这里直接用 pdftotext1706.03762v7.pdf 转成纯文本文件,再交给 Chonkie 切片:

from pathlib import Path
import subprocess
 
from chonkie import (
    CodeChunker,
    RecursiveChunker,
    SemanticChunker,
    SentenceChunker,
    TableChunker,
    TokenChunker,
)
 
pdf_path = next(
    path
    for path in (
        Path("rag/1706.03762v7.pdf"),
        Path("1706.03762v7.pdf"),
    )
    if path.exists()
)
text_path = pdf_path.with_suffix(".txt")
subprocess.run(["pdftotext", pdf_path, text_path], check=True)
sample_text = text_path.read_text(encoding="utf-8")
 
print("text chars:", len(sample_text))

pdftotext 适合这份以可复制文本为主的论文;复杂版面、扫描件、表格或公式仍应使用前面提到的专业解析工具。

内容类型切片方式Chonkie 入口
普通技术文档标题 → 段落 → 句子RecursiveChunker
FAQ保持问答完整SentenceChunker 或自定义分隔符
表格按行组切分并重复表头TableChunker
代码按类、函数或方法切分CodeChunker
固定长度基线按 token 或字符数切分TokenChunker
语义边界按相邻内容语义变化切分SemanticChunker
长上下文问答小块检索、返回所属大块两级 Chunker 组合

固定长度切片

固定 token 数加少量重叠,适合快速建立基线。它实现简单,但可能切断段落、表格和代码,不应该覆盖所有内容类型。

fixed_chunks = TokenChunker(
    tokenizer="character",
    chunk_size=600,
).chunk(sample_text)
 
print("fixed chunks:", len(fixed_chunks))
print("first chunk chars:", len(fixed_chunks[0].text))

句子和递归切片

SentenceChunker 尽量在句子边界切分;RecursiveChunker 会继续按段落、句子、标点、空白和字符逐级寻找边界:

sentence_chunks = SentenceChunker(
    tokenizer="character",
    chunk_size=600,
    chunk_overlap=60,
).chunk(sample_text)
 
recursive_chunks = RecursiveChunker(
    tokenizer="character",
    chunk_size=600,
).chunk(sample_text)
 
print("sentence chunks:", len(sentence_chunks))
print("recursive chunks:", len(recursive_chunks))
print("first sentence chunk chars:", len(sentence_chunks[0].text))
print("first recursive chunk chars:", len(recursive_chunks[0].text))

父子块切片

使用较小子块做精确召回,命中后取回更大的父块:

flowchart LR
    Q["Query"] --> C["检索子块"]
    C --> P["根据 parent_id 取回父块"]
    P --> R["Reranker"]
    R --> L["LLM"]

它适合答案需要较长上下文,但大块直接生成 Embedding 又不够精确的场景。

Chonkie 没有要求父子块必须由某个专用类生成,可以组合两个 Chunker:

parent_chunks = RecursiveChunker(
    tokenizer="character",
    chunk_size=1600,
).chunk(sample_text)
 
child_records = [
    {
        "parent_id": parent_index,
        "text": child.text,
    }
    for parent_index, parent in enumerate(parent_chunks)
    for child in SentenceChunker(
        tokenizer="character",
        chunk_size=400,
        chunk_overlap=40,
    ).chunk(parent.text)
]
 
print("parent chunks:", len(parent_chunks))
print("child chunks:", len(child_records))

向量数据库只索引子块;子块命中后,通过 parent_id 取回父块原文。

重叠切片

相邻 chunk 保留少量重复内容,可以降低答案正好跨越边界时的漏召回风险。

overlap_chunks = TokenChunker(
    tokenizer="character",
    chunk_size=600,
    chunk_overlap=0.1,
).chunk(sample_text)
 
print("overlap chunks:", len(overlap_chunks))
print(
    "first two ranges:",
    (overlap_chunks[0].start_index, overlap_chunks[0].end_index),
    (overlap_chunks[1].start_index, overlap_chunks[1].end_index),
)

chunk_overlap=0.1 表示保留 10% 的重叠。

重叠不是越大越好。重叠过大会:

  • 增加向量数量和存储;
  • 让 Top-N 被近乎相同的 chunk 占满;
  • 增加 Reranker 去重和排序难度。

语义、代码和表格切片

SemanticChunker 使用 Embedding 判断相邻句子的语义变化。首次运行会下载模型:

semantic_chunks = SemanticChunker(
    embedding_model="minishlab/potion-base-8M",
    chunk_size=200,
).chunk(sample_text[:5000])
 
print("semantic chunks:", len(semantic_chunks))
print(semantic_chunks[0].text[:200])

CodeChunker 使用 Tree-sitter 按代码语法结构切分。代码切片应传入真正的源代码,而不是论文文本:

code_text = """
def add(a, b):
    return a + b
 
def multiply(a, b):
    return a * b
"""
 
code_chunks = CodeChunker(
    tokenizer="character",
    chunk_size=45,
    language="python",
).chunk(code_text)
 
print("code chunks:", len(code_chunks))
for chunk in code_chunks:
    print(chunk.text)

TableChunker 接收 Markdown 或 HTML 表格。使用行数作为单位时,每个 chunk 都会保留表头:

table_text = """| Model | Score |
| --- | ---: |
| A | 0.81 |
| B | 0.84 |
| C | 0.88 |"""
 
table_chunks = TableChunker(
    tokenizer="row",
    chunk_size=2,
).chunk(table_text)
 
print("table chunks:", len(table_chunks))
for chunk in table_chunks:
    print(chunk.text)

Chunk 大小

模型最大长度是硬上限,不是推荐大小。技术文档可以先用以下配置建立基线:

目标大小:300~600 tokens
最大大小:800 tokens
重叠:10%~15%

这只是起点。不同内容应分别配置:

  • FAQ 通常不需要人为凑到目标长度;
  • 代码应优先保持函数完整;
  • 表格应优先保持表头和行组完整;
  • 父子块可以用较小子块检索、较大父块返回。

长度应使用当前 Embedding 模型的 tokenizer 计算,不要假设一个汉字等于一个 token。

每个 Chunk 保存什么?

Chunk 字段会直接决定向量数据库的表结构。下一节 [向量数据库 ](向量数据库.md# 用 - chonkie - 生成数据并在 - lancedb - 建表) 使用上面 Chonkie 生成的真实 chunk,演示字段设计和 LanceDB 建表。

建库前检查

flowchart LR
    A["解析工具输出"] --> B["抽样检查结构"]
    B --> C["执行切片"]
    C --> D["检查 Chunk 和元数据"]
    D --> E["运行 Recall@N 评测"]
    E --> F{"达标?"}
    F -- "否" --> G["调整切片参数"]
    G --> C
    F -- "是" --> H["生成 Embedding 并建库"]

至少检查:

  • 是否存在空 chunk;
  • 是否存在超过模型上限的 chunk;
  • 标题是否与正文一起保留;
  • 表格和代码是否被切断;
  • id 是否唯一;
  • 来源、页码和权限是否可追溯;
  • Top-N 中是否出现大量重复 chunk。

最终使用真实业务问题标注相关 chunk,比较不同切片配置的 Recall@N、MRR、重复率和 token 成本。没有业务评测时,所谓 “最佳 chunk 大小” 只是猜测。

完整 RAG 框架和其他索引路线见 RAG 框架和索引方案。完成切片后,可按照 向量数据库 生成 Embedding 并写入 LanceDB,再参考 RAG 召回方式Reranker 模型 完成检索与精排。