上下文扩展
上下文扩展通过扩展相邻分片,解决分片截断导致的语义不完整问题。
问题场景
分片截断问题
文档分片时,可能会在语义不完整的位置切断:
markdown
原文:
...分布式锁的实现方式有多种,常见的包括:
1. 基于数据库的实现
2. 基于 Redis 的实现
3. 基于 ZooKeeper 的实现...
分片结果:
Chunk 5: ...分布式锁的实现方式有多种,常见的包括:
Chunk 6: 1. 基于数据库的实现
Chunk 7: 2. 基于 Redis 的实现
Chunk 8: 3. 基于 ZooKeeper 的实现...如果检索命中 Chunk 6,用户只能看到 "1. 基于数据库的实现",缺乏上下文。
解决方案
自动扩展命中分片的前后相邻分片:
配置
yaml
molandev:
rag:
context-expansion:
# 是否启用上下文补全
enabled: false
# 向前扩展的 chunk 数量
before-chunks: 1
# 向后扩展的 chunk 数量
after-chunks: 1实现原理
第一步: 批量获取相邻分片
java
// 伪代码 - 获取相邻分片
public List<RetrievedDocument> expand(List<RetrievedDocument> docs) {
int beforeChunks = config.getBeforeChunks(); // 向前扩展数
int afterChunks = config.getAfterChunks(); // 向后扩展数
// 收集需要扩展的 chunk ID
List<Long> vectorIds = docs.stream()
.map(RetrievedDocument::getChunkId)
.collect(Collectors.toList());
// 批量获取相邻分片
List<KlDocumentChunkEntity> expandedChunks =
documentChunkService.getAdjacentChunksBatch(vectorIds, beforeChunks, afterChunks);
// ...
}第二步: 按文档分组
将同一文档的分片归为一组:
java
// 按文档 ID 分组
Map<String, List<KlDocumentChunkEntity>> chunksByDoc =
expandedChunks.stream()
.collect(Collectors.groupingBy(KlDocumentChunkEntity::getDocumentId));第三步: 区间合并
当多个命中分片在同一文档内且位置相邻时,自动合并区间:
java
for (List<KlDocumentChunkEntity> chunks : chunksByDoc.values()) {
// 计算合并后的区间
int minStart = chunks.stream()
.mapToInt(KlDocumentChunkEntity::getCharStartIndex)
.min()
.orElse(0);
int maxEnd = chunks.stream()
.mapToInt(KlDocumentChunkEntity::getCharEndIndex)
.max()
.orElse(0);
// 合并内容
String mergedContent = chunks.stream()
.sorted(Comparator.comparing(KlDocumentChunkEntity::getPosition))
.map(KlDocumentChunkEntity::getContent)
.collect(Collectors.joining("\n"));
// 创建扩展后的文档
RetrievedDocument expandedDoc = new RetrievedDocument();
expandedDoc.setContent(mergedContent);
expandedDoc.setMetadata(Map.of(
"charStartIndex", minStart,
"charEndIndex", maxEnd
));
}区间合并示例
命中 Chunk 6, 8 (位置相邻)
↓
合并为一个大区间
↓
避免返回重复内容合并规则:
- 允许 100 字符间隙
- 按位置排序后合并内容
- 更新合并后的字符区间
扩展示例
扩展前
检索命中: Chunk 6
内容: "1. 基于数据库的实现"扩展后 (before=1, after=1)
合并: Chunk 5 + Chunk 6 + Chunk 7
内容: "分布式锁的实现方式有多种,常见的包括:\n1. 基于数据库的实现\n2. 基于 Redis 的实现"配置调优
| 场景 | before-chunks | after-chunks | 说明 |
|---|---|---|---|
| 短分片 (< 500 字符) | 2 | 2 | 需要更多上下文 |
| 长分片 (> 1000 字符) | 1 | 1 | 分片本身已足够 |
| 章节边界 | 1 | 0 | 只需前向上下文 |
注意事项
- 过多扩展 — 会导致返回内容过长,增加 LLM 处理成本
- 过少扩展 — 可能无法解决截断问题
- 默认值 —
before-chunks: 1, after-chunks: 1适合大部分场景
性能影响
| 指标 | 无扩展 | 有扩展 (before=1, after=1) |
|---|---|---|
| 数据库查询 | 无 | 1 次批量查询 |
| 返回内容大小 | 原始分片 | 约 3 倍大小 |
| 延迟增加 | 无 | ~50ms |
与其他功能配合
| 功能 | 配合效果 |
|---|---|
| 向量检索 | 补充语义完整性 |
| 混合检索 | 补充关键词上下文 |
| 重排序 | 重排序后再扩展,更精准 |
推荐流程: 检索 → 重排序 → 上下文扩展
最佳实践
何时启用?
| 场景 | 是否启用 | 说明 |
|---|---|---|
| FAQ 问答 | ❌ | 分片短且完整 |
| 技术文档检索 | ✅ | 分片长,需上下文 |
| 代码检索 | ❌ | 代码分片通常自包含 |
| 规范文档检索 | ✅ | 规范常有前后依赖 |
与 Prompt 配合
扩展后的内容作为参考资料传入 Prompt:
【参考资料】
资料1: [分布式锁指南]
分布式锁的实现方式有多种,常见的包括:
1. 基于数据库的实现
2. 基于 Redis 的实现
【用户问题】
分布式锁有哪些实现方式?