003.大模型 RAG 文档切分进阶实战

一:数据清洗与预处理

在文档切分之前,针对文本内容需要进行适当的数据清洗和预处理,这一步骤可以显著提高切分质量和后续检索效果。数据清洗和预处理就是在源头把关,确保流入 RAG 系统的每一滴水都是干净的,这样最终输出的答案才能清澈见底。这个环节投入 1 小时的工作,可能在后续每个环节都为你节省 10 小时的调试和优化时间,并且将噪声内容清洗掉以后,会降低后期向量存储成本,提升检索的速度和回答的答案质量。对于企业级项目来说,这是性价比最高的投入之一。

1.1 总体目标与原则

数据清洗的核心目标是:

1.2 不同类型数据的清洗步骤

1. 纯文本文档清洗

import re

# text 文本处理
def clean_text(text):
    # 去除多余空白
    text = re.sub(r'\s+', ' ', text)
    # 修复断开的单词
    text = re.sub(r'(\w+)-\s+(\w+)', r'\1\2', text)
    # 标准化引号
    text = text.replace('"', '"').replace('"', '"')
    return text.strip()

# html 文本处理
def clean_html(text):
    return re.sub(r'<.*?>', '', text)  # 移除 HTML 标签

# markdown 文本处理
def clean_markdown(text):
    text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)  # 移除粗体标记但保留文字
    text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text)  # 移除链接标记但保留文字
    return text

2. 表格数据清洗

表格数据需要特别注意结构完整性:

import pandas as pd

def clean_table(df):
    if type(df) != pd.DataFrame:
        df = pd.DataFrame(df)

    # 去除完全空白的行和列
    df = df.dropna(how='all').dropna(axis=1, how='all')
    # 填充 NaN 值
    df = df.fillna('')
    # 删除完全重复的行 (不要使用 inplace=True,它会返回 None)
    df = df.drop_duplicates()
    # 标准化列名
    df.columns = [str(col).strip() for col in df.columns]

    return df

3. 图像文档清洗

图像文档(如扫描的 PDF)需要 OCR 预处理:

import cv2
from matplotlib import pyplot as plt

# 显示图像的函数
def show_image(img, title="Image", cmap=None):
    plt.figure(figsize=(4, 2))
    if cmap:
        plt.imshow(img, cmap=cmap)
    else:
        plt.imshow(img)
    plt.title(title)
    plt.axis('off')
    plt.show()
def clean_image(img_path):
    """
    图像降噪 - 去除扫描文档中的噪声点
    """
    # 读取图像
    img = cv2.imread(img_path)
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    # 转换为灰度图
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    show_image(img_rgb, "Original Image")

    # 方法 1: 高斯模糊去噪
    gaussian_denoised = cv2.GaussianBlur(gray, (5, 5), 0)
    # 方法 2: 中值滤波去噪 (对椒盐噪声效果好)
    median_denoised = cv2.medianBlur(gray, 3)
    # 方法 3: 双边滤波 (保持边缘)
    bilateral_denoised = cv2.bilateralFilter(gray, 9, 75, 75)

    show_image(gaussian_denoised, "Gaussian Denoised", cmap='gray')
    show_image(median_denoised, "Median Denoised", cmap='gray')
    show_image(bilateral_denoised, "Bilateral Denoised", cmap='gray')

    return gray, gaussian_denoised, median_denoised, bilateral_denoised

# 使用示例
gray, gaussian, median, bilateral = clean_image("手写公式.png")

4. 代码块清洗

代码块需要保持格式和缩进:

import re

def clean_code(code_text, remove_comments=False):
    """
    增强版代码清洗
    """
    if not code_text:
        return ""

    cleaned_lines = []
    in_multiline_comment = False

    for line in code_text.split('\n'):
        # 移除行尾空白
        clean_line = line.rstrip()

        # 可选:移除单行注释
        if remove_comments:
            if not in_multiline_comment:
                # 检查是否进入多行注释
                if '"""' in clean_line or "'''" in clean_line:
                    in_multiline_comment = not in_multiline_comment
                    # 简单处理:直接跳过含有多行注释符号的行
                    continue
                # 移除单行注释(# 后面的内容)
                clean_line = re.sub(r'#.*$', '', clean_line)
            else:
                # 在多行注释中,跳过这行
                if '"""' in clean_line or "'''" in clean_line:
                    in_multiline_comment = False
                continue

        # 如果行不为空,或者我们保留空行(这里保留一个空行)
        if clean_line or (cleaned_lines and not cleaned_lines[-1]):
            cleaned_lines.append(clean_line)

    # 重新组合并确保首尾没有空行
    result = '\n'.join(cleaned_lines).strip()

    # 确保以换行符结束(可选)
    if result and not result.endswith('\n'):
        result += '\n'

    return result

5. 混合文档清洗

混合文档需要分类处理不同内容:

def clean_mixed_content(content_type, content):
        """
        统一清洗入口
        content_type: 'text', 'table', 'image', 'code'
        """
        try:
            if content_type == 'text':
                text = clean_text(content)
                text = clean_html(text)
                text = clean_markdown(text)
                return text
            elif content_type == 'table':
                return clean_table(content)
            elif content_type == 'image':
                return clean_image(content)
            elif content_type == 'code':
                return clean_code(content)
            else:
                return content
        except Exception as e:
            return content

def batch_clean(documents):
    """
    批量清洗文档
    documents: 列表,每个元素是 (content_type, content) 元组
    """
    results = []
    for i, (doc_type, content) in enumerate(documents):
        cleaned = clean_mixed_content(doc_type, content)
        results.append((doc_type, cleaned))

    return results

1.3. 数据清洗的最佳实践

数据清洗是文档切分的基础工作,良好的清洗能够显著提高后续切分质量和检索效果。

  1. 保持原始结构:清洗过程中尽量保留文档的原始结构和层次关系
  2. 最小化信息损失:只去除明确的噪声,避免删除可能有用的内容
  3. 标准化格式:统一标点符号、引号、连字符等格式元素
  4. 处理特殊字符:转义或替换可能影响后续处理的特殊字符
  5. 版本控制:保留原始文档副本,以便需要时回滚

二:RAG 文档切分概述

RAG(Retrieval-Augmented Generation)系统中的文档切分是构建高效检索系统的关键步骤。文档切分,也称为分块(Chunking),是将长文档分割成更小、更易于管理的片段的过程,防止长文档有大部分的噪音数据进入上下文中,这些片段随后被转换为向量并存储在向量数据库中,以便在查询时进行快速检索。

