Skip to content

上下文扩展 ​

上下文扩展通过扩展相邻分片,解决分片截断导致的语义不完整问题。

问题场景 ​

分片截断问题 ​

文档分片时,可能会在语义不完整的位置切断:

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-chunksafter-chunks说明
短分片 (< 500 字符)22需要更多上下文
长分片 (> 1000 字符)11分片本身已足够
章节边界10只需前向上下文

注意事项 ​

  • 过多扩展 — 会导致返回内容过长,增加 LLM 处理成本
  • 过少扩展 — 可能无法解决截断问题
  • 默认值 — before-chunks: 1, after-chunks: 1 适合大部分场景

性能影响 ​

指标无扩展有扩展 (before=1, after=1)
数据库查询无1 次批量查询
返回内容大小原始分片约 3 倍大小
延迟增加无~50ms

与其他功能配合 ​

功能配合效果
向量检索补充语义完整性
混合检索补充关键词上下文
重排序重排序后再扩展,更精准

推荐流程: 检索 → 重排序 → 上下文扩展


最佳实践 ​

何时启用? ​

场景是否启用说明
FAQ 问答❌分片短且完整
技术文档检索✅分片长,需上下文
代码检索❌代码分片通常自包含
规范文档检索✅规范常有前后依赖

与 Prompt 配合 ​

扩展后的内容作为参考资料传入 Prompt:

【参考资料】

资料1: [分布式锁指南]
分布式锁的实现方式有多种,常见的包括:
1. 基于数据库的实现
2. 基于 Redis 的实现

【用户问题】
分布式锁有哪些实现方式?

下一步 ​