Spring AI 学习篇(八)| RAG效果优化第一弹:检索优化

Spring AI 学习篇(八)| RAG效果优化第一弹:检索优化

一、本章核心学习目标

学完本章,你将能够:

  1. 理解基础RAG检索阶段的核心痛点与局限性
  2. 掌握混合检索技术,结合向量检索与关键词检索的优势
  3. 实现查询重写,解决用户模糊查询的问题
  4. 掌握检索结果过滤、去重与排序的高级技巧
  5. 实现多轮检索与上下文扩充
  6. 建立量化的检索效果评估体系
  7. 将基础RAG的检索准确率提升30%以上

二、前置知识准备

  • 已经完成第7篇的学习,成功实现了基础RAG系统
  • 理解向量相似度检索的原理与局限性
  • 熟练使用Spring AI VectorStoreChatClient
  • 掌握提示词工程的基本方法

三、为什么基础RAG需要检索优化?

我们在第7篇实现的基础RAG系统已经可以工作,但它存在4个致命的缺陷,导致实际场景中准确率往往不足60%:

1. 纯向量检索的语义偏差

向量检索擅长捕捉语义相似性 ,但不擅长精确关键词匹配

例如:用户搜索"Spring AI 1.0的Maven依赖配置"

  • 纯向量检索可能会返回"Spring Boot依赖配置"的相关内容
  • 但会漏掉包含精确关键词"spring-ai-bom"的最相关文档

2. 用户查询的模糊性与口语化

用户的提问往往是模糊、口语化的,不适合直接用于检索。

例如:用户问"这个东西怎么用啊?"

  • 没有上下文的情况下,检索系统完全不知道"这个东西"指的是什么
  • 直接用这个问题检索,会返回大量无关结果

3. 检索结果的冗余与噪声

基础RAG会返回Top K个最相似的文档,但其中往往包含重复、无关或低质量的内容。

例如:同一个文档被切成多个分块,全部被检索出来,导致上下文重复

4. 单轮检索的信息不完整性

很多复杂问题需要多轮检索才能获取足够的信息,单轮检索往往只能找到部分答案。

预告式提及:检索优化是提升RAG效果性价比最高的环节。本章我们会解决前4个问题,下一章我们会学习重排序和上下文压缩,进一步提升生成阶段的效果。

四、优化技术一:混合检索(效果提升20-30%)⭐⭐⭐⭐⭐

混合检索是目前最有效的检索优化技术之一。它的核心思想是:同时使用向量检索(语义匹配)和关键词检索(精确匹配),然后将两者的结果合并,取长补短

1. 混合检索的工作原理

复制代码
用户查询 → 向量检索(返回Top 20) → 结果合并 → 取交集/并集 → 排序 → 返回Top 5
          → 关键词检索(返回Top 20)
  • 向量检索:捕捉语义相似的内容,擅长处理同义词、近义词和语义转述
  • 关键词检索:捕捉精确匹配的内容,擅长处理专业术语、产品名称和代码片段
  • 两者结合:准确率比单一检索方式提升20-30%

2. Spring AI 1.0原生混合检索实现

Spring AI 1.0已经内置了对混合检索的支持,并且BGE-M4等新一代嵌入模型已经内置了稀疏向量支持,不需要单独部署BM25服务。

(1) 启用混合检索

SearchRequest中启用混合检索(以 Spring AI 1.0 实际 API 为准):

java 复制代码
// 基础向量检索(之前的写法)
List<Document> results = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query(query)
                .topK(5)
                .build()
);

// 混合检索(优化后的写法,具体配置方式以实际向量数据库 API 为准)
List<Document> results = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query(query)
                .topK(5)
                // 混合检索的具体配置取决于向量数据库实现
                .build()
);

(2) 混合权重调优指南

场景 推荐向量权重 推荐关键词权重
通用文档、技术文档 0.7 0.3
代码文档、API文档 0.5 0.5
产品手册、说明书 0.6 0.4
新闻、资讯 0.8 0.2