Pasted image 20260716100556.png

2.1. 文档切分的重要性

文档切分直接影响 RAG 系统的性能表现:

2.2. 切分粒度对 RAG 效果的影响

粒度是 RAG 性能的"第一性变量"。不同粒度级别对系统性能有显著影响:

粒度级别 检索准确性 生成质量 计算成本 典型策略
细粒度(句子级) 低(上下文不足) SentenceWindowNodeParser
中等粒度(段落级) 中高 中高 SentenceSplitter
粗粒度(文档级) 低(噪声多) 高(上下文完整) 直接使用 Document

2.3. 文档切分的基本流程

文档切分通常包括以下基本步骤:

  1. 文档加载:使用适当的文档加载器加载文档内容(unstructured,Reader 等)
  2. 预处理:根据文档类型进行必要的清洗和格式化
  3. 切分策略选择:根据文档特点和需求选择合适的切分方法
  4. 执行切分:应用选定的切分策略将文档分割成块
  5. 后处理:对切分结果进行必要的调整和优化

2.4. 切分效果评估指标

评估文档切分效果的常用指标包括:


三:LlamaIndex 核心对象概念

LlamaIndex 提供了灵活的文档处理框架,理解其核心概念是有效使用文档切分功能的基础。它们是将大文档首先会读取为 document 对象,然后拆解为适宜检索的语义片段(节点 Node)。选择合适的切分器对于提升 RAG 系统的检索精度和回答质量至关重要。

3.1. Document 和 Node 的概念

Pasted image 20260716101031.png

维度 Document (文档) Node (节点)
概念层级 顶层数据容器,代表一个完整的数据源(如一个 PDF 文件、一个 API 响应) 基础数据单元,由 Document 解析 / 分块而成,代表其中一段文本
核心职责 数据的统一与标准化:将不同来源、格式的数据封装成统一对象,便于系统处理 数据的精细化组织与关联:通过分块、元数据和关系,构建细粒度的数据网络以支持高效检索
内容与关系 包含原始、完整的数据内容;Document 之间通常独立 包含数据片段;Node 之间可通过关系(如父子、先后)构建复杂的图结构
典型使用场景 数据加载与初始化,统一元数据管理(如为整个文档设置来源、作者) 构建各类索引(向量、关键词等)的基础,实现精确的语义检索,构建复杂的关系知识图谱

关于Document

关于 Node

Document 和 Node 在 LlamaIndex 中分别扮演着数据容器和语义单元的角色。理解它们的分工与协作,是有效使用 LlamaIndex 构建高效检索系统的关键。Document 负责承载原始数据,而 Node 则作为构建索引、进行语义检索和生成回答的真正基石。

3.2. 元数据传播机制

LlamaIndex 中的元数据传播遵循继承原则:

  1. Document 级元数据:自动传播到所有由该 Document 生成的 Node
  2. Node 级元数据:可以覆盖或补充 Document 级元数据
  3. 关系元数据:存储 Node 之间的关系信息,如父子、前后关系
from llama_index.core import Document
# 导入 SentenceSplitter 句子分割器
from llama_index.core.node_parser import SentenceSplitter

# 创建 Document 并设置元数据
doc = Document(
    text="这是一份关于 RAG 技术的文档...",
    metadata={
        "file_name": "rag_guide.pdf",
        "category": "技术文档",
        "author": "AI 研究团队",
        "created_date": "2023-11-15"
    }
)

# 从 Document 创建 Node 时,元数据会自动传播
splitter = SentenceSplitter()
nodes = splitter.get_nodes_from_documents([doc])

# 每个 node 都会继承 doc 的 metadata
nodes[0].metadata 

3.3. Node 结构

属性名 类型 说明
id_ (node_id) str 节点的唯一标识符,可自动生成或手动指定
text str 节点包含的文本内容(chunk)
metadata Dict[str, Any] 存储文档的元数据信息(如文件名、页码等)
embedding List[float] 节点的向量嵌入表示
relationships Dict[NodeRelationship, RelatedNodeInfo] 节点间关系映射
hash str 内容的哈希值,用于去重和变更检测
excluded_embed_metadata_keys List[str] embedding 时排除的元数据键
excluded_llm_metadata_keys List[str] LLM 处理时排除的元数据键
start_char_idx Optional[int] 在原始文档中的起始字符位置
end_char_idx Optional[int] 在原始文档中的结束字符位置
text_template str 文本格式化模板
metadata_template str 元数据格式化模板

LlamaIndex 中,切分后的基本单元是 Node,每个 Node 包含以下核心属性:

from llama_index.core.schema import TextNode

# Node 的基本结构
node = TextNode(
    text="这是切分后的文本内容",           # 文本内容
    metadata={                            # 元数据
        "file_name": "document.pdf",
        "page_number": 1,
        "chunk_id": 0
    },
    id_="node_id_123",                   # 唯一标识符
    embeddings=[]                        # 文本嵌入向量(可选)
)

3.4. 关系结构

切分后的 Node 之间可以建立多种关系:

  1. 前后关系:表示 Node 在原文档中的顺序
  2. 父子关系:表示层次化切分中的层级关系
  3. 相似关系:表示语义相似的 Node
from llama_index.core.schema import NodeRelationship
import json

# Node 的基本结构
node0 = TextNode(
    text="这是切分后的文本内容 0",           # 文本内容
    metadata={                            # 元数据
        "file_name": "document.pdf",
        "page_number": 1,
        "chunk_id": 0
    },
    id_="node_id_0",                   # 唯一标识符
)

node1 = TextNode(
    text="这是切分后的文本内容 1",           # 文本内容
    metadata={                            # 元数据
        "file_name": "document.pdf",
        "page_number": 1,
        "chunk_id": 1
    },
    id_="node_id_1",                   # 唯一标识符
)

node2 = TextNode(
    text="这是切分后的文本内容 2",           # 文本内容
    metadata={                            # 元数据
        "file_name": "document.pdf",
        "page_number": 1,
        "chunk_id": 2
    },
    id_="node_id_2",                   # 唯一标识符
)

# 顺序关系
node1.relationships[NodeRelationship.SOURCE] = node0.id_
node1.relationships[NodeRelationship.NEXT] = node2.id_
node2.relationships[NodeRelationship.PREVIOUS] = node1.id_

# 层次化关系
node2.relationships[NodeRelationship.SOURCE] = node0.id_
node1.relationships[NodeRelationship.CHILD] = node2.id_
node2.relationships[NodeRelationship.PARENT] = node1.id_

