LlamaIndex - 02 NodeParser(节点解析器)
LlamaIndex 文档切分 Node 教程(完整版)
一、核心概念:Document vs Node
- Document:原始文档对象,可以是一整个文件或一段文本
- Node:文档切分后的"块",包含文本内容、元数据以及节点间的关系信息(如前后节点、父节点引用)
from llama_index.core.schema import TextNode, NodeRelationship, RelatedNodeInfo
# 手动创建 Node 并设置关系
node1 = TextNode(text="这是第一个文本块", id_="node_1")
node2 = TextNode(text="这是第二个文本块", id_="node_2")
# 设置节点间的前后关系
node1.relationships[NodeRelationship.NEXT] = RelatedNodeInfo(node_id=node2.node_id)
node2.relationships[NodeRelationship.PREVIOUS] = RelatedNodeInfo(node_id=node1.node_id)四、最常用的切分方式:SentenceSplitter
SentenceSplitter 是 LlamaIndex 默认的 Node Parser,它会按句子边界进行切分,避免在句子中间切断。
4.1 基本用法
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
# 1. 加载文档
documents = SimpleDirectoryReader("./data/").load_data()
# 2. 创建切分器
parser = SentenceSplitter(chunk_size=512, chunk_overlap=20)
# 3. 执行切分,得到 Nodes
nodes = parser.get_nodes_from_documents(documents)
print(f"原始文档数量: {len(documents)}")
print(f"切分后节点数量: {len(nodes)}")4.2 全局配置方式
如果你希望在整个应用中统一使用相同的切分配置,可以通过 Settings 对象设置:
from llama_index.core import Settings
from llama_index.core.node_parser import SentenceSplitter
# 方式一:设置完整的 Node Parser
Settings.text_splitter = SentenceSplitter(chunk_size=1024, chunk_overlap=20)
# 方式二:只修改默认的 chunk 参数
Settings.chunk_size = 512
Settings.chunk_overlap = 20
# 之后所有的操作都会自动应用这个切分设置五、进阶切分器详解
5.1 MarkdownNodeParser - 保留 Markdown 结构
适用于处理 Markdown 格式的文档,能识别标题、代码块、列表等结构:
from llama_index.core.node_parser import MarkdownNodeParser
parser = MarkdownNodeParser()
nodes = parser.get_nodes_from_documents(documents)
# 输出的 node 会保留标题层级等结构信息5.2 CodeSplitter - 代码专用切分
用于切分代码文件,它会按抽象语法树(AST)节点进行分割,保证每个 Node 包含完整的函数或类定义:
from llama_index.core.node_parser import CodeSplitter
# 指定代码语言和块大小
parser = CodeSplitter(
language="python",
chunk_lines=40, # 每个块的最大行数
chunk_lines_overlap=15, # 块之间的重叠行数
max_chars=1500, # 每个块的最大字符数
)
nodes = parser.get_nodes_from_documents(documents)5.3 SemanticSplitterNodeParser - 语义切分
这是更高级的切分方式:根据句子间的语义相似度来决定在哪里切分,而不是机械地按字符数切割。
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding
# 需要提供嵌入模型来计算语义相似度
embed_model = OpenAIEmbedding()
parser = SemanticSplitterNodeParser(
embed_model=embed_model,
buffer_size=1, # 每次比较的句子数量
breakpoint_percentile_threshold=95, # 相似度阈值
)
nodes = parser.get_nodes_from_documents(documents)参数说明:
| 参数 | 说明 |
|---|---|
buffer_size | 分组评估语义相似度时的句子数量,设为1则逐句比较 |
breakpoint_percentile_threshold | 余弦相似度百分位数阈值,值越小切分越细 |
embed_model | 用于计算语义相似度的嵌入模型(必填) |
六、使用 IngestionPipeline 进行切分
IngestionPipeline 可以将切分与其他数据转换步骤(如元数据提取、嵌入生成)组合在一起:
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.extractors import TitleExtractor
pipeline = IngestionPipeline(
transformations=[
SentenceSplitter(chunk_size=512, chunk_overlap=20),
TitleExtractor(), # 自动提取每个节点的标题
]
)
# 执行管道
nodes = pipeline.run(documents=documents, show_progress=True)七、第三方切分器集成
7.1 DashScopeJsonNodeParser(阿里通义)
使用阿里巴巴通义实验室开发的文本切分模型,特别适合中文文档:
from llama_index.node_parser.dashscope import DashScopeJsonNodeParser
from llama_index.core.ingestion import IngestionPipeline
os.environ["DASHSCOPE_API_KEY"] = "your_api_key"
node_parser = DashScopeJsonNodeParser(
chunk_size=100,
overlap_size=0,
separator=" |,|,|。|?|!|\n|\?|\!",
language="cn" # 中文模式
)
pipeline = IngestionPipeline(transformations=[node_parser])
nodes = pipeline.run(documents=documents)7.2 Preprocess API
使用外部 API 服务进行智能切分,能识别文档的布局和语义结构:
from llama_index.readers.preprocess import PreprocessReader
loader = PreprocessReader(
api_key="your-api-key",
filepath="document.pdf"
)
nodes = loader.get_nodes() # 直接获取切分好的 nodes八、Node 的后续使用
切分完成后,最典型的用途是构建向量索引:
from llama_index.core import VectorStoreIndex
# 使用 nodes 构建索引
index = VectorStoreIndex(nodes)
# 创建查询引擎
query_engine = index.as_query_engine()
# 执行查询
response = query_engine.query("你的问题")
print(response)二、什么数据需要切分?什么时候不需要?
这是实际应用中最常见的问题。并非所有数据都需要切分,选择错误的处理方式会严重影响系统效果。
2.1 需要切分的数据类型
以下数据类型必须进行切分,否则无法有效检索:
| 数据类型 | 切分必要性 | 推荐切分器 |
|---|---|---|
| 📄 纯文本文档 | ✅ 必须切分 | SentenceSplitter |
| 📝 Markdown文档 | ✅ 必须切分 | MarkdownNodeParser |
| 🌐 HTML网页 | ✅ 必须切分 | MarkdownNodeParser(先转Markdown) |
| 💻 代码文件 | ✅ 必须切分 | CodeSplitter |
| 📑 长文本日志 | ✅ 必须切分 | SentenceSplitter |
2.2 不需要切分的数据类型
以下结构化数据天然具有原子单位,直接检索即可,切分反而会破坏其结构:
| 数据类型 | 原子单位 | LlamaIndex 推荐方式 |
|---|---|---|
| 🗄️ SQL数据库 | 行/记录 | SQLTableRetriever + 文本到SQL |
| 📋 JSON/XML | 字段/路径 | 字段路径访问 + Jsonalyzer |
| 📊 CSV表格 | 行(如果行级语义完整) | PandasQueryEngine |
| 🕸️ 知识图谱 | 三元组/节点 | KnowledgeGraphIndex |
| 📈 时间序列 | 时间点/时段 | TimeIndex |
| 🔢 纯数值数组 | 元素 | 直接数学计算 |
# SQL 数据示例 - 不需要切分
from llama_index.core.query_engine import NLSQLTableQueryEngine
query_engine = NLSQLTableQueryEngine(
sql_database=sql_database,
tables=["users", "orders"],
)
response = query_engine.query("查询过去30天订单金额前10的用户")
# JSON 数据示例 - 不需要切分
import json
data = json.load(open("data.json"))
# 直接按路径访问即可,不需要切分
value = data["users"][0]["address"]["city"]2.3 半结构化数据:混合处理策略
实际数据常常是表格 + 自然语言描述的混合体,需要分别处理:
# 产品手册(半结构化示例)
## 规格参数(表格形式)
| 型号 | 价格 | 重量 | 库存 |
|------|------|------|------|
| A100 | $10000 | 30kg | 50台 |
| V100 | $8000 | 25kg | 30台 |
详细说明:A100适用于大规模AI训练,支持FP64精度...混合处理方案:
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.extractors import TableExtractor
# 方案1:表格转文本 + 正常切分
table_text = "型号A100价格10000美元重量30kg库存50台。型号V100价格8000美元重量25kg库存30台。"
nodes = SentenceSplitter(chunk_size=512).get_nodes_from_documents([table_text])
# 方案2:使用专门的TableNodeParser
from llama_index.core.node_parser import TableNodeParser
table_nodes = TableNodeParser().get_nodes_from_documents(table_documents)
# 方案3:混合索引 - 表格走结构化查询,文本走向量检索
from llama_index.core import VectorStoreIndex, SQLTableNodeMapping
# 文本部分:切分后建向量索引
text_nodes = SentenceSplitter().get_nodes_from_documents(text_docs)
vector_index = VectorStoreIndex(text_nodes)
# 表格部分:保留原结构,不走切分
sql_query_engine = NLSQLTableQueryEngine(sql_database)2.4 判断流程图
def decide_chunking_strategy(data):
"""
判断数据类型应采用切分还是直接检索
"""
# 1. 是否为纯文本/半文本
if is_free_text(data):
return "USE_CHUNKING", "SentenceSplitter"
# 2. 是否有明确的原子单位(行、字段、记录)
if has_clear_atomic_unit(data): # 如:SQL行、JSON字段
return "NO_CHUNKING", "Direct query / Path access"
# 3. 单个记录是否超过token限制
if estimate_tokens(data) > 8192: # LLM上下文窗口
return "USE_CHUNKING", "SentenceSplitter (large chunk)"
# 4. 数据结构是否严格层级化
if is_strictly_hierarchical(data): # 如:深度嵌套JSON
return "NO_CHUNKING", "Jsonalyzer"
return "USE_CHUNKING", "SentenceSplitter (default)"2.5 核心原则总结
| 原则 | 说明 | 示例 |
|---|---|---|
| 原子性优先 | 如果数据本身具有原子单位(行、记录、字段),直接使用该单位检索 | SQL行、JSON字段 |
| 破坏结构不切 | 切分会破坏数据关系(如父-子引用、外键约束)时,不切分 | 树形JSON、XML |
| 超限才切 | 只有当单个单位超过LLM上下文窗口时,才考虑切分 | 超长文本字段 |
| 混合分别处理 | 半结构化数据中的表格和文字分开处理,使用不同策略 | 产品规格表 + 描述文字 |
三、自动切分 vs 手动切分
3.1 VectorStoreIndex 的自动切分
VectorStoreIndex 在构建索引时会自动完成文档的切分:
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
# 加载文档
documents = SimpleDirectoryReader("./data/").load_data()
# 构建索引 —— 这里会自动完成切分!
index = VectorStoreIndex.from_documents(documents)自动完成的操作:
- 将每个
Document切分成更小的Node对象(文本块) - 为每个
Node生成向量嵌入(embedding) - 将向量存储到索引中,准备检索
控制自动切分的方式:
from llama_index.core import Settings
from llama_index.core.node_parser import SentenceSplitter
# 方式一:通过全局 Settings 配置
Settings.text_splitter = SentenceSplitter(chunk_size=512, chunk_overlap=50)
index = VectorStoreIndex.from_documents(documents)
# 方式二:通过 transformations 参数直接传入
text_splitter = SentenceSplitter(chunk_size=500, chunk_overlap=50)
index = VectorStoreIndex.from_documents(
documents,
transformations=[text_splitter]
)3.2 为什么需要手动切分?
既然 VectorStoreIndex 会自动切分,为什么还要手动操作?因为手动切分给你精细控制权。自动切分是"一键傻瓜式"操作,适合快速原型开发;而生产环境中,你往往需要以下手动控制能力。
优势一:自定义元数据(最关键)
每个 Node 可以附带不同的元数据,这对检索质量影响巨大:
from llama_index.core.schema import TextNode
# 手动创建节点,为每个块添加特定元数据
nodes = [
TextNode(
text="A100 显卡支持 FP64 高精度计算",
metadata={
"page": 5,
"section": "技术规格",
"product": "A100",
"importance": "high",
"price_range": "$10000+"
}
),
TextNode(
text="安装 A100 需要 300W 电源和 PCIe 4.0 插槽",
metadata={
"page": 12,
"section": "安装指南",
"product": "A100",
"importance": "medium"
}
)
]
# 自动切分做不到这种细粒度的元数据
index = VectorStoreIndex(nodes)优势二:自定义节点关系
可以手动建立节点间的引用关系,实现更精准的检索:
from llama_index.core.schema import TextNode, NodeRelationship, RelatedNodeInfo
# 创建父子节点关系
parent_node = TextNode(text="第一章:AI 芯片概述", id_="parent_1")
child_node_1 = TextNode(
text="1.1 GPU 架构详解...",
id_="child_1",
relationships={
NodeRelationship.PARENT: RelatedNodeInfo(node_id="parent_1")
}
)
child_node_2 = TextNode(
text="1.2 TPU 对比分析...",
id_="child_2",
relationships={
NodeRelationship.PARENT: RelatedNodeInfo(node_id="parent_1")
}
)
# 检索时可以获得层级上下文自动切分生成的节点是扁平的、无关系的。
优势三:过滤和预处理
在切分阶段可以做很多自动切分做不到的事:
# 手动切分前可以进行智能过滤
documents = load_documents()
filtered_nodes = []
for doc in documents:
# 1. 移除噪音内容
if "[广告]" in doc.text:
continue
# 2. 合并太短的块
if len(doc.text) < 50:
# 合并到上一个节点
pass
# 3. 根据内容决定切分策略
if "表格" in doc.metadata.get("type", ""):
# 表格用特殊方式处理
nodes = table_parser.parse(doc)
else:
nodes = text_parser.parse(doc)
filtered_nodes.extend(nodes)优势四:保证完整性约束
某些内容绝对不能被切分,自动切分无法保证:
# 代码示例:函数定义必须完整
# 自动切分可能在函数中间切断
def complex_function(a, b, c):
# 100 行代码...
if a > b:
# 自动切分可能在这里切断!导致语法错误
pass
# 手动切分:按 AST 节点切分,保证函数完整
from llama_index.core.node_parser import CodeSplitter
code_parser = CodeSplitter(language="python", chunk_lines=50)
nodes = code_parser.get_nodes_from_documents(code_docs)优势五:多模态和复杂数据结构
# 混合内容:图片 + 文字
nodes = [
TextNode(
text="图 1 显示了 A100 的核心架构",
metadata={"image_path": "a100_diagram.png", "image_id": "fig1"}
),
TextNode(
text="该架构包含 6192 个 CUDA 核心...",
metadata={"references": ["fig1"]}
)
]
# 或者预先生成的 embedding
from llama_index.core.embeddings import OpenAIEmbedding
embed_model = OpenAIEmbedding()
node = TextNode(text="重要内容")
node.embedding = embed_model.get_text_embedding("重要内容") # 预计算3.3 自动切分 vs 手动切分 对比总结
| 能力 | 自动切分 | 手动切分 |
|---|---|---|
| 快速上手 | ✅ 一行代码 | ❌ 需要写代码 |
| 自定义元数据 | ❌ 只能文档级别 | ✅ 节点级别精细控制 |
| 节点关系(父子/前后) | ❌ 扁平结构 | ✅ 任意关系 |
| 内容过滤/清洗 | ❌ 无法干预 | ✅ 完全控制 |
| 完整性保证 | ⚠️ 可能切断关键内容 | ✅ 可保证 |
| 多模态支持 | ❌ 有限 | ✅ 灵活 |
| 性能优化(如预计算 embedding) | ⚠️ 批量计算 | ✅ 精细控制 |
3.4 实践建议:渐进式优化
# 阶段一:快速验证(用自动切分)
index = VectorStoreIndex.from_documents(documents)
# 测试效果...
# 阶段二:发现问题后,切换到手动切分
# 例如:检索结果缺失某些关键信息、上下文不连贯
# 阶段三:针对性优化
from llama_index.core.node_parser import SentenceSplitter
# 先用解析器生成基础 nodes
parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents(documents)
# 再手动增强
for i, node in enumerate(nodes):
# 添加缺失的元数据
node.metadata["chunk_index"] = i
node.metadata["source_file"] = documents[0].metadata.get("file_name")
# 标记重要节点
if "summary" in node.text.lower():
node.metadata["is_summary"] = True
index = VectorStoreIndex(nodes)3.5 一句话总结
| 方式 | 适用场景 |
|---|---|
| 自动切分 | 快且省事,适合探索和简单场景 |
| 手动切分 | 可控且精准,适合生产和复杂场景 |
💡 核心提示:当你发现检索结果不够好时,手动切分往往是第一个需要优化的地方。
九、切分策略总结与建议
9.1 按文档类型选择策略
| 文档类型 | 推荐切分器 | chunk_size / 说明 | 是否需要切分 |
|---|---|---|---|
| 普通文本文档 | SentenceSplitter | 512-1024 | ✅ 必须 |
| Markdown文档 | MarkdownNodeParser | 保留标题结构 | ✅ 必须 |
| 代码文件 | CodeSplitter | 按AST节点切分 | ✅ 必须 |
| 长文档/书籍 | SemanticSplitterNodeParser | 按语义边界切分 | ✅ 必须 |
| SQL数据库 | 不切分 | 使用NLSQLTableQueryEngine | ❌ 不需要 |
| JSON/XML | 不切分 | 路径直接访问 | ❌ 不需要 |
| CSV小表 | 不切分 | 使用PandasQueryEngine | ❌ 不需要 |
| 半结构化混合 | 表格不切分 + 文本切分 | 混合策略 | ⚠️ 视情况 |
| 中文文档 | DashScopeJsonNodeParser | 专门优化中文分词 | ✅ 必须 |
9.2 关键提示
- chunk_size:越小检索越精准但可能丢失上下文;越大上下文更完整但检索噪音增加
- chunk_overlap:通常设置为
chunk_size的 10%-20%,用于保持相邻块之间的信息连续性 - 结构化数据优先不切:SQL、JSON等有原子单位的数据,直接检索即可
- 半结构化混合处理:表格部分走结构化查询,文本部分走向量检索
- 自动切分 vs 手动切分:快速验证用自动,生产环境根据需求选择手动
- 实验验证:不同的切分方式对 RAG 效果影响显著,建议在实际任务中进行实验对比
9.3 快速决策表
# 你的数据是...?
data_type = input("数据类型: ")
if data_type in ["PDF", "TXT", "MD", "HTML", "代码"]:
print("→ 使用 SentenceSplitter 或 MarkdownNodeParser 切分")
elif data_type in ["SQL", "数据库", "表格(结构化)"]:
print("→ 不需要切分,使用 NLSQLTableQueryEngine")
elif data_type in ["JSON", "XML", "API响应"]:
print("→ 不需要切分,直接路径访问")
elif data_type == "半结构化(表格+文字)":
print("→ 混合策略:表格保留结构,文字切分")
else:
print("→ 默认使用 SentenceSplitter")
# 你想控制到什么程度?
control_level = input("控制程度(快速/精细): ")
if control_level == "快速":
print("→ 使用 VectorStoreIndex.from_documents() 自动切分")
else:
print("→ 手动创建 Nodes,自定义元数据、关系和预处理逻辑")这个完整版教程现在涵盖了:
- ✅ 核心概念(Document vs Node)
- ✅ 什么数据需要/不需要切分
- ✅ 自动切分 vs 手动切分的对比和选择
- ✅ 各种切分器的详细使用
- ✅ 进阶功能和第三方集成
- ✅ 实践建议和决策指南