最佳实践:中文场景推荐使用0.7:0.3的权重比。

3. 不支持内置稀疏向量的模型怎么办?

如果你使用的是不支持内置稀疏向量的旧模型(如BGE-M3),Spring AI也支持集成独立的BM25检索:

java 复制代码
// 集成BM25关键词检索(适用于旧模型)
List<Document> bm25Results = bm25Store.search(query, 20);
List<Document> vectorResults = vectorStore.similaritySearch(query, 20);

// 合并结果并去重
Set<String> seenIds = new HashSet<>();
List<Document> mergedResults = new ArrayList<>();
for (Document doc : Stream.concat(vectorResults.stream(), bm25Results.stream()).toList()) {
    if (seenIds.add(doc.getId())) {
        mergedResults.add(doc);
    }
}

// 取前5个结果
return mergedResults.stream().limit(5).toList();

五、优化技术二:查询重写(效果提升15-20%)⭐⭐⭐⭐

查询重写是第二有效的检索优化技术。它的核心思想是:让大模型先将用户的模糊、口语化查询改写成更清晰、更适合检索的查询语句

1. 为什么需要查询重写?

用户的查询往往存在以下问题:

  • 模糊不清:"这个怎么配置?"
  • 口语化:"Spring AI咋对接DeepSeek啊?"
  • 指代不明:"它的最新版本是多少?"
  • 包含冗余信息:"我想问一下,那个Spring AI 1.0版本的,它的Maven依赖应该怎么写啊?"

2. Spring AI实现查询重写

提示 :Spring AI 提供了内置的 RewriteQueryTransformer。本章采用手动构建方式,帮助你理解查询重写的原理。

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class QueryRewriterService {

    private final ChatClient chatClient;

    public QueryRewriterService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    /**
     * 重写用户查询,使其更适合检索
     */
    public String rewriteQuery(String userQuery) {
        String prompt = """
                你是一个专业的查询重写专家。请将用户的查询改写成一个更清晰、更简洁、更适合检索的查询语句。
                要求:
                1. 去除口语化、冗余的表达
                2. 补充必要的上下文信息
                3. 使用更专业的术语
                4. 只返回重写后的查询语句,不要任何解释
                5. 如果用户查询已经很清晰,直接返回原查询
                
                例如:
                用户:我想问一下,那个Spring AI 1.0版本的,它的Maven依赖应该怎么写啊?
                重写:Spring AI 1.0 Maven依赖配置
                
                用户:这个怎么配置?
                重写:Spring AI DeepSeek对接配置
                
                用户查询:{userQuery}
                """;

        return chatClient.prompt()
                .user(prompt.replace("{userQuery}", userQuery))
                .call()
                .content();
    }
}

3. 集成到RAG系统中

java 复制代码
// 在RagService的chat方法中添加查询重写步骤
public String chat(String query) {
    // 第一步:重写用户查询
    String rewrittenQuery = queryRewriterService.rewriteQuery(query);
    
    // 第二步:使用重写后的查询进行混合检索
    List<Document> relevantDocs = vectorStore.similaritySearch(
            SearchRequest.builder().query(rewrittenQuery)
                    .topK(20)
                    .build()
    );
    
    // 后续步骤不变...
}

4. 高级查询重写策略

对于复杂问题,可以使用更高级的重写策略:

  • 问题分解:将复杂问题分解为多个简单问题,分别检索
  • 关键词提取:提取查询中的核心关键词,用于关键词检索
  • 意图识别:识别用户的真实意图,调整检索策略

六、优化技术三:检索结果后处理(效果提升10-15%)

检索返回的原始结果往往包含冗余、噪声和低质量的内容,需要进行后处理。

1. 相似度阈值过滤

过滤掉相似度太低的结果,避免引入无关信息:

java 复制代码
List<Document> filteredResults = vectorStore.similaritySearch(
        SearchRequest.builder().query(query)
                .topK(20)
                .similarityThreshold(0.7)
                .build()
);