# 打印节点关系
print(node1.relationships) # {<NodeRelationship.SOURCE: '1'>: 'node_id_0', <NodeRelationship.NEXT: '3'>: 'node_id_2', <NodeRelationship.CHILD: '5'>: 'node_id_2'}
print(node2.relationships) # {<NodeRelationship.PREVIOUS: '2'>: 'node_id_1', <NodeRelationship.SOURCE: '1'>: 'node_id_0', <NodeRelationship.PARENT: '4'>: 'node_id_1'}

关系类型说明


四:文档切分核心原则

4.1. 语义完整性原则

核心思想:切分应尽量不破坏语义单元的完整性,避免在句子或段落中间进行不合理的分割。

4.2. 长度控制原则

核心思想:控制每个文本块的长度,使其适应模型的上下文限制和检索需求。

4.3. 重叠率原则

Pasted image 20260716104319.png

核心思想:在相邻文本块之间设置适当的重叠区域,避免重要信息在边界处丢失。

4.4. 特殊格式策略原则

核心思想:针对特殊格式的文档(如代码、表格、列表)采用专门的切分策略。


五:切分工具选型与实战

维度 TextSplitter NodeParser
核心定位 基础的文本分割工具 高级的文档解析与节点生成框架
处理逻辑 通常基于固定规则,如长度、标点或字符递归分割 除基础分割外,可集成语义分割、代码解析等复杂策略
输出结果 文本块(字符串列表) Node 对象列表,包含文本、元数据及节点间关系信息
语义感知 通常不具备 部分解析器(如 SemanticSplitterNodeParser)具备语义感知能力
性能特点 轻量快速,计算开销小 功能更强的解析器(如语义分割)可能速度较慢,计算成本高
适用场景 基于语义相似度进行文本切分,保持语义连贯性

处理主题转换自然、结构复杂的文档

对切分质量要求较高的生产环境
构建生产级 RAG 系统

处理复杂文档(如代码、学术论文)

需要利用节点间关系(如父节点、子节点)的复杂查询

5.1. Text-Splitters(文本分割器)类型

Text-Splitters 专注于将任意文本字符串拆分成多个片段,按字符 / 句子 / token / 自定义分隔规则切分,通常只关心文本长度与重叠上下文,不会理解文件格式。

1. TokenTextSplitter Token 切分器

TokenTextSplitter 按照 token 长度进行切分,适用于需要精确控制 token 数量的场景,特别是在有严格 token 限制的嵌入模型或语言模型中使用。

核心特性

from llama_index.core.node_parser import TokenTextSplitter

# 初始化 TokenTextSplitter
token_splitter = TokenTextSplitter(
    chunk_size=512,        # 每 chunk 目标 token 数(可调)
    chunk_overlap=64,      # 重叠 token 数(可调)
    separator=" "          # 分隔符(一般用空格)
)

# 将 documents 转成 nodes(LlamaIndex 内部 node / fragment)
# nodes_from_tokens = token_splitter.get_nodes_from_documents(documents)
# print(nodes_from_tokens[1].get_content())

# 测试文本
test_text = """在检索增强生成(RAG)系统中,文档切分与 Node 转换作为连接原始数据与语言模型的关键预处理环节,直接决定了系统的检索精度、生成质量及整体性能。行业实践数据表明,90% 的 RAG 效果问题源于元数据与分块策略不当,而通过优化分块策略可使检索准确率提升 30 - 50%,语义分块较固定分块的准确率优势可达 27%。这一技术环节的重要性体现在:分块过大易引入冗余噪音,增加语言模型理解负担;分块过小或切分不当则可能破坏语义连贯性,导致完整知识点被拆分;未能适配文档结构的机械分块方式还会忽视标题、列表等结构化信息,影响信息提取完整性。
LlamaIndex 作为连接自定义数据与大语言模型(LLMs)的核心框架,通过将文档(如 PDF、文本文件)分解为包含文本内容、向量嵌入和元数据的 Node 组件,构建了结构化文档管理的技术范式。其核心抽象在于将原始文档转换为语义连贯的 Node 集合,向量存储仅保留 Node 内容的嵌入向量与文本信息,这一机制简化了索引构建流程并提升了检索相关性。文档切分与 Node 转换的质量不仅影响向量检索的效率,更决定了上下文增强(Context Augmentation)这一 RAG 核心能力的实现效果。
本文聚焦文档切分与 Node 转换的技术实践,结合 LlamaIndex 框架的实现机制,系统调研分块策略设计、元数据管理及 Node 组件化等关键技术点。通过分析行业最佳实践与典型案例,旨在为 RAG 系统开发者提供可落地的优化方案,解决分块噪音、语义断裂、结构信息丢失等核心痛点,为构建高性能检索增强生成应用奠定技术基础。"""

# 按照文本来进行切分
nodes_from_tokens = token_splitter.split_text(test_text)
# 打印切分后的文本数量
print( "切分后的文本数量: " + str(len(nodes_from_tokens))) # 切分后的文本数量: 2
print("===============================================")
# 打印第一个文本
print("第一个文本:")
print(nodes_from_tokens[0])
print("===============================================")
# 打印第二个文本
print("第二个文本:")
print(nodes_from_tokens[1])
print("===============================================")

2. SentenceSplitter 句子切分器

SentenceSplitter 是一种基于自然语言句子和段落边界进行分割的解析器,类似于 LangChain 的 RecursiveCharacterTextSplitter。它优先在句子结束处或段落分隔符处进行分割,尽量避免在句子中间切断,以保持语义单元的完整性。

Pasted image 20260716141712.png

核心特性与工作原理

关键参数

参数名 类型 说明 默认值
chunk_size int 每个文本块的目标最大 token 数 1024
chunk_overlap int 相邻文本块之间重叠的 token 数 200
separator str 用于分割的主要分隔符 " " (空格)
paragraph_separator str 用于识别段落的分隔符 "\n\n\n"

适用场景: SentenceSplitter 非常适用于自然语言文本,如新闻文章、博客文章、书籍章节等结构清晰的散文体内容。它是 LlamaIndex 中最常用且默认的文本分割器之一。

from llama_index.core.node_parser import SentenceSplitter
# 初始化 SentenceSplitter
sentence_splitter = SentenceSplitter(
    chunk_size=512,      # 这里 chunk_size 表示 token 近似或字符近似,视版本调整
    chunk_overlap=64
)

递归测试

Pasted image 20260716141933.png

from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import Document

