chunk分割的原理和一些优化记录
1. Chunk分割的原理
在代码(src/core/hybrid_retrieval_db.py)中,chunk分割的核心逻辑在 _split_text 方法:
def _split_text(self, text: str) -> List[str]:
"""
将文本按句子分割成有重叠的块。
这种方法通过在块之间保留重叠的句子来确保上下文的连续性,
有效避免了像"安全费用"和"30元"这样的关键信息被分割在不同块中而丢失关联的问题。
Args:
text: 原始文本。
"""
chunk_overlap = self.chunk_overlap
# 1. 使用正则表达式按句子分割,同时保留分隔符。
if not text:
return []
parts = re.split(r'([。!?;\n])', text)
sentences = []
for i in range(0, len(parts), 2):
sentence = parts[i]
if i + 1 < len(parts):
sentence += parts[i+1]
sentence = sentence.strip()
if sentence:
sentences.append(sentence)
if not sentences:
return []
# 2. 将句子合并成块,并创建重叠。
chunks = []
current_chunk_sentences = []
current_len = 0
for i in range(len(sentences)):
sentence = sentences[i]
sentence_len = len(sentence)
# 如果加上新句子会超长,就先将当前块保存
# (+1 是为了空格)
if current_chunk_sentences and current_len + sentence_len + 1 > self.chunk_size:
chunks.append(" ".join(current_chunk_sentences))
# 创建重叠:从上一个块的末尾取N个句子作为新块的开头
start_index = max(0, len(current_chunk_sentences) - chunk_overlap)
current_chunk_sentences = current_chunk_sentences[start_index:]
current_len = sum(len(s) for s in current_chunk_sentences) + max(0, len(current_chunk_sentences) - 1)
# 如果单个句子就超长,直接把它作为一个块
if sentence_len > self.chunk_size:
# 如果前面还有内容,先保存
if current_chunk_sentences:
chunks.append(" ".join(current_chunk_sentences))
current_chunk_sentences = []
current_len = 0
chunks.append(sentence)
else:
current_chunk_sentences.append(sentence)
current_len += sentence_len
if len(current_chunk_sentences) > 1:
current_len += 1 # 加空格的长度
# 不要忘记最后一个块
if current_chunk_sentences:
chunks.append(" ".join(current_chunk_sentences))
return chunks
原理简述
- 按句子分割:首先用正则表达式将全文本按“句子”分割(包括中文的句号、问号、感叹号、分号、换行等)。
- 滑动窗口+重叠:将若干句子合并成一个“块”(chunk),每个块的长度不超过设定的
chunk_size。块与块之间有重叠(overlap),即每个新块会包含上一个块结尾的若干句子(由chunk_overlap控制)。 - 特殊处理:如果某个句子本身就很长,超过了
chunk_size,则单独作为一个块。
伪代码流程
- 按句子分割文本,得到句子列表。
- 用滑动窗口将句子拼接成块,每个块长度不超过
chunk_size。 - 每个新块的开头会包含上一个块结尾的若干句子(overlap),保证上下文连续。
- 返回所有块。
2. Chunk分割的好处
1. 提升检索的粒度和相关性
- 检索时不是以整篇文档为单位,而是以“块”为单位,能更精细地定位到相关内容。
- 用户的查询更容易命中具体的知识点或事实,而不是返回整篇大文档。
2. 避免关键信息被割裂
- 通过“重叠”机制(overlap),可以防止关键信息(如“安全费用”与“30元”)被分在不同块而丢失上下文。
- 这样即使用户的查询跨越了两个块的边界,也能在重叠区域内被检索到。
3. 提升向量检索的效果
- 向量检索模型(如embedding)对长文本的效果有限,分块后每个块长度适中,能更好地表达语义。
- 避免了长文本embedding稀释关键信息的问题。
4. 支持大文档的分布式存储与增量更新
- 大文档被分成多个块,可以分批存储、检索和更新,提升系统的可扩展性和效率。
5. 便于后续的高阶处理
- 分块后可以对每个块单独做embedding、打标签、摘要、事实验证等操作,灵活性更高。
3. 直观例子
假设有如下文本:
“安全费用按照30元/吨的标准提取。所有费用需专款专用。违反规定将追责。”
- 分割后可能得到:
- Chunk1: “安全费用按照30元/吨的标准提取。”
- Chunk2: “安全费用按照30元/吨的标准提取。所有费用需专款专用。”
- Chunk3: “所有费用需专款专用。违反规定将追责。”
这样,“30元/吨”和“专款专用”的信息都能在至少一个chunk中完整出现,查询时不会遗漏。
4. 总结
- chunk分割是将长文本按句子滑动分块,并设置重叠,保证每个块既不太长也不割裂关键信息。
- 好处:提升检索相关性、避免信息割裂、优化向量表达、支持大文档处理、便于后续分析。
其他多项优化措施:
1. 分批处理(Batching)与显存优化
- 分批添加文档到向量数据库
在add_documents_to_db方法中,文档块是分批(batch_size,默认32)处理的,而不是一次性全部处理。这样可以有效避免显存溢出,提升大批量数据处理的稳定性和效率。
for start in range(0, len(documents), batch_size):
...
with torch.inference_mode():
embeddings = self.embedding_model.encode(batch_docs, is_query=False)
...
self.collection.add(...)
# 释放MPS显存
if torch.backends.mps.is_available():
torch.mps.empty_cache()
- 自动释放显存
针对 Apple Silicon (MPS) 设备,处理完每个batch后主动调用torch.mps.empty_cache()释放显存,防止内存泄漏。
2. 设备自适应与数据类型优化
- 自动选择最佳设备
Embedding 和 Reranker 模型会自动检测并优先使用 MPS(Apple Silicon)、CUDA(NVIDIA GPU)或CPU,充分利用硬件加速。
if torch.backends.mps.is_available():
self.device = torch.device("mps")
elif torch.cuda.is_available():
self.device = torch.device("cuda")
else:
self.device = torch.device("cpu")
- 根据设备选择数据类型
GPU/MPS上使用 float16,CPU上用 float32,进一步提升推理速度和节省内存。
3. 文本分块(Chunk)与重叠优化
- 按句子分块+重叠
文本分块时采用滑动窗口+重叠(overlap)策略,既保证了检索粒度,又避免了关键信息被割裂,提高了检索相关性和召回率。
4. 推理模式优化
- 使用
torch.inference_mode()
在所有embedding和推理相关操作中,使用torch.inference_mode(),关闭梯度计算,减少内存消耗和提升推理速度。
5. 高效的向量归一化
- Embedding归一化
在Qwen3Embedding.encode方法中,所有向量都做了L2归一化(F.normalize),保证余弦相似度的有效性和稳定性。
6. 增量更新与分布式存储友好
- 支持文档的增量更新和分块存储
文档被分块后,可以单独增删改查,便于大规模数据的分布式管理和高效更新。
7. 日志与进度输出
- 详细的进度和状态输出
关键步骤均有详细的日志输出,便于监控和调试。
总结
优化措施主要包括:
- 分批处理与显存管理
- 设备自适应与数据类型优化
- 文本分块与重叠
- 推理模式优化
- 向量归一化
- 增量更新与分布式友好
- 详细日志输出