阈值调优指南

  • 0.8以上:高置信度,几乎没有无关内容
  • 0.7-0.8:中等置信度,可能有少量无关内容
  • 0.6-0.7:低置信度,不建议使用

2. 结果去重

去除重复的文档分块:

java 复制代码
Set<String> seenContents = new HashSet<>();
List<Document> uniqueResults = new ArrayList<>();
for (Document doc : filteredResults) {
    String content = doc.getContent().trim();
    if (seenContents.add(content)) {
        uniqueResults.add(doc);
    }
}

3. 元数据过滤

使用元数据进一步过滤结果,比如只返回特定分类或特定时间的文档:

java 复制代码
List<Document> results = vectorStore.similaritySearch(
        SearchRequest.builder().query(query)
                .topK(20)
                .filterExpression("category == 'Spring AI' && uploadTime > 1704067200000")
                .build()
);

七、优化技术四:多轮检索(效果提升5-10%)

对于复杂问题,单轮检索往往无法获取足够的信息,可以进行多轮检索。

1. 多轮检索的工作原理

复制代码
用户查询 → 第一轮检索 → 分析检索结果 → 判断是否需要补充检索 → 第二轮检索 → 合并结果

2. Spring AI实现多轮检索

java 复制代码
public List<Document> multiRoundSearch(String query) {
    // 第一轮检索
    List<Document> firstRoundResults = vectorStore.similaritySearch(
            SearchRequest.builder().query(query)
                    .topK(10)
                    .build()
    );
    
    // 分析第一轮结果是否足够回答问题
    boolean needMoreInfo = needMoreInformation(query, firstRoundResults);
    
    if (!needMoreInfo) {
        return firstRoundResults;
    }
    
    // 生成补充查询
    String supplementaryQuery = generateSupplementaryQuery(query, firstRoundResults);
    
    // 第二轮检索
    List<Document> secondRoundResults = vectorStore.similaritySearch(
            SearchRequest.builder().query(supplementaryQuery)
                    .topK(10)
                    .build()
    );
    
    // 合并结果并去重
    Set<String> seenIds = new HashSet<>();
    List<Document> mergedResults = new ArrayList<>();
    for (Document doc : Stream.concat(firstRoundResults.stream(), secondRoundResults.stream()).toList()) {
        if (seenIds.add(doc.getId())) {
            mergedResults.add(doc);
        }
    }
    
    return mergedResults;
}

八、检索效果量化评估

不要凭感觉判断检索效果,一定要建立量化的评估体系。

1. 核心评估指标

指标 计算公式 含义 目标值
准确率@K 前K个结果中相关结果的数量 / K 检索结果的精确性 >80%
召回率@K 前K个结果中相关结果的数量 / 所有相关结果的数量 检索结果的完整性 >70%
MRR 第一个相关结果位置的倒数的平均值 相关结果的排序质量 >0.7

2. 构建评估数据集

准备一个包含100-200个问题的评估数据集,每个问题标注对应的正确文档ID。

json 复制代码
[
  {
    "query": "Spring AI 1.0稳定版发布于哪一年?",
    "relevant_doc_ids": ["doc1", "doc2"]
  },
  {
    "query": "如何在Spring AI中对接DeepSeek?",
    "relevant_doc_ids": ["doc3", "doc4", "doc5"]
  }
]

3. 自动评估脚本

编写一个自动评估脚本,定期测试检索效果:

