################################ Tutorial 3: 节点解析 ################################ .. include:: ../links.ref .. include:: ../tags.ref .. include:: ../abbrs.ref .. contents:: 目录 :local: :depth: 2 节点解析概述 ============ 节点解析(Node Parsing)是将文档分割成更小单元的过程。 好的分割策略直接影响检索质量和最终回答的准确性。 .. code-block:: text 节点解析流程: ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Document │───►│ Parser │───►│ Nodes │ │ (完整文档) │ │ (解析器) │ │ (文本块) │ └─────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ 关系建立 │ │ (前后节点) │ └─────────────┘ 为什么需要节点解析 ------------------ .. list-table:: :header-rows: 1 :widths: 30 70 * - 原因 - 说明 * - 嵌入限制 - 嵌入模型有最大 token 限制(如 8192) * - 上下文限制 - LLM 上下文窗口有限 * - 检索精度 - 小块更容易精确匹配查询 * - 成本控制 - 减少不必要的 token 消耗 节点(Node)结构 ================ 基本属性 -------- .. code-block:: python from llama_index.core.schema import TextNode # 创建节点 node = TextNode( text="这是节点的文本内容...", id_="node_001", metadata={ "source": "document.pdf", "page": 1, "chunk_index": 0 } ) print(f"节点 ID: {node.id_}") print(f"文本长度: {len(node.text)}") print(f"元数据: {node.metadata}") 节点关系 -------- .. code-block:: python from llama_index.core.schema import NodeRelationship, RelatedNodeInfo # 节点之间可以有父子、前后关系 node1 = TextNode(text="第一段内容...", id_="node_1") node2 = TextNode(text="第二段内容...", id_="node_2") # 建立前后关系 node1.relationships[NodeRelationship.NEXT] = RelatedNodeInfo( node_id=node2.id_ ) node2.relationships[NodeRelationship.PREVIOUS] = RelatedNodeInfo( node_id=node1.id_ ) print(f"Node1 的下一个节点: {node1.relationships.get(NodeRelationship.NEXT)}") SentenceSplitter ================ 最常用的解析器,基于句子边界分割文本。 基本用法 -------- .. code-block:: python from llama_index.core.node_parser import SentenceSplitter # 创建解析器 splitter = SentenceSplitter( chunk_size=256, # 目标块大小(字符数) chunk_overlap=20 # 块之间的重叠 ) # 解析文档 from llama_index.core import Document doc = Document(text=""" 人工智能是计算机科学的一个分支。它试图理解智能的实质。 机器学习是人工智能的核心。深度学习是机器学习的子集。 自然语言处理让机器理解人类语言。计算机视觉让机器看懂图像。 这些技术正在改变我们的生活方式。未来将有更多应用场景。 """) nodes = splitter.get_nodes_from_documents([doc]) for i, node in enumerate(nodes): print(f"\n--- Node {i} ---") print(f"内容: {node.text}") print(f"长度: {len(node.text)}") 配置选项 -------- .. code-block:: python splitter = SentenceSplitter( chunk_size=512, # 块大小 chunk_overlap=50, # 重叠大小 separator=" ", # 主要分隔符 paragraph_separator="\n\n", # 段落分隔符 secondary_chunking_regex="[^,.;。?!]+[,.;。?!]?", # 二级分割正则 ) TokenTextSplitter ================= 基于 token 数量分割,更精确控制大小。 .. code-block:: python from llama_index.core.node_parser import TokenTextSplitter # 基于 token 分割 token_splitter = TokenTextSplitter( chunk_size=256, # token 数量 chunk_overlap=20, # 重叠 token 数 separator=" " ) nodes = token_splitter.get_nodes_from_documents([doc]) for i, node in enumerate(nodes): print(f"Node {i}: {len(node.text)} chars") SemanticSplitter ================ 基于语义相似度分割,保持语义完整性。 .. code-block:: python # pip install llama-index-embeddings-openai from llama_index.core.node_parser import SemanticSplitterNodeParser from llama_index.embeddings.openai import OpenAIEmbedding # 创建语义分割器 embed_model = OpenAIEmbedding() semantic_splitter = SemanticSplitterNodeParser( buffer_size=1, # 上下文缓冲 breakpoint_percentile_threshold=95, # 分割阈值 embed_model=embed_model ) nodes = semantic_splitter.get_nodes_from_documents([doc]) print(f"语义分割产生 {len(nodes)} 个节点") 工作原理 -------- .. code-block:: text 语义分割原理: 1. 将文本按句子切分 2. 计算相邻句子的嵌入向量 3. 计算相邻句子的相似度 4. 在相似度低于阈值的位置分割 ┌────────────────────────────────────────────────┐ │ 句子1 │ 句子2 │ 句子3 │ 句子4 │ 句子5 │ └────────────────────────────────────────────────┘ ↓ ↓ ↓ ↓ 0.95 0.92 0.45 0.91 相似度 ↓ 低于阈值,分割点 MarkdownNodeParser ================== 专门为 Markdown 文档设计的解析器。 .. code-block:: python from llama_index.core.node_parser import MarkdownNodeParser md_doc = Document(text=""" # 第一章:简介 这是简介部分的内容。介绍了基本概念。 ## 1.1 背景 背景信息说明。 ## 1.2 目标 目标描述。 # 第二章:方法 方法论述。 """) md_parser = MarkdownNodeParser() nodes = md_parser.get_nodes_from_documents([md_doc]) for node in nodes: print(f"标题级别: {node.metadata.get('header_path', 'N/A')}") print(f"内容: {node.text[:50]}...") print("---") HTMLNodeParser ============== 解析 HTML 文档,保留结构信息。 .. code-block:: python from llama_index.core.node_parser import HTMLNodeParser html_doc = Document(text="""

