去年在做一个历史知识问答项目时,我发现传统的关键词检索对"草船借箭发生在哪一回"这类问题完全无能为力。这促使我开始探索如何将RAG(检索增强生成)技术应用于中文长文本场景。经过三个月的迭代,我们最终在《三国演义》上实现了Recall@5=0.8的效果。这个系统最特别之处在于采用了"章回识别+滑动窗口"的混合切分策略,完美适配了古典小说的结构特点。
这个方案的核心价值在于:
处理《三国演义》这类章回体小说时,我发现直接使用固定窗口切分会破坏原有的章节结构。比如"温酒斩华雄"这个典故,如果切分时正好把"温酒"和"华雄"分到两个chunk中,检索效果就会大打折扣。
我们的解决方案是采用两级切分:
python复制# 章回识别正则表达式(简化版)
chapter_pattern = r'^正文\s+?第[一二三四五六七八九十百千0-9]+[回章节].*$'
# 实际处理中还加入了以下增强:
1. 处理"第XX回"和"第XX章"等变体
2. 排除目录中的章节标题
3. 处理章节标题中的特殊符号(如◆等)
在确定窗口大小时,我们对比了多种配置:
| 窗口大小 | 重叠大小 | Recall@5 | 特点 |
|---|---|---|---|
| 512 | 80 | 0.53 | 容易丢失关键事件 |
| 256 | 64 | 0.67 | 部分典故仍会断裂 |
| 128 | 32 | 0.80 | 最佳平衡点 |
| 64 | 16 | 0.75 | 过于碎片化 |
经过实测,128/32的配置在保持语义完整性和检索精度之间取得了最佳平衡。这里有个实用技巧:可以先抽取100个典型事件,人工检查不同参数下的切分效果。
我们选择Qdrant主要基于以下考量:
创建集合时需要特别注意:
bash复制# 正确做法:根据实际向量维度创建集合
PUT /collections/sanguo {
"vectors": {
"size": 768, # 必须与嵌入模型输出维度一致
"distance": "Cosine"
}
}
# 常见错误:维度不匹配会导致召回异常
在实际部署中,我们发现不同版本的Qdrant对批量写入的支持有差异。最终实现的写入逻辑包含自动降级机制:
java复制// 伪代码:带降级的写入逻辑
try {
// 首选单点格式
response = qdrantClient.upsert(singlePointFormat);
} catch (QdrantException e) {
if (e.contains("missing field ids")) {
// 降级到batch格式
response = qdrantClient.upsert(batchFormat);
}
}
我们设计的15个测试用例覆盖了多种类型:
评测时采用宽松匹配规则:
未命中的3个案例主要分为两类:
针对这些问题,我们后续加入了:
在16核32G的机器上:
我们发现重复查询的章节命中率很高,于是加入了两级缓存:
这使热门查询的响应时间降低到20ms以内。
这套方案稍作调整就可应用于:
最近我们正在尝试将滑动窗口改为动态大小,根据标点密度自动调整,初步测试显示Recall@5可以提升到0.83。
关键经验:处理中文长文本时,保留原有的结构信息比单纯追求语义切分更重要。下一步我们计划引入事件时间线建模,进一步改善对跨章节事件的检索能力。