RAG 建库前需要把原始文件转换成结构化文本,再切成适合检索的 chunk。
flowchart LR A["原始文件"] --> B["文档解析工具"] B --> C["Markdown / JSON"] C --> D["按结构切片"] D --> E["补充元数据"] E --> F["Embedding"] F --> G[("向量数据库")]
文档清洗交给成熟工具
PDF、Office、网页和扫描件的解析涉及 OCR、版面分析、表格识别、公式识别和阅读顺序恢复,没有必要在 RAG 项目里重新实现。
比如 PDF 就有以下成熟工具:
常见切片方式
下面使用专用的文档切片库 Chonkie 演示常见切片方式。
为了让示例保持简单,这里直接用 pdftotext 将 1706.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 模型 完成检索与精排。