NodeParser

方法节里「20 μL、60°C、3 只小鼠」经常被切在两段:检索只命中前半,生成就编造后半。LlamaIndex 的 NodeParserDocument 切成 TextNode,默认 SentenceSplitter 先按句切开再打包到 chunk_size,并用 chunk_overlap 让边界句出现在相邻两块里。

段末注释NodeParser = list[Document] → list[Node] 的切分器;chunk_size 按 token 计(默认 tiktoken),不是字符数。

SentenceSplitter 按句打包,重叠窗口避免切断方法句(科普示意)


1. 一句话定位

维度 内容
角色 知识层的切分:来源正文 → 可嵌入、可引用的块
输入 → 输出 list[Document]list[TextNode](继承 metadata,挂 SOURCE / PREV / NEXT)
典型调用入口 SentenceSplitter(...).get_nodes_from_documents();或 Settings.text_splitter / from_documents(transformations=...)
与 LangChain / LangGraph 近邻是 langchain-text-splitters;LangGraph 不切块

出现背景:裸按字符 text[i:i+n] 会切断术语与数字。SentenceSplitter 在「不超过 token 预算」的约束下尽量在句号处断开,并把 Document 的 metadata / 排除键拷到每个子 Node。

切分策略(层级、语义切分、评测口径)见 RAG 目录;本篇只讲 LlamaIndex 怎么把剂量拧到 API 上。


2. 前置依赖与环境

1
pip install -U llama-index-core
  • Python 3.10+;llama-index-core 0.14.x
  • 本篇不调 LLMSemanticSplitterNodeParser 才需要 embedding,此处不展开。
  • from_documents 若未传 transformations 且未改 Settings.text_splitter,会用全局默认 splitter。

3. 实现逻辑

1
2
3
4
5
6
7
8
1. 准备 Document(text + metadata + 稳定 doc_id)
2. SentenceSplitter(chunk_size, chunk_overlap)
3. get_nodes_from_documents(docs)
4. 内部:按句切开 → 累积 token 直到 chunk_size → 输出一块
5. 下一块从 overlap 对应的尾部句子接着取
6. 每个 Node 继承 metadata;写 SOURCE;可选 PREVIOUS/NEXT
7. 记录 start_char_idx / end_char_idx,便于原文对齐
8. 这些 Node 再交给 VectorStoreIndex 或 IngestionPipeline 的下一步

字段级变形

1
2
3
4
Document.text = "句A。句B。句C。句D。"  # 假设每句约 20 token,chunk_size=45, overlap≈20
→ Node0.text ≈ "句A。句B。"
→ Node1.text ≈ "句B。句C。" # 句B 在 overlap 里
metadata 两块相同;SOURCE 都指向同一 doc_id

4. 原理说明

主轴是:parser 不改 Document,只派生 Node;token 预算决定打包,句子边界决定刀口。

1
2
3
4
5
6
7
8
9
10
1. get_nodes_from_documents 取出每个 Document.text
2. 用分隔符(句号、换行、paragraph_separator)得到句子列表
3. 用与模型相近的 tokenizer 累加 token
4. 累加即将超过 chunk_size:封上当前块为 TextNode
5. 回退 overlap 个 token 对应的句子,作为下一块开头
6. 拷贝 metadata、excluded_*、text_template
7. relationships[SOURCE] = 父 Document;include_prev_next_rel 则链邻居
8. start_char_idx / end_char_idx 映射回原文字符区间
少了第 5 步(overlap=0)→ 「20 μL」与「退火 60°C」分家,召回只得半句
少了第 6 步 → 块上没有 species,metadata 过滤全空

SentenceSplitter(类,llama_index.core.node_parser)出现在步骤 2。功能:句感知打包。默认 chunk_size=1024chunk_overlap=20

get_nodes_from_documents(documents, show_progress=False)(方法)出现在步骤 1。输入非空 list[Document];输出 list[TextNode],长度 ≥1(极短文可能 1 块)。

Settings.text_splitter 出现在索引构建:VectorStoreIndex.from_documents 未传 transformations 时读它。局部覆盖:

1
2
3
4
index = VectorStoreIndex.from_documents(
documents,
transformations=[SentenceSplitter(chunk_size=512, chunk_overlap=64)],
)

其它 parser(按需,不在本篇示例展开):