# SentenceSplitter 测试递归切分
# 示例文本:一个长句子和一个短句子
text = "这是一个非常长的句子,它包含了多个逗号分隔的部分,和一些短的文本,因此整个句子的长度会很容易超过我们设定的块大小限制。短期。"
document = Document(text=text)

# 初始化 SentenceSplitter,设定块大小非常小以触发递归分割
splitter = SentenceSplitter(chunk_size=10, chunk_overlap=0, separator=",。!?!?.\n¡¿")
nodes = splitter.get_nodes_from_documents([document])

# 打印分割结果
print("=" * 60)
for i, node in enumerate(nodes):
    print(f"块 {i+1}: {node.text}")

print("=" * 60)
print("打印Node信息:")
print(nodes[0].__dict__)

3. CodeSplitter 代码切分器

CodeSplitter 专为编程语言源代码设计,利用编程语言的抽象语法树(AST)来理解代码结构,确保将代码按功能单元进行分割。

核心特性

from llama_index.core.node_parser import CodeSplitter
from llama_index.core.schema import Document

# 示例 Python 代码:一个简单的函数和类
sample_code = '''
def calculate_fibonacci(n):
    """计算斐波那契数列的第n项"""
    if n <= 1:
        return n
    else:
        return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)
'''

# 创建文档对象
# document = Document(text=sample_code)
# 初始化 CodeSplitter,指定 Python 语言
code_splitter = CodeSplitter(
    language="python",    # 指定编程语言
    chunk_lines=10,       # 每块大约行数
    chunk_lines_overlap=2, # 块之间重叠行数
    max_chars=600        # 每块最大字符数
)

# 执行切分
# nodes = code_splitter.get_nodes_from_documents([document])
nodes = code_splitter.split_text(sample_code)

print(f"原始代码字符数: {len(sample_code)}")
print(f"切分后的节点数量: {len(nodes)}")
print("\n" + "="*50 + "\n")

# 显示切分结果
for i, node in enumerate(nodes):
    print(f"节点 {i+1} (字符数: {len(node)}):")
    print("-" * 30)
    print(node)
    print("\n" + "="*50 + "\n")

5.2. File-Based Node Parsers(文件型节点切分器)

File-Based Node Parsers 面向文件类型与结构(如 Markdown、JSON、PDF、HTML、代码文件等),会根据文件的语义 / 格式选择专门解析器,把整个文件解析成带元数据的 Node,可能保持章节 / 层级、标题、表格等结构。

1. MarkdownNodeParser markdown 切分器

切分器 MarkdownNodeParser 专门用于处理 Markdown 文件,能够识别 Markdown 的层级结构(如标题、列表、代码块等),并据此进行切分。

# 文本类型是 markdown 的
from llama_index.core.node_parser import MarkdownNodeParser
from llama_index.core.readers import SimpleDirectoryReader

markdown_docs = SimpleDirectoryReader(input_files=["扩展调参.md"]).load_data()
# markdown_docs = [Document(text="# 主标题\n\n这是第一段。\n\n## 子标题\n\n这是第二段。")]

# 创建 Markdown 解析器
parser = MarkdownNodeParser()

# 从 Markdown 文件创建节点
nodes = parser.get_nodes_from_documents(markdown_docs)

# 显示切分结果
for i, node in enumerate(nodes):
    print(f"节点 {i+1} (字符数: {len(node.text)}):")
    print("-" * 30)
    print(node.text)
    print("\n" + "="*50 + "\n")

2. JSONNodeParser Json 切分器

JSONNodeParser 用于处理 JSON 文件,能够根据 JSON 结构进行切分,保持数据的层次关系。

from llama_index.core.node_parser import JSONNodeParser

json = """
{
    "id_": "0a1eee9a-635a-4391-8b74-75bf3c648f0e",
    "embedding": null,
    "metadata": {
        "document_id": "FULadzkWmovlfkxSgLPcE4oWnPf"
    },
    "excluded_embed_metadata_keys": [],
    "excluded_llm_metadata_keys": [],
    "mimetype": "text/plain"
}
"""

# 创建 JSON 解析器
parser = JSONNodeParser()
json_docs = [Document(text=json)]

# 从 JSON 文件创建节点
nodes = parser.get_nodes_from_documents(json_docs)

# 显示切分结果
for i, node in enumerate(nodes):
    print(f"节点 {i+1} (字符数: {len(node.text)}):")
    print("-" * 30)
    print(node.text)
    print("\n" + "="*50 + "\n")

3. SemanticSplitterNodeParser 语义切分器

SemanticSplitterNodeParser 通过嵌入模型计算文本块间的语义相似度,实现自适应断点识别,核心解决固定分块的语义割裂问题。其检索准确率较固定分块提升 20% 左右,适合对上下文连贯性要求高的场景(如学术论文、长文档理解)。

实现原理

  1. 句子分割:将文档拆分为独立句子单元
  2. 嵌入计算:通过嵌入模型(如 OpenAIEmbedding、BAAI/bge-m3)生成句子向量,计算成本较高
  3. 相似度判断:计算相邻句子向量的余弦相似度
  4. 断点识别:当相似度低于设定阈值(如 breakpoint_percentile_threshold=90)时执行切分
  5. 块生成:合并语义相近的句子为完整分块,适用于对语义连贯性要求高的场景

简单来说,它的工作流程是:先将文本拆分成句子,然后通过滑动窗口计算句子群的综合语义,最后在语义发生显著变化的地方(即相似度低于阈值时) 进行分割,它的分割点是动态的、由语义决定的,因此无法像固定大小的分割器那样,简单地在前一个块的末尾和后一个块的开头插入一段重叠的文本;这种基于语义的分割方式,其设计目标之一就是让每个分割出的文本块在语义上尽可能独立和完整。buffer_size 参数在某种程度上扮演了维持上下文连贯性的角色,因为它确保了在判断是否分割时,已经考虑了当前句子周围一定范围内的语义上下文

from llama_index.core import Document
from llama_index.embeddings.openai import OpenAIEmbedding

import os
from llama_index.core.settings import Settings
from dotenv import load_dotenv

load_dotenv()

# 设置为全局默认 Embedding 模型
Settings.embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    api_key=os.getenv("OPENAI_API_KEY"),
    api_base=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
)
from llama_index.core.node_parser import SemanticSplitterNodeParser
from typing import List

