欢迎光临
我们一直在努力

RAG 父子分块关联设计:解决检索与上下文矛盾

RAG 父子分块关联设计:解决检索与上下文矛盾

你是不是也卡在这一步

搭了个 RAG 系统,文档切了块、向量建了索引、检索也跑通了,结果问答效果一塌糊涂——

  • 问"公司请假流程第三步是什么",AI 回答了第一步和第二步,第三步的内容在下一个块里,检索根本没命中?

  • 把块切大一点(800 字),上下文倒是完整了,但向量检索的精度直线下降——一个 800 字的块里混了三个话题,查询"请假扣薪规则"命中了讲"请假审批流程"的块,语义被稀释?

  • 把块切小一点(200 字),精度上去了,但每个块都只有半截信息,AI 拿到的上下文支离破碎,回答永远在说"根据文档……"但具体根据哪段完全对不上?

  • 切大也不行,切小也不行,到底有没有办法让"精准匹配"和"完整上下文"同时满足?

如果中了任何一条,问题不在模型,也不在嵌入算法,而在你的分块策略——固定大小切分天生就无法同时兼顾检索精度和上下文完整性。

父子分块(Parent-Child Chunking)就是为解决这个问题而生的。核心思路一句话:用小块做检索(精准匹配),用大块做生成(完整上下文),两者通过 parent_id 关联。

#mermaid-svg-NOKRwPgjQvAIOcjM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NOKRwPgjQvAIOcjM .error-icon{fill:#552222;}#mermaid-svg-NOKRwPgjQvAIOcjM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NOKRwPgjQvAIOcjM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NOKRwPgjQvAIOcjM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NOKRwPgjQvAIOcjM .marker.cross{stroke:#333333;}#mermaid-svg-NOKRwPgjQvAIOcjM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NOKRwPgjQvAIOcjM p{margin:0;}#mermaid-svg-NOKRwPgjQvAIOcjM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster-label text{fill:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster-label span{color:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster-label span p{background-color:transparent;}#mermaid-svg-NOKRwPgjQvAIOcjM .label text,#mermaid-svg-NOKRwPgjQvAIOcjM span{fill:#333;color:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM .node rect,#mermaid-svg-NOKRwPgjQvAIOcjM .node circle,#mermaid-svg-NOKRwPgjQvAIOcjM .node ellipse,#mermaid-svg-NOKRwPgjQvAIOcjM .node polygon,#mermaid-svg-NOKRwPgjQvAIOcjM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NOKRwPgjQvAIOcjM .rough-node .label text,#mermaid-svg-NOKRwPgjQvAIOcjM .node .label text,#mermaid-svg-NOKRwPgjQvAIOcjM .image-shape .label,#mermaid-svg-NOKRwPgjQvAIOcjM .icon-shape .label{text-anchor:middle;}#mermaid-svg-NOKRwPgjQvAIOcjM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NOKRwPgjQvAIOcjM .rough-node .label,#mermaid-svg-NOKRwPgjQvAIOcjM .node .label,#mermaid-svg-NOKRwPgjQvAIOcjM .image-shape .label,#mermaid-svg-NOKRwPgjQvAIOcjM .icon-shape .label{text-align:center;}#mermaid-svg-NOKRwPgjQvAIOcjM .node.clickable{cursor:pointer;}#mermaid-svg-NOKRwPgjQvAIOcjM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NOKRwPgjQvAIOcjM .arrowheadPath{fill:#333333;}#mermaid-svg-NOKRwPgjQvAIOcjM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NOKRwPgjQvAIOcjM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NOKRwPgjQvAIOcjM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NOKRwPgjQvAIOcjM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NOKRwPgjQvAIOcjM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NOKRwPgjQvAIOcjM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster text{fill:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM .cluster span{color:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NOKRwPgjQvAIOcjM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NOKRwPgjQvAIOcjM rect.text{fill:none;stroke-width:0;}#mermaid-svg-NOKRwPgjQvAIOcjM .icon-shape,#mermaid-svg-NOKRwPgjQvAIOcjM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NOKRwPgjQvAIOcjM .icon-shape p,#mermaid-svg-NOKRwPgjQvAIOcjM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NOKRwPgjQvAIOcjM .icon-shape .label rect,#mermaid-svg-NOKRwPgjQvAIOcjM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NOKRwPgjQvAIOcjM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NOKRwPgjQvAIOcjM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NOKRwPgjQvAIOcjM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

父子分块

文档

切父块 800~2000 tokens

父块存 docstore

切子块 100~300 tokens

子块向量化入向量库

检索命中子块

通过 parent_id 找父块

返回父块给 LLM

固定分块

文档

按 500 字一刀切

每块独立向量化

检索返回该块

切大: 语义稀释

切小: 上下文断裂

固定分块 vs 父子分块

| 对比维度 | 固定大小切分 | 父子分块 |

| — | — | — |

| 切块策略 | 一刀切,每块大小相同 | 两级切分,父块大、子块小 |

| 检索对象 | 直接检索原始块 | 检索子块,返回父块 |

| 检索精度 | 块越大精度越低,块越小精度越高 | 子块小,精度高 |

| 上下文完整性 | 块越小越容易断裂 | 父块大,上下文完整 |

| 存储成本 | 一份向量 | 子块向量 + 父块原文 |

| 实现复杂度 | 极低 | 中等(需维护关联关系) |

| 适合场景 | 短文档、简单问答 | 长文档、多主题、高精度问答 |

| Token 浪费 | 可能返回大量无关内容 | 子块精准命中,父块按需返回 |

一句话:固定分块是"一个尺寸通吃",父子分块是"检索用小尺子,生成用大尺子,各干各的活"。

一、核心概念:什么是父子分块

1.1 两个角色

