乐于分享
好东西不私藏

文档处理与切分策略:从原始文档到高质量 Chunk 的工程实践

文档处理与切分策略:从原始文档到高质量 Chunk 的工程实践

文档处理与切分策略:从原始文档到高质量 Chunk 的工程实践

场景还原:切分不当导致的检索灾难

你们团队构建了一个法律文档检索系统。用户问:"2023年劳动合同法修订中关于试用期的规定是什么?"

Agent 检索到了一个 Chunk:"2023年修订的劳动合同法对试用期做出了重要调整..."然后回答: "根据检索到的信息,2023年确实对试用期规定做了调整,但具体条款内容需要查看原文。"

用户继续追问:"具体调整了哪些内容?" Agent 又检索到另一个 Chunk:"试用期最长不得超过六个月,同一用人单位与同一劳动者只能约定一次试用期..." 但缺少前提——这是在什么条件下适用的?工资标准是多少?

问题出在哪里?文档切分时把一条完整的法律条款拆成了三个 Chunk:

  • Chunk A: 修订背景
  • Chunk B: 期限规定
  • Chunk C: 工资标准

用户的问题需要 A+B+C 的完整信息才能回答,但检索只召回了 B。 这就是切分粒度不当导致的语义断裂。


第一层:问题特征诊断 —— 文档切分是什么问题?

1.1 Chunk 切分的核心矛盾

粒度问题
表现
后果
太粗
单个 Chunk 包含多个主题
检索时引入无关信息,LLM 注意力分散
太细
句子被切断,逻辑不完整
检索到片段但无法理解完整含义
边界不当
在关键信息中间切断
丢失依赖关系(如"前述规定"失去指向)

1.2 文档预处理流水线

阶段
核心任务
常见问题
格式识别
判断文档类型,选择解析器
PDF 扫描件 vs 文本 PDF
内容提取
提取正文,去除页眉页脚
表格、图片、脚注的处理
清洗去噪
去除乱码、重复、无效字符
OCR 错误、编码问题
元数据提取
标题、章节、时间戳、作者
层级关系识别
Chunk 切分
按策略切分文本
粒度控制、边界保持
质量验证
检查切分质量
过短/过长的 Chunk

1.3 切分质量评估指标

指标
定义
理想值
评估方法
语义完整性
Chunk 是否表达完整思想
>90%
人工抽样检查
主题一致性
Chunk 内是否单一主题
>85%
主题模型分析
上下文可恢复
相邻 Chunk 能否拼接还原
>95%
重叠度检查
检索相关性
查询与召回 Chunk 的相关度
>80%
人工标注测试集

第二层:根因机制分析 —— 为什么切分这么难?

2.1 因果链:从文档特性到切分困境

R1: 文档结构多样 —— 不同格式需要不同的解析策略。 PDF 可能是扫描件需要 OCR,HTML 有标签结构可利用,Word 有样式信息。

R2: 语义边界模糊 —— 自然语言没有明确的"段落结束"标记。 一个主题可能在多个句子中展开,难以确定切分点。

R3: 依赖关系复杂 —— "上述规定"、"如下图所示"、"根据第3条" 这些指代关系如果恰好在切分点两侧,信息就会丢失。

R4: 领域差异大 —— 法律文档需要保持条款完整, 技术文档需要保持代码块完整,新闻需要保持事件完整。 没有通用最优策略。

R5: 查询模式未知 —— 切分时不知道用户会问什么。 粗粒度适合概括性问题,细粒度适合具体细节问题。

2.2 代码解剖:一个存在切分问题的实现

# ── AI 视角 ──
# "这是一个简单的文本切分函数,按固定长度切分"
# ── 实际含义 ──
# 这种切分方式存在严重的语义断裂问题

defnaive_chunk(text: str, chunk_size: int = 500) -> list:
"""
    简单固定长度切分 - 问题多多
    """

    chunks = []
for i inrange(0len(text), chunk_size):
        chunk = text[i:i + chunk_size]
        chunks.append(chunk)
return chunks

# 问题分析:
# 1. 可能在单词中间切断 - "试用" 变成 "试" 和 "用期"
# 2. 可能在句子中间切断 - 丢失语义完整性
# 3. 没有保留上下文 - 无法处理跨Chunk的指代
# 4. 没有考虑文档结构 - 标题和正文混在一起

2.3 五种切分策略对比


第三层:策略对比选择 —— 有哪些切分策略?

3.1 五种切分策略详解