# 1 中文拆句器(更鲁棒)
def split_chinese_sentences(text: str) -> List[str]:
    """
    将中文文本按常见句末标点拆分,尽量保留标点并去除多余空白。
    处理省略号、连续标点和英文句点等。
    """
    if not text:
        return []
    # 将长省略号统一处理
    text = text.replace("……", "…")
    # 在常见句末标点后断句(保留标点)
    pieces = re.split(r'(?<=[。!?…\?\!\.])\s*', text)
    # 清理空白与空串
    sentences = [p.strip() for p in pieces if p and p.strip()]
    return sentences

long_chinese_text = (
    "本季度公司财务表现良好,营收增长 15%,净利润同比提升 10%。"
    "在产品方面,我们完成了新一代搜索引擎的内测,搜索精度和召回率都有明显提升。"
    "同时,基础设施团队迁移到新的集群架构,缩短了部署时间并降低了成本。"
    "关于市场推广,最近在北京与上海分别举办了两场线下用户交流会,"
    "收集到了大量用户反馈,尤其是对移动端体验的改进建议。"
    "另一方面,我们正在探索与第三方数据提供商的合作,"
    "以期在广告定向和推荐系统上获得更准确的信号。"
    "此外,法律合规团队提醒需关注新的隐私合规要求,"
    "包括数据最小化和用户可解释性方面的合规文档准备。"
    "最后,团队在招聘方面也有所动作,已开放多个后端与算法岗位。"
)

# 创建 Document
doc = Document(text=long_chinese_text, metadata={"doc_id": "示例文档 1"})

# SemanticChunker 会使用嵌入相似度来决定是否将句子合并到同一 chunk 中。
splitter = SemanticSplitterNodeParser(
    buffer_size=2,                        # buffer_size 用于把多少句子为一组做相似度计算。
    breakpoint_percentile_threshold=80,   # 相似度阈值,低于该阈值的相似度会被视为断点。
    embed_model=Settings.embed_model,     # embed_model 表示用于计算嵌入的模型。
    sentence_splitter=split_chinese_sentences, # 自定义中文拆句器
    include_metadata=True,                # 是否包含 metadata
    include_prev_next_rel=True,           # 是否包含上一句与下一句的关系
)

# also baseline splitter
# nodes_from_semantic = splitter.get_nodes_from_documents(documents)
nodes_from_semantic = splitter.get_nodes_from_documents([doc])

print("=== 切割后(chunks) ===")
for idx, node in enumerate(nodes_from_semantic):
    print(f"--- chunk {idx} ---")
    # node.text: chunk 的主文本(通常是若干句子合并)
    print("chunk.text:", node.text)
    # 如果 include_metadata=True,会有一些 metadata(例如原始句子索引、chunk_type 等)
    print("metadata keys:", list(node.metadata.keys()))
    # 如果有 prev/next 关系或 chunk 索引,可一并查看
    # 例如:node.metadata.get('chunk_index'), node.metadata.get('chunk_type')
    print("metadata (sample):", {k: node.metadata.get(k) for k in ['chunk_index','chunk_type'] if k in node.metadata})
    print()

调优建议

4. SentenceWindowNodeParser 句子窗口切分器

SentenceWindowNodeParser 的工作流程核心在于检索单元上下文窗口的分离:

  1. 精细索引:在索引构建阶段,它会将文档拆分成单个句子作为基础节点(Node)。这种细粒度拆分有助于向量模型更好地表征句子语义,从而在检索时能更精准地找到相关句子。
  2. 窗口上下文:每个句子节点都会在元数据(metadata)中存储其周围句子构成的窗口文本。检索时,系统首先找到最相关的句子节点,然后将其替换为对应的上下文窗口,再将这个更大的文本块传递给 LLM 生成答案。

这种方法有效缓解了 RAG 系统中"检索精度"与"生成答案所需上下文完整性"之间的矛盾。

特性维度 具体说明
核心原理 将文档按句子拆分并建立索引,检索时返回匹配句子及其周围句子(滑动窗口)。
主要优势 检索与上下文解耦:检索用小粒度句子提升精度,提供给 LLM 的是包含更完整上下文的窗口文本。
关键参数 windowsize:控制窗口大小;windowmetadata_key:存储窗口文本的元数据键名。
典型应用场景 处理文档结构清晰、句子间逻辑连贯的文档,如技术文档、学术论文、法律合同等。
import re
from typing import List
from llama_index.core import Document
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

# 创建 SentenceWindowNodeParser
node_parser = SentenceWindowNodeParser(
    sentence_splitter=split_chinese_sentences,  # 自定义中文拆句器
    window_size=1,                              # 左右各 1 句作为window
    window_metadata_key="window",               # metadata 中存储窗口的 key
    original_text_metadata_key="original_text", # 可选:保存原始文本
    include_metadata=True,                      # 是否包含 metadata
    include_prev_next_rel=True,                 # 是否包含上一句与下一句的关系
)

text = "本报告中的信息均来源于我们认为可靠的已公开资料,本公司对这些信息的真实性、准确性及完整性不作任何保证。 本报告中的信息、意见等均仅供客户参考,该等信息、意见并未考虑到获取本报告人员的具体投资目的、财务状况以 及特定需求,在任何时候均不构成对任何人的个人推荐。客户应当对本报告中的信息和意见进行独立评估,并应同时 思量各自的投资目的、财务状况以及特定需求,必要时就法律、商业、财务、税收等方面咨询专家的意见。客户应自 主作出投资决策并自行承担投资风险。本公司特别提示,本公司不会与任何客户以任何形式分享证券投资收益或分担 证券投资损失,任何形式的分享证券投资收益或者分担证券投资损失的书面或口头承诺均为无效。市场有风险,投资 须谨慎。对依据或者使用本报告所造成的一切后果,本公司和关联人员均不承担任何法律责任。"
doc = Document(text=text, metadata={"doc_id": "示例文档 1"})
nodes = node_parser.get_nodes_from_documents([doc])

# 在检索 / 查询时把 window 替换回去
# 用于 QueryEngine 的 node_postprocessor 示例:把 node.text 替换为 metadata['window'](如果存在)
postproc = MetadataReplacementPostProcessor(target_metadata_key="window")

# 当你用 index.as_query_engine(..., node_postprocessors=[postproc]) 时,
# 被检索到的 node.text 会被 postproc 替换为 metadata['window'](即包含左右句的文本)
# 之后传给 LLM 的就是带上下文的窗口文本,而不是单个句子

使用注意事项

5. HierarchicalNodeParser 结构切分器