主标题

这是第一段内容。

子标题

这是第二段内容。

""") html_parser = HTMLNodeParser( tags=["p", "h1", "h2", "li"] # 要提取的标签 ) nodes = html_parser.get_nodes_from_documents([html_doc]) for node in nodes: print(f"标签: {node.metadata.get('tag', 'N/A')}") print(f"内容: {node.text}") CodeSplitter ============ 专门处理代码文件的解析器。 .. code-block:: python from llama_index.core.node_parser import CodeSplitter code_doc = Document(text=''' def hello_world(): """打印问候语""" print("Hello, World!") class Calculator: """简单计算器类""" def add(self, a, b): """加法""" return a + b def subtract(self, a, b): """减法""" return a - b def main(): calc = Calculator() print(calc.add(1, 2)) ''') code_splitter = CodeSplitter( language="python", chunk_lines=10, # 每块的行数 chunk_lines_overlap=2, # 重叠行数 max_chars=1000 # 最大字符数 ) nodes = code_splitter.get_nodes_from_documents([code_doc]) for i, node in enumerate(nodes): print(f"\n--- Code Block {i} ---") print(node.text) HierarchicalNodeParser ====================== 创建层次化的节点结构,支持父子关系。 .. code-block:: python from llama_index.core.node_parser import HierarchicalNodeParser # 创建层次化解析器 hierarchical_parser = HierarchicalNodeParser.from_defaults( chunk_sizes=[2048, 512, 128] # 从大到小的层次 ) nodes = hierarchical_parser.get_nodes_from_documents([doc]) # 查看节点层次 for node in nodes: parent_ref = node.relationships.get(NodeRelationship.PARENT) child_refs = node.relationships.get(NodeRelationship.CHILD, []) print(f"Node: {node.id_[:8]}...") print(f" Parent: {parent_ref.node_id[:8] if parent_ref else 'None'}...") print(f" Children: {len(child_refs) if isinstance(child_refs, list) else 0}") 组合使用 ======== .. code-block:: python from llama_index.core.node_parser import ( SentenceSplitter, MarkdownNodeParser, CodeSplitter ) from llama_index.core import Document class SmartNodeParser: """根据文档类型选择合适的解析器""" def __init__(self): self.parsers = { "markdown": MarkdownNodeParser(), "code": CodeSplitter(language="python"), "default": SentenceSplitter(chunk_size=512) } def parse(self, doc: Document) -> list: doc_type = doc.metadata.get("type", "default") parser = self.parsers.get(doc_type, self.parsers["default"]) return parser.get_nodes_from_documents([doc]) # 使用示例 smart_parser = SmartNodeParser() # Markdown 文档 md_doc = Document( text="# Title\n\nContent here.", metadata={"type": "markdown"} ) # 代码文档 code_doc = Document( text="def foo(): pass", metadata={"type": "code"} ) md_nodes = smart_parser.parse(md_doc) code_nodes = smart_parser.parse(code_doc) 最佳实践 ======== 分块大小选择 ------------ .. list-table:: :header-rows: 1 :widths: 25 25 50 * - 场景 - 推荐大小 - 说明 * - 问答系统 - 256-512 - 精确检索,减少噪音 * - 摘要生成 - 1024-2048 - 保持上下文完整性 * - 代码分析 - 按函数/类分割 - 保持代码逻辑完整 * - 长文档 - 层次化分割 - 多粒度检索 重叠策略 -------- .. code-block:: python # 不同重叠策略的效果 # 1. 无重叠 - 可能丢失边界信息 no_overlap = SentenceSplitter(chunk_size=256, chunk_overlap=0) # 2. 小重叠 - 基本连续性 small_overlap = SentenceSplitter(chunk_size=256, chunk_overlap=20) # 3. 大重叠 - 更好的上下文,但增加冗余 large_overlap = SentenceSplitter(chunk_size=256, chunk_overlap=50) 元数据保留 ---------- .. code-block:: python from llama_index.core.node_parser import SentenceSplitter splitter = SentenceSplitter( chunk_size=512, include_metadata=True, # 保留文档元数据 include_prev_next_rel=True # 保留前后关系 ) # 自定义元数据处理 def process_nodes(nodes, additional_metadata): for i, node in enumerate(nodes): node.metadata.update(additional_metadata) node.metadata["chunk_index"] = i node.metadata["total_chunks"] = len(nodes) return nodes 实战示例 ======== 构建一个完整的文档处理管道。 .. code-block:: python from llama_index.core import Document, VectorStoreIndex, Settings from llama_index.core.node_parser import ( SentenceSplitter, MarkdownNodeParser, ) from typing import List class DocumentProcessor: """文档处理器""" def __init__(self): self.sentence_splitter = SentenceSplitter( chunk_size=512, chunk_overlap=50 ) self.md_parser = MarkdownNodeParser() def detect_doc_type(self, doc: Document) -> str: """检测文档类型""" filename = doc.metadata.get("file_name", "") if filename.endswith(".md"): return "markdown" elif filename.endswith((".py", ".js", ".java")): return "code" return "text" def process(self, documents: List[Document]) -> List: """处理文档列表""" all_nodes = [] for doc in documents: doc_type = self.detect_doc_type(doc) if doc_type == "markdown": nodes = self.md_parser.get_nodes_from_documents([doc]) else: nodes = self.sentence_splitter.get_nodes_from_documents([doc]) # 添加处理元数据 for node in nodes: node.metadata["doc_type"] = doc_type all_nodes.extend(nodes) return all_nodes # 使用处理器 processor = DocumentProcessor() documents = [ Document( text="# Guide\n\nThis is a guide.", metadata={"file_name": "guide.md"} ), Document( text="Regular text content here.", metadata={"file_name": "readme.txt"} ) ] nodes = processor.process(documents) print(f"处理后得到 {len(nodes)} 个节点") # 构建索引 index = VectorStoreIndex(nodes) 小结 ==== 本教程介绍了: - 节点解析的概念和重要性 - 各种解析器:SentenceSplitter、TokenTextSplitter、SemanticSplitter 等 - 特定格式解析器:Markdown、HTML、Code - 层次化解析器的使用 - 分块大小和重叠策略的选择 - 完整的文档处理管道 下一步 ------ 在下一个教程中,我们将学习嵌入和向量存储, 了解如何将节点转换为向量并高效存储。 练习 ==== 1. 比较 SentenceSplitter 和 SemanticSplitter 的分割效果 2. 为不同类型的文档选择合适的解析器 3. 实现一个自定义的解析器 4. 测试不同 chunk_size 对检索效果的影响