父块(Parent Chunk):文档的第一级切分,通常 800~2000 tokens。保留完整的语义上下文——一个父块可能包含一整节内容,比如"请假流程的完整说明"。父块不做向量化,直接存入文档存储(docstore,就是一个键值对仓库,key 是块 ID,value 是块原文,你可以理解为一个字典),通过唯一 ID 被子块引用。

子块(Child Chunk):父块的二次切分,通常 100~300 tokens。语义聚焦——一个子块只讲一个点,比如"请假流程第三步:提交审批"。子块做向量化,存入向量库,是检索的入口。

打个比方:父块是一本书的某一章,子块是这一章里的某一节。你通过目录(向量检索)找到具体那一节(子块),但阅读时翻开的是整章(父块),这样上下文不会断。

1.2 关联机制:parent_id

父子块之间的关联通过元数据字段实现,核心字段就一个:parent_id。

#mermaid-svg-76i9v2KHnfTHzOZ9{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-76i9v2KHnfTHzOZ9 .error-icon{fill:#552222;}#mermaid-svg-76i9v2KHnfTHzOZ9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-76i9v2KHnfTHzOZ9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .marker.cross{stroke:#333333;}#mermaid-svg-76i9v2KHnfTHzOZ9 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-76i9v2KHnfTHzOZ9 p{margin:0;}#mermaid-svg-76i9v2KHnfTHzOZ9 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster-label text{fill:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster-label span{color:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster-label span p{background-color:transparent;}#mermaid-svg-76i9v2KHnfTHzOZ9 .label text,#mermaid-svg-76i9v2KHnfTHzOZ9 span{fill:#333;color:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .node rect,#mermaid-svg-76i9v2KHnfTHzOZ9 .node circle,#mermaid-svg-76i9v2KHnfTHzOZ9 .node ellipse,#mermaid-svg-76i9v2KHnfTHzOZ9 .node polygon,#mermaid-svg-76i9v2KHnfTHzOZ9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .rough-node .label text,#mermaid-svg-76i9v2KHnfTHzOZ9 .node .label text,#mermaid-svg-76i9v2KHnfTHzOZ9 .image-shape .label,#mermaid-svg-76i9v2KHnfTHzOZ9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-76i9v2KHnfTHzOZ9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .rough-node .label,#mermaid-svg-76i9v2KHnfTHzOZ9 .node .label,#mermaid-svg-76i9v2KHnfTHzOZ9 .image-shape .label,#mermaid-svg-76i9v2KHnfTHzOZ9 .icon-shape .label{text-align:center;}#mermaid-svg-76i9v2KHnfTHzOZ9 .node.clickable{cursor:pointer;}#mermaid-svg-76i9v2KHnfTHzOZ9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .arrowheadPath{fill:#333333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-76i9v2KHnfTHzOZ9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-76i9v2KHnfTHzOZ9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-76i9v2KHnfTHzOZ9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster text{fill:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 .cluster span{color:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-76i9v2KHnfTHzOZ9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-76i9v2KHnfTHzOZ9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-76i9v2KHnfTHzOZ9 .icon-shape,#mermaid-svg-76i9v2KHnfTHzOZ9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-76i9v2KHnfTHzOZ9 .icon-shape p,#mermaid-svg-76i9v2KHnfTHzOZ9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-76i9v2KHnfTHzOZ9 .icon-shape .label rect,#mermaid-svg-76i9v2KHnfTHzOZ9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-76i9v2KHnfTHzOZ9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-76i9v2KHnfTHzOZ9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-76i9v2KHnfTHzOZ9 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

父块 Parent Chunk

子块 Child Chunk

parent_id 回溯

id: child_003

text: 请假流程第三步

metadata.parent_id = parent_001

embedding: 0.12, -0.34…

id: parent_001

text: 3.1 请假流程完整说明

metadata: source, page

无 embedding 不做向量化