策略
原理
优点
缺点
适用场景
固定长度
按字符数切分
简单快速
切断语义
对质量要求不高的原型
语义边界
按句子/段落切分
保持语义完整
粒度不均
通用文本
递归层级
先按大段落,再细分
层次清晰
实现复杂
长文档
结构感知
利用文档标记(标题/列表)
保留结构信息
依赖格式
结构化文档
LLM 智能
模型判断最佳切分点
质量最高
成本高、慢
高质量要求场景

3.2 Chunk 大小决策矩阵

文档类型
推荐 Chunk 大小
重叠大小
原因
法律合同
1000-2000 字符
200 字符
条款完整,逻辑连贯
技术文档
500-1000 字符
100 字符
概念独立,便于定位
新闻文章
300-600 字符
50 字符
事件独立,快速浏览
代码文档
函数/类为单位
N/A
保持代码完整性
FAQ/问答
问题+答案为单位
N/A
保持问答完整性

3.3 重叠策略对比

重叠策略
重叠比例
优点
缺点
无重叠
0%
存储效率最高
边界信息丢失
小重叠
10-20%
平衡存储和连续性
轻微冗余
大重叠
30-50%
最大程度保持上下文
存储成本翻倍
滑动窗口
连续滑动
细粒度覆盖
计算成本高

第四层:系统性解决方案 —— 如何实现高质量切分?

4.1 生产级文档处理流水线

import re
from typing importListDictOptional
from dataclasses import dataclass

@dataclass
classChunk:
"""文本块数据结构"""
idstr
    text: str
    metadata: Dict
    start_pos: int
    end_pos: int

classDocumentProcessor:
"""生产级文档处理器"""

def__init__(self, chunk_size: int = 500, overlap: int = 50):
self.chunk_size = chunk_size
self.overlap = overlap

defprocess(self, doc_id: str, text: str
                doc_type: str = "general"
) -> List[Chunk]:
"""
        完整处理流程
        """

# 1. 清洗
        cleaned = self._clean(text)

# 2. 根据文档类型选择切分策略
if doc_type == "legal":
            chunks = self._chunk_by_article(cleaned)
elif doc_type == "markdown":
            chunks = self._chunk_by_markdown(cleaned)
elif doc_type == "code":
            chunks = self._chunk_by_function(cleaned)
else:
            chunks = self._chunk_semantic(cleaned)

# 3. 添加元数据
returnself._add_metadata(doc_id, chunks, text)

def_clean(self, text: str) -> str:
"""清洗文本"""
# 去除多余空白
        text = re.sub(r'\s+'' ', text)
