先说结论:语法结构+语义切片+父子检索
一、Markdown 的"先天优势":它生来就是半结构化的
Markdown 不是纯文本,也不是富文本,它恰好站在中间------既有明确的语法结构,又以纯文本形态存在。这给了你三种切片策略完美的施展空间:
| Markdown 特性 | 对应策略 | 为什么重要 |
|---|---|---|
# ## ### 标题层级 |
语法结构切片 | 天然的层级边界,定义了文档的"骨架",让你可以直接用标题节点作为父文档边界 |
代码块、表格、列表 |
语法结构切片 | 这些是原子信息单元,一旦被切断就失去意义。Markdown 用明确的标记语法把它们保护起来,你的切片器可以精确识别并保持其完整 |
| 标题下的长段落 | 语义切片 | 当某个章节下的说明文字过长时,你用语义模型在句子间找转折点。Markdown 的段落边界(空行)给了你初步的切分提示,语义模型再做精细修正 |
| 整个文档的层级关系 | 父子检索 | 标题的嵌套关系天然形成了"章节(父)--- 段落(子)"的树状结构,直接映射到父子文档模型,无需额外构造 |
一句话总结:Markdown 的语法标记,恰好就是文档的"结构元数据"和"切割边界指示器",你不需要靠算法去猜测,直接解析即可。
二、这三者组合起来,解决了一个根本矛盾
RAG 系统有一个永恒的权衡:粒度困境。
-
切片太大:语义被稀释,检索不准,且浪费 Token。
-
切片太小:丢失上下文,LLM 看到的是"断章取义"的碎片。
你的组合策略,恰好是破解这个困境的优雅方案:
语法结构切片 → 保证"该在一起的东西绝不分离"(代码块、表格) 语义切片 → 保证"该分开的地方在语义最弱处下刀"(长段落) 父子检索 → 保证"检索时精准匹配,返回时完整上下文"
这三者各司其职:
| 环节 | 解决的问题 |
|---|---|
| 语法结构切片 | 保底线:原子信息不被破坏 |
| 语义切片 | 提精度:在安全区域内找最佳断点 |
| 父子检索 | 补上下文:用最小的检索单元,返回最大的理解单元 |
三、父子文档在存储层面怎么放
方案一:纯向量库方案 (All-in-Vector-DB)
这是最简单直接的做法,父文档和子文档都存入同一个向量数据库,通过元数据字段区分。
存储结构设计
在你的向量库集合(Collection)中,每一条记录(一个切片)都是一个独立的文档,通过 doc_type 字段来区分身份。
| 字段名 | 父文档记录示例 | 子文档记录示例 |
|---|---|---|
doc_id |
parent_sec_01 |
child_sec_01_p1 |
doc_type |
parent |
child |
parent_doc_id |
null 或自身ID |
parent_sec_01 |
content |
## 部署流程\n\n整个部署分为环境准备、代码上传... (完整章节内容) |
环境准备阶段需要确保系统版本... (精准小切片) |
embedding |
父文档的向量 | 子文档的向量 |
metadata |
{title: "部署流程", level: 2, ...} |
{title: "部署流程", chunk_num: 1, ...} |
检索逻辑(两步走)
-
检索子文档 :用问题向量去查,并加上过滤条件
doc_type == "child",找到最相似的子文档。 -
提取父文档 :从命中的子文档记录中,读出
parent_doc_id字段。 -
二次查询 :用这个ID,再去向量库精确查找
doc_id == parent_doc_id的那条记录,这就是你要返回给LLM的完整上下文。
优点 :架构统一,一个数据库解决所有问题,管理简单。
缺点:父文档通常篇幅较长,向量化它既占用空间,其生成的向量在检索时也用不上(因为你只想搜子文档),有一定资源浪费。
方案二:向量库(存子)+ 文档存储(存父)混合方案【推荐】
这更符合父子检索的精髓:各司其职 。你完全不需要对父文档进行向量化。
存储结构设计
-
向量数据库 :只存子文档 。把你的混合切片直接存进去,并多记一个
parent_doc_id。 -
轻量级文档存储 :只存父文档。用来根据ID快速获取完整内容。可以是一个内存字典、Redis、甚至就是一个简单的JSON文件。
1. 向量库中的子文档记录
这里就没有 doc_type 了,因为全是子文档。
| 字段名 | 子文档记录示例 |
|---|---|
doc_id |
child_sec_01_p1 |
parent_doc_id |
parent_sec_01 |
content |
环境准备阶段需要确保系统版本... (这是用于生成嵌入的文本) |
embedding |
子文档的向量 |
2. 文档存储中的父文档记录
这里完全不碰向量库。
| Key (父文档ID) | Value (完整内容) |
|---|---|
parent_sec_01 |
## 部署流程\n\n整个部署分为环境准备、代码上传、服务启动三步... |
parent_sec_02 |
## 监控告警\n\n系统上线后,必须配置以下核心监控项... |
检索逻辑(更高效)
-
检索子文档 :用户提问 -> 向量库检索,直接命中最佳
child_sec_01_p1。 -
读取父文档 :拿到
parent_doc_id=parent_sec_01。 -
外部查询 :去你的字典或Redis里,用
parent_sec_01作为Key,把完整章节内容取出来。 -
返回给LLM:完整的上下文。
优点:
-
资源高效:不用为长篇幅的父文档生成和存储无用的向量,省算力、省空间。
-
更新灵活:修改父文档内容不需要重新向量化,直接修改字典/Redis即可。
-
逻辑清晰:索引和内容真正分离。
四、整体流程
┌────────────────────────────────────────────────────────────┐ │ 离线入库阶段 │ │ │ │ deploy.md │ │ │ │ │ ▼ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 语法树解析 │ → │ 定义父文档 │ → │ 混合切片(子文档) │ │ │ │ (mistune) │ │ (H2 章节) │ │ (语法+语义) │ │ │ └──────────┘ └──────┬───────┘ └────────┬─────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ │ │ │ Redis │ │ 向量数据库 │ │ │ │ (父文档) │ │ (子文档) │ │ │ └─────────┘ └──────────┘ │ │ ▲ │ │ │ └──── parent_doc_id ──┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ┌─────────────────────────▼──────────────────────────────────┐ │ 在线检索阶段 │ │ │ │ 用户: "生产环境怎么安装依赖?" │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ 向量化查询 │ │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ 命中子切片 ┌──────────┐ │ │ │ 向量数据库 │ ───────────────→│ 返回parent │ │ │ └──────────┘ "pip install..." │ _doc_id │ │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌─────────┐ │ │ │ Redis │ │ │ │ 取完整章节 │ │ │ └────┬────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 拼接Prompt │ │ │ │ 喂给 LLM │ │ │ └─────────────┘ │ │ │ │ │ ▼ │ │ LLM: "生产环境安装依赖需 │ │ 确保Python版本..." │ │ │ └────────────────────────────────────────────────────────────┘