HierarchicalNodeParser 结合文档结构(标题、章节、段落)和语义边界进行多层次切分,适合处理 Markdown、PDF、Word 等结构化文档,尤其适用于说明书、规约、设计文档等场景。其核心优势在于保留文档原生逻辑层级,支持父节点(章节标题 + 简介)与子节点(具体段落)的嵌套组织。

Pasted image 20260716144258.png

默认设置优先原则与场景适配

import re
import json
from typing import List
from llama_index.core import Document
from llama_index.core.node_parser.relational.hierarchical import HierarchicalNodeParser

# 构造示例中文较长文档
text = (
    "第一章:公司发展背景。公司成立于 2005 年,最初是一家小型软件外包公司。"
    "随着云计算与大数据的兴起,公司在 2010 年转型为云服务提供商,"
    "并在 2015 年完成了 A 轮融资,融资金额达数千万美元。"
    "第二章:产品与服务。公司主要提供数据分析平台、实时流处理系统和人工智能模型服务。"
    "其中,数据分析平台支持海量日志处理;流处理系统可实现毫秒级别延迟。"
    "第三章:市场与竞争。国内外竞争者众多,我们面临来自大型互联网公司的压力,"
    "但我们的优势在于垂直行业深耕与定制化服务。"
    "第四章:未来展望。我们计划在 2026 年前进入国际市场,并开展亚太地区的业务。"
)

# 创建 HierarchicalNodeParser
node_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[300, 120],   # 例:300 字符 / 120 字符级别
    chunk_overlap=30,         # 重叠区域大小
    include_metadata=True,      # 是否包含 metadata
    include_prev_next_rel=True  # 是否包含前后关系
)

# 注意:默认内部会为每个级别创建 SentenceSplitter 用于拆分
nodes = node_parser.get_nodes_from_documents([Document(text=text, metadata={"doc_id":"示例文档"})])

print("=== 切分后(nodes) ===")
for idx, node in enumerate(nodes):
    print(f"--- node {idx} ---")
    print("relationships:", node.relationships)
    print("=" * 60)
    print("text:", repr(node.text))
    print("metadata keys:", list(node.metadata.keys()))
    # 可显示 chunk size 或 parent id
    print("metadata sample:", {k: node.metadata.get(k) for k in ["chunk_size","chunk_level","doc_id"] if k in node.metadata})
    print()

5.3. 混合策略

结合多种切分策略,发挥各自优势

from unstructured.partition.pdf import partition_pdf

# 使用 partition_pdf 函数解析 PDF 文档 为 elements 类型
elements = partition_pdf(
    filename="甬兴证券 -AI 行业点评报告:海外科技巨头持续发力 AI,龙头公司中报业绩亮眼 .pdf",
    strategy="hi_res",  # 使用高精度模式
    extract_images_in_pdf=False,
)

1. Unstructured 的 chunk_by_title

核心思想: 利用 Title 元素作为分段标志,将 Title 与其后的内容组合成语义完整的 chunk

# 导入 unstructured 的 chunk_by_title 切分方法
from unstructured.chunking.title import chunk_by_title

# 文档切分
chunked_elements = chunk_by_title(
    elements,                       # 读取的元素列表
    max_characters=800,             # 每个 chunk 的最大字符数
    combine_text_under_n_chars=150, # 小于该字符数的文本块会合并
)

print(f"✓ 解析出 {len(elements)} 个元素")
print(f"✓ 切分成 {len(chunked_elements)} 个chunks")
import json
from llama_index.core.schema import Document

llamaindex_documents = []

for chunk in chunked_elements:
    # 提取 metadata 并清理不需要的字段
    metadata = chunk.metadata.to_dict()
    # 增强 metadata: 添加元素类型信息
    metadata['element_type'] = type(chunk).__name__

    # 如果有原始元素,可以提取更多信息
    if hasattr(chunk.metadata, 'orig_elements') and chunk.metadata.orig_elements:
        # 提取所有原始元素的类型
        metadata['orig_element_types'] = [type(e).__name__ for e in chunk.metadata.orig_elements]
        # 标记是否包含 Title
        metadata['contains_title'] = any(
            type(e).__name__ == 'Title' for e in chunk.metadata.orig_elements
        )

    # 移除一些序列化时可能有问题的字段
    metadata.pop('languages', None)
    metadata.pop('orig_elements', None)  # 太大,不适合存储在向量数据库

    # 创建 Document 对象
    doc = Document(
        text=chunk.text,
        metadata=metadata,
    )
    llamaindex_documents.append(doc)

print("\n" + "=" * 80)
for i, doc in enumerate(llamaindex_documents[:3]):
    print(f"\n--- Document {i+1} ---")
    print(f"文本长度: {len(doc.text)} 字符")
    print(f"文本预览: {doc.text[:80]}...")
    print(f"Metadata keys: {list(doc.metadata.keys())}")
    print(f"Metadata: {json.dumps(doc.metadata, ensure_ascii=False, indent=2)}")
from llama_index.core.schema import TextNode

# 直接转换为 Node 格式
nodes = []
for i, chunk in enumerate(chunked_elements):
    # 处理 metadata
    metadata = chunk.metadata.to_dict()

    # 如果有原始元素,可以提取更多信息
    if hasattr(chunk.metadata, 'orig_elements') and chunk.metadata.orig_elements:
        # 提取所有原始元素的类型
        metadata['orig_element_types'] = [type(e).__name__ for e in chunk.metadata.orig_elements]
        # 标记是否包含 Title
        metadata['contains_title'] = any(
            type(e).__name__ == 'Title' for e in chunk.metadata.orig_elements
        )

    # 移除 languages
    metadata.pop('languages', None)
    # 移除 orig_elements
    metadata.pop('orig_elements', None)

    # 创建 TextNode
    node = TextNode(
        text=chunk.text,
        metadata=metadata,
        id_=f"chunk_{i}",
    )
    nodes.append(node)

print(f"✓ 创建 {len(nodes)} 个TextNodes")
print("=" * 80)
print(nodes[0].text)
print("=" * 80)
print(nodes[0].metadata)

2. HierarchicalNodeParser 结合 SemanticSplitterNodeParser 语义切分

核心思想: 创建多层级的 chunk 结构,对长文本再进一步使用 SemanticSplitterNodeParser 或其他切分器进行切分

from llama_index.core.node_parser import (
    SentenceSplitter,
    SemanticSplitterNodeParser,
    HierarchicalNodeParser
)
# from llama_index.embeddings.openai import OpenAIEmbedding
# 创建嵌入模型
# embed_model = OpenAIEmbedding(model="text-embedding-ada-002")

