文档处理与切分策略:从原始文档到高质量 Chunk 的工程实践
场景还原:切分不当导致的检索灾难
你们团队构建了一个法律文档检索系统。用户问:"2023年劳动合同法修订中关于试用期的规定是什么?"
Agent 检索到了一个 Chunk:"2023年修订的劳动合同法对试用期做出了重要调整..."然后回答: "根据检索到的信息,2023年确实对试用期规定做了调整,但具体条款内容需要查看原文。"
用户继续追问:"具体调整了哪些内容?" Agent 又检索到另一个 Chunk:"试用期最长不得超过六个月,同一用人单位与同一劳动者只能约定一次试用期..." 但缺少前提——这是在什么条件下适用的?工资标准是多少?
问题出在哪里?文档切分时把一条完整的法律条款拆成了三个 Chunk:
Chunk A: 修订背景 Chunk B: 期限规定 Chunk C: 工资标准
用户的问题需要 A+B+C 的完整信息才能回答,但检索只召回了 B。 这就是切分粒度不当导致的语义断裂。
第一层:问题特征诊断 —— 文档切分是什么问题?
1.1 Chunk 切分的核心矛盾

1.2 文档预处理流水线

1.3 切分质量评估指标
第二层:根因机制分析 —— 为什么切分这么难?
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(0, len(text), chunk_size):
chunk = text[i:i + chunk_size]
chunks.append(chunk)
return chunks
# 问题分析:
# 1. 可能在单词中间切断 - "试用" 变成 "试" 和 "用期"
# 2. 可能在句子中间切断 - 丢失语义完整性
# 3. 没有保留上下文 - 无法处理跨Chunk的指代
# 4. 没有考虑文档结构 - 标题和正文混在一起2.3 五种切分策略对比

第三层:策略对比选择 —— 有哪些切分策略?
3.1 五种切分策略详解
3.2 Chunk 大小决策矩阵
3.3 重叠策略对比
第四层:系统性解决方案 —— 如何实现高质量切分?
4.1 生产级文档处理流水线
import re
from typing importList, Dict, Optional
from dataclasses import dataclass
@dataclass
classChunk:
"""文本块数据结构"""
id: str
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[-1] notin'.。!?;':
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 验证检查清单
全局审视总结
四层递进逻辑回顾

核心洞察:文档切分的本质是在存储效率与检索精度之间寻找最优平衡点。 没有绝对最优的切分策略,只有最适合特定文档类型和查询模式的策略。
延伸思考
个人思考与判断
切分策略选择的核心假设是"文档类型是已知且稳定的"。 但在实际场景中,一个知识库可能包含多种类型文档(合同、邮件、技术文档混合)。 这种情况下,统一的切分策略必然对某些类型不是最优。
最可能出问题的环节是重叠大小的设置。重叠太小导致上下文断裂, 重叠太大导致存储成本激增和检索噪音。建议根据文档的"连贯性需求"动态调整—— 法律文档需要大重叠保持条款连贯,FAQ 可以小重叠。
替代方案的审视
适用边界与局限性
这套切分方案最适合:
结构化或半结构化的文档(有标题、段落、列表等标记) 文档类型相对统一的场景 对检索质量有较高要求的生产环境
不适用:
完全无结构的流式文本(如聊天记录) 需要保持全局依赖关系的长篇叙事(如小说) 实时性要求极高、无法接受批处理延迟的场景
对于多类型混合的知识库,建议先进行文档分类,再针对不同类型应用不同的切分策略。
夜雨聆风