刀口 适用
TokenTextSplitter 纯 token 窗 无标点日志
HierarchicalNodeParser 多尺寸父子块 先宽召回再精读子块
SentenceWindowNodeParser 一句 + 左右窗口 后续 auto-merging
SemanticSplitterNodeParser 嵌入相似度断点 段落主题切换;要 embedding

5. 最小可运行示例

1
pip install -U llama-index-core
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from llama_index.core import Document
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import NodeRelationship

doc = Document(
text=(
"定量 PCR 以小鼠肝脏 GAPDH 为内参。反应体系 20 μL。"
"退火温度 60°C,循环 40 次。每个生物学重复使用 3 只 SPF 级 C57BL/6 小鼠。"
"阴性对照以无模板水代替 cDNA。"
),
doc_id="paper_001",
metadata={"species": "小鼠", "section": "methods"},
)

# 关键参数:小 chunk_size 便于看见切块;overlap 让「20 μL」与后句有机会同窗
parser = SentenceSplitter(chunk_size=48, chunk_overlap=16)
nodes = parser.get_nodes_from_documents([doc])

# 输出:每块 text、字符区间、SOURCE、邻接
for i, n in enumerate(nodes):
src = n.relationships.get(NodeRelationship.SOURCE)
nxt = n.relationships.get(NodeRelationship.NEXT)
print(i, n.start_char_idx, n.end_char_idx, n.metadata["species"])
print(" ", n.text)
print(" SOURCE", None if src is None else src.node_id, "NEXT", None if nxt is None else nxt.node_id)
# 预期:≥2 块;每块 metadata.species==小鼠;SOURCE==paper_001;相邻块有 NEXT

chunk_overlap 改成 0 再跑一遍,比较「20 μL」是否与「60°C」分家——这就是 overlap 的可观测差。


6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
chunk_size int,默认 1024 每块目标 token 上限 过大混入噪声、挤上下文;过小切断方法句、块数暴涨 方法节 256~512;综述 512~1024 按生成模型窗口与 top_k 反算
chunk_overlap int,默认 20 相邻块重叠的 token 预算 过小跨句指代丢失;过大重复占检索位 约为 chunk_size 的 10%~20% 与 size 成对调,不要只拧一个
separator str,默认 " " 句子再细拆时的分隔 中文空格少,主要靠句号/换行 保持默认 不要改成空串
paragraph_separator str,默认 "\n\n\n" 段边界 与你的文件实际空行数不一致则整段当一句 Markdown 常用 "\n\n" print 原文里的换行再设
include_metadata bool,默认 True 是否把 Document.metadata 拷到 Node False 则引用丢页码/物种 保持 True 几乎总开
include_prev_next_rel bool,默认 True 是否写 PREVIOUS/NEXT False 则窗口扩展/邻块拼接做不了 层级/窗口检索保持 True 省一点存储可关,默认开
Settings.chunk_size int 不换 splitter 类、只改全局默认块长 与显式 SentenceSplitter(chunk_size=...) 谁后生效看调用点 与上表同源 索引构建处显式传 transformations 更不易踩

7. 适用 / 不适用

维度 适用 不适用
任务形态 叙事/方法段落、句边界清晰 超长无标点表、纯代码——改 TokenTextSplitter 或按标题切
集成约束 默认 tiktoken 计数与 OpenAI 系接近 本地 tokenizer 与 tiktoken 差很大时,块长体感会偏
工程阶段 任何 from_documents 之前 已按标题/HTML 树切好——直接造 TextNode,跳过 parser

需要父子层级时用 HierarchicalNodeParser,不要把 chunk_size 拧到 64 冒充层级。


8. 易踩坑

  1. chunk_size 当字符数:中文 1024 token 远不是 1024 字,块会比直觉大或小。用 len(nodes) 和每块 text 目测。
  2. overlap=0 切方法句:剂量与条件分家,看起来像幻觉,其实是召回半句。
  3. 改了 parser 却走 from_documents 默认:忘记传 transformations / Settings.text_splitter,索引仍是旧刀口。
  4. 切完丢掉 start_char_idx:后面做引用高亮时无法对齐 PDF 原文。

小结

  • SentenceSplitter:按句打包到 token 预算,overlap 保跨句条件。
  • 子 Node 继承 metadata,并挂 SOURCE / PREV / NEXT
  • 接到索引时用 transformations=Settings.text_splitter,不要假设默认值等于你设过的数字。
  • 切分策略对错用 RAG 评测看,本篇只保证 API 剂量拧在刀口上。

参考链接

-------------本文结束感谢您的阅读-------------