java 复制代码
public void evaluateRetrieval() {
    List<EvaluationItem> items = loadEvaluationDataset();
    int total = items.size();
    int correctAt5 = 0;
    double mrr = 0.0;
    
    for (EvaluationItem item : items) {
        List<Document> results = vectorStore.similaritySearch(
                SearchRequest.builder().query(item.getQuery()).topK(5).build());
        List<String> resultIds = results.stream().map(Document::getId).toList();
        
        // 计算准确率@5
        long relevantCount = resultIds.stream().filter(item.getRelevantDocIds()::contains).count();
        if (relevantCount > 0) {
            correctAt5++;
        }
        
        // 计算MRR
        for (int i = 0; i < resultIds.size(); i++) {
            if (item.getRelevantDocIds().contains(resultIds.get(i))) {
                mrr += 1.0 / (i + 1);
                break;
            }
        }
    }
    
    double precisionAt5 = (double) correctAt5 / total;
    double averageMrr = mrr / total;
    
    System.out.println("准确率@5: " + precisionAt5);
    System.out.println("MRR: " + averageMrr);
}

九、企业级最佳实践

1. 优先实现混合检索和查询重写。 这两个技术的投入产出比最高------混合检索改一行代码,查询重写加一个 Service,合计提升检索准确率 30%+。不要一上来就上重排序。

2. 逐步迭代,每次只改一个变量。 不要同时加混合检索、查询重写、阈值过滤。每次只开启一个优化,跑一遍评估数据集,确认效果正向后再叠下一个。

3. 建立评估数据集并持续维护。 准备至少 20 个标准问题 + 预期答案。每次调整参数后用脚本自动跑评估。凭"感觉"调参数不可靠。

4. 监控线上检索质量。 生产环境中记录检索召回数、空结果率、用户追问率。第15篇会专门讲可观测性方案。

十、常见坑与解决方案

1. ❌ 混合检索权重设置不当

问题 :关键词权重太高,导致语义匹配效果差;或者向量权重太高,导致精确匹配效果差

解决方案:从0.7:0.3的默认权重开始,根据评估结果逐步调整

2. ❌ 查询重写过度

问题 :大模型重写后的查询改变了用户的原意

解决方案

  • 在提示词中明确要求"不要改变用户的原意"
  • 保留原查询,同时使用原查询和重写后的查询进行检索
  • 对重写后的结果进行校验,如果和原查询差异太大,使用原查询

3. ❌ 相似度阈值设置过高

问题 :过滤掉了很多相关的结果,导致召回率低

解决方案:从0.7的默认阈值开始,根据评估结果逐步调整

4. ❌ 多轮检索导致性能下降

问题 :多轮检索增加了响应时间,影响用户体验

解决方案

  • 限制多轮检索的次数(最多2轮)
  • 异步执行多轮检索
  • 对检索结果进行缓存

十一、本章总结与下章预告

本章总结

  1. 基础RAG的检索阶段存在语义偏差、查询模糊、结果冗余和信息不完整四大问题
  2. 混合检索结合向量检索和关键词检索的优势,是效果提升最明显的优化技术
  3. 查询重写将用户的模糊查询改写成更适合检索的查询语句
  4. 检索结果后处理过滤掉冗余和噪声内容,提升结果质量
  5. 多轮检索补充单轮检索的信息不足
  6. 建立量化的评估体系,用数据说话,不要凭感觉优化

预告式提及:经过本章的优化,我们的检索准确率已经提升了30%以上。但检索返回的结果中仍然包含很多无关的信息。下一章我们将学习RAG效果优化第二弹:重排序与上下文压缩,进一步提升生成阶段的效果。

下章预告

下一章我们将深入学习RAG效果优化的第二部分:重排序与上下文压缩。你将学会:

  • 为什么需要重排序?重排序模型的工作原理
  • Spring AI集成BGE Reranker v3实现重排序
  • 上下文压缩技术:只保留最相关的信息
  • 分块召回与合并策略
  • 高级RAG架构:检索-重排序-生成三阶段流水线

十二、课后练习

  1. 为你的RAG系统添加混合检索功能,对比开启前后的检索效果差异
  2. 实现查询重写功能,测试不同提示词对重写效果的影响
  3. 添加相似度阈值过滤和结果去重功能
  4. 构建一个包含20个问题的评估数据集,量化评估优化前后的准确率和MRR
  5. 尝试调整混合检索的权重和相似度阈值,找到最适合你的知识库的配置