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 的实现

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

下一步