# 创建层次化解析器
hierarchical_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[512, 256, 128]  # 父段落、子段落、孙段落
)

# 创建语义分割器
semantic_splitter = SemanticSplitterNodeParser(
    buffer_size=1,                      # 用于把多少句子为一组做相似度计算。
    breakpoint_percentile_threshold=90, # 相似度阈值,低于该阈值的相似度会被视为断点。
    embed_model=Settings.embed_model             # 嵌入模型
)

# 创建句子分割器作为后备
sentence_splitter = SentenceSplitter(
    chunk_size=500,
    chunk_overlap=50
)

# 混合策略:先使用层次化解析器,再对过大的块使用语义分割器
def hybrid_chunking(documents):
    # 第一阶段:层次化解析
    nodes = hierarchical_parser.get_nodes_from_documents(documents)

    # 第二阶段:对过大的节点进行语义分割
    final_nodes = []
    for node in nodes:
        if len(node.text) > 500:  # 对过大的节点进行二次分割
            sub_nodes = semantic_splitter.get_nodes_from_documents([node])
            final_nodes.extend(sub_nodes)
        else:
            final_nodes.append(node)

    return final_nodes

# 应用混合策略
nodes = hybrid_chunking(documents)
nodes[25].text

3. Metadata 增强的自定义切分

from llama_index.core.node_parser import SentenceSplitter
from llama_index.core import Document
from typing import List

# ============= 步骤 1: 预处理 unstructured elements =============
def create_documents_with_title_context(elements) -> List[Document]:
    """
    将 unstructured elements 转换为 Documents,并将 Title 信息注入 metadata
    """
    documents = []
    current_title_hierarchy = {
        "h1": "",
        "h2": "",
        "h3": "",
    }
    accumulated_text = []
    accumulated_metadata = {}

    for elem in elements:
        elem_type = type(elem).__name__
        elem_metadata = elem.metadata.to_dict()

        # 如果是 Title,更新层级信息
        if elem_type == "Title":
            # 如果有累积的文本,先创建 Document
            if accumulated_text:
                doc = Document(
                    text="\n\n".join(accumulated_text),
                    metadata=accumulated_metadata.copy()
                )
                documents.append(doc)
                accumulated_text = []

            # 简单的层级判断(可根据实际情况改进)
            title_text = elem.text
            if len(title_text) < 20:  # 短标题可能是低层级
                current_title_hierarchy["h3"] = title_text
            elif len(title_text) < 40:
                current_title_hierarchy["h2"] = title_text
                current_title_hierarchy["h3"] = ""
            else:
                current_title_hierarchy["h1"] = title_text
                current_title_hierarchy["h2"] = ""
                current_title_hierarchy["h3"] = ""

            # 初始化新 section 的 metadata
            accumulated_metadata = {
                **elem_metadata,
                "section_title": title_text,
                "title_h1": current_title_hierarchy["h1"],
                "title_h2": current_title_hierarchy["h2"],
                "title_h3": current_title_hierarchy["h3"],
            }

            # Title 本身也加入文本
            accumulated_text.append(f"# {title_text}")

        else:
            # 非 Title 元素,累积到当前 section
            if not accumulated_metadata:
                # 如果还没有 metadata(文档开头没有 Title 的情况)
                accumulated_metadata = {
                    **elem_metadata,
                    "section_title": "前言",
                }

            accumulated_text.append(elem.text)

            # 更新 metadata(保留最新的 page_number 等信息)
            accumulated_metadata.update({
                k: v for k, v in elem_metadata.items()
                if k in ['page_number', 'filename']
            })

    # 处理最后累积的文本
    if accumulated_text:
        doc = Document(
            text="\n\n".join(accumulated_text),
            metadata=accumulated_metadata
        )
        documents.append(doc)

    return documents

# ============= 步骤 2: 应用自定义处理 =============
enriched_documents = create_documents_with_title_context(elements)

# ============= 步骤 3: 使用 SentenceSplitter 进一步切分 =============
"""
如果 Documents 还是太大,可以进一步切分
关键: metadata 会自动继承到所有 child nodes
"""
node_parser = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separator=" ",
)

nodes = node_parser.get_nodes_from_documents(enriched_documents)

print("\n" + "=" * 80)
print("进一步切分后的 Nodes (metadata 已继承)")
print("=" * 80)
print(f"总节点数: {len(nodes)}")
for i, node in enumerate(nodes[:3]):
    print(f"\n--- Node {i+1} ---")
    print(f"Node ID: {node.node_id}")
    print(f"文本预览: {node.text[:80]}...")
    print(f"继承的 Metadata: {json.dumps(node.metadata, ensure_ascii=False, indent=2)}")

使用 Metadata Extractor 进一步增强

"""
可选: 使用 LLM 自动提取更多 metadata
"""
# 导入 metadata 提取器
from llama_index.core.extractors import (
    TitleExtractor,
    SummaryExtractor,
    KeywordExtractor,
    QuestionsAnsweredExtractor,
)
# 导入 IngestionPipeline 管道
from llama_index.core.ingestion import IngestionPipeline

# 定义 metadata 提取器
extractors = [
    TitleExtractor(nodes=5),  # 从前 5 个节点提取标题
    KeywordExtractor(keywords=10),  # 提取 10 个关键词
    SummaryExtractor(summaries=["self"]),  # 生成摘要
    QuestionsAnsweredExtractor(questions=3),  # 生成 3 个问题
]

# 构建处理管道
pipeline = IngestionPipeline(
    transformations=[
        node_parser,  # 先切分
        *extractors,  # 再提取 metadata
    ]
)

enhanced_nodes = pipeline.run(documents=enriched_documents)

# 查看增强后的metadata
for node in enhanced_nodes[:2]:
    print(f"\\nNode: {node.text[:50]}...")
    print(f"Metadata: {node.metadata}")

六:切分策略选择与优化

6.1. 切分工具选择

场景 推荐工具 原因
普通文本 / 报告 / 网页 SentenceSplitter 简单快速,保持句子完整性
长文本 / Embedding 限制场景 TokenTextSplitter 精确控制 token 数量
精确语义块 / 摘要场景 SemanticSplitterNodeParser 基于语义的智能切分
技术文档、Notebook CodeSplitter 保持代码结构完整性
教程、技术博客 MarkdownNodeParser 识别 Markdown 层级结构
教材、论文类文档 HierarchicalNodeParser 分层切分,保持文档结构

6.2. 切分策略选择