关联流程就三步:

  • 索引阶段:文档 → 切父块 → 父块存 docstore → 父块再切子块 → 每个子块注入 parent_id → 子块向量化入向量库
  • 检索阶段:用户 query → 向量检索命中最相似的子块 → 从子块元数据取出 parent_id → 去 docstore 捞出对应父块
  • 生成阶段:把父块(而非子块)作为上下文喂给 LLM → LLM 拿到完整上下文 → 生成准确回答
  • #mermaid-svg-7mEjFBWxlYP8R7Qk{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7mEjFBWxlYP8R7Qk .error-icon{fill:#552222;}#mermaid-svg-7mEjFBWxlYP8R7Qk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7mEjFBWxlYP8R7Qk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .marker.cross{stroke:#333333;}#mermaid-svg-7mEjFBWxlYP8R7Qk svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7mEjFBWxlYP8R7Qk p{margin:0;}#mermaid-svg-7mEjFBWxlYP8R7Qk .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster-label text{fill:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster-label span{color:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster-label span p{background-color:transparent;}#mermaid-svg-7mEjFBWxlYP8R7Qk .label text,#mermaid-svg-7mEjFBWxlYP8R7Qk span{fill:#333;color:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .node rect,#mermaid-svg-7mEjFBWxlYP8R7Qk .node circle,#mermaid-svg-7mEjFBWxlYP8R7Qk .node ellipse,#mermaid-svg-7mEjFBWxlYP8R7Qk .node polygon,#mermaid-svg-7mEjFBWxlYP8R7Qk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .rough-node .label text,#mermaid-svg-7mEjFBWxlYP8R7Qk .node .label text,#mermaid-svg-7mEjFBWxlYP8R7Qk .image-shape .label,#mermaid-svg-7mEjFBWxlYP8R7Qk .icon-shape .label{text-anchor:middle;}#mermaid-svg-7mEjFBWxlYP8R7Qk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .rough-node .label,#mermaid-svg-7mEjFBWxlYP8R7Qk .node .label,#mermaid-svg-7mEjFBWxlYP8R7Qk .image-shape .label,#mermaid-svg-7mEjFBWxlYP8R7Qk .icon-shape .label{text-align:center;}#mermaid-svg-7mEjFBWxlYP8R7Qk .node.clickable{cursor:pointer;}#mermaid-svg-7mEjFBWxlYP8R7Qk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .arrowheadPath{fill:#333333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7mEjFBWxlYP8R7Qk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7mEjFBWxlYP8R7Qk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7mEjFBWxlYP8R7Qk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster text{fill:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk .cluster span{color:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7mEjFBWxlYP8R7Qk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7mEjFBWxlYP8R7Qk rect.text{fill:none;stroke-width:0;}#mermaid-svg-7mEjFBWxlYP8R7Qk .icon-shape,#mermaid-svg-7mEjFBWxlYP8R7Qk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7mEjFBWxlYP8R7Qk .icon-shape p,#mermaid-svg-7mEjFBWxlYP8R7Qk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7mEjFBWxlYP8R7Qk .icon-shape .label rect,#mermaid-svg-7mEjFBWxlYP8R7Qk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7mEjFBWxlYP8R7Qk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7mEjFBWxlYP8R7Qk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7mEjFBWxlYP8R7Qk :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    用户 Query

    向量检索子块

    命中子块

    读取 parent_id

    从 docstore 取父块

    父块作为上下文喂给 LLM

    LLM 生成回答

    1.3 元数据字段设计

    除了 parent_id,实际项目中还常用这些字段来增强检索和管理:

    | 字段 | 类型 | 作用 | 示例值 |

    | — | — | — | — |

    | parent_id | string | 子块关联父块的唯一标识 | "parent_001" |

    | child_id | string | 子块自身唯一标识 | "child_003" |

    | child_index | int | 子块在父块中的顺序(从 0 开始) | 2 |

    | source | string | 原始文档名 | "员工手册.pdf" |

    | page | int | 原文页码 | 15 |

    | section | string | 所属章节标题 | "3.1 请假流程" |

    | token_count | int | 块的 token 数 | 187 |

    | embedding_model | string | 向量化用的模型名 | "bge-m3" |

    parent_id 是必须的,其他字段按需添加。如果你做的是多文档系统,source 和 page 一定要加——出了问题得能追溯到原文。

    二、父子分块关联架构设计

    这一章是全文的核心:不依赖任何框架,从架构层面讲清楚父子关联怎么设计。不管你用 LangChain、LlamaIndex 还是完全自研,这套设计思路都通用。

    2.1 三种关联架构总览

    目前主流的父子关联方案有三种,适用于不同场景:

    | 架构 | 关联方式 | 代表框架 | 适合场景 |

    | — | — | — | — |

    | 显式 parent_id | 子块元数据中存储父块 ID | LangChain | 需要精细控制关联字段 |

    | 隐式 relationships | 框架自动维护节点间层级关系 | LlamaIndex | 希望减少手动管理 |

    | 数据库单表 | 同一张表通过 parent_id 字段自关联 | 自研 RAG | 不用框架、完全自主控制 |

    架构一:显式 parent_id(LangChain 模式)

    子块元数据中显式存储 parent_id 字段,父块存储在独立的 docstore 中。检索时:子块命中 → 读 parent_id → 查 docstore → 返回父块。关联关系清晰可控,可以自定义更多元数据字段,但需要维护两个存储系统(向量库 + docstore)。

    架构二:隐式 relationships(LlamaIndex 模式)

    Node(LlamaIndex 中的基本数据单元,类似 LangChain 的 Document,但自带层级关系)通过 relationships 字段(Node 的元数据字段,记录与其他 Node 的父子关系)自动维护父子关系。不需要显式管理 parent_id,框架自动处理回溯。封装更彻底,代码量少,但关联逻辑隐藏在框架内部,排查问题时不直观。

    架构三:数据库单表自关联(自研模式)

    不用任何框架,自己设计数据库表。父块和子块存在同一张表,通过 parent_id 字段自关联。向量可以用 pgvector(PostgreSQL 的向量检索扩展)或单独的向量库。完全可控,适合自研 RAG 系统。

    2.2 自研场景:MySQL 单表设计

    如果你不用 LangChain 也不用 LlamaIndex,而是自己搭 RAG 系统,最直接的方案是用一张表同时存父块和子块:

    — 父子分块单表设计:一张表搞定父块 + 子块

    CREATE TABLE rag_chunks (

    id VARCHAR(64) PRIMARY KEY,

    parent_id VARCHAR(64) DEFAULT NULL, — NULL 表示父块,非 NULL 表示子块

    chunk_type ENUM('parent', 'child') NOT NULL,

    content TEXT NOT NULL, — 块原文

    embedding JSON DEFAULT NULL, — 子块存向量(JSON 数组),父块为 NULL

    source VARCHAR(255) DEFAULT NULL, — 原始文档名

    page INT DEFAULT NULL, — 原文页码

    section VARCHAR(255) DEFAULT NULL, — 所属章节

    child_index INT DEFAULT NULL, — 子块在父块中的顺序

    token_count INT DEFAULT 0, — 块的 token 数

    embedding_model VARCHAR(100) DEFAULT NULL, — 向量化模型名

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    INDEX idx_parent_id (parent_id),

    INDEX idx_chunk_type (chunk_type),

    INDEX idx_source (source)

    );

    查询逻辑非常直观:

    — 场景 1:向量检索命中子块后,通过 parent_id 找父块

    SELECT * FROM rag_chunks

    WHERE id = (

    SELECT parent_id FROM rag_chunks WHERE id = 'child_003'

    );

    — 场景 2:查看某个父块下的所有子块(按顺序排列)

    SELECT * FROM rag_chunks

    WHERE parent_id = 'parent_001'

    ORDER BY child_index;

    — 场景 3:批量查询多个命中子块对应的父块(实际检索场景)

    SELECT DISTINCT parent.* FROM rag_chunks AS child

    JOIN rag_chunks AS parent ON child.parent_id = parent.id

    WHERE child.id IN ('child_003', 'child_007', 'child_012');

    向量检索本身不在 MySQL 里做(MySQL 不擅长向量相似度计算),而是用外部向量库(如 Milvus、Qdrant)或 PostgreSQL 的 pgvector 扩展。MySQL 只负责存原文和关联关系。检索流程:向量库返回子块 ID 列表 → MySQL 根据 ID 查原文和 parent_id → 返回父块。

    2.3 单向关联 vs 双向关联

    设计关联字段时,有一个关键决策:只在子块上存 parent_id(单向),还是同时在父块上也存 child_ids 列表(双向)?

    单向关联(推荐):只有子块存 parent_id,父块不知道自己有哪些子块。找父块 O(1)(直接用 parent_id 查),找子块 O(n)(需要 WHERE parent_id = ? 扫描)。适合绝大多数 RAG 场景——检索时只需要"子找父",不需要"父找子"。

    双向关联:子块存 parent_id,父块也存 child_ids(JSON 数组)。找父块 O(1),找子块 O(1)(直接读父块的 child_ids)。适合需要频繁"父找子"的场景,比如文档更新时批量删除某父块下的所有子块。

    | 维度 | 单向关联 | 双向关联 |

    | — | — | — |

    | 存储成本 | 低 | 高(父块多存 child_ids) |

    | 子找父 | O(1) | O(1) |

    | 父找子 | O(n),需查询 | O(1),直接读 |

    | 一致性维护 | 简单 | 复杂(增删子块需同步更新父块) |

    | 推荐场景 | 大多数 RAG 系统 | 需要频繁父找子 |

    结论:除非你的业务有高频"父找子"需求(比如文档版本更新时批量删子块),否则用单向关联就够了。双向关联的一致性维护成本很容易被低估——每次增删子块都要同步更新父块的 child_ids,一旦漏了就是脏数据。

    2.4 父子 ID 生成规则

    ID 设计看似小事,但直接影响调试效率和系统稳定性:

    | 方案 | 格式 | 优点 | 缺点 |

    | — | — | — | — |

    | UUID | 550e8400-e29b-41d4… | 全局唯一,无碰撞 | 长,不可读,调试困难 |

    | ULID | 01ARZ3NDEKTSV4RRFFQ69G5FAV | 唯一 + 时间排序 | 需引入额外库 |

    | 复合 ID | handbook_p0_c2 | 可读性极强,调试方便 | 需保证命名规范 |

    | 哈希 ID | a3f2b1c8 | 短,固定长度 | 不可读,有碰撞风险 |

    推荐用复合 ID,格式:{doc_abbrev}_{p_index}_{c_index}

    • 父块:handbook_p0、handbook_p1、handbook_p2
    • 子块:handbook_p0_c0、handbook_p0_c1、handbook_p0_c2

    这样调试时一眼就能看出是哪个文档、第几个父块、第几个子块。出问题时日志里看到 handbook_p0_c2,立刻知道是"员工手册第一个父块的第三个子块",而看到 550e8400-e29b-41d4… 只能一头雾水。

    多文档场景下,文档缩写要做映射表,避免冲突。比如 handbook 是员工手册、policy 是制度文件、manual 是技术手册。在文档入库时自动生成缩写并注册到映射表中。

    三、LangChain 实战:ParentDocumentRetriever

    LangChain 把父子分块封装成了 ParentDocumentRetriever(父文档检索器:封装了父子分块的完整流程,自动管理切分、存储和回溯),开箱即用,核心就四个组件。

    3.1 架构总览

    #mermaid-svg-NXbc856FIAxpWCT0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NXbc856FIAxpWCT0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NXbc856FIAxpWCT0 .error-icon{fill:#552222;}#mermaid-svg-NXbc856FIAxpWCT0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NXbc856FIAxpWCT0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NXbc856FIAxpWCT0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NXbc856FIAxpWCT0 .marker.cross{stroke:#333333;}#mermaid-svg-NXbc856FIAxpWCT0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NXbc856FIAxpWCT0 p{margin:0;}#mermaid-svg-NXbc856FIAxpWCT0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NXbc856FIAxpWCT0 .cluster-label text{fill:#333;}#mermaid-svg-NXbc856FIAxpWCT0 .cluster-label span{color:#333;}#mermaid-svg-NXbc856FIAxpWCT0 .cluster-label span p{background-color:transparent;}#mermaid-svg-NXbc856FIAxpWCT0 .label text,#mermaid-svg-NXbc856FIAxpWCT0 span{fill:#333;color:#333;}#mermaid-svg-NXbc856FIAxpWCT0 .node rect,#mermaid-svg-NXbc856FIAxpWCT0 .node circle,#mermaid-svg-NXbc856FIAxpWCT0 .node ellipse,#mermaid-svg-NXbc856FIAxpWCT0 .node polygon,#mermaid-svg-NXbc856FIAxpWCT0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NXbc856FIAxpWCT0 .rough-node .label text,#mermaid-svg-NXbc856FIAxpWCT0 .node .label text,#mermaid-svg-NXbc856FIAxpWCT0 .image-shape .label,#mermaid-svg-NXbc856FIAxpWCT0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-NXbc856FIAxpWCT0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NXbc856FIAxpWCT0 .rough-node .label,#mermaid-svg-NXbc856FIAxpWCT0 .node .label,#mermaid-svg-NXbc856FIAxpWCT0 .image-shape .label,#mermaid-svg-NXbc856FIAxpWCT0 .icon-shape .label{text-align:center;}#mermaid-svg-NXbc856FIAxpWCT0 .node.clickable{cursor:pointer;}#mermaid-svg-NXbc856FIAxpWCT0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NXbc856FIAxpWCT0 .arrowheadPath{fill:#333333;}#mermaid-svg-NXbc856FIAxpWCT0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NXbc856FIAxpWCT0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NXbc856FIAxpWCT0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NXbc856FIAxpWCT0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NXbc856FIAxpWCT0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NXbc856FIAxpWCT0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NXbc856FIAxpWCT0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NXbc856FIAxpWCT0 .cluster text{fill:#333;}#mermaid-svg-NXbc856FIAxpWCT0 .cluster span{color:#333;}#mermaid-svg-NXbc856FIAxpWCT0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NXbc856FIAxpWCT0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NXbc856FIAxpWCT0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-NXbc856FIAxpWCT0 .icon-shape,#mermaid-svg-NXbc856FIAxpWCT0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NXbc856FIAxpWCT0 .icon-shape p,#mermaid-svg-NXbc856FIAxpWCT0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NXbc856FIAxpWCT0 .icon-shape .label rect,#mermaid-svg-NXbc856FIAxpWCT0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NXbc856FIAxpWCT0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NXbc856FIAxpWCT0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NXbc856FIAxpWCT0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    ParentDocumentRetriever

    父块

    父块再切

    子块 + parent_id

    parent_id 回溯

    parent_splitter父块切分器800~2000 tokens

    docstore父块存储InMemoryStore / Redis

    child_splitter子块切分器100~300 tokens

    vectorstore子块向量库Chroma / FAISS

    四个组件的职责:

    • parent_splitter:递归字符切分器(按分隔符优先级递归切分文本,先按段落切,段落太大再按句子切),把文档切成大块
    • child_splitter:同样的切分器,但参数不同,把父块再切成小块
    • docstore:文档存储,存父块原文,开发用 InMemoryStore(内存级键值存储,重启数据就丢,生产环境要换 Redis)
    • vectorstore:向量库,存子块向量和元数据,开发用 Chroma(轻量级向量数据库,适合开发阶段),生产用 Milvus

    3.2 完整代码

    from langchain.retrievers import ParentDocumentRetriever
    from langchain.storage import InMemoryStore
    from langchain_chroma import Chroma
    from langchain_community.document_loaders import TextLoader
    from langchain_ollama import OllamaEmbeddings
    from langchain_text_splitters import RecursiveCharacterTextSplitter

    # === 1. 加载文档 ===
    loader = TextLoader("employee_handbook.txt")
    docs = loader.load()

    # === 2. 配置嵌入模型(本地 Ollama,免费)===
    embeddings = OllamaEmbeddings(model="nomic-embed-text")

    # === 3. 配置父块切分器和子块切分器 ===
    # 父块:大块,保留完整上下文
    parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=2000,
    chunk_overlap=200,
    )

    # 子块:小块,精准语义匹配
    child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400,
    chunk_overlap=50,
    )

    # === 4. 初始化向量库(存子块)和文档存储(存父块)===
    vectorstore = Chroma(
    collection_name="child_chunks",
    embedding_function=embeddings,
    )

    # docstore:InMemoryStore 是内存级存储,重启就丢
    # 生产环境换成 Redis:from langchain.storage import RedisStore
    docstore = InMemoryStore()

    # === 5. 创建 ParentDocumentRetriever ===
    retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=docstore,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
    )

    # === 6. 索引文档(自动完成:切父块 → 存 docstore → 切子块 → 向量化)===
    retriever.add_documents(docs)

    # === 7. 检索 ===
    query = "请假流程第三步是什么"
    results = retriever.invoke(query)

    for i, doc in enumerate(results):
    print(f"— 结果 {i+1} —")
    print(f"内容: {doc.page_content[:200]}")
    print(f"元数据: {doc.metadata}")
    print()

    运行后你会看到:检索返回的是父块(完整上下文),而不是子块。虽然向量检索命中的是子块(因为子块小、语义聚焦),但最终返回给你的 LLM 的是父块——这就是父子分块的魔力。

    3.3 关键参数调优

    | 参数 | 作用 | 推荐值 | 调优建议 |

    | — | — | — | — |

    | parent_splitter.chunk_size | 父块大小 | 1500~2000 | 太大浪费 Token,太小上下文不全 |

    | parent_splitter.chunk_overlap | 父块重叠 | 200 | 防止边界信息丢失 |

    | child_splitter.chunk_size | 子块大小 | 200~400 | 太小语义不完整,太大精度下降 |

    | child_splitter.chunk_overlap | 子块重叠 | 50 | 保持子块间衔接 |

    | vectorstore | 向量库选择 | Chroma(开发)/ Milvus(生产) | 生产环境别用 InMemoryStore |

    | docstore | 父块存储 | InMemoryStore(开发)/ Redis(生产) | 父块量大时换持久化存储 |

    父块大小和子块大小的比值建议在 5:1 到 10:1 之间。比如父块 2000 tokens,子块 200~400 tokens。比值太小(比如 2:1)父子分块退化为固定分块,没有意义;比值太大(比如 20:1)一个父块关联太多子块,检索时容易捞到大量无关内容。

    3.4 进阶:只检索不返回父块(子块模式)

    有时候你不想要完整父块,只想要命中的子块本身(比如做精确定位、标注出处)。LangChain 也支持这种模式——直接用 vectorstore 检索,绕过 ParentDocumentRetriever 的父块回溯逻辑:

    # 直接检索子块(不走父块回溯)
    child_results = vectorstore.similarity_search(query, k=3)

    for doc in child_results:
    print(f"子块内容: {doc.page_content[:100]}")
    print(f"parent_id: {doc.metadata.get('parent_id')}")
    print(f"child_index: {doc.metadata.get('child_index')}")
    print()

    这种模式适合:先检索子块做精确定位,确认命中后,再通过 parent_id 从 docstore 里取出父块——分两步走,灵活控制。

    四、LlamaIndex 实战:AutoMergingRetriever

    LlamaIndex 的实现思路不同——它不用 parent_id 显式关联,而是通过 Node 的 relationships 字段自动维护层级关系,配合 AutoMergingRetriever(自动合并检索器:检索到多个同一父块下的子块时,自动合并为父块返回)实现按需合并。

    4.1 架构总览

    #mermaid-svg-loxHEejor7ytfKdb{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-loxHEejor7ytfKdb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-loxHEejor7ytfKdb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-loxHEejor7ytfKdb .error-icon{fill:#552222;}#mermaid-svg-loxHEejor7ytfKdb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-loxHEejor7ytfKdb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-loxHEejor7ytfKdb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-loxHEejor7ytfKdb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-loxHEejor7ytfKdb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-loxHEejor7ytfKdb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-loxHEejor7ytfKdb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-loxHEejor7ytfKdb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-loxHEejor7ytfKdb .marker.cross{stroke:#333333;}#mermaid-svg-loxHEejor7ytfKdb svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-loxHEejor7ytfKdb p{margin:0;}#mermaid-svg-loxHEejor7ytfKdb .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-loxHEejor7ytfKdb .cluster-label text{fill:#333;}#mermaid-svg-loxHEejor7ytfKdb .cluster-label span{color:#333;}#mermaid-svg-loxHEejor7ytfKdb .cluster-label span p{background-color:transparent;}#mermaid-svg-loxHEejor7ytfKdb .label text,#mermaid-svg-loxHEejor7ytfKdb span{fill:#333;color:#333;}#mermaid-svg-loxHEejor7ytfKdb .node rect,#mermaid-svg-loxHEejor7ytfKdb .node circle,#mermaid-svg-loxHEejor7ytfKdb .node ellipse,#mermaid-svg-loxHEejor7ytfKdb .node polygon,#mermaid-svg-loxHEejor7ytfKdb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-loxHEejor7ytfKdb .rough-node .label text,#mermaid-svg-loxHEejor7ytfKdb .node .label text,#mermaid-svg-loxHEejor7ytfKdb .image-shape .label,#mermaid-svg-loxHEejor7ytfKdb .icon-shape .label{text-anchor:middle;}#mermaid-svg-loxHEejor7ytfKdb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-loxHEejor7ytfKdb .rough-node .label,#mermaid-svg-loxHEejor7ytfKdb .node .label,#mermaid-svg-loxHEejor7ytfKdb .image-shape .label,#mermaid-svg-loxHEejor7ytfKdb .icon-shape .label{text-align:center;}#mermaid-svg-loxHEejor7ytfKdb .node.clickable{cursor:pointer;}#mermaid-svg-loxHEejor7ytfKdb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-loxHEejor7ytfKdb .arrowheadPath{fill:#333333;}#mermaid-svg-loxHEejor7ytfKdb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-loxHEejor7ytfKdb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-loxHEejor7ytfKdb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-loxHEejor7ytfKdb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-loxHEejor7ytfKdb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-loxHEejor7ytfKdb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-loxHEejor7ytfKdb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-loxHEejor7ytfKdb .cluster text{fill:#333;}#mermaid-svg-loxHEejor7ytfKdb .cluster span{color:#333;}#mermaid-svg-loxHEejor7ytfKdb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-loxHEejor7ytfKdb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-loxHEejor7ytfKdb rect.text{fill:none;stroke-width:0;}#mermaid-svg-loxHEejor7ytfKdb .icon-shape,#mermaid-svg-loxHEejor7ytfKdb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-loxHEejor7ytfKdb .icon-shape p,#mermaid-svg-loxHEejor7ytfKdb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-loxHEejor7ytfKdb .icon-shape .label rect,#mermaid-svg-loxHEejor7ytfKdb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-loxHEejor7ytfKdb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-loxHEejor7ytfKdb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-loxHEejor7ytfKdb :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    relationships

    relationships

    relationships

    relationships

    SentenceSplitter层级切分器自动维护父子关系

    Node A 父节点

    Node B 父节点

    A-1 子节点

    A-2 子节点

    B-1 子节点

    B-2 子节点

    和 LangChain 的关键区别:LangChain 是"命中子块就返回父块"(一对一),LlamaIndex 的 AutoMergingRetriever 是"同一父块下命中的子块达到阈值才合并为父块"(按需合并),避免不必要地返回整个大块。

    4.2 完整代码

    from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext
    from llama_index.core.node_parser import SentenceSplitter
    from llama_index.core.retrievers import AutoMergingRetriever
    from llama_index.embeddings.ollama import OllamaEmbedding

    # === 1. 加载文档 ===
    documents = SimpleDirectoryReader("./docs").load_data()

    # === 2. 配置嵌入模型(本地 Ollama,免费)===
    embed_model = OllamaEmbedding(
    model_name="nomic-embed-text",
    base_url="http://localhost:11434",
    )

    # === 3. 层级切分器 ===
    # SentenceSplitter:句子切分器,在句子边界处切分,保持语义完整性
    # 切分时自动维护 Node 之间的 parent/child relationships
    node_parser = SentenceSplitter(
    chunk_size=256,
    chunk_overlap=32,
    )

    # === 4. 解析为 Node(自动建立 parent/child relationships)===
    nodes = node_parser.get_nodes_from_documents(documents)

    # === 5. 构建向量索引 ===
    # StorageContext:LlamaIndex 的存储上下文管理器
    # 自动管理所有索引和文档数据的存取与持久化
    storage_context = StorageContext.from_defaults()
    index = VectorStoreIndex(
    nodes,
    embed_model=embed_model,
    storage_context=storage_context,
    )

    # === 6. 创建 AutoMergingRetriever ===
    # simple_ratio_thresh:合并阈值
    # 当同一父块下命中的子块比例超过此值时,自动合并为父块返回
    # 例如 0.4 表示:4 个子块命中 2 个(50% >= 40%)就合并
    base_retriever = index.as_retriever(similarity_top_k=6)
    auto_merging_retriever = AutoMergingRetriever(
    base_retriever,
    storage_context,
    simple_ratio_thresh=0.4,
    )

    # === 7. 查询 ===
    query = "请假流程第三步是什么"
    results = auto_merging_retriever.retrieve(query)

    for i, node in enumerate(results):
    print(f"— 结果 {i+1} —")
    print(f"内容: {node.text[:200]}")
    print(f"节点层级: {node.metadata.get('level', 'N/A')}")
    print()

    4.3 关键参数调优

    | 参数 | 作用 | 推荐值 | 调优建议 |

    | — | — | — | — |

    | SentenceSplitter.chunk_size | 叶子节点大小 | 200~300 | 越小精度越高,但层级越深 |

    | SentenceSplitter.chunk_overlap | 节点重叠 | 30~50 | 防止句子被切断 |

    | similarity_top_k | 初始检索数量 | 6~10 | 太少合并不触发,太多浪费计算 |

    | simple_ratio_thresh | 合并阈值 | 0.3~0.5 | 越高越保守,越低越激进 |

    simple_ratio_thresh 是 AutoMergingRetriever 的灵魂参数。设 0.4 表示:如果一个父块有 4 个子块,命中了 2 个(占比 50% >= 40%),就合并返回父块;只命中 1 个(占比 25% < 40%),返回子块本身。调高阈值 = 更保守地合并(倾向返回小块),调低 = 更激进地合并(倾向返回大块)。

    五、LangChain vs LlamaIndex:选哪个

    | 对比维度 | LangChain ParentDocumentRetriever | LlamaIndex AutoMergingRetriever |

    | — | — | — |

    | 关联机制 | parent_id 元数据字段 | Node relationships(自动维护) |

    | 合并策略 | 命中子块就返回父块(一对一) | 命中子块比例达阈值才合并(按需合并) |

    | 父块存储 | docstore(InMemoryStore / Redis) | StorageContext(自动管理) |

    | 切分灵活度 | 两个独立 splitter,父子各自配 | 一个 parser 自动分层 |

    | 代码量 | 约 20 行 | 约 15 行 |

    | 适合场景 | 需要显式控制关联关系 | 希望自动管理层级关系 |

    | 生产成熟度 | 高 | 高 |

    | 学习曲线 | 中等 | 低(封装更彻底) |

    | 持久化 | 需手动配置 docstore 持久化 | StorageContext 自带持久化 |

    怎么选:

    • 已经在用 LangChain → 直接上 ParentDocumentRetriever,不用换框架
    • 已经在用 LlamaIndex → 直接上 AutoMergingRetriever,天然集成
    • 从零开始 → 推荐先用 LlamaIndex,封装更彻底,代码更少
    • 需要精细控制关联字段(比如加 section、page、child_index)→ LangChain 更灵活
    • 完全自研、不用框架 → 参考第二章的 MySQL 单表设计,自己实现

    六、常见问题与排坑

    | 问题 | 原因 | 解决方案 |

    | — | — | — |

    | 检索结果全是超长文本,Token 炸了 | 父块设太大,或一个 query 命中多个父块 | 调小父块 chunk_size,或限制返回父块数量 |

    | 检索效果和固定分块差不多 | 父子块大小差距太小(比值 < 3:1) | 拉大比值,父块 2000、子块 200~400 |

    | docstore 内存爆了 | InMemoryStore 不适合大规模数据 | 换 Redis 或 Elasticsearch 做持久化 docstore |

    | AutoMergingRetriever 从不触发合并 | simple_ratio_thresh 太高或 similarity_top_k 太少 | 降低阈值到 0.3,增大 top_k 到 8~10 |

    | 父块内容和子块内容完全重复 | 父块就是子块的简单拼接 | 这是正常的——父块确实包含子块内容,关键是返回时用父块提供完整上下文 |

    | 检索命中的子块 parent_id 在 docstore 里找不到 | 父块没被正确存入 docstore | 检查 retriever.add_documents() 是否执行成功,确认 docstore 的 key 和子块的 parent_id 一致 |

    | 多文档场景下 parent_id 冲突 | 不同文档的父块 ID 重复 | 用文档名做前缀:parent_id = f"{doc_name}_{chunk_index}" |

    | 更新文档后旧子块还在向量库里 | 只删了文档没清理向量库 | 先删向量库再删 docstore,或用 retriever.delete() 统一清理 |

    | 嵌入模型换了但旧向量没更新 | 换模型后没有重新向量化 | 换嵌入模型必须全量重建索引,新旧向量不可混用 |

    | 自研 MySQL 方案查询太慢 | parent_id 没加索引 | 确保 parent_id 字段有索引,批量查询用 IN + JOIN |

    七、什么情况不该用父子分块

    父子分块不是万能的,以下场景有更好的选择。

    场景一:短文档(单篇 < 1000 字)

    如果文档本身就不长,一个块就能装下全部内容,父子分块纯属多余——父块就是全文,子块就是全文,没有分层的意义。直接用固定分块或整篇作为一个块即可。

    场景二:问答对类文档(FAQ)

    FAQ 文档天然按"问-答"对组织,每个问答对就是最小的语义单元,不需要再拆分。用父子分块反而会把一个完整的问答对拆碎。直接按问答对切分,每个块 = 一个 Q + 一个 A。

    场景三:对延迟要求极高(<100ms)

    父子分块需要两次查询(先查向量库命中子块,再查 docstore 取父块),比固定分块多一次 I/O。如果要求极低延迟(实时搜索联想),这多出来的一次 I/O 可能不可接受。

    场景四:内存极度受限

    父子分块需要额外存储父块原文(在 docstore 里),相比固定分块多了一份完整的文档副本。如果运行环境内存很小(比如边缘设备),这份额外存储可能是个负担。

    场景五:文档主题单一、结构简单

    如果一份文档从头到尾只讲一个主题(比如一篇产品介绍),没有多章节、多话题的切换,固定分块就够用了。父子分块的优势在于"大文档多主题"场景——主题越分散,父子分块收益越大。

    父子分块 vs 其他分块策略:什么时候用哪个

    | 策略 | 核心思路 | 适合场景 | 不适合场景 |

    | — | — | — | — |

    | 固定大小切分 | 按 token 数一刀切 | 短文档、快速原型 | 长文档、多主题 |

    | 父子分块 | 小块检索、大块生成 | 长文档、高精度问答 | 短文档、FAQ |

    | 语义分块 | 在语义边界处切分 | 对语义完整性要求高 | 需要固定块大小做批处理 |

    | 句子窗口 | 每句附加上下文窗口 | 逐句精确匹配场景 | 大段落问答 |

    | 递归分块 | 按分隔符层级递归切分 | 通用场景 | 需要层级关联的场景 |

    速查表

    | 项目 | 内容 |

    | — | — |

    | 策略名称 | 父子分块(Parent-Child Chunking) |

    | 核心思路 | 子块做检索(精准匹配),父块做生成(完整上下文) |

    | 关联机制 | parent_id 元数据字段(LangChain)/ Node relationships(LlamaIndex)/ 数据库自关联(自研) |

    | 父块大小 | 800~2000 tokens |

    | 子块大小 | 100~300 tokens |

    | 父子比值 | 5:1 ~ 10:1 |

    | 关联方向 | 单向(子 → 父)推荐,双向按需 |

    | ID 生成 | 复合 ID 推荐:{doc_abbrev}_{p_index}_{c_index} |

    | LangChain 实现 | ParentDocumentRetriever + InMemoryStore + Chroma |

    | LlamaIndex 实现 | SentenceSplitter + AutoMergingRetriever + StorageContext |

    | 自研实现 | MySQL 单表 rag_chunks + parent_id 自关联 + 外部向量库 |

    | 额外存储 | 父块原文(docstore / StorageContext / 数据库) |

    | 检索流程 | query → 向量检索子块 → parent_id 找父块 → 返回父块 |

    | 合并策略(LlamaIndex) | simple_ratio_thresh 控制合并阈值 |

    | 生产级 docstore | Redis / Elasticsearch(替代 InMemoryStore) |

    | 嵌入模型推荐 | BGE-M3 / nomic-embed-text(本地 Ollama) |

    | 适合文档类型 | 长文档、多主题、多章节 |

    | 不适合场景 | 短文档、FAQ、极低延迟、内存受限 |

    核心知识点回顾

    1. 核心矛盾:检索精度和上下文完整性不可兼得

    块越小,向量检索越精准(语义聚焦),但上下文越容易断裂;块越大,上下文越完整,但语义被稀释,检索精度下降。固定大小切分无法同时满足两个需求。

    2. 父子分块的解法:分离检索和生成

    用子块(100~300 tokens)做向量检索,保证精准匹配;用父块(800~2000 tokens)做 LLM 生成,保证上下文完整。两者通过 parent_id 关联,各司其职。

    3. 关联架构有三种实现路径

    显式 parent_id(LangChain)、隐式 relationships(LlamaIndex)、数据库单表自关联(自研)。三种路径的关联本质相同——子块持有父块的引用,检索时通过引用回溯父块。选择取决于你用的框架和控制粒度需求。

    4. 自研场景的核心设计决策

    单表 vs 分表:推荐单表,parent_id 为 NULL 表示父块,非 NULL 表示子块,简单直接。单向 vs 双向:推荐单向,除非有高频"父找子"需求。ID 生成:推荐复合 ID,可读性远胜 UUID。

    5. 父子块大小比值是关键调参点

    5:1 到 10:1 之间。比值太小退化为固定分块,比值太大导致一个父块关联过多子块,检索时捞到大量无关内容。先用 5:1(比如父块 1500、子块 300)起步,根据效果调整。

    6. 不是所有场景都需要父子分块

    短文档、FAQ、单一主题文档用固定分块就够了。父子分块的收益在"长文档 + 多主题 + 高精度问答"场景最大。别为了用而用——多一份存储、多一次 I/O,是有代价的。

    极简流程总结

    父子关联完整链路(一段话版,适合面试复盘):

    文档先切父块(800~2000 tokens)存入 docstore,父块再切子块(100~300 tokens)并注入 parent_id 后向量化入向量库。检索时 query 向量匹配命中子块,通过子块元数据中的 parent_id 回溯到 docstore 取出完整父块,将父块作为上下文喂给 LLM 生成回答。核心是小块管检索精度,大块管上下文完整性,parent_id 焊死两者关联。自研场景下,父子块可同存一张表(parent_id 为 NULL 是父块,非 NULL 是子块),单向关联即可,复合 ID 便于调试。

    赞(0)
    未经允许不得转载:171主机测评 » RAG 父子分块关联设计:解决检索与上下文矛盾
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址