犀牛鸟 2026 研究笔记 estelledc.github.io

案例 #1700:深层标题 breadcrumb 与分块上下文

PR:#1700 | Issue #1674 | 2026-06-16 合入 | 2 文件 +107/-3 | 作者 adawang1210

结论先行

heading 分块策略下,当一个浅层标题(如 ## 5 Classification)下的整段内容超过 ChunkSize 时,chunker 会用 SplitText 做二次切分。修复前,所有子 chunk 共用同一段 section 级 breadcrumb,章内更深的 ### / #### 标题不会进入 Chunk.ContextHeader,导致检索命中正文却 丢失小节语义,问答失败。

修复:在 section 内部按 rune 偏移 追踪子标题层级,为每个 sub-chunk 分配其起始位置处生效的 最深 breadcrumb

适合谁读:改 internal/infrastructure/chunker/ 的贡献者;做规范/标准类文档 ingest 的 RAG 工程师。

Ch27 分块源码 · Ch40 分块挑战 · 与 case-1725 同属「下游 chunker 假设被上游格式打破」。


1. 问题现象(What)

典型文档形状

Issue #1674 来自 国家标准 / 行业规范 类 Markdown:浅层 ## 作为主干章节,下面密集嵌套 ####### 条款表。例如:

# Standard XYZ
## 5 Classification
### 5.9 Special cases
#### 5.9.2 Parameter defaults
(此处正文很长,超过 ChunkSize…)

用户可见现象

复现条件

  1. chunking_config.strategy: "heading"
  2. 某一 DominantHeadingLevel(重复出现的浅层标题)下 section 长度 > ChunkSize
  3. section 内部存在 level 更深 的 Markdown 标题(### / ####
  4. 触发 splitByHeadingsImpl → legacy SplitText 二次切分

定位步骤

rg "splitByHeadings|ContextHeader|sectionBreadcrumbs" internal/infrastructure/chunker/
go test ./internal/infrastructure/chunker/ -run TestSplitByHeadings_DeepSubHeading -v

2. 根因分析(Why)

类比

一本书按「章」装订,但某一章太厚,印刷厂强行从中间撕开成多叠。修复前:每一叠外面只贴「第 5 章」标签;修复后:每一叠贴「第 5 章 > 5.9 节 > 5.9.2 款」标签。读者(检索 + LLM)靠标签定位,而不只是靠中间某一段正文。

数据流

flowchart TD
    A[Markdown 文档] --> B[splitByHeadingsImpl]
    B --> C{section 长度 > ChunkSize?}
    C -->|否| D[单 chunk<br/>ContextHeader = section breadcrumb]
    C -->|是| E[SplitText 切成 sub-chunks]
    E --> F{修复前}
    E --> G{修复后}
    F --> H[每个 sub-chunk<br/>ContextHeader = 同一 section breadcrumb ❌]
    G --> I[sectionBreadcrumbs 扫描章内 ###/####]
    I --> J[breadcrumbAtOffset 按 sub.Start 取最深标题 ✅]
    D --> K[embed + 检索 + Generate]
    H --> K
    J --> K

代码级根因

observeSubHeadings 只在 划分 section 边界 时维护 heading hierarchy。当 section 过大进入 SplitText 分支时:

// 修复前:所有 sub-chunk 共用 section 入口时的 breadcrumb
ContextHeader: breadcrumb,

没有在 section 文本内部 为每个 sub-chunk 的 Start 偏移重新解析当前生效的子标题栈。

PR 作者提供的失败断言(修复前):

关键文件

文件 职责
internal/infrastructure/chunker/heading_splitter.go splitByHeadingsImplsectionBreadcrumbsbreadcrumbAtOffset
internal/infrastructure/chunker/heading_splitter_test.go TestSplitByHeadings_DeepSubHeadingInLargeSection

设计约束(PR 明确未改动的部分):


3. 解决方案(How)

算法(两步)

  1. sectionBreadcrumbs:在 section 的 rune 序列内扫描 level > primaryLevel 的 Markdown 标题,产出 (runeStart, breadcrumb) 列表;offset 0 处 seed 为 section 入口 hierarchy
  2. breadcrumbAtOffset(sub.Start, fallback):取 最后一个 runeStart ≤ sub.Start 的 breadcrumb;若无则回退 section 级 breadcrumb
subBreadcrumbs := sectionBreadcrumbs(sectionRunes, primaryLevel, sectionStart)
// 每个 sub-chunk:
ContextHeader: breadcrumbAtOffset(subBreadcrumbs, sub.Start, breadcrumb),

为何不用「按 ### 先切 section 再切 chunk」?

测试

TestSplitByHeadings_DeepSubHeadingInLargeSection

验证命令:

go test ./internal/infrastructure/chunker/ -run TestSplitByHeadings_DeepSubHeadingInLargeSection -v
go vet ./internal/infrastructure/chunker/

4. 与 RAG 管线

影响
Chunk ContextHeader 前缀进入 chunk 文本 / metadata
Embed 向量含更多层级语义(「5.9.2 款」而非仅「第 5 章」)
Retrieve 规范编号类 query 的语义匹配改善
Generate LLM prompt 中 citation 上下文更可读

未解决:query-dependent 最优分块(Ch40)— 本 PR 仍是 离线 ingest 策略改进。

与 #1248:feedback 归因到 chunk_id;若 chunk 的展示上下文(ContextHeader)错误,用户点踩时看到的 snippet 也会 误导 — metadata 完整性是归因前提。


5. 可复用模式


6. 常见误区

错误认知:heading 策略下每个 ### 都会单独成 chunk。
正确理解:只有 DominantHeadingLevel 决定 section 边界;更 deep 的标题只在 section 内部 提供语义,section 过大时仍会被 SplitText 切开。

错误认知:检索命中正文就够了,ContextHeader 只是装饰。
正确理解:ContextHeader 进入 embed 与展示;规范类文档条款号常在标题里。

错误认知:在 code fence 里识别 # 标题更「完整」。
正确理解:fence 内 # 是代码语法,识别会误切 section。

错误认知:改 chunker 只影响展示不影响向量。
正确理解:ContextHeader 通常与正文一起参与 embed,层级语义会进向量空间。


7. 实验建议

  1. lab/corpus/weknora-arch.md,人工加长某一 ### 小节至 > ChunkSize
  2. ingest 两次(修复前/后 commit 或本地 patch)
  3. hybrid-search query:「{小节标题} 中的具体配置项」
  4. 对比 top-1 chunk 的 ContextHeader 是否含该 ### 标题

8. 思考点

  1. 若文档用 编号列表 而非 Markdown 标题(1.1.2 纯文本),本算法能否扩展?需要改哪一层?
  2. breadcrumbAtOffset 取「最后一个 ≤ Start」— 若 sub-chunk 跨越 两个 ### 边界,应继承哪一个?当前行为合理吗?
  3. 画一张从「用户 query 含条款号」到「LLM 答案」的图,标出 ContextHeader 在哪一步注入。
  4. case-1725 在 docparser 层归一化 HTML 表;本 PR 在 chunker 层补 breadcrumb — 两者如何组合成「ingest 质量 checklist」?

章末自检


延伸阅读