文档类型 推荐策略 关键参数
结构化文档(Markdown、PDF、Word) HierarchicalNodeParser chunk_sizes=[2048, 512, 128]
纯文本(小说、新闻、邮件) SentenceSplitter chunksize=500, chunkoverlap=50
代码文件 CodeSplitter language="python", max_chars=1000
混合文档(文本 + 表格 + 图像) Unstructured + SentenceSplitter chunksize=500, chunkoverlap=50
长文档(书籍、报告) SemanticSplitter buffersize=1, breakpointpercentile_threshold=90

6.3. 切分策略优化方法

1. 参数调优

  1. 块大小(chunk_size)优化
    • 小块(< 300 tokens):高精度,低上下文,适用于精确匹配
    • 中等块(300 - 800 tokens):平衡精度与上下文,适用于大多数场景
    • 大块(> 800 tokens):高上下文,低精度,适用于长文档理解
  2. 重叠区域(chunk_overlap)优化
    • 小重叠(< 50 tokens):减少冗余,提高检索效率
    • 中等重叠(50 - 150 tokens):平衡上下文连续性与效率
    • 大重叠(> 150 tokens):确保上下文连续性,增加冗余
  3. 语义断点阈值(breakpoint_percentile_threshold)优化
    • 低阈值(< 80):产生更多小块,提高检索精度
    • 中等阈值(80 - 90):平衡块大小与语义连贯性
    • 高阈值(> 90):产生更大块,保持语义连贯性

七:切分效果评估

7.1. 评估指标

  1. 检索指标
    • 召回率(Recall):检索到的相关文档占所有相关文档的比例
    • 精确率(Precision):检索到的文档中相关文档的比例
    • F1 分数:召回率和精确率的调和平均
  2. 生成质量指标
    • 相关性:生成内容与查询的相关程度
    • 完整性:生成内容是否包含完整信息
    • 准确性:生成内容是否准确无误
  3. 效率指标
    • 检索时间:从查询到返回结果的时间
    • 生成时间:从检索结果到生成答案的时间
    • 资源消耗:计算资源(CPU、GPU、内存)使用情况
from llama_index.core.evaluation import (
    SemanticSimilarityEvaluator,
    FaithfulnessEvaluator,
    RelevancyEvaluator
)

def quick_evaluation_demo():
    """快速评估演示"""
    # 1. 配置 API 密钥(请替换为您的实际密钥)
    if not os.getenv("OPENAI_API_KEY"):
        return

    # 2. 初始化模型和评估器
    # embed_model = OpenAIEmbedding(model="text-embedding-3-large")
    # llm = OpenAI(model="gpt-4")

    # 创建语义相似度评估器,通过比较生成的答案与参考答案,两者在语义上的接近程度。(答案 vs 参考答案)
    semantic_evaluator = SemanticSimilarityEvaluator(
        embed_model=Settings.embed_model,
        similarity_threshold=0.8
    )
    # 创建忠实度评估器,检测幻觉,确保答案源于给定上下文(答案 vs 上下文)
    faithfulness_evaluator = FaithfulnessEvaluator(llm=Settings.llm)
    # 创建相关性评估器,评估答案与问题的匹配度及上下文的辅助作用(问题 vs 答案 vs 上下文)
    relevancy_evaluator = RelevancyEvaluator(llm=Settings.llm)

    # 3. 准备测试数据
    sample_document = """
    人工智能(Artificial Intelligence,AI)是计算机科学的一个分支,
    它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。

    机器学习是人工智能的一个核心研究领域,它使计算机有能力在不被明确编程的情况下进行学习。
    机器学习专注于计算机程序的开发,这些程序可以访问数据并使用它学习为自己。

    深度学习是机器学习的子集,它基于人工神经网络的表征学习方法。
    深度学习在图像、语音、文本等感知任务上取得了突破性进展。
    """
    # 切分文档
    splitter = SentenceSplitter(chunk_size=200, chunk_overlap=30)
    chunks = splitter.split_text(sample_document)

    print(f"文档已切分为 {len(chunks)} 个片段:")
    for i, chunk in enumerate(chunks):
        print(f"  片段 {i+1}: {chunk[:50]}...")

    # 4. 执行各项评估
    # 测试查询和响应
    test_query = "什么是人工智能?"
    test_response = "人工智能是计算机科学的一个分支,研究如何让机器表现出智能行为。"
    test_reference = "人工智能是计算机科学的一个分支,企图了解智能的实质"

    # 语义相似度评估
    semantic_result = semantic_evaluator.evaluate(
        query=test_query,
        response=test_response,
        reference=test_reference,
        contexts=[chunks[0]]
    )
    print(f"相似度分数: {semantic_result.score: .3f}")
    print(f"是否通过: {semantic_result.passing}")
    print(f"反馈: {semantic_result.feedback}")

    # 忠实度评估
    faithfulness_result = faithfulness_evaluator.evaluate(
        query=test_query,
        response=test_response,
        contexts=[chunks[0]]
    )
    print(f"忠实度分数: {faithfulness_result.score: .3f}")
    print(f"是否通过: {faithfulness_result.passing}")
    print(f"反馈: {faithfulness_result.feedback}")

    # 相关性评估
    relevancy_result = relevancy_evaluator.evaluate(
        query=test_query,
        response=test_response,
        contexts=[chunks[0]]
    )
    print(f"相关性分数: {relevancy_result.score:.3f}")
    print(f"是否通过: {relevancy_result.passing}")
    print(f"反馈: {relevancy_result.feedback}")

    # 5. 综合评估结果
    metrics = {
        '语义相似度': semantic_result.score,
        '忠实度': faithfulness_result.score,
        '相关性': relevancy_result.score
    }

    for metric, score in metrics.items():
        status = "✓ 通过" if score >= 0.7 else "✗ 未通过"
        print(f"{metric}: {score:.3f} {status}")

    avg_score = sum(metrics.values()) / len(metrics)
    print(f"\n平均分数: {avg_score: .3f}")
    overall_status = "✓ 整体通过" if avg_score >= 0.7 else "✗ 整体未通过"
    print(f"整体评估: {overall_status}")

    return "评估完成!"

7.2. 参考文献

  1. LlamaIndex 官方文档. Node Parsers. https://developers.llamaindex.ai/python/framework-api-reference/node_parsers/
  2. LlamaIndex 官方文档. Evaluating. https://developers.llamaindex.ai/python/framework-api-reference/evaluation/
  3. 图片参考出处:https://blog.dailydoseofds.com/p/5-chunking-strategies-for-rag