docs/ParagraphSemanticChunking-zh.md
Paragraph Semantic Chunking(下文简称 P 策略)面向 DOCX 等具有清晰章节结构的文档。其核心目标是:让分块边界尽可能对齐文档原生的语义边界(标题、段落、表格行),而不是仅由 token 长度计数决定切点。
P 策略主要解决以下四类问题:
P 策略仅对 native 抽取引擎生成的 .blocks.jsonl 结构化产物有效;对非结构化输入会自动降级为 R 策略(见 §8)。
| 维度 | R 策略(Recursive) | V 策略(SemanticVector) | P 策略(ParagraphSemantic) |
|---|---|---|---|
| 切分依据 | 字符分隔符级联(段落 → 换行 → 中文标点 → 空格 → 字符)+ token 预算 | 句子级 embedding 距离阈值(百分位 / 标准差 / 四分位距 / 梯度)寻找语义断层 | DOCX outline level 与 parent_headings + 表格行边界 + 锚点 + 层级感知合并 |
| 块大小控制 | chunk_token_size 硬上限 | chunk_token_size 仅为 advisory ceiling,超限时通过 R 二次切分 | target_max 硬上限 + target_ideal 软目标 + 表格阈值 + 尾部吸收阈值多重协同 |
| 表格处理 | 不感知表格,可能在表格中间切断 | 不感知表格 | 表格小于 table_max 保持完整;大表按 JSON 行数组 / HTML <tr> 行边界切片,并重新包裹为合法 <table> |
| 表格上下文 | 依赖窗口偶然覆盖 | 依赖 embedding 距离 | 首切片粘连前置说明、末切片粘连后置解释、连续大表桥接文字双向重叠 |
| 块间重叠 | 全局 chunk_overlap_token_size | 不会出现重叠 | 章节边界不会重叠;同章节长正文 fallback 到 R 时按 CHUNK_P_OVERLAP_SIZE 重叠;连续大表桥接文字可同时进入前后两个表格块 |
| heading 元数据 | 通常无 | 通常无 | 继承或提升 heading;拆分后追加 [part n] 后缀;保留 parent_headings 和 level |
| 嵌入计算开销 | 无 | 高(需对每个句子计算 embedding) | 无 |
| 依赖输入 | 任意文本 | 任意文本 + Embedding 模型 | 必须有 .blocks.jsonl sidecar(即 native 引擎抽取结果),否则降级为 R |
| 场景 | 推荐 | 理由 |
|---|---|---|
| DOCX 且章节层级清晰、含大表格、含细碎条款 | P | 充分利用标题层级与表格行边界,块边界最贴合语义;避免跨主题污染 |
| 文档以散文 / 评论 / 长篇正文为主,没有明确章节结构 | V | 按语义相似度切分能在话题切换点形成自然边界,比字符切分更稳定 |
| 输入是纯文本、Markdown、代码、日志,或追求最低算力开销 | R | 无嵌入开销,分隔符级联对中英文混合文本足够稳定 |
| 通用配置(不确定文件类型) | R | P 在无 sidecar 时自动降级到 R;V 在无 Embedding 模型时也降级到 R |
| 标题样式混乱、正文中大量伪标题的文档 | R 或 V | P 依赖 native parser 正确识别标题,标题错乱会导致基础块边界偏移 |
| 单行超大表格或不可解析表格 | 任意 | 三种策略最终都会走字符级 fallback;P 仍保留表格上下文粘连优势 |
native 引擎:在 LIGHTRAG_PARSER 中显式声明,例如 docx:native-P;否则即使写了 P,也会因为缺少 .blocks.jsonl 退化到 R。.blocks.jsonl 产物。P 策略以 native parser 在 fixlevel=0 模式下产生的 .blocks.jsonl 为输入,每个 type == "content" 行被视为一个标题级基础块,然后在该基础上执行表格切片、长块拆分和层级合并:
DOCX
↓ native parser (fixlevel=0)
.blocks.jsonl + sidecar (.tables.json / .equations.json / .drawings.json / .blocks.assets/)
↓ Stage B:超大表格按行边界切片并赋予 first/middle/last 角色
↓ Stage B.1:连续大表之间桥接文字双向重叠
↓ Stage C:锚点驱动的长文本块再切分
↓ Stage D:层级感知的双相位合并
↓ Stage E:[part n] 行级来源追溯编号
最终 chunk 列表
P 策略的关键不变量:
.blocks.jsonl 内容行之间的文本绝不会被复制到对方块里,避免“张冠李戴”。chunk_overlap_token_size 保留 R 风格 overlap,减少长正文中途切断。chunking_by_paragraph_semantic() 接收以下输入:
| 参数 | 来源 | 说明 |
|---|---|---|
content | full_docs[doc_id].content | 拼接后的合并文本,用于 sidecar 缺失时降级 |
blocks_path | full_docs[doc_id].lightrag_document_path | .blocks.jsonl 路径,是 P 策略的主输入 |
chunk_token_size | chunk_options.chunk_token_size / CHUNK_P_SIZE | 目标硬上限 N,默认 2000 |
chunk_overlap_token_size | CHUNK_P_OVERLAP_SIZE / chunk_overlap_token_size | 同一内容行内长正文 fallback 与表格桥接预算的上限,默认 100 |
tokenizer | LightRAG 已解析好的 tokenizer | 所有 token 计数与文本 overlap 截取的基准 |
P 策略不接收 split_by_character / split_by_character_only,因为正常路径由标题和段落结构驱动。
.blocks.jsonl 约定P 策略只处理 type == "content" 行。每个内容行通常包含:
content:该标题下的正文文本,可能包含普通段落、<table ... /> 标签、<equation ... /> 公式、<drawing ... /> 图形。heading:当前标题。parent_headings:父级标题链。level:标题级别(1positions:原始段落定位(用于追溯)。native parser 的 fixlevel=0 模式保证「一条标题下的正文作为一个基础块」,不在解析阶段做 token 阈值拆分。表格保持完整插入到 content 中。
最终输出为有序 chunk 列表,每个元素:
{
"tokens": int, # 真实 token 数(合并后会复测)
"content": str, # 块文本(可能包含 <table> 标签)
"chunk_order_index": int, # 块顺序索引
"heading": str, # 拆分后追加 [part n] 后缀
"parent_headings": list[str], # 父级标题链,不追加后缀
"level": int, # 标题层级
}
实现内部还会临时使用 paragraphs、table_chunk_role、uuid、uuid_end、type 等字段辅助拆分和合并,但不会进入最终输出。
[part n] 后缀规则.blocks.jsonl 内容行被拆成多个片段时,所有片段的 heading 字段追加 [part 1]、[part 2] …parent_headings 不追加后缀。[表格片段N] 后缀已统一由 [part n] 替代。P 策略的阈值不是固定常量,而是按 chunk_token_size(记为 N)动态推导:
| 名称 | 计算式 | N = 2000 时取值 | 技术含义 |
|---|---|---|---|
target_max | N | 2000 | 文本块硬上限 |
target_ideal | 0.75 × N | 1500 | 文本块理想目标,达到此值后停止参与普通同级合并 |
table_max | 0.625 × N | 1250 | 表格触发切片阈值 |
table_ideal | 0.375 × N | 750 | 表格切片理想大小 |
table_min_last | 0.32 × table_max | 400 | 表格末片回吞阈值(小于此值且能合并则回吞至前一切片) |
small_tail_threshold | 0.125 × N | 250 | 尾部碎块吸收阈值 |
max_anchor_candidate_length | 固定 | 100 字符 | 长块拆分锚点候选段落长度上限 |
比例约束关系:table_max < target_ideal < target_max、table_ideal < table_max。这些比例源自审计模式经验值(大块 8000、小表 5000、理想表 3000、表格尾块 1600),现按 chunk_token_size 等比缩放。
标题识别由 native parser 完成,P chunker 自身不扫描 docx body、也不判断标题样式。
native parser 在 fixlevel=0 模式下:
styles.xml,按 <w:basedOn> 建立样式继承链,回溯有效 <w:outlineLvl>。document.xml 段落,沿继承链解析大纲级别;原始 outline level 0level 1current_heading_stack,遇新标题时清理不浅于当前 level 的旧标题,计算 parent_headings。<table id="..." format="json">...</table> 等),写入对应 sidecar。P chunker 直接读取 .blocks.jsonl,每个 content 行作为后续 Stage B/C 的独立处理单元。这意味着 [part n] 编号按每个原始 content 行独立重置。
Stage B 只处理 token 数超过 table_max 的表格。其目标不是单纯拆表,而是在行边界优先拆分的基础上保留表格边界上下文。
format="json":按 JSON 顶层行数组切片。format="html":按 <tr>...</tr> 行切片。切片前预扣 <table {attrs}></table> 外壳 token 开销,使重新包裹后的切片尽量不超过 table_max。每个切片重新包裹为合法的 <table> 标签,便于下游解析。
若某个行子集重新包裹后仍超过 table_max,则在该行子集内继续细分。只有切片已经收敛到单行、且该单行自身超过限制时,才退化为字符级切分。该机制使可被行边界表达的表格内容尽量保留合法表格结构。
若表格末片 token 数低于 table_min_last,且与前一切片合并后不超过 table_max,则将末片回吞至前一切片,减少无效短表格块。
每个表格切片被赋予内部字段 table_chunk_role,并按角色决定与周围段落的粘连方式:
| 角色 | 含义 | 粘连策略 |
|---|---|---|
first | 原始表格的首切片 | 追加到当前累积块尾部,使表格前置说明与首切片进入同一块 |
middle | 原始表格的中间切片 | 独立输出,避免与无关正文合并 |
last | 原始表格的末切片 | 作为新累积块起点,使后置解释自动追加到末切片之后 |
none | 非表格切片或未拆分的完整表格 | 按普通文本块处理 |
table_chunk_role 是内部字段,最终输出不会保留,但在 Stage D 中继续作为合并约束使用(见 §9.1)。
当同一原始内容行中出现「大表 A、短桥接文字、大表 B」的模式,且两张表均被拆分时,桥接文字按上下文预算进行双向分配:
prev_budget = min(chunk_overlap_token_size, target_max - 左侧末切片当前 token 数)。next_budget = min(chunk_overlap_token_size, target_max - 右侧首切片当前 token 数)。单侧预算还会被限制到不超过 chunk_token_size / 2,避免桥接文字主导整个块。
这与普通相邻 chunk overlap 的差异:
Stage C 处理 Stage B 后仍超过 target_max 的内容块。
把内容按段落恢复,选择满足以下条件的段落作为候选锚点:
<table 开头)。max_anchor_candidate_length(100 字符)。根据目标子块数量计算理想切分位置,从候选锚点中选择距离理想位置最近的锚点。被选中的锚点晋升为后续子块的新 heading,原 heading 写入该子块的 parent_headings。
若不存在合格锚点:
target_max。chunking_by_recursive_character),使用 chunk_overlap_token_size 保持相邻文本片段的连续性。无锚点 fallback 路径保证算法不会丢弃内容,并尽量遵守用户配置的块大小上限。
Stage D 解决细碎章节场景下「块过碎」和「跨主题污染」的矛盾。核心思想是自深层级向浅层级处理,先合并同级小块,再允许浅层块吸收深层块,同时引入尺寸约束、表格切片角色约束和标题路径约束。
target_max;已达到 target_ideal 的块原则上不继续参与普通同级合并。middle 表格切片锁定独立;first、last 按方向参与合并,防止表格边界上下文被错误吞并。level 之间发生;跨级吸收只允许浅层吸收深层,禁止深层反向吸收浅层。parent_headings 一致,或处于同一父标题路径所限定的连续范围内。这是避免跨主题污染的关键。针对当前 level 的相邻块,若两者均低于 target_ideal,且满足上述约束,则合并为一个块。
表格切片角色的方向规则:
| 块角色 | 可向后吸收下一块 | 可被前一块吸收 |
|---|---|---|
none | 是 | 是 |
first | 是 | 否 |
middle | 否 | 否 |
last | 否 | 是 |
若一个已达到 target_ideal 的块后面紧跟一串同级小块,且该串小块总 token 数低于 small_tail_threshold、合并后真实 token 数不超过 target_max,则一次性吸收该串小块。遇到 middle 表格切片时停止。
对于 Phase A 后仍未饱和的小块,尝试跨级合并,但仅允许浅层吸收深层:
last 角色向后吸收;middle 仍不参与合并。由于合并时会插入换行连接符,逐块 token 数相加可能低估合并结果。每次提交合并前,都要对拼接后的真实文本重新计算 token 数,确认不超过 target_max 后再提交。
合并后保留主块的 heading。如果多个 part 片段被合并,最终 heading 保留主块的 part 后缀,不会额外拼接多个 part 标签。
P 策略有多层降级保护:
| 触发条件 | 降级行为 |
|---|---|
blocks_path 缺失、不可读、无有效 content 行 | 整体降级到 chunking_by_recursive_character(),传入解析出的 chunk_overlap_token_size |
| Stage B 中表格无法识别 JSON / HTML 结构 | 该表格调用 R 策略字符切分 |
Stage B 中单行表格自身超过 table_max | 该单行调用 R 策略字符切分 |
| Stage C 中长块没有合格短段落锚点 | 表格优先 → 贪心打包 → 单段落超长再降级 R 字符切分 |
重要:整体 fallback 后不再具备标题层级、表格角色和桥接文字双向重叠能力;但能保证文档仍产生检索块,不因结构化 sidecar 缺失而被静默丢弃。
| 配置 | 默认 | 说明 |
|---|---|---|
CHUNK_P_SIZE | 2000(未设时使用 DEFAULT_CHUNK_P_SIZE,不沿用 CHUNK_SIZE) | P 专用 chunk_token_size;段落语义合并需要比全局默认更大的上限,因此独立默认而非回退到 CHUNK_SIZE |
CHUNK_P_OVERLAP_SIZE | 未设(沿用 CHUNK_OVERLAP_SIZE) | P 专用 overlap;只影响同一内容行内长正文 fallback 和表格桥接预算,不让表格行级切片互相重叠 |
CHUNK_OVERLAP_SIZE / LightRAG(chunk_overlap_token_size=…) | 100 | 未设 P 专用 overlap 时的全局兜底 |
配置语法、优先级链、addon_params["chunker"] 运行时改值等详见 FileProcessingConfiguration-zh.md §3。
启用 P 的典型 LIGHTRAG_PARSER 写法:
LIGHTRAG_PARSER=docx:native-P,*:legacy-R
CHUNK_P_SIZE=2000
CHUNK_P_OVERLAP_SIZE=100
或在单文件覆盖:
my-proposal.[native-P].docx
确认 native parser 是否成功产生 .blocks.jsonl:
ls -l INPUT/__parsed__/<doc>.docx.parsed/<doc>.blocks.jsonl
若文件不存在或为空,P 策略会整体降级为 R,不会获得 P 的任何收益。常见原因:
LIGHTRAG_PARSER=docx:native-...。pipeline_status 错误条目)。每行一个 JSON,过滤 type == "content" 后查看 heading / level / parent_headings 是否符合预期:
jq -c 'select(.type=="content") | {level, heading, parent_headings}' \
INPUT/__parsed__/<doc>.docx.parsed/<doc>.blocks.jsonl | head
若 heading 大量为空或 level 异常,说明 native parser 没正确识别标题样式 —— 此时 P 策略的层级合并和锚点提升都会失效。
查看 text_chunks 存储中的 chunk 元数据:
jq '.[] | {heading, level, tokens, parent_headings}' \
rag_storage/kv_store_text_chunks.json | head -30
应观察到:
[part 1] / [part n](说明 Stage B 拆分发生)。target_ideal 的块(说明 Stage D 生效)。parent_headings 在不同章节切换处发生跳变,同章节内保持稳定。理想分布:大多数 chunk 落在 [target_ideal, target_max] 区间(即 N=2000 时约 1500~2000 token);明显偏小的块通常是 middle 表格切片(锁定独立)或紧靠章节边界的尾块。
若出现大量低于 small_tail_threshold 的尾块,可能是:
parent_headings 的相邻小块无法合并)。middle 表格切片堆积(表格本身就很大)。按以下顺序排查:
full_docs[doc_id].process_options 是否包含 P?full_docs[doc_id].parse_format 是否为 lightrag?若为 raw,说明走的是 legacy 路径,P 会自动降级到 R。lightrag_document_path 指向的 .blocks.jsonl 是否存在、是否非空?paragraph_semantic ... fallback to recursive_character 字样?<table format="json"> 或 <table format="html">(看 .blocks.jsonl)。未识别格式的表格只能走字符切分,无法启动 Stage B 的角色机制。table_max。低于阈值的表格保持完整,不会触发首/中/末切片。parent_headings 是否一致:父标题路径一致性约束会阻止跨主题合并。level 是否一致:同级合并要求相同 level,跨级吸收只允许浅吸深。middle 表格切片:会阻断尾部整批吸收。target_max 的块正常情况下 Stage D 的真实 token 复测会拒绝超限合并,但以下场景仍可能出现超限块:
target_max,无锚点可拆,最终走 R 字符切分但单 chunk 仍超限。enforce_chunk_token_limit_before_embedding 在 embedding 前会做最后的硬切分,下游不会真把超限 chunk 嵌入向量库。[part n] 后缀异常[part 1]:检查是否在 Stage D 中被合并 —— 合并后保留主块的 part 后缀,不拼接多个。[表格片段N] 后缀:说明使用了旧版 chunker 输出的数据,新版统一为 [part n],需要重新分块。P 策略相关日志关键字(用于 grep 排查):
paragraph_semantic — 模块入口fallback to recursive_character — 整体或单段落降级table_chunk_role — 表格角色相关bridge — Stage B.1 桥接文字处理anchor — Stage C 锚点选择