检索增强生成(Retrieval-Augmented Generation,RAG)系统的召回质量与生成质量,长期被一个看似琐碎却决定上限的参数支配:分块(chunking)策略。本文不把“分块大小”当作一个待调超参,而是把它还原为检索粒度与生成粒度之间的结构性矛盾——检索需要短小、语义聚焦、向量可分单元;生成需要完整、上下文自洽、可被引用的语义单元。固定长度切分之所以在工程上被系统性抛弃,并非因为它“不够先进”,而是因为它把这两个目标强行压到同一尺度上,导致边界截断、指代断裂与召回-生成错配三类可观测的失效模式。
本文以“检索单元与生成单元解耦”为贯穿全文的分析主线,系统梳理分块策略的演进谱系,给出分块大小的可操作选择路径(含公式化估算与验证方法),并深入 LangChain4j 的父子索引(Parent-Child / Small-to-Big)实现细节,重点解决一个常被教程略过的工程问题:子块向量、父块正文、以及路由所需的结构化元数据,在向量库中究竟如何分层存储与字段映射。文中给出可运行的 Java 代码骨架、元数据字段设计表、参数敏感性分析,并对前沿方向(语义分块、late chunking、上下文检索)作出审慎预判。
关键词:RAG;分块策略;父子索引;LangChain4j;向量数据库;元数据设计;检索粒度
01 / INTRO导语:一个被低估的参数,一条被忽视的矛盾
在 Java 技术栈里做 RAG,最常见的对话是这样的:向量库选型讨论了三周,embedding 模型换了四个,重排序(rerank)也接上了,但回答质量始终“差一口气”。排查到最后,问题往往落在最不起眼的地方——文档是怎么被切成块的。
分块(chunking)在 RAG 流水线中的位置极其靠前:它决定了进入 embedding 模型的文本单元,也决定了检索命中后回填给大模型的上下文内容。位置靠前意味着误差放大——切分阶段丢失的语义边界,后续任何环节都无法凭空恢复。检索器再强,也只能在切好的块里挑;生成模型再强,也只能在回填的文本里推理。
但分块大小长期被当作一个“经验值”传播:512 token、1000 字符、带 20% 重叠。这些数字并非毫无根据,它们大多来自早期 embedding 模型的输入窗口约束与公开评测的经验区间。问题在于,这些数字被去语境化地复制,脱离了文档类型、查询类型与模型能力三个前提。
笔者认为,分块大小之争的本质不是“大一点好还是小一点好”,而是一条结构性矛盾:检索的最优粒度与生成的最优粒度天然不一致。检索偏好短块——向量表示更聚焦,相似度计算不易被无关内容稀释;生成偏好长块——上下文完整,指代可解析,论证链不中断。固定长度切分把这两个目标压到同一个尺度上,于是无论选哪个值,都必然在一端妥协。父子索引(Parent-Child Indexing)之所以成为当前工程主流,正是因为它承认这条矛盾,并用“两级粒度 + 元数据路由”把它拆开处理。
本文的主线由此确立:分块策略的演进,本质上是从“单一粒度妥协”走向“多粒度解耦”的过程。后续所有章节——失效模式分析、参数估算、LangChain4j 实现、元数据字段设计——都服务于这条主线。
02 / DEFINITION背景与问题定义:RAG 中“块”到底是什么
2.1 块的三重身份
在讨论大小之前,必须先明确“块”在 RAG 流水线中承担了三种不同身份,而固定长度切分的问题恰恰源于它试图用同一个对象同时满足三者:
- 向量化单元:块是 embedding 模型的输入,其向量进入索引。此身份要求块语义聚焦、主题单一,避免多主题混合导致向量“均值化”。
- 检索单元:块是相似度计算的返回对象。此身份要求块与典型查询在语义空间上可比,且块内信息足以判断相关性。
- 生成上下文单元:块被拼接进 prompt 供大模型阅读。此身份要求块自洽、可引用、指代完整。
本文评述:三重身份对“长度”的偏好并不一致。向量化与检索偏好短(通常 128–512 token 区间),生成偏好长(通常 512–2048 token 甚至更长,取决于任务)。固定长度切分只能选一个折中点,而折中点对两端都不是最优。父子索引的价值就在于:让子块承担前两个身份,父块承担第三个身份。
2.2 形式化定义
设原始文档为字符序列 D = d₁d₂…dₙ,切分函数 C 将 D 映射为块集合 {c₁, c₂, …, cₘ}。固定长度切分可写为:
cᵢ = D[ (i-1)·(L - O) + 1 : (i-1)·(L - O) + L ]
其中 L 为块长,O 为重叠长度。该函数只依赖位置索引,不依赖内容——这是它全部优点的来源(简单、确定、可并行),也是它全部缺点的来源(无视语义边界)。
语义切分则把 C 定义为在语义边界集合 B = {b₁, …, bₖ} 处切分,边界可由段落、标题层级、句子相似度突变等信号确定。父子索引进一步引入映射 π: child → parent,使检索命中的子块可回溯到更大的父块。
2.3 问题边界
需要明确本文讨论的边界:聚焦文本类文档(技术文档、法规、论文、工单)的 RAG,不展开多模态分块;聚焦稠密向量检索为主、稀疏/混合检索为辅的架构;工程实现以 LangChain4j + 主流向量库为参照。跨语言、表格密集、代码仓库等特殊文档类型会在第 10 章作为边界条件单独讨论。
03 / FAILURE MODES固定长度切分为什么被抛弃:三类失效模式
“被抛弃”需要证据。工程实践中固定长度切分并未完全消失——它仍是快速原型的默认选项,也是很多框架的 fallback。但作为生产级默认策略,它已被系统性降级。原因可归纳为三类可观测的失效模式。
3.1 边界截断:语义单元被腰斩
固定长度切分在字符位置切分,与句子、段落、列表项、表格行的边界无关。一个典型后果是:一个完整定义被切成两半,前半句进入块 A,后半句进入块 B。检索时若查询命中定义的前半部分,块 A 被召回,但回填给模型的上下文缺少后半句,模型只能基于残缺信息作答,或产生“看似合理实则错误”的补全。
这类失效在定义密集、条款密集的文档中尤其严重:法规条文、API 参考、合同条款。它们的语义单元边界与自然段落高度重合,而固定长度切分对此完全无感。
本文评述:边界截断的隐蔽性在于它不产生“检索失败”,而只产生“检索到但不完整”。监控指标上召回率可能正常,但答案质量下降。这使得它比检索失败更难定位——失败是显性的,不完整是隐性的。
3.2 指代断裂:跨块引用失效
自然语言充满指代与省略:“该方法”“上述条件”“如图 3 所示”“其返回值”。这些指代的先行词往往在上一段甚至上一节。固定长度切分把文档切成互不重叠(或仅机械重叠)的块后,块内的指代失去锚点。
重叠(overlap)是对此的朴素缓解:让相邻块共享一段文本,使先行词有机会同时出现在两个块中。但重叠是位置驱动的,不是指代驱动的——它无法保证先行词恰好落在重叠窗口内。重叠率越高,冗余存储与重复召回越严重;重叠率越低,指代断裂越频繁。这是一个没有最优解的权衡,因为切分函数根本不知道指代关系在哪里。
3.3 召回-生成错配:粒度目标冲突
这是最根本的一类。前两类是“切得不好”,这一类是“切得再好也不够”,因为它触及第 1 章提出的结构性矛盾。
设想把块长设为 256 token 以优化检索:向量聚焦,相似度区分度好,召回精准。但回填给生成模型的上下文只有 256 token,模型缺乏足够背景,容易答非所问或过度推断。反过来,把块长设为 1024 token 以优化生成:上下文充足,但向量被多主题稀释,检索精度下降,且单块占用更多上下文预算,能回填的块数减少。
本文评述:三类失效中,前两类可通过更好的切分算法缓解,第三类只能通过架构层面的粒度解耦解决。这解释了为什么“语义分块”虽好,却仍不足以单独支撑生产系统——它优化了切分质量,但没有解决粒度目标冲突。父子索引补的正是这一块。
04 / EVOLUTION分块策略演进谱系与本文评述
把分块策略按“是否感知语义边界”和“是否支持多粒度”两个维度排列,可以得到一条清晰的演进谱系。这条谱系是本文主线的直接体现。
4.1 第一代:固定长度 + 重叠
代表实现是各框架的 RecursiveCharacterTextSplitter 类工具(LangChain 生态的通用做法,LangChain4j 中对应 DocumentSplitters.recursive(...))。它按分隔符优先级递归切分:先按段落,段落仍超长则按句子,再超长则按字符。这比纯定长好,但本质仍是“长度约束优先,语义边界次之”。
本文评述:递归切分是工程上的合理折中,它的价值在于把“硬切”降级为“尽量在自然边界切”。但它仍受单一长度参数支配,且分隔符优先级是全局固定的,无法适配不同文档结构。
4.2 第二代:结构感知切分
利用文档自身结构:Markdown 标题层级、HTML DOM、PDF 版面分析、代码 AST。切分边界来自结构而非字符位置。这类方法在技术文档、法规、代码库上收益显著,因为结构边界与语义边界高度相关。
局限在于结构解析成本与鲁棒性:PDF 版面分析在扫描件、双栏、表格上仍不稳定;HTML 清洗不彻底会引入噪声。工程上通常采用“结构优先、长度兜底”的混合策略。
4.3 第三代:语义切分
基于句子级 embedding 的相邻相似度突变来定位边界:把文档切成句子,计算相邻句子向量余弦相似度,在相似度低于阈值处切分。代表思路在开源社区广泛讨论(如 LangChain 的 SemanticChunker 一类实现)。
本文评述:语义切分提升了块内一致性,但代价是切分阶段就要跑一遍 embedding,且阈值对文档类型敏感,需要校准。更关键的是,它仍输出单一粒度——块既是检索单元也是生成单元,第 3.3 节的错配依然存在。语义切分是“更好的单粒度”,不是“多粒度”。
4.4 第四代:多粒度 / 父子索引
显式维护两级(或多级)粒度:子块用于检索,父块用于生成,二者通过 ID 映射关联。检索命中子块后,按映射取回父块正文回填。这是当前生产系统的主流架构,也是本文第 6–8 章的重点。
本文评述:父子索引的本质是把“切分”从一次操作变成一次建模——它不再问“切多大”,而是问“检索粒度和生成粒度分别多大,如何关联”。这是范式层面的转变,而非参数层面的优化。
4.5 演进谱系总览
05 / SIZING分块大小到底设多少:估算、验证与边界条件
这一章回答标题中的第一个问题。需要先说清楚:不存在普适最优值,但存在可复现的估算路径与验证方法。任何给出“就设 512”的答案,都省略了前提。
5.1 三个约束条件
分块大小受三个硬约束夹逼:
- embedding 模型输入窗口:多数主流 embedding 模型支持 512–8192 token 输入,但有效表示能力随长度衰减。超过训练分布的长文本,向量质量下降。
- 生成模型上下文预算:回填 k 个块,每块 L token,加上系统提示与问题,总预算须留有余量。上下文窗口不是越大越好,长上下文存在“中间遗忘”现象(lost in the middle),即模型对上下文中部信息的利用率低于首尾。
- 查询的信息需求粒度:事实型查询(“X 的默认值是多少”)需要短而精的块;综合型查询(“比较 A 与 B 的三种方案”)需要覆盖多段的长上下文。
本文评述:第三个约束最容易被忽略,却最关键。分块大小应当由查询分布反推,而不是由文档长度正推。先采样真实查询,标注每条查询需要多少上下文才能回答,得到需求长度分布,再据此定块长。这个顺序在工程上经常被颠倒。
5.2 估算公式
在父子索引架构下,子块与父块分别估算:
子块目标长度 L_child ≈ 查询语义单元的平均长度 × 聚焦系数 α 父块目标长度 L_parent ≈ 单次回答所需上下文中位数 × 安全系数 β 回填块数 k ≈ floor( (C_ctx - C_prompt - C_query) / L_parent )
其中 C_ctx 为生成模型上下文窗口,C_prompt 为系统提示开销,C_query 为查询开销。α 通常取 1.5–3(查询往往比其目标段落短),β 取 1.2–1.5(留出指代与过渡的余量)。
举例(以下为模拟数据,用于说明计算过程,非实测):若查询语义单元平均 30 token,α=2,则 L_child≈60–90 token,实践中常取 128–256 以留余量;若回答所需上下文中位数 400 token,β=1.3,则 L_parent≈520 token,取 512。若 C_ctx=8192,C_prompt=800,C_query=100,则 k≈floor((8192-800-100)/512)=14,实际工程中通常取 3–6,因为过多父块会稀释注意力并增加成本。
5.3 验证方法:可操作的评估闭环
估算只是起点,必须用评估闭环校准。建议的最小闭环:
- 构造查询集:从真实日志采样 100–300 条查询,覆盖事实型、比较型、流程型三类。
- 标注黄金上下文:为每条查询标注回答所需的原文片段(可多人标注取交集)。
- 网格搜索:在 L_child ∈ {128, 256, 512}、L_parent ∈ {512, 1024, 2048}、k ∈ {3, 5, 8} 上跑检索,计算上下文召回率(黄金片段被回填的比例)与上下文精确率(回填内容中相关比例)。
- 端到端抽检:对候选配置生成答案,人工或模型评判忠实度与完整性。
本文评述:很多团队只测端到端答案质量,不测上下文召回率,导致无法区分“检索没找到”与“生成没用好”。把评估拆成检索层与生成层,是定位分块问题的前提。上下文召回率是分块策略最直接的代理指标。
5.4 边界条件
以下情形会推翻上述估算,需要单独处理:
- 表格与代码:表格行、代码函数是不可分割的语义单元,应按结构切分,长度约束让位于完整性。
- 多跳问答:答案需要跨多个段落组合,此时父块应更大或采用“父块的父块”多级结构。
- 低资源语言/领域术语密集:tokenizer 对非英文、专业术语的切分效率不同,按 token 计的块长需按实际 tokenizer 校准,不能按字符数估算。
06 / PARENT-CHILD父子索引原理:检索单元与生成单元的解耦
6.1 核心机制
父子索引(又称 Small-to-Big、Parent Document Retriever)的机制可概括为三步:
- 离线建索引:文档先切成较大的父块,父块再切成较小的子块。子块做 embedding 入向量库;父块正文存入文档存储(或向量库的 payload/metadata)。子块记录其父块 ID。
- 在线检索:查询向量与子块向量做相似度检索,得到 top-k 子块。
- 回填生成:按子块的父块 ID 取回父块正文,去重后拼接进 prompt。
本文评述:这个机制的关键不在“两级切分”,而在ID 映射的存储位置。映射放在向量库元数据里,还是放在独立文档库,决定了检索延迟、一致性与运维复杂度。第 8 章会展开这个设计决策。
6.2 为什么有效:信息论视角
从信息论看,检索是一个“在噪声中定位信号”的过程。子块短,向量表示的信噪比高——块内主题单一,向量方向稳定;父块长,信息熵高——包含更多上下文,但作为检索单元会被稀释。父子索引把“定位”与“供给”分离:用高信噪比的子块定位,用高信息量的父块供给。这是对第 3.3 节错配的直接回应。
6.3 与“句子窗口检索”的区别
另一种常见解耦是句子窗口检索(Sentence Window Retrieval):以句子为检索单元,命中后回填该句子前后各 N 句。它与父子索引目标相似,但实现不同:句子窗口是动态扩展(按位置取邻居),父子索引是预定义映射(按 ID 取父块)。前者无需预存父块,存储开销小;后者边界更稳定,不受切分位置漂移影响。工程上二者可组合。
07 / LANGCHAIN4JLangChain4j 中的父子索引落地路径
这一章回答标题中的第二个问题。LangChain4j 是 Java 生态中较成熟的 LLM 应用框架,其 RAG 模块提供了文档切分、embedding 存储、检索增强的完整链路。父子索引在 LangChain4j 中没有名为“ParentChildIndex”的一键组件,需要组合既有抽象实现。以下给出可操作的路径。
7.1 依赖与基础组件
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
向量库按需引入(如 langchain4j-pgvector、langchain4j-milvus、langchain4j-qdrant 等)。版本号以官方发布为准,本文不锁定具体版本。
7.2 两级切分与元数据注入
LangChain4j 的 Document 对象携带 Metadata(键值对),这是实现父子映射的载体。核心思路:先切父块并生成父块 ID,再切子块并把父块 ID 写入子块元数据。
DocumentLoader loader = ...; // 如 FileSystemDocumentLoader
Document original = loader.loadDocument(path);
// 1) 父块切分:较大粒度
DocumentSplitter parentSplitter =
DocumentSplitters.recursive(1200, 100);
List<Document> parents = parentSplitter.split(original);
// 2) 为每个父块分配稳定 ID,并写入元数据
List<Document> childDocs = new ArrayList<>();
DocumentSplitter childSplitter =
DocumentSplitters.recursive(256, 32);
for (int i = 0; i < parents.size(); i++) {
Document parent = parents.get(i);
String parentId = original.metadata().getString("doc_id")
+ "#p" + i;
// 父块正文单独持久化(见 7.3)
parentStore.put(parentId, parent.text());
for (Document child : childSplitter.split(parent)) {
Document enriched = child.toBuilder()
.metadata(Metadata.from(child.metadata().toMap()))
.metadata("parent_id", parentId)
.metadata("doc_id", original.metadata().getString("doc_id"))
.metadata("chunk_type", "child")
.build();
childDocs.add(enriched);
}
}
本文评述:parent_id 的生成必须稳定且可重建——同一文档重复入库时 ID 不变,否则增量更新会产生孤儿映射。用“文档 ID + 父块序号”是简单可靠的做法;若父块内容会变,可改用内容哈希。
7.3 父块正文的存储选择
父块正文有三种存法,各有取舍:
本文评述:中小规模(百万级块以内)优先选独立 KV,存储放大问题在向量库上会被放大数倍,且正文更新会触发向量库重写。大规模且延迟敏感时,可考虑把父块正文压缩后放 payload,或只对高频父块做缓存。
7.4 检索与回填实现
EmbeddingStore<TextSegment> store = ...; // 子块向量库
EmbeddingModel model = ...;
int TOP_K_CHILD = 12;
int MAX_PARENT = 5;
Embedding queryVec = model.embed(query).content();
List<EmbeddingMatch<TextSegment>> matches =
store.findRelevant(queryVec, TOP_K_CHILD, 0.6);
// 按 parent_id 去重,保序(相似度降序)
LinkedHashMap<String, Double> parentScores = new LinkedHashMap<>();
for (EmbeddingMatch<TextSegment> m : matches) {
String pid = m.embedded().metadata().getString("parent_id");
parentScores.putIfAbsent(pid, m.score());
if (parentScores.size() >= MAX_PARENT) break;
}
// 取父块正文
List<String> contexts = new ArrayList<>();
for (String pid : parentScores.keySet()) {
String text = parentStore.get(pid);
if (text != null) contexts.add(text);
}
关键细节:去重必须在父块层做。多个子块可能命中同一父块,若不去重,回填会重复同一段正文,浪费上下文预算并可能诱导模型重复表述。保序应按子块最高相似度排序,使最相关的父块排在前面——这同时缓解了长上下文的“中间遗忘”。
7.5 与 ContentRetriever 的集成
LangChain4j 的 ContentRetriever 抽象可直接承载上述逻辑:自定义实现类在 retrieve(Query) 中完成“子块检索 → 父块回填”,返回 List<Content>。这样可无缝接入 RetrievalAugmentor 与 AiServices,保持上层调用不变。
08 / METADATA向量库元数据字段设计:分层存储与路由
这一章是标题第三个问题的核心。父子索引在向量库中的落地,成败很大程度取决于元数据字段设计。字段设计不当,会导致过滤失效、去重困难、增量更新产生孤儿数据。
8.1 字段分层
建议把元数据分为三层,职责清晰:
本文评述:chunk_type 字段常被忽略,但它在“父子记录混存同一集合”时是必需的过滤条件。若父块正文也以记录形式存入向量库(即使不建向量),检索时必须加 chunk_type == "child" 过滤,否则父块可能被误召回。
8.2 过滤与索引的配合
不同向量库对元数据过滤的支持差异较大:有的支持在 ANN 检索时做前置过滤(pre-filter),有的只能后置过滤(post-filter)。前置过滤要求字段有索引,否则性能骤降。工程上应:
- 为高频过滤字段(
doc_id、source、chunk_type)建索引; - 避免在元数据存大文本(父块正文),除非确认该库的 payload 支持良好;
- 用
parent_id做去重时,确保其基数可控,避免超长字符串影响索引效率。
8.3 增量更新与孤儿清理
文档更新时,旧子块必须先删后插,否则旧向量残留会污染检索。删除条件应为 doc_id == X(覆盖该文档全部子块)。若父块正文存于独立 KV,还需同步删除旧 parent_id 记录。建议用“先写新版本(带版本号元数据)→ 切换读路径 → 异步删旧版本”的双写策略,避免更新窗口内检索为空。
本文评述:孤儿映射是父子索引最常见的生产事故来源——子块删了、父块没删,或反之。把 parent_id 设计成可推导(doc_id + 序号)而非随机 UUID,能让清理逻辑变得可验证:按 doc_id 前缀批量删除即可。
09 / DATA关键数据、案例与参数敏感性
9.1 公开评测中的分块敏感性
分块大小对 RAG 效果的影响,在公开评测中已有若干可引用的观察。需要说明:不同评测的语料、查询集、模型不同,绝对数值不可直接横向比较,但趋势具有参考价值。
- 检索增强生成的系统性评测工作(如 RAGAS 框架相关论文与文档)指出,检索质量指标(context precision/recall)对切分策略高度敏感,且这种敏感性在答案质量指标上会被进一步放大。
- 长上下文利用的研究(如“Lost in the Middle”一类工作)表明,模型对上下文中部信息的利用率显著低于首尾,这意味着“把父块堆得越多越好”并不成立,回填块数存在收益递减点。
- 部分向量库与 embedding 提供方的工程博客给出经验区间:子块 128–512 token、父块 512–2048 token 是常见起点,但均强调需按业务评测校准。
本文评述:这些观察共同指向一个结论——分块参数不存在跨领域的稳定最优值,只存在针对特定查询分布的局部最优。因此第 5.3 节的评估闭环不是可选项,而是必需项。
9.2 参数敏感性(模拟数据)
下表为模拟数据,用于说明参数间的交互趋势,非真实实验结论。设定:查询集 200 条,黄金上下文按段落标注,评估上下文召回率(CR)与回填 token 数。