# 去除特殊字符
        text = re.sub(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]''', text)
return text.strip()

def_chunk_semantic(self, text: str) -> List[str]:
"""基于语义边界的切分"""
# 先按段落切分
        paragraphs = text.split('\n\n')

        chunks = []
        current_chunk = ""

for para in paragraphs:
# 如果当前段落加上会超限,先保存当前 chunk
iflen(current_chunk) + len(para) > self.chunk_size:
if current_chunk:
                    chunks.append(current_chunk.strip())
# 保留重叠部分
iflen(current_chunk) > self.overlap:
                    current_chunk = current_chunk[-self.overlap:] + "\n\n" + para
else:
                    current_chunk = para
else:
                current_chunk += "\n\n" + para if current_chunk else para

# 添加最后一个 chunk
if current_chunk:
            chunks.append(current_chunk.strip())

return chunks

def_chunk_by_markdown(self, text: str) -> List[str]:
"""基于 Markdown 结构的切分"""
# 按标题切分
import re
        pattern = r'(#+\s+.+?)(?=\n#+\s|$)'
        sections = re.findall(pattern, text, re.DOTALL)

        chunks = []
for section in sections:
# 如果 section 太大,再细分
iflen(section) > self.chunk_size:
                sub_chunks = self._chunk_semantic(section)
                chunks.extend(sub_chunks)
else:
                chunks.append(section)

return chunks

def_add_metadata(self, doc_id: str, chunks: List[str], 
                     original_text: str
) -> List[Chunk]:
"""添加元数据"""
        result = []
for i, chunk_text inenumerate(chunks):
# 计算在原文中的位置
            start_pos = original_text.find(chunk_text[:50])
            end_pos = start_pos + len(chunk_text) if start_pos >= 0else -1

            result.append(Chunk(
id=f"{doc_id}_chunk_{i}",
                text=chunk_text,
                metadata={
"doc_id": doc_id,
"chunk_index": i,
"total_chunks"len(chunks),
"char_count"len(chunk_text),
"word_count"len(chunk_text.split())
                },
                start_pos=start_pos,
                end_pos=end_pos
            ))

return result

# 使用示例
processor = DocumentProcessor(chunk_size=500, overlap=50)
chunks = processor.process(
    doc_id="contract_2024_001",
    text=contract_text,
    doc_type="legal"
)

4.2 切分效果评估

classChunkingEvaluator:
"""切分效果评估器"""

defevaluate(self, chunks: List[Chunk], 
                 sample_queries: List[str]
) -> Dict:
"""
        多维度评估切分质量
        """

        metrics = {
"distribution"self._analyze_size_distribution(chunks),
"boundary_quality"self._check_boundary_quality(chunks),
"retrieval_test"self._test_retrieval(chunks, sample_queries)
        }

return metrics

def_analyze_size_distribution(self, chunks: List[Chunk]) -> Dict:
"""分析 Chunk 大小分布"""
        sizes = [len(c.text) for c in chunks]
return {
"mean"sum(sizes) / len(sizes),
"median"sorted(sizes)[len(sizes)//2],
"min"min(sizes),
"max"max(sizes),
"too_short"len([s for s in sizes if s < 100]),
"too_long"len([s for s in sizes if s > 2000])
        }

def_check_boundary_quality(self, chunks: List[Chunk]) -> Dict:
"""检查边界质量"""
        issues = []

for i, chunk inenumerate(chunks):
# 检查是否在单词中间切断
            text = chunk.text
if text andnot text[0].isupper() and i > 0:
                issues.append(f"Chunk {i} may cut word at start")

# 检查是否在句子中间
if text and text[-1notin'.。!?;':
                issues.append(f"Chunk {i} may cut sentence at end")

return {
"total_chunks"len(chunks),
"issues_found"len(issues),
"issue_rate"len(issues) / len(chunks),
"details": issues[:10]  # 只显示前10个
        }

def_test_retrieval(self, chunks: List[Chunk], 
                       queries: List[str]
) -> Dict:
"""测试检索效果"""
# 这里需要接入实际的检索逻辑
# 简化示例:检查查询词是否出现在 Chunk 中
        results = []
for query in queries:
            matching = [c for c in chunks if query.lower() in c.text.lower()]
            results.append({
"query": query,
"matches"len(matching),
"coverage"len(matching) / len(chunks)
            })

return {
"queries_tested"len(queries),
"avg_matches"sum(r["matches"for r in results) / len(results),
"results": results
        }

4.3 验证检查清单

检查项
检查内容
通过标准
大小分布
Chunk 大小是否均匀
均值±50% 范围内占 80%
边界质量
切分点是否在语义边界
问题率 < 5%
语义完整
单个 Chunk 是否表达完整思想
人工评分 > 80%
检索测试
测试查询能否召回相关 Chunk
召回率 > 70%
重叠效果
相邻 Chunk 是否有足够重叠
重叠比例 10-20%

全局审视总结

四层递进逻辑回顾

核心洞察:文档切分的本质是在存储效率与检索精度之间寻找最优平衡点。 没有绝对最优的切分策略,只有最适合特定文档类型和查询模式的策略。


延伸思考

个人思考与判断

切分策略选择的核心假设是"文档类型是已知且稳定的"。 但在实际场景中,一个知识库可能包含多种类型文档(合同、邮件、技术文档混合)。 这种情况下,统一的切分策略必然对某些类型不是最优。

最可能出问题的环节是重叠大小的设置。重叠太小导致上下文断裂, 重叠太大导致存储成本激增和检索噪音。建议根据文档的"连贯性需求"动态调整—— 法律文档需要大重叠保持条款连贯,FAQ 可以小重叠。

替代方案的审视

替代路径
与当前方案的差异
为什么没有采用
不切分
整篇文档作为单个 Chunk
超出 Embedding 模型上下文限制,检索精度差
句子级切分
每个句子是一个 Chunk
粒度太细,丢失段落级上下文
模型自动决定
让 LLM 输出切分点
成本高、速度慢,不适合大规模处理
用户标注切分
人工标注最佳切分点
成本极高,无法扩展

适用边界与局限性

这套切分方案最适合:

  • 结构化或半结构化的文档(有标题、段落、列表等标记)
  • 文档类型相对统一的场景
  • 对检索质量有较高要求的生产环境

不适用:

  • 完全无结构的流式文本(如聊天记录)
  • 需要保持全局依赖关系的长篇叙事(如小说)
  • 实时性要求极高、无法接受批处理延迟的场景

对于多类型混合的知识库,建议先进行文档分类,再针对不同类型应用